diff --git a/src/core/colorquantizer.cpp b/src/core/colorquantizer.cpp index d08f87b4..b97cf676 100644 --- a/src/core/colorquantizer.cpp +++ b/src/core/colorquantizer.cpp @@ -1,32 +1,33 @@ #include "colorquantizer.hpp" #include <qcolor.h> +#include <qlogging.h> +#include <qloggingcategory.h> #include <qobject.h> #include <qqmllist.h> #include <qthreadpool.h> #include <qtypes.h> +namespace { +Q_LOGGING_CATEGORY(logColorQuantizer, "quickshell.colorquantizer", QtWarningMsg); +} + ColorQuantizerOperation::ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize) : source(source) , maxDepth(depth) , rescaleSize(rescaleSize) { - mColors = QList<QColor>(); + setAutoDelete(false); + colors = QList<QColor>(); } -void ColorQuantizerOperation::run() { - quantizeImage(); +void ColorQuantizerOperation::quantizeImage(const QAtomicInteger<bool>& shouldCancel) { + if (shouldCancel.loadAcquire()) return; - emit done(mColors); -} + colors.clear(); -void ColorQuantizerOperation::quantizeImage() { - mColors.clear(); + if (source->isEmpty()) return; - if (source->isEmpty()) { - return; - } - - QImage image(source->toLocalFile()); + auto image = QImage(source->toLocalFile()); if ((image.width() > rescaleSize || image.height() > rescaleSize) && rescaleSize > 0) { image = image.scaled( @@ -38,48 +39,52 @@ void ColorQuantizerOperation::quantizeImage() { } if (image.isNull()) { - qWarning() << "Failed to load image from" << source; + qCWarning(logColorQuantizer) << "Failed to load image from" << source; return; } QList<QColor> pixels; - for (int y = 0; y < image.height(); ++y) { - for (int x = 0; x < image.width(); ++x) { - QRgb pixel = image.pixel(x, y); - if (qAlpha(pixel) == 0) { - continue; - } + for (int y = 0; y != image.height(); ++y) { + for (int x = 0; x != image.width(); ++x) { + auto pixel = image.pixel(x, y); + if (qAlpha(pixel) == 0) continue; pixels.append(QColor::fromRgb(pixel)); } } - QDateTime startTime = QDateTime::currentDateTime(); + auto startTime = QDateTime::currentDateTime(); - mColors = quantization(pixels, 0); + colors = quantization(pixels, 0); - QDateTime endTime = QDateTime::currentDateTime(); - qint64 milliseconds = startTime.msecsTo(endTime); + auto endTime = QDateTime::currentDateTime(); + auto milliseconds = startTime.msecsTo(endTime); qDebug() << "Color Quantization took: " << milliseconds << "ms"; } -QList<QColor> ColorQuantizerOperation::quantization(QList<QColor>& rgbValues, qreal depth) { +QList<QColor> ColorQuantizerOperation::quantization( + QList<QColor>& rgbValues, + qreal depth, + const QAtomicInteger<bool>& shouldCancel +) { + if (shouldCancel.loadAcquire()) return QList<QColor>(); + if (depth >= maxDepth || rgbValues.isEmpty()) { - if (rgbValues.isEmpty()) { - return QList<QColor>(); - } + if (rgbValues.isEmpty()) return QList<QColor>(); - int totalR = 0; - int totalG = 0; - int totalB = 0; + auto totalR = 0; + auto totalG = 0; + auto totalB = 0; + + for (const auto& color: rgbValues) { + if (shouldCancel.loadAcquire()) return QList<QColor>(); - for (const QColor& color: rgbValues) { totalR += color.red(); totalG += color.green(); totalB += color.blue(); } - QColor avgColor( + auto avgColor = QColor( qRound(totalR / static_cast<double>(rgbValues.size())), qRound(totalG / static_cast<double>(rgbValues.size())), qRound(totalB / static_cast<double>(rgbValues.size())) @@ -88,17 +93,17 @@ QList<QColor> ColorQuantizerOperation::quantization(QList<QColor>& rgbValues, qr return QList<QColor>() << avgColor; } - QString dominantChannel = findBiggestColorRange(rgbValues); - std::ranges::sort(rgbValues, [dominantChannel](const QColor& a, const QColor& b) { + auto dominantChannel = findBiggestColorRange(rgbValues); + std::ranges::sort(rgbValues, [dominantChannel](const auto& a, const auto& b) { if (dominantChannel == "r") return a.red() < b.red(); else if (dominantChannel == "g") return a.green() < b.green(); return a.blue() < b.blue(); }); - qsizetype mid = rgbValues.size() / 2; + auto mid = rgbValues.size() / 2; - QList<QColor> leftHalf = rgbValues.mid(0, mid); - QList<QColor> rightHalf = rgbValues.mid(mid + 1); + auto leftHalf = rgbValues.mid(0, mid); + auto rightHalf = rgbValues.mid(mid); QList<QColor> result; result.append(quantization(leftHalf, depth + 1)); @@ -107,17 +112,17 @@ QList<QColor> ColorQuantizerOperation::quantization(QList<QColor>& rgbValues, qr return result; } -QString ColorQuantizerOperation::findBiggestColorRange(const QList<QColor>& rgbValues) { - if (rgbValues.isEmpty()) return "r"; +QChar ColorQuantizerOperation::findBiggestColorRange(const QList<QColor>& rgbValues) { + if (rgbValues.isEmpty()) return 'r'; - int rMin = 255; - int gMin = 255; - int bMin = 255; - int rMax = 0; - int gMax = 0; - int bMax = 0; + auto rMin = 255; + auto gMin = 255; + auto bMin = 255; + auto rMax = 0; + auto gMax = 0; + auto bMax = 0; - for (const QColor& color: rgbValues) { + for (const auto& color: rgbValues) { rMin = qMin(rMin, color.red()); gMin = qMin(gMin, color.green()); bMin = qMin(bMin, color.blue()); @@ -127,56 +132,101 @@ QString ColorQuantizerOperation::findBiggestColorRange(const QList<QColor>& rgbV bMax = qMax(bMax, color.blue()); } - int rRange = rMax - rMin; - int gRange = gMax - gMin; - int bRange = bMax - bMin; + auto rRange = rMax - rMin; + auto gRange = gMax - gMin; + auto bRange = bMax - bMin; - int biggestRange = qMax(rRange, qMax(gRange, bRange)); + auto biggestRange = qMax(rRange, qMax(gRange, bRange)); if (biggestRange == rRange) { - return "r"; + return 'r'; } else if (biggestRange == gRange) { - return "g"; + return 'g'; } else { - return "b"; + return 'b'; } } -QList<QColor> ColorQuantizer::colors() { return mColors; } +void ColorQuantizerOperation::finishRun() { + QMetaObject::invokeMethod(this, &ColorQuantizerOperation::finished, Qt::QueuedConnection); +} + +void ColorQuantizerOperation::finished() { + emit this->done(colors); + delete this; +} + +void ColorQuantizerOperation::run() { + if (!this->shouldCancel) { + this->quantizeImage(); + + if (this->shouldCancel.loadAcquire()) { + qCDebug(logColorQuantizer) << "Color quantization" << this << "cancelled"; + } + } + + this->finishRun(); +} + +void ColorQuantizerOperation::tryCancel() { this->shouldCancel.storeRelease(true); } + +void ColorQuantizer::componentComplete() { + componentCompleted = true; + if (!mSource.isEmpty()) quantizeAsync(); +} void ColorQuantizer::setSource(const QUrl& source) { if (mSource != source) { mSource = source; - emit sourceChanged(); - quantizeAsync(); + emit this->sourceChanged(); + + if (this->componentCompleted && !mSource.isEmpty()) quantizeAsync(); } } void ColorQuantizer::setDepth(qreal depth) { if (mDepth != depth) { mDepth = depth; - emit depthChanged(); + emit this->depthChanged(); + + if (this->componentCompleted) quantizeAsync(); } } void ColorQuantizer::setRescaleSize(int rescaleSize) { if (mRescaleSize != rescaleSize) { mRescaleSize = rescaleSize; - emit rescaleSizeChanged(); + emit this->rescaleSizeChanged(); + + if (this->componentCompleted) quantizeAsync(); } } void ColorQuantizer::operationFinished(const QList<QColor>& result) { - mColors = result; - emit colorsChanged(); - isProcessing = false; + bColors = result; + emit this->colorsChanged(); + this->liveOperation = nullptr; } void ColorQuantizer::quantizeAsync() { - if (isProcessing) return; + if (this->liveOperation) this->cancelAsync(); - qDebug() << "Starting color quantization asynchronously"; - isProcessing = true; - auto* task = new ColorQuantizerOperation(&mSource, mDepth, mRescaleSize); - QObject::connect(task, &ColorQuantizerOperation::done, this, &ColorQuantizer::operationFinished); - QThreadPool::globalInstance()->start(task); + qCDebug(logColorQuantizer) << "Starting color quantization asynchronously"; + this->liveOperation = new ColorQuantizerOperation(&mSource, mDepth, mRescaleSize); + QObject::connect( + this->liveOperation, + &ColorQuantizerOperation::done, + this, + &ColorQuantizer::operationFinished + ); + QThreadPool::globalInstance()->start(this->liveOperation); +} + +void ColorQuantizer::cancelAsync() { + if (!this->liveOperation) return; + + this->liveOperation->tryCancel(); + QThreadPool::globalInstance()->waitForDone(); + + QObject::disconnect(this->liveOperation, nullptr, this, nullptr); + this->liveOperation = nullptr; } diff --git a/src/core/colorquantizer.hpp b/src/core/colorquantizer.hpp index 1c0435de..0e9461a1 100644 --- a/src/core/colorquantizer.hpp +++ b/src/core/colorquantizer.hpp @@ -1,9 +1,10 @@ #pragma once #include <qlist.h> -#include <qmutex.h> #include <qobject.h> +#include <qproperty.h> #include <qqmlintegration.h> +#include <qqmlparserstatus.h> #include <qrunnable.h> #include <qtmetamacros.h> #include <qtypes.h> @@ -14,22 +15,33 @@ class ColorQuantizerOperation Q_OBJECT; public: - ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize); + explicit ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize); void run() override; + void tryCancel(); signals: void done(QList<QColor> colors); +private slots: + void finished(); + private: - QList<QColor> mColors; + static QChar findBiggestColorRange(const QList<QColor>& rgbValues); + + void quantizeImage(const QAtomicInteger<bool>& shouldCancel = false); + QList<QColor> quantization( + QList<QColor>& rgbValues, + qreal depth, + const QAtomicInteger<bool>& shouldCancel = false + ); + void finishRun(); + + QAtomicInteger<bool> shouldCancel = false; + QList<QColor> colors; QUrl* source; qreal maxDepth; qreal rescaleSize; - - void quantizeImage(); - QList<QColor> quantization(QList<QColor>& rgbValues, qreal depth); - static QString findBiggestColorRange(const QList<QColor>& rgbValues); }; ///! Color Quantization Utility @@ -45,13 +57,17 @@ private: /// rescaleSize: 64 // Rescale to 64x64 for faster processing /// } /// ``` +class ColorQuantizer + : public QObject + , public QQmlParserStatus { -class ColorQuantizer: public QObject { Q_OBJECT; + QML_ELEMENT; + Q_INTERFACES(QQmlParserStatus); /// Access the colors resulting from the color quantization performed. /// > [!NOTE] The amount of colors returned from the quantization is determined by /// > the property depth, specifically 2ⁿ where n is the depth. - Q_PROPERTY(QList<QColor> colors READ colors NOTIFY colorsChanged); + Q_PROPERTY(QList<QColor> colors READ default BINDABLE bindableColors); /// Path to the image you'd like to run the color quantization on. Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged); @@ -64,14 +80,21 @@ class ColorQuantizer: public QObject { /// > [!NOTE] Results from color quantization doesn't suffer much when rescaling, it's /// > reccommended to rescale, otherwise the quantization process will take much longer. Q_PROPERTY(qreal rescaleSize READ rescaleSize WRITE setRescaleSize NOTIFY rescaleSizeChanged); - QML_ELEMENT; public: - QList<QColor> colors(); + explicit ColorQuantizer(QObject* parent = nullptr): QObject(parent) {} + + void componentComplete() override; + void classBegin() override {} + + [[nodiscard]] QBindable<QList<QColor>> bindableColors() { return &this->bColors; } + [[nodiscard]] QUrl source() const { return mSource; } void setSource(const QUrl& source); + [[nodiscard]] qreal depth() const { return mDepth; } void setDepth(qreal depth); + [[nodiscard]] qreal rescaleSize() const { return mRescaleSize; } void setRescaleSize(int rescaleSize); @@ -85,11 +108,19 @@ public slots: void operationFinished(const QList<QColor>& result); private: - bool isProcessing = false; - QList<QColor> mColors; + void quantizeAsync(); + void cancelAsync(); + + bool componentCompleted = false; + ColorQuantizerOperation* liveOperation = nullptr; QUrl mSource; qreal mDepth = 0; qreal mRescaleSize = 0; - void quantizeAsync(); + Q_OBJECT_BINDABLE_PROPERTY( + ColorQuantizer, + QList<QColor>, + bColors, + &ColorQuantizer::colorsChanged + ); };