summaryrefslogtreecommitdiff
path: root/plugin/src/Caelestia
diff options
context:
space:
mode:
author2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-09-24 01:37:53 +1000
committer2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-09-24 01:37:53 +1000
commit2ccd3a8662c46e1be9cfb21a8d60751c98e78065 (patch)
treeda9c846b547c32b5bd6a8a0df46fc6eb6a862d91 /plugin/src/Caelestia
parentplayers: persist active player across reloads (diff)
downloadcaelestia-shell-2ccd3a8662c46e1be9cfb21a8d60751c98e78065.tar.gz
caelestia-shell-2ccd3a8662c46e1be9cfb21a8d60751c98e78065.tar.bz2
caelestia-shell-2ccd3a8662c46e1be9cfb21a8d60751c98e78065.zip
plugin: add image analyser
Diffstat (limited to 'plugin/src/Caelestia')
-rw-r--r--plugin/src/Caelestia/CMakeLists.txt1
-rw-r--r--plugin/src/Caelestia/cutils.cpp226
-rw-r--r--plugin/src/Caelestia/cutils.hpp15
-rw-r--r--plugin/src/Caelestia/imageanalyser.cpp223
-rw-r--r--plugin/src/Caelestia/imageanalyser.hpp61
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