diff options
Diffstat (limited to 'plugin/src/Caelestia')
| -rw-r--r-- | plugin/src/Caelestia/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | plugin/src/Caelestia/cutils.cpp | 226 | ||||
| -rw-r--r-- | plugin/src/Caelestia/cutils.hpp | 15 | ||||
| -rw-r--r-- | plugin/src/Caelestia/imageanalyser.cpp | 223 | ||||
| -rw-r--r-- | plugin/src/Caelestia/imageanalyser.hpp | 61 |
5 files changed, 285 insertions, 241 deletions
diff --git a/plugin/src/Caelestia/CMakeLists.txt b/plugin/src/Caelestia/CMakeLists.txt index 4ad308f..9dea712 100644 --- a/plugin/src/Caelestia/CMakeLists.txt +++ b/plugin/src/Caelestia/CMakeLists.txt @@ -45,6 +45,7 @@ qml_module(caelestia appdb.hpp appdb.cpp requests.hpp requests.cpp toaster.hpp toaster.cpp + imageanalyser.hpp imageanalyser.cpp LIBRARIES Qt::Gui Qt::Quick diff --git a/plugin/src/Caelestia/cutils.cpp b/plugin/src/Caelestia/cutils.cpp index b5f7167..27074ee 100644 --- a/plugin/src/Caelestia/cutils.cpp +++ b/plugin/src/Caelestia/cutils.cpp @@ -116,232 +116,6 @@ bool CUtils::deleteFile(const QUrl& path) const { return QFile::remove(path.toLocalFile()); } -void CUtils::getDominantColour(QQuickItem* item, QJSValue callback) { - this->getDominantColour(item, 128, callback); -} - -void CUtils::getDominantColour(QQuickItem* item, int rescaleSize, QJSValue callback) { - if (!item) { - qWarning() << "CUtils::getDominantColour: an item is required"; - return; - } - - if (!item->window()) { - // Fail silently to avoid warning - return; - } - - const QSharedPointer<const QQuickItemGrabResult> grabResult = item->grabToImage(); - - QObject::connect( - grabResult.data(), &QQuickItemGrabResult::ready, this, [grabResult, rescaleSize, callback, this]() { - const auto future = QtConcurrent::run(&CUtils::findDominantColour, this, grabResult->image(), rescaleSize); - auto* watcher = new QFutureWatcher<QColor>(this); - auto* engine = qmlEngine(this); - - QObject::connect(watcher, &QFutureWatcher<QColor>::finished, this, [=]() { - if (callback.isCallable()) { - callback.call({ engine->toScriptValue(QVariant::fromValue(watcher->result())) }); - } - watcher->deleteLater(); - }); - watcher->setFuture(future); - }); -} - -void CUtils::getDominantColour(const QString& path, QJSValue callback) { - this->getDominantColour(path, 128, callback); -} - -void CUtils::getDominantColour(const QString& path, int rescaleSize, QJSValue callback) { - if (path.isEmpty()) { - qWarning() << "CUtils::getDominantColour: given path is empty"; - return; - } - - const auto future = QtConcurrent::run([=, this]() { - const QImage image(path); - - if (image.isNull()) { - qWarning() << "CUtils::getDominantColour: failed to load image" << path; - return QColor(); - } - - return findDominantColour(image, rescaleSize); - }); - auto* watcher = new QFutureWatcher<QColor>(this); - auto* engine = qmlEngine(this); - - QObject::connect(watcher, &QFutureWatcher<QColor>::finished, this, [=]() { - if (watcher->result().isValid() && callback.isCallable()) { - callback.call({ engine->toScriptValue(QVariant::fromValue(watcher->result())) }); - } - watcher->deleteLater(); - }); - watcher->setFuture(future); -} - -QColor CUtils::findDominantColour(const QImage& image, int rescaleSize) const { - if (image.isNull()) { - qWarning() << "CUtils::findDominantColour: image is null"; - return QColor(); - } - - QImage img = image; - - if (rescaleSize > 0 && (img.width() > rescaleSize || img.height() > rescaleSize)) { - img = img.scaled(rescaleSize, rescaleSize, Qt::KeepAspectRatio, Qt::FastTransformation); - } - - if (img.format() != QImage::Format_ARGB32) { - img = img.convertToFormat(QImage::Format_ARGB32); - } - - std::unordered_map<quint32, int> colours; - const uchar* data = img.bits(); - const int width = img.width(); - const int height = img.height(); - const qsizetype bytesPerLine = img.bytesPerLine(); - - for (int y = 0; y < height; ++y) { - const uchar* line = data + y * bytesPerLine; - for (int x = 0; x < width; ++x) { - const uchar* pixel = line + x * 4; - - if (pixel[3] == 0) { - continue; - } - - quint32 r = static_cast<quint32>(pixel[0] & 0xF8); - quint32 g = static_cast<quint32>(pixel[1] & 0xF8); - quint32 b = static_cast<quint32>(pixel[2] & 0xF8); - - quint32 colour = (r << 16) | (g << 8) | b; - ++colours[colour]; - } - } - - quint32 dominantColour = 0; - int maxCount = 0; - for (const auto& [colour, count] : colours) { - if (count > maxCount) { - dominantColour = colour; - maxCount = count; - } - } - - return QColor((0xFFu << 24) | dominantColour); -} - -void CUtils::getAverageLuminance(QQuickItem* item, QJSValue callback) { - this->getAverageLuminance(item, 128, callback); -} - -void CUtils::getAverageLuminance(QQuickItem* item, int rescaleSize, QJSValue callback) { - if (!item) { - qWarning() << "CUtils::getAverageLuminance: an item is required"; - return; - } - - if (!item->window()) { - // Fail silently to avoid warning - return; - } - - const QSharedPointer<const QQuickItemGrabResult> grabResult = item->grabToImage(); - - QObject::connect( - grabResult.data(), &QQuickItemGrabResult::ready, this, [grabResult, rescaleSize, callback, this]() { - const auto future = - QtConcurrent::run(&CUtils::findAverageLuminance, this, grabResult->image(), rescaleSize); - auto* watcher = new QFutureWatcher<qreal>(this); - - QObject::connect(watcher, &QFutureWatcher<qreal>::finished, this, [=]() { - if (callback.isCallable()) { - callback.call({ QJSValue(watcher->result()) }); - } - watcher->deleteLater(); - }); - watcher->setFuture(future); - }); -} - -void CUtils::getAverageLuminance(const QString& path, QJSValue callback) { - this->getAverageLuminance(path, 128, callback); -} - -void CUtils::getAverageLuminance(const QString& path, int rescaleSize, QJSValue callback) { - if (path.isEmpty()) { - qWarning() << "CUtils::getAverageLuminance: given path is empty"; - return; - } - - const auto future = QtConcurrent::run([=, this]() { - const QImage image(path); - - if (image.isNull()) { - qWarning() << "CUtils::getAverageLuminance: failed to load image" << path; - return 0.0; - } - - return findAverageLuminance(image, rescaleSize); - }); - auto* watcher = new QFutureWatcher<qreal>(this); - - QObject::connect(watcher, &QFutureWatcher<qreal>::finished, this, [=]() { - if (callback.isCallable()) { - callback.call({ QJSValue(watcher->result()) }); - } - watcher->deleteLater(); - }); - watcher->setFuture(future); -} - -qreal CUtils::findAverageLuminance(const QImage& image, int rescaleSize) const { - if (image.isNull()) { - qWarning() << "CUtils::findAverageLuminance: image is null"; - return 0.0; - } - - QImage img = image; - - if (rescaleSize > 0 && (img.width() > rescaleSize || img.height() > rescaleSize)) { - img = img.scaled(rescaleSize, rescaleSize, Qt::KeepAspectRatio, Qt::FastTransformation); - } - - if (img.format() != QImage::Format_ARGB32) { - img = img.convertToFormat(QImage::Format_ARGB32); - } - - const uchar* data = img.bits(); - const int width = img.width(); - const int height = img.height(); - const qsizetype bytesPerLine = img.bytesPerLine(); - - qreal totalLuminance = 0.0; - int count = 0; - - for (int y = 0; y < height; ++y) { - const uchar* line = data + y * bytesPerLine; - for (int x = 0; x < width; ++x) { - const uchar* pixel = line + x * 4; - - if (pixel[3] == 0) { - continue; - } - - const qreal r = pixel[0] / 255.0; - const qreal g = pixel[1] / 255.0; - const qreal b = pixel[2] / 255.0; - - totalLuminance += std::sqrt(0.299 * r * r + 0.587 * g * g + 0.114 * b * b); - ++count; - } - } - - return count == 0 ? 0.0 : totalLuminance / count; -} - QString CUtils::toLocalFile(const QUrl& url) const { if (!url.isLocalFile()) { qWarning() << "CUtils::toLocalFile: given url is not a local file" << url; diff --git a/plugin/src/Caelestia/cutils.hpp b/plugin/src/Caelestia/cutils.hpp index 57ece14..027226d 100644 --- a/plugin/src/Caelestia/cutils.hpp +++ b/plugin/src/Caelestia/cutils.hpp @@ -23,22 +23,7 @@ public: Q_INVOKABLE bool copyFile(const QUrl& source, const QUrl& target, bool overwrite = true) const; Q_INVOKABLE bool deleteFile(const QUrl& path) const; - - Q_INVOKABLE void getDominantColour(QQuickItem* item, QJSValue callback); - Q_INVOKABLE void getDominantColour(QQuickItem* item, int rescaleSize, QJSValue callback); - Q_INVOKABLE void getDominantColour(const QString& path, QJSValue callback); - Q_INVOKABLE void getDominantColour(const QString& path, int rescaleSize, QJSValue callback); - - Q_INVOKABLE void getAverageLuminance(QQuickItem* item, QJSValue callback); - Q_INVOKABLE void getAverageLuminance(QQuickItem* item, int rescaleSize, QJSValue callback); - Q_INVOKABLE void getAverageLuminance(const QString& path, QJSValue callback); - Q_INVOKABLE void getAverageLuminance(const QString& path, int rescaleSize, QJSValue callback); - Q_INVOKABLE QString toLocalFile(const QUrl& url) const; - -private: - QColor findDominantColour(const QImage& image, int rescaleSize) const; - qreal findAverageLuminance(const QImage& image, int rescaleSize) const; }; } // namespace caelestia diff --git a/plugin/src/Caelestia/imageanalyser.cpp b/plugin/src/Caelestia/imageanalyser.cpp new file mode 100644 index 0000000..31fe839 --- /dev/null +++ b/plugin/src/Caelestia/imageanalyser.cpp @@ -0,0 +1,223 @@ +#include "imageanalyser.hpp" + +#include <QtConcurrent/qtconcurrentrun.h> +#include <QtQuick/qquickitemgrabresult.h> +#include <qfuturewatcher.h> +#include <qimage.h> +#include <qquickwindow.h> + +namespace caelestia { + +ImageAnalyser::ImageAnalyser(QObject* parent) + : QObject(parent) + , m_futureWatcher(new QFutureWatcher<AnalyseResult>(this)) + , m_source("") + , m_sourceItem(nullptr) + , m_rescaleSize(128) + , m_dominantColour(0, 0, 0) + , m_luminance(0) { + QObject::connect(m_futureWatcher, &QFutureWatcher<AnalyseResult>::finished, this, [this]() { + if (!m_futureWatcher->future().isResultReadyAt(0)) { + return; + } + + const auto result = m_futureWatcher->result(); + if (m_dominantColour != result.first) { + m_dominantColour = result.first; + emit dominantColourChanged(); + } + if (qFuzzyCompare(m_luminance + 1.0, result.second + 1.0)) { + m_luminance = result.second; + emit luminanceChanged(); + } + }); +} + +QString ImageAnalyser::source() const { + return m_source; +} + +void ImageAnalyser::setSource(const QString& source) { + if (m_source == source) { + return; + } + + m_source = source; + emit sourceChanged(); + + if (m_sourceItem) { + m_sourceItem = nullptr; + emit sourceItemChanged(); + } + + requestUpdate(); +} + +QQuickItem* ImageAnalyser::sourceItem() const { + return m_sourceItem; +} + +void ImageAnalyser::setSourceItem(QQuickItem* sourceItem) { + if (m_sourceItem == sourceItem) { + return; + } + + m_sourceItem = sourceItem; + emit sourceItemChanged(); + + if (!m_source.isEmpty()) { + m_source = ""; + emit sourceChanged(); + } + + requestUpdate(); +} + +int ImageAnalyser::rescaleSize() const { + return m_rescaleSize; +} + +void ImageAnalyser::setRescaleSize(int rescaleSize) { + if (m_rescaleSize == rescaleSize) { + return; + } + + m_rescaleSize = rescaleSize; + emit rescaleSizeChanged(); + + requestUpdate(); +} + +QColor ImageAnalyser::dominantColour() const { + return m_dominantColour; +} + +qreal ImageAnalyser::luminance() const { + return m_luminance; +} + +void ImageAnalyser::requestUpdate() { + if (m_source.isEmpty() && !m_sourceItem) { + return; + } + + if (!m_sourceItem || (m_sourceItem->window() && m_sourceItem->window()->isVisible() && m_sourceItem->width() > 0 && + m_sourceItem->height() > 0)) { + update(); + } else if (m_sourceItem) { + if (!m_sourceItem->window()) { + QObject::connect(m_sourceItem, &QQuickItem::windowChanged, this, &ImageAnalyser::requestUpdate, + Qt::SingleShotConnection); + } else if (!m_sourceItem->window()->isVisible()) { + QObject::connect(m_sourceItem->window(), &QQuickWindow::visibleChanged, this, &ImageAnalyser::requestUpdate, + Qt::SingleShotConnection); + } + if (m_sourceItem->width() <= 0) { + QObject::connect( + m_sourceItem, &QQuickItem::widthChanged, this, &ImageAnalyser::requestUpdate, Qt::SingleShotConnection); + } + if (m_sourceItem->height() <= 0) { + QObject::connect(m_sourceItem, &QQuickItem::heightChanged, this, &ImageAnalyser::requestUpdate, + Qt::SingleShotConnection); + } + } +} + +void ImageAnalyser::update() { + if (m_source.isEmpty() && !m_sourceItem) { + return; + } + + if (m_futureWatcher->isRunning()) { + m_futureWatcher->cancel(); + } + + if (m_sourceItem) { + const QSharedPointer<const QQuickItemGrabResult> grabResult = m_sourceItem->grabToImage(); + QObject::connect(grabResult.data(), &QQuickItemGrabResult::ready, this, [grabResult, this]() { + m_futureWatcher->setFuture(QtConcurrent::run(&ImageAnalyser::analyse, grabResult->image(), m_rescaleSize)); + }); + } else { + m_futureWatcher->setFuture(QtConcurrent::run([=, this](QPromise<AnalyseResult>& promise) { + const QImage image(m_source); + analyse(promise, image, m_rescaleSize); + })); + } +} + +void ImageAnalyser::analyse(QPromise<AnalyseResult>& promise, const QImage& image, int rescaleSize) { + if (image.isNull()) { + qWarning() << "ImageAnalyser::analyse: image is null"; + return; + } + + QImage img = image; + + if (rescaleSize > 0 && (img.width() > rescaleSize || img.height() > rescaleSize)) { + img = img.scaled(rescaleSize, rescaleSize, Qt::KeepAspectRatio, Qt::FastTransformation); + } + + if (promise.isCanceled()) { + return; + } + + if (img.format() != QImage::Format_ARGB32) { + img = img.convertToFormat(QImage::Format_ARGB32); + } + + if (promise.isCanceled()) { + return; + } + + const uchar* data = img.bits(); + const int width = img.width(); + const int height = img.height(); + const qsizetype bytesPerLine = img.bytesPerLine(); + + std::unordered_map<quint32, int> colours; + qreal totalLuminance = 0.0; + int count = 0; + + for (int y = 0; y < height; ++y) { + const uchar* line = data + y * bytesPerLine; + for (int x = 0; x < width; ++x) { + if (promise.isCanceled()) { + return; + } + + const uchar* pixel = line + x * 4; + + if (pixel[3] == 0) { + continue; + } + + const quint32 mr = static_cast<quint32>(pixel[0] & 0xF8); + const quint32 mg = static_cast<quint32>(pixel[1] & 0xF8); + const quint32 mb = static_cast<quint32>(pixel[2] & 0xF8); + ++colours[(mr << 16) | (mg << 8) | mb]; + + const qreal r = pixel[0] / 255.0; + const qreal g = pixel[1] / 255.0; + const qreal b = pixel[2] / 255.0; + totalLuminance += std::sqrt(0.299 * r * r + 0.587 * g * g + 0.114 * b * b); + ++count; + } + } + + quint32 dominantColour = 0; + int maxCount = 0; + for (const auto& [colour, colourCount] : colours) { + if (promise.isCanceled()) { + return; + } + + if (colourCount > maxCount) { + dominantColour = colour; + maxCount = colourCount; + } + } + + promise.addResult(qMakePair(QColor((0xFFu << 24) | dominantColour), count == 0 ? 0.0 : totalLuminance / count)); +} + +} // namespace caelestia diff --git a/plugin/src/Caelestia/imageanalyser.hpp b/plugin/src/Caelestia/imageanalyser.hpp new file mode 100644 index 0000000..bbea2b3 --- /dev/null +++ b/plugin/src/Caelestia/imageanalyser.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include <QtQuick/qquickitem.h> +#include <qfuture.h> +#include <qfuturewatcher.h> +#include <qobject.h> +#include <qqmlintegration.h> + +namespace caelestia { + +class ImageAnalyser : public QObject { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(QString source READ source WRITE setSource NOTIFY sourceChanged) + Q_PROPERTY(QQuickItem* sourceItem READ sourceItem WRITE setSourceItem NOTIFY sourceItemChanged) + Q_PROPERTY(int rescaleSize READ rescaleSize WRITE setRescaleSize NOTIFY rescaleSizeChanged) + Q_PROPERTY(QColor dominantColour READ dominantColour NOTIFY dominantColourChanged) + Q_PROPERTY(qreal luminance READ luminance NOTIFY luminanceChanged) + +public: + explicit ImageAnalyser(QObject* parent = nullptr); + + [[nodiscard]] QString source() const; + void setSource(const QString& source); + + [[nodiscard]] QQuickItem* sourceItem() const; + void setSourceItem(QQuickItem* sourceItem); + + [[nodiscard]] int rescaleSize() const; + void setRescaleSize(int rescaleSize); + + [[nodiscard]] QColor dominantColour() const; + [[nodiscard]] qreal luminance() const; + + Q_INVOKABLE void requestUpdate(); + +signals: + void sourceChanged(); + void sourceItemChanged(); + void rescaleSizeChanged(); + void dominantColourChanged(); + void luminanceChanged(); + +private: + using AnalyseResult = QPair<QColor, qreal>; + + QFutureWatcher<AnalyseResult>* const m_futureWatcher; + + QString m_source; + QQuickItem* m_sourceItem; + int m_rescaleSize; + + QColor m_dominantColour; + qreal m_luminance; + + void update(); + static void analyse(QPromise<AnalyseResult>& promise, const QImage& image, int rescaleSize); +}; + +} // namespace caelestia |