diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 6778e984..eca7270d 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -37,6 +37,7 @@ qt_add_library(quickshell-core STATIC common.cpp iconprovider.cpp scriptmodel.cpp + colorquantizer.cpp ) qt_add_qml_module(quickshell-core diff --git a/src/core/colorquantizer.cpp b/src/core/colorquantizer.cpp new file mode 100644 index 00000000..abe6af8e --- /dev/null +++ b/src/core/colorquantizer.cpp @@ -0,0 +1,230 @@ +#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) { + setAutoDelete(false); +} + +void ColorQuantizerOperation::quantizeImage(const QAtomicInteger<bool>& shouldCancel) { + if (shouldCancel.loadAcquire() || source->isEmpty()) return; + + colors.clear(); + + auto image = QImage(source->toLocalFile()); + if ((image.width() > rescaleSize || image.height() > rescaleSize) && rescaleSize > 0) { + image = image.scaled( + static_cast<int>(rescaleSize), + static_cast<int>(rescaleSize), + Qt::KeepAspectRatio, + Qt::SmoothTransformation + ); + } + + if (image.isNull()) { + 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) { + auto pixel = image.pixel(x, y); + if (qAlpha(pixel) == 0) continue; + + pixels.append(QColor::fromRgb(pixel)); + } + } + + auto startTime = QDateTime::currentDateTime(); + + colors = quantization(pixels, 0); + + auto endTime = QDateTime::currentDateTime(); + auto milliseconds = startTime.msecsTo(endTime); + qCDebug(logColorQuantizer) << "Color Quantization took: " << milliseconds << "ms"; +} + +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>(); + + auto totalR = 0; + auto totalG = 0; + auto totalB = 0; + + for (const auto& color: rgbValues) { + if (shouldCancel.loadAcquire()) return QList<QColor>(); + + totalR += color.red(); + totalG += color.green(); + totalB += color.blue(); + } + + auto avgColor = QColor( + qRound(totalR / static_cast<double>(rgbValues.size())), + qRound(totalG / static_cast<double>(rgbValues.size())), + qRound(totalB / static_cast<double>(rgbValues.size())) + ); + + return QList<QColor>() << avgColor; + } + + 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(); + }); + + auto mid = rgbValues.size() / 2; + + auto leftHalf = rgbValues.mid(0, mid); + auto rightHalf = rgbValues.mid(mid); + + QList<QColor> result; + result.append(quantization(leftHalf, depth + 1)); + result.append(quantization(rightHalf, depth + 1)); + + return result; +} + +char ColorQuantizerOperation::findBiggestColorRange(const QList<QColor>& rgbValues) { + if (rgbValues.isEmpty()) return 'r'; + + auto rMin = 255; + auto gMin = 255; + auto bMin = 255; + auto rMax = 0; + auto gMax = 0; + auto bMax = 0; + + for (const auto& color: rgbValues) { + rMin = qMin(rMin, color.red()); + gMin = qMin(gMin, color.green()); + bMin = qMin(bMin, color.blue()); + + rMax = qMax(rMax, color.red()); + gMax = qMax(gMax, color.green()); + bMax = qMax(bMax, color.blue()); + } + + auto rRange = rMax - rMin; + auto gRange = gMax - gMin; + auto bRange = bMax - bMin; + + auto biggestRange = qMax(rRange, qMax(gRange, bRange)); + if (biggestRange == rRange) { + return 'r'; + } else if (biggestRange == gRange) { + return 'g'; + } else { + return 'b'; + } +} + +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 this->sourceChanged(); + + if (this->componentCompleted && !mSource.isEmpty()) quantizeAsync(); + } +} + +void ColorQuantizer::setDepth(qreal depth) { + if (mDepth != depth) { + mDepth = depth; + emit this->depthChanged(); + + if (this->componentCompleted) quantizeAsync(); + } +} + +void ColorQuantizer::setRescaleSize(int rescaleSize) { + if (mRescaleSize != rescaleSize) { + mRescaleSize = rescaleSize; + emit this->rescaleSizeChanged(); + + if (this->componentCompleted) quantizeAsync(); + } +} + +void ColorQuantizer::operationFinished(const QList<QColor>& result) { + bColors = result; + this->liveOperation = nullptr; + emit this->colorsChanged(); +} + +void ColorQuantizer::quantizeAsync() { + if (this->liveOperation) this->cancelAsync(); + + 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 new file mode 100644 index 00000000..b8456db4 --- /dev/null +++ b/src/core/colorquantizer.hpp @@ -0,0 +1,128 @@ +#pragma once + +#include <qlist.h> +#include <qobject.h> +#include <qproperty.h> +#include <qqmlintegration.h> +#include <qqmlparserstatus.h> +#include <qrunnable.h> +#include <qtmetamacros.h> +#include <qtypes.h> + +class ColorQuantizerOperation + : public QObject + , public QRunnable { + Q_OBJECT; + +public: + explicit ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize); + + void run() override; + void tryCancel(); + +signals: + void done(QList<QColor> colors); + +private slots: + void finished(); + +private: + static char 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; +}; + +///! Color Quantization Utility +/// A color quantization utility used for getting prevalent colors in an image, by +/// averaging out the image's color data recursively. +/// +/// #### Example +/// ```qml +/// ColorQuantizer { +/// id: colorQuantizer +/// source: Qt.resolvedUrl("./yourImage.png") +/// depth: 3 // Will produce 8 colors (2³) +/// rescaleSize: 64 // Rescale to 64x64 for faster processing +/// } +/// ``` +class ColorQuantizer + : public QObject + , public QQmlParserStatus { + + 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 default NOTIFY colorsChanged 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); + + /// Max depth for the color quantization. Each level of depth represents another + /// binary split of the color space + Q_PROPERTY(qreal depth READ depth WRITE setDepth NOTIFY depthChanged); + + /// The size to rescale the image to, when rescaleSize is 0 then no scaling will be done. + /// > [!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); + +public: + 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); + +signals: + void colorsChanged(); + void sourceChanged(); + void depthChanged(); + void rescaleSizeChanged(); + +public slots: + void operationFinished(const QList<QColor>& result); + +private: + void quantizeAsync(); + void cancelAsync(); + + bool componentCompleted = false; + ColorQuantizerOperation* liveOperation = nullptr; + QUrl mSource; + qreal mDepth = 0; + qreal mRescaleSize = 0; + + Q_OBJECT_BINDABLE_PROPERTY( + ColorQuantizer, + QList<QColor>, + bColors, + &ColorQuantizer::colorsChanged + ); +}; diff --git a/src/core/module.md b/src/core/module.md index c8b17ab9..b9404ea9 100644 --- a/src/core/module.md +++ b/src/core/module.md @@ -29,5 +29,6 @@ headers = [ "qsmenuanchor.hpp", "clock.hpp", "scriptmodel.hpp", + "colorquantizer.hpp", ] -----