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..d08f87b4 --- /dev/null +++ b/src/core/colorquantizer.cpp @@ -0,0 +1,182 @@ +#include "colorquantizer.hpp" + +#include <qcolor.h> +#include <qobject.h> +#include <qqmllist.h> +#include <qthreadpool.h> +#include <qtypes.h> + +ColorQuantizerOperation::ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize) + : source(source) + , maxDepth(depth) + , rescaleSize(rescaleSize) { + mColors = QList<QColor>(); +} + +void ColorQuantizerOperation::run() { + quantizeImage(); + + emit done(mColors); +} + +void ColorQuantizerOperation::quantizeImage() { + mColors.clear(); + + if (source->isEmpty()) { + return; + } + + QImage image(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()) { + qWarning() << "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; + } + + pixels.append(QColor::fromRgb(pixel)); + } + } + + QDateTime startTime = QDateTime::currentDateTime(); + + mColors = quantization(pixels, 0); + + QDateTime endTime = QDateTime::currentDateTime(); + qint64 milliseconds = startTime.msecsTo(endTime); + qDebug() << "Color Quantization took: " << milliseconds << "ms"; +} + +QList<QColor> ColorQuantizerOperation::quantization(QList<QColor>& rgbValues, qreal depth) { + if (depth >= maxDepth || rgbValues.isEmpty()) { + if (rgbValues.isEmpty()) { + return QList<QColor>(); + } + + int totalR = 0; + int totalG = 0; + int totalB = 0; + + for (const QColor& color: rgbValues) { + totalR += color.red(); + totalG += color.green(); + totalB += color.blue(); + } + + QColor avgColor( + 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; + } + + QString dominantChannel = findBiggestColorRange(rgbValues); + std::ranges::sort(rgbValues, [dominantChannel](const QColor& a, const QColor& 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; + + QList<QColor> leftHalf = rgbValues.mid(0, mid); + QList<QColor> rightHalf = rgbValues.mid(mid + 1); + + QList<QColor> result; + result.append(quantization(leftHalf, depth + 1)); + result.append(quantization(rightHalf, depth + 1)); + + return result; +} + +QString 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; + + for (const QColor& 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()); + } + + int rRange = rMax - rMin; + int gRange = gMax - gMin; + int bRange = bMax - bMin; + + int biggestRange = qMax(rRange, qMax(gRange, bRange)); + if (biggestRange == rRange) { + return "r"; + } else if (biggestRange == gRange) { + return "g"; + } else { + return "b"; + } +} + +QList<QColor> ColorQuantizer::colors() { return mColors; } + +void ColorQuantizer::setSource(const QUrl& source) { + if (mSource != source) { + mSource = source; + emit sourceChanged(); + quantizeAsync(); + } +} + +void ColorQuantizer::setDepth(qreal depth) { + if (mDepth != depth) { + mDepth = depth; + emit depthChanged(); + } +} + +void ColorQuantizer::setRescaleSize(int rescaleSize) { + if (mRescaleSize != rescaleSize) { + mRescaleSize = rescaleSize; + emit rescaleSizeChanged(); + } +} + +void ColorQuantizer::operationFinished(const QList<QColor>& result) { + mColors = result; + emit colorsChanged(); + isProcessing = false; +} + +void ColorQuantizer::quantizeAsync() { + if (isProcessing) return; + + 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); +} diff --git a/src/core/colorquantizer.hpp b/src/core/colorquantizer.hpp new file mode 100644 index 00000000..1c0435de --- /dev/null +++ b/src/core/colorquantizer.hpp @@ -0,0 +1,95 @@ +#pragma once + +#include <qlist.h> +#include <qmutex.h> +#include <qobject.h> +#include <qqmlintegration.h> +#include <qrunnable.h> +#include <qtmetamacros.h> +#include <qtypes.h> + +class ColorQuantizerOperation + : public QObject + , public QRunnable { + Q_OBJECT; + +public: + ColorQuantizerOperation(QUrl* source, qreal depth, qreal rescaleSize); + + void run() override; + +signals: + void done(QList<QColor> colors); + +private: + QList<QColor> mColors; + 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 +/// 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 { + Q_OBJECT; + /// 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); + + /// 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); + QML_ELEMENT; + +public: + QList<QColor> colors(); + [[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: + bool isProcessing = false; + QList<QColor> mColors; + QUrl mSource; + qreal mDepth = 0; + qreal mRescaleSize = 0; + + void quantizeAsync(); +}; 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", ] -----