diff --git a/src/imagecolors.cpp b/src/imagecolors.cpp index 9807daaa..fbf1aad2 100644 --- a/src/imagecolors.cpp +++ b/src/imagecolors.cpp @@ -1,449 +1,467 @@ /* * Copyright 2020 Marco Martin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 2.010-1301, USA. */ #include "imagecolors.h" +#include "platformtheme.h" #include #include #include #include #include +#define return_fallback(value) if (m_futureImageData == nullptr || !m_futureImageData->future().isFinished()) {\ + return value;\ +} + +#define return_fallback_finally(value, finally) if (m_futureImageData == nullptr || !m_futureImageData->future().isFinished()) {\ + return value.isValid() ? value : static_cast(qmlAttachedPropertiesObject(this, true))->finally();\ +} ImageColors::ImageColors(QObject *parent) : QObject(parent) { m_imageSyncTimer = new QTimer(this); m_imageSyncTimer->setSingleShot(true); m_imageSyncTimer->setInterval(100); /* connect(m_imageSyncTimer, &QTimer::timeout, this, [this]() { generatePalette(); });*/ } ImageColors::~ImageColors() {} void ImageColors::setSource(const QVariant &source) { if (source.canConvert()) { setSourceItem(source.value()); } else if (source.canConvert()) { setSourceImage(source.value()); } else if (source.canConvert()) { setSourceImage(source.value().pixmap(128, 128).toImage()); } else if (source.canConvert()) { setSourceImage(QIcon::fromTheme(source.toString()).pixmap(128, 128).toImage()); } else { return; } m_source = source; emit sourceChanged(); } QVariant ImageColors::source() const { return m_source; } void ImageColors::setSourceImage(const QImage &image) { if (m_window) { disconnect(m_window.data(), nullptr, this, nullptr); } if (m_sourceItem) { disconnect(m_sourceItem.data(), nullptr, this, nullptr); } if (m_grabResult) { disconnect(m_grabResult.data(), nullptr, this, nullptr); m_grabResult.clear(); } m_sourceItem.clear(); m_sourceImage = image; update(); } QImage ImageColors::sourceImage() const { return m_sourceImage; } void ImageColors::setSourceItem(QQuickItem *source) { if (m_sourceItem == source) { return; } if (m_window) { disconnect(m_window.data(), nullptr, this, nullptr); } if (m_sourceItem) { disconnect(m_sourceItem, nullptr, this, nullptr); } m_sourceItem = source; update(); if (m_sourceItem) { auto syncWindow = [this] () { if (m_window) { disconnect(m_window.data(), nullptr, this, nullptr); } m_window = m_sourceItem->window(); if (m_window) { connect(m_window, &QWindow::visibleChanged, this, &ImageColors::update); } }; connect(m_sourceItem, &QQuickItem::windowChanged, this, syncWindow); syncWindow(); } } QQuickItem *ImageColors::sourceItem() const { return m_sourceItem; } void ImageColors::update() { if (m_futureImageData) { m_futureImageData->cancel(); m_futureImageData->deleteLater(); } auto runUpdate = [this]() { QFuture future = QtConcurrent::run([this](){return generatePalette(m_sourceImage);}); m_futureImageData = new QFutureWatcher(this); connect(m_futureImageData, &QFutureWatcher::finished, this, [this] () { if (!m_futureImageData) { return; } m_imageData = m_futureImageData->future().result(); m_futureImageData->deleteLater(); m_futureImageData = nullptr; emit paletteChanged(); }); m_futureImageData->setFuture(future); }; if (!m_sourceItem || !m_window) { if (!m_sourceImage.isNull()) { runUpdate(); } return; } if (m_grabResult) { disconnect(m_grabResult.data(), nullptr, this, nullptr); m_grabResult.clear(); } m_grabResult = m_sourceItem->grabToImage(QSize(128, 128)); if (m_grabResult) { connect(m_grabResult.data(), &QQuickItemGrabResult::ready, this, [this, runUpdate]() { m_sourceImage = m_grabResult->image(); m_grabResult.clear(); runUpdate(); }); } } inline int squareDistance(QRgb color1, QRgb color2) { // https://en.wikipedia.org/wiki/Color_difference // Using RGB distance for performance, as CIEDE2000 istoo complicated if (qRed(color1) - qRed(color2) < 128) { return 2 * pow(qRed(color1) - qRed(color2), 2) + 4 * pow(qGreen(color1) - qGreen(color2), 2) + 3 * pow(qBlue(color1) - qBlue(color2), 2); } else { return 3 * pow(qRed(color1) - qRed(color2), 2) + 4 * pow(qGreen(color1) - qGreen(color2), 2) + 2 * pow(qBlue(color1) - qBlue(color2), 2); } } void ImageColors::positionColor(QRgb rgb, QList &clusters) { for (auto &stat : clusters) { if (squareDistance(rgb, stat.centroid) < s_minimumSquareDistance) { stat.colors.append(rgb); return; } } ImageData::colorStat stat; stat.colors.append(rgb); stat.centroid = rgb; clusters << stat; } ImageData ImageColors::generatePalette(const QImage &sourceImage) { ImageData imageData; if (sourceImage.isNull() || sourceImage.width() == 0) { return imageData; } imageData.m_clusters.clear(); imageData.m_samples.clear(); QColor sampleColor; int r = 0; int g = 0; int b = 0; int c = 0; for (int x = 0; x < sourceImage.width(); ++x) { for (int y = 0; y < sourceImage.height(); ++y) { sampleColor = sourceImage.pixelColor(x, y); if (sampleColor.alpha() == 0) { continue; } QRgb rgb = sampleColor.rgb(); c++; r += qRed(rgb); g += qGreen(rgb); b += qBlue(rgb); imageData.m_samples << rgb; positionColor(rgb, imageData.m_clusters); } } if (imageData.m_samples.isEmpty()) { return imageData; } imageData.m_average = QColor(r/c, g/c, b/c, 255); for (int iteration = 0; iteration < 5; ++iteration) { for (auto &stat : imageData.m_clusters) { r = 0; g = 0; b = 0; c = 0; for (auto color : stat.colors) { c++; r += qRed(color); g += qGreen(color); b += qBlue(color); } r = r / c; g = g / c; b = b / c; stat.centroid = qRgb(r, g, b); stat.ratio = qreal(stat.colors.count()) / qreal(imageData.m_samples.count()); stat.colors = QList({stat.centroid}); } for (auto color : imageData.m_samples) { positionColor(color, imageData.m_clusters); } } std::sort(imageData.m_clusters.begin(), imageData.m_clusters.end(), [](const ImageData::colorStat &a, const ImageData::colorStat &b) { return a.colors.size() > b.colors.size(); }); // compress blocks that became too similar auto sourceIt = imageData.m_clusters.end(); QList::iterator> itemsToDelete; while (sourceIt != imageData.m_clusters.begin()) { sourceIt--; for (auto destIt = imageData.m_clusters.begin(); destIt != imageData.m_clusters.end() && destIt != sourceIt; destIt++) { if (squareDistance((*sourceIt).centroid, (*destIt).centroid) < s_minimumSquareDistance) { const qreal ratio = (*sourceIt).ratio / (*destIt).ratio; const int r = ratio * qreal(qRed((*sourceIt).centroid)) + (1 - ratio) * qreal(qRed((*destIt).centroid)); const int g = ratio * qreal(qGreen((*sourceIt).centroid)) + (1 - ratio) * qreal(qGreen((*destIt).centroid)); const int b = ratio * qreal(qBlue((*sourceIt).centroid)) + (1 - ratio) * qreal(qBlue((*destIt).centroid)); (*destIt).ratio += (*sourceIt).ratio; (*destIt).centroid = qRgb(r, g, b); itemsToDelete << sourceIt; break; } } } for (const auto &i : itemsToDelete) { imageData.m_clusters.erase(i); } imageData.m_highlight = QColor(); imageData.m_dominant = QColor(imageData.m_clusters.first().centroid); imageData.m_closestToBlack = Qt::white; imageData.m_closestToWhite = Qt::black; imageData.m_palette.clear(); bool first = true; for (const auto &stat : imageData.m_clusters) { QVariantMap entry; const QColor color(stat.centroid); entry[QStringLiteral("color")] = color; entry[QStringLiteral("ratio")] = stat.ratio; QColor contrast = QColor(255 - color.red(), 255 - color.green(), 255 - color.blue()); contrast.setHsl(contrast.hslHue(), contrast.hslSaturation(), 128 + (128 - contrast.lightness())); QColor tempContrast; int minimumDistance = 4681800; //max distance: 4*3*2*3*255*255 for (const auto &stat : imageData.m_clusters) { const int distance = squareDistance(contrast.rgb(), stat.centroid); if (distance < minimumDistance) { tempContrast = QColor(stat.centroid); minimumDistance = distance; } } if (imageData.m_clusters.size() <= 3) { if (qGray(imageData.m_dominant.rgb()) < 120) { contrast = QColor(230, 230, 230); } else { contrast = QColor(20, 20, 20); } // TODO: replace m_clusters.size() > 3 with entropy calculation } else if (squareDistance(contrast.rgb(), tempContrast.rgb()) < s_minimumSquareDistance * 1.5) { contrast = tempContrast; } else { contrast = tempContrast; contrast.setHsl(contrast.hslHue(), contrast.hslSaturation(), contrast.lightness() > 128 ? qMin(contrast.lightness() + 20, 255) : qMax(0, contrast.lightness() - 20)); } entry[QStringLiteral("contrastColor")] = contrast; if (first) { imageData.m_dominantContrast = contrast; imageData.m_dominant = color; } first = false; if (!imageData.m_highlight.isValid() || ColorUtils::chroma(color) > ColorUtils::chroma(imageData.m_highlight)) { imageData.m_highlight = color; } if (qGray(color.rgb()) > qGray(imageData.m_closestToWhite.rgb())) { imageData.m_closestToWhite = color; } if (qGray(color.rgb()) < qGray(imageData.m_closestToBlack.rgb())) { imageData.m_closestToBlack = color; } imageData.m_palette << entry; } return imageData; } QVariantList ImageColors::palette() const { + return_fallback(m_fallbackPalette) return m_imageData.m_palette; } ColorUtils::Brightness ImageColors::paletteBrightness() const { + return_fallback(m_fallbackPaletteBrightness) return qGray(m_imageData.m_dominant.rgb()) < 128 ? ColorUtils::Dark : ColorUtils::Light; } QColor ImageColors::average() const { + return_fallback_finally(m_fallbackAverage, linkBackgroundColor) return m_imageData.m_average; } QColor ImageColors::dominant() const { + return_fallback_finally(m_fallbackAverage, linkBackgroundColor) return m_imageData.m_dominant; } QColor ImageColors::dominantContrast() const { + return_fallback_finally(m_fallbackAverage, linkBackgroundColor) return m_imageData.m_dominantContrast; } QColor ImageColors::foreground() const { + return_fallback_finally(m_fallbackAverage, textColor) if (paletteBrightness() == ColorUtils::Dark) { if (qGray(m_imageData.m_closestToWhite.rgb()) < 200) { return QColor(230, 230, 230); } return m_imageData.m_closestToWhite; } else { if (qGray(m_imageData.m_closestToBlack.rgb()) > 80) { return QColor(20, 20, 20); } return m_imageData.m_closestToBlack; } } QColor ImageColors::background() const { + return_fallback_finally(m_fallbackAverage, backgroundColor) if (paletteBrightness() == ColorUtils::Dark) { if (qGray(m_imageData.m_closestToBlack.rgb()) > 80) { return QColor(20, 20, 20); } return m_imageData.m_closestToBlack; } else { if (qGray(m_imageData.m_closestToWhite.rgb()) < 200) { return QColor(230, 230, 230); } return m_imageData.m_closestToWhite; } } QColor ImageColors::highlight() const { + return_fallback_finally(m_fallbackAverage, linkColor) return m_imageData.m_highlight; } QColor ImageColors::closestToWhite() const { + return_fallback(Qt::white) if (qGray(m_imageData.m_closestToWhite.rgb()) < 200) { return QColor(230, 230, 230); } return m_imageData.m_closestToWhite; } QColor ImageColors::closestToBlack() const { + return_fallback(Qt::black) if (qGray(m_imageData.m_closestToBlack.rgb()) > 80) { return QColor(20, 20, 20); } return m_imageData.m_closestToBlack; } #include "moc_imagecolors.cpp" diff --git a/src/imagecolors.h b/src/imagecolors.h index 137956cb..bf74ca5a 100644 --- a/src/imagecolors.h +++ b/src/imagecolors.h @@ -1,212 +1,277 @@ /* * Copyright 2020 Marco Martin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 2.010-1301, USA. */ #pragma once #include "colorutils.h" #include #include #include #include #include #include #include #include class QTimer; struct ImageData { struct colorStat { QList colors; QRgb centroid = 0; qreal ratio = 0; }; struct colorSet { QColor average; QColor text; QColor background; QColor highlight; }; QList m_samples; QList m_clusters; QVariantList m_palette; bool m_darkPalette = true; QColor m_dominant; QColor m_dominantContrast; QColor m_average; QColor m_highlight; QColor m_closestToBlack; QColor m_closestToWhite; }; class ImageColors : public QObject { Q_OBJECT /** * The source from which colors should be extracted from. * * `source` can be one of the following: * * Item * * QImage * * QIcon * * Icon name * * Note that an Item's color palette will only be extracted once unless you * call `update()`, regardless of how the item hanges. */ Q_PROPERTY(QVariant source READ source WRITE setSource NOTIFY sourceChanged) /** * A list of colors and related information about then. * * Each list item has the following properties: * * `color`: The color of the list item. * * `ratio`: How dominant the color is in the source image. * * `contrastingColor`: The color from the source image that's closest to the inverse of `color`. * * The list is sorted by `ratio`; the first element is the most * dominant color in the source image and the last element is the * least dominant color of the image. * * \note K-means clustering is used to extract these colors; see https://en.wikipedia.org/wiki/K-means_clustering. */ Q_PROPERTY(QVariantList palette READ palette NOTIFY paletteChanged) /** * Information whether the palette is towards a light or dark color * scheme, possible values are: * * ColorUtils.Light * * ColorUtils.Dark */ Q_PROPERTY(ColorUtils::Brightness paletteBrightness READ paletteBrightness NOTIFY paletteChanged) /** * The average color of the source image. */ Q_PROPERTY(QColor average READ average NOTIFY paletteChanged) /** * The dominant color of the source image. * * The dominant color of the image is the color of the largest * cluster in the image. * * \sa https://en.wikipedia.org/wiki/K-means_clustering */ Q_PROPERTY(QColor dominant READ dominant NOTIFY paletteChanged) /** * Suggested "contrasting" color to the dominant one. It's the color in the palette nearest to the negative of the dominant */ Q_PROPERTY(QColor dominantContrast READ dominantContrast NOTIFY paletteChanged) /** * An accent color extracted from the source image. * * The accent color is the color cluster with the highest CIELAB * chroma in the source image. * * \sa https://en.wikipedia.org/wiki/Colorfulness#Chroma */ Q_PROPERTY(QColor highlight READ highlight NOTIFY paletteChanged) /** * A color suitable for rendering text and other foreground * over the source image. * * On dark items, this will be the color closest to white in * the image if it's light enough, or a bright gray otherwise. * On light items, this will be the color closest to black in * the image if it's dark enough, or a dark gray otherwise. */ Q_PROPERTY(QColor foreground READ foreground NOTIFY paletteChanged) /** * A color suitable for rendering a background behind the * source image. * * On dark items, this will be the color closest to black in the * image if it's dark enough, or a dark gray otherwise. * On light items, this will be the color closest to white * in the image if it's light enough, or a bright gray otherwise. */ Q_PROPERTY(QColor background READ background NOTIFY paletteChanged) /** * The lightest color of the source image. */ Q_PROPERTY(QColor closestToWhite READ closestToWhite NOTIFY paletteChanged) /** * The darkest color of the source image. */ Q_PROPERTY(QColor closestToBlack READ closestToBlack NOTIFY paletteChanged) + /** + * The value to return when palette is not available, e.g. when + * ImageColors is still computing it or the source is invalid. + */ + Q_PROPERTY(QVariantList fallbackPalette MEMBER m_fallbackPalette NOTIFY fallbackPaletteChanged) + + /** + * The value to return when paletteBrightness is not available, e.g. when + * ImageColors is still computing it or the source is invalid. + */ + Q_PROPERTY(ColorUtils::Brightness fallbackPaletteBrightness MEMBER m_fallbackPaletteBrightness NOTIFY fallbackPaletteBrightnessChanged) + + /** + * The value to return when average is not available, e.g. when + * ImageColors is still computing it or the source is invalid. + */ + Q_PROPERTY(QColor fallbackAverage MEMBER m_fallbackAverage NOTIFY fallbackAverageChanged) + + /** + * The value to return when dominant is not available, e.g. when + * ImageColors is still computing it or the source is invalid. + */ + Q_PROPERTY(QColor fallbackDominant MEMBER m_fallbackDominant NOTIFY fallbackDominantChanged) + + /** + * The value to return when dominantContrasting is not available, e.g. when + * ImageColors is still computing it or the source is invalid. + */ + Q_PROPERTY(QColor fallbackDominantContrasting MEMBER m_fallbackDominantContrasting NOTIFY fallbackDominantContrastingChanged) + + /** + * The value to return when highlight is not available, e.g. when + * ImageColors is still computing it or the source is invalid. + */ + Q_PROPERTY(QColor fallbackHighlight MEMBER m_fallbackHighlight NOTIFY fallbackHighlightChanged) + + /** + * The value to return when foreground is not available, e.g. when + * ImageColors is still computing it or the source is invalid. + */ + Q_PROPERTY(QColor fallbackForeground MEMBER m_fallbackForeground NOTIFY fallbackForegroundChanged) + + /** + * The value to return when background is not available, e.g. when + * ImageColors is still computing it or the source is invalid. + */ + Q_PROPERTY(QColor fallbackBackground MEMBER m_fallbackBackground NOTIFY fallbackBackgroundChanged) + public: explicit ImageColors(QObject* parent = nullptr); ~ImageColors(); void setSource(const QVariant &source); QVariant source() const; void setSourceImage(const QImage &image); QImage sourceImage() const; void setSourceItem(QQuickItem *source); QQuickItem *sourceItem() const; Q_INVOKABLE void update(); QVariantList palette() const; ColorUtils::Brightness paletteBrightness() const; QColor average() const; QColor dominant() const; QColor dominantContrast() const; QColor highlight() const; QColor foreground() const; QColor background() const; QColor closestToWhite() const; QColor closestToBlack() const; Q_SIGNALS: void sourceChanged(); void paletteChanged(); + void fallbackPaletteChanged(); + void fallbackPaletteBrightnessChanged(); + void fallbackAverageChanged(); + void fallbackDominantChanged(); + void fallbackDominantContrastingChanged(); + void fallbackHighlightChanged(); + void fallbackForegroundChanged(); + void fallbackBackgroundChanged(); private: static inline void positionColor(QRgb rgb, QList &clusters); static ImageData generatePalette(const QImage &sourceImage); // Arbitrary number that seems to work well static const int s_minimumSquareDistance = 32000; QPointer m_window; QVariant m_source; QPointer m_sourceItem; QSharedPointer m_grabResult; QImage m_sourceImage; QTimer *m_imageSyncTimer; QFutureWatcher *m_futureImageData = nullptr; ImageData m_imageData; + + QVariantList m_fallbackPalette; + ColorUtils::Brightness m_fallbackPaletteBrightness; + QColor m_fallbackAverage; + QColor m_fallbackDominant; + QColor m_fallbackDominantContrasting; + QColor m_fallbackHighlight; + QColor m_fallbackForeground; + QColor m_fallbackBackground; };