File indexing completed on 2024-05-05 04:46:59

0001 /*
0002  *  Copyright 2020 Marco Martin <mart@kde.org>
0003  *
0004  *  This program is free software; you can redistribute it and/or modify
0005  *  it under the terms of the GNU General Public License as published by
0006  *  the Free Software Foundation; either version 2 of the License, or
0007  *  (at your option) any later version.
0008  *
0009  *  This program is distributed in the hope that it will be useful,
0010  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
0011  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0012  *  GNU General Public License for more details.
0013  *
0014  *  You should have received a copy of the GNU General Public License
0015  *  along with this program; if not, write to the Free Software
0016  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  2.010-1301, USA.
0017  */
0018 
0019 #include "imagecolors.h"
0020 #include "platformtheme.h"
0021 
0022 #include <QDebug>
0023 #include <QTimer>
0024 #include <QtConcurrent>
0025 
0026 #include <cmath>
0027 
0028 #define return_fallback(value)                                                                                                                                 \
0029     if (m_imageData.m_samples.size() == 0) {                                                                                                                   \
0030         return value;                                                                                                                                          \
0031     }
0032 
0033 #define return_fallback_finally(value, finally)                                                                                                                \
0034     if (m_imageData.m_samples.size() == 0) {                                                                                                                   \
0035         return value.isValid() ? value : static_cast<Maui::PlatformTheme *>(qmlAttachedPropertiesObject<Maui::PlatformTheme>(this, true))->finally();  \
0036     }
0037 
0038 ImageColors::ImageColors(QObject *parent)
0039     : QObject(parent)
0040 {
0041     m_imageSyncTimer = new QTimer(this);
0042     m_imageSyncTimer->setSingleShot(true);
0043     m_imageSyncTimer->setInterval(100);
0044     /* connect(m_imageSyncTimer, &QTimer::timeout, this, [this]() {
0045         generatePalette();
0046      });*/
0047 }
0048 
0049 ImageColors::~ImageColors()
0050 {
0051 }
0052 
0053 void ImageColors::setSource(const QVariant &source)
0054 {
0055      if (source.canConvert<QQuickItem *>()) {
0056         qDebug() << "can convert to item";
0057         setSourceItem(source.value<QQuickItem *>());
0058     } else if (source.canConvert<QImage>()) {
0059         qDebug() << "can convert to image";
0060 
0061         setSourceImage(source.value<QImage>());
0062     } else if (source.canConvert<QIcon>()) {
0063         qDebug() << "can convert to icon";
0064 
0065         setSourceImage(source.value<QIcon>().pixmap(128, 128).toImage());
0066     } else if (source.canConvert<QString>()) {
0067         qDebug() << "can convert to string";
0068         if(source.toString().isEmpty())
0069         {
0070             return;
0071         }
0072 
0073         if(source.toString().startsWith("qrc:"))
0074         {
0075             qDebug() << "SET IMAGE FROM QRC IMAGE COLORS" << source.toString();
0076             setSourceImage(QImage(source.toString().replace("qrc", "")));
0077         }else
0078         {
0079 
0080         setSourceImage(QIcon::fromTheme(source.toString()).pixmap(128, 128).toImage());
0081         }
0082     } else {
0083         return;
0084     }
0085 
0086     m_source = source;
0087     Q_EMIT sourceChanged();
0088 }
0089 
0090 QVariant ImageColors::source() const
0091 {
0092     return m_source;
0093 }
0094 
0095 void ImageColors::setSourceImage(const QImage &image)
0096 {
0097     if (m_window) {
0098         disconnect(m_window.data(), nullptr, this, nullptr);
0099     }
0100     if (m_sourceItem) {
0101         disconnect(m_sourceItem.data(), nullptr, this, nullptr);
0102     }
0103     if (m_grabResult) {
0104         disconnect(m_grabResult.data(), nullptr, this, nullptr);
0105         m_grabResult.clear();
0106     }
0107 
0108     m_sourceItem.clear();
0109 
0110     m_sourceImage = image;
0111     update();
0112 }
0113 
0114 QImage ImageColors::sourceImage() const
0115 {
0116     return m_sourceImage;
0117 }
0118 
0119 void ImageColors::setSourceItem(QQuickItem *source)
0120 {
0121     if (m_sourceItem == source) {
0122         return;
0123     }
0124 
0125     if (m_window) {
0126         disconnect(m_window.data(), nullptr, this, nullptr);
0127     }
0128     if (m_sourceItem) {
0129         disconnect(m_sourceItem, nullptr, this, nullptr);
0130     }
0131     m_sourceItem = source;
0132     update();
0133 
0134     if (m_sourceItem) {
0135         auto syncWindow = [this]() {
0136             if (m_window) {
0137                 disconnect(m_window.data(), nullptr, this, nullptr);
0138             }
0139             m_window = m_sourceItem->window();
0140             if (m_window) {
0141                 connect(m_window, &QWindow::visibleChanged, this, &ImageColors::update);
0142             }
0143         };
0144 
0145         connect(m_sourceItem, &QQuickItem::windowChanged, this, syncWindow);
0146         syncWindow();
0147     }
0148 }
0149 
0150 QQuickItem *ImageColors::sourceItem() const
0151 {
0152     return m_sourceItem;
0153 }
0154 
0155 void ImageColors::update()
0156 {
0157     if (m_futureImageData) {
0158         m_futureImageData->cancel();
0159         m_futureImageData->deleteLater();
0160     }
0161     auto runUpdate = [this]() {
0162         QFuture<ImageData> future = QtConcurrent::run([this]() {
0163             return generatePalette(m_sourceImage);
0164         });
0165         m_futureImageData = new QFutureWatcher<ImageData>(this);
0166         connect(m_futureImageData, &QFutureWatcher<ImageData>::finished, this, [this]() {
0167             if (!m_futureImageData) {
0168                 return;
0169             }
0170             m_imageData = m_futureImageData->future().result();
0171             m_futureImageData->deleteLater();
0172             m_futureImageData = nullptr;
0173 
0174             Q_EMIT paletteChanged();
0175         });
0176         m_futureImageData->setFuture(future);
0177     };
0178 
0179     if (!m_sourceItem || !m_window) {
0180         if (!m_sourceImage.isNull()) {
0181             runUpdate();
0182         }
0183         return;
0184     }
0185 
0186     if (m_grabResult) {
0187         disconnect(m_grabResult.data(), nullptr, this, nullptr);
0188         m_grabResult.clear();
0189     }
0190 
0191     m_grabResult = m_sourceItem->grabToImage(QSize(128, 128));
0192 
0193     if (m_grabResult) {
0194         connect(m_grabResult.data(), &QQuickItemGrabResult::ready, this, [this, runUpdate]() {
0195             m_sourceImage = m_grabResult->image();
0196             m_grabResult.clear();
0197             runUpdate();
0198         });
0199     }
0200 }
0201 
0202 inline int squareDistance(QRgb color1, QRgb color2)
0203 {
0204     // https://en.wikipedia.org/wiki/Color_difference
0205     // Using RGB distance for performance, as CIEDE2000 istoo complicated
0206     if (qRed(color1) - qRed(color2) < 128) {
0207         return 2 * pow(qRed(color1) - qRed(color2), 2) //
0208             + 4 * pow(qGreen(color1) - qGreen(color2), 2) //
0209             + 3 * pow(qBlue(color1) - qBlue(color2), 2);
0210     } else {
0211         return 3 * pow(qRed(color1) - qRed(color2), 2) //
0212             + 4 * pow(qGreen(color1) - qGreen(color2), 2) //
0213             + 2 * pow(qBlue(color1) - qBlue(color2), 2);
0214     }
0215 }
0216 
0217 void ImageColors::positionColor(QRgb rgb, QList<ImageData::colorStat> &clusters)
0218 {
0219     for (auto &stat : clusters) {
0220         if (squareDistance(rgb, stat.centroid) < s_minimumSquareDistance) {
0221             stat.colors.append(rgb);
0222             return;
0223         }
0224     }
0225 
0226     ImageData::colorStat stat;
0227     stat.colors.append(rgb);
0228     stat.centroid = rgb;
0229     clusters << stat;
0230 }
0231 
0232 ImageData ImageColors::generatePalette(const QImage &sourceImage)
0233 {
0234     ImageData imageData;
0235 
0236     if (sourceImage.isNull() || sourceImage.width() == 0) {
0237         return imageData;
0238     }
0239 
0240     imageData.m_clusters.clear();
0241     imageData.m_samples.clear();
0242 
0243     QColor sampleColor;
0244     int r = 0;
0245     int g = 0;
0246     int b = 0;
0247     int c = 0;
0248     for (int x = 0; x < sourceImage.width(); ++x) {
0249         for (int y = 0; y < sourceImage.height(); ++y) {
0250             sampleColor = sourceImage.pixelColor(x, y);
0251             if (sampleColor.alpha() == 0) {
0252                 continue;
0253             }
0254             QRgb rgb = sampleColor.rgb();
0255             c++;
0256             r += qRed(rgb);
0257             g += qGreen(rgb);
0258             b += qBlue(rgb);
0259             imageData.m_samples << rgb;
0260             positionColor(rgb, imageData.m_clusters);
0261         }
0262     }
0263 
0264     if (imageData.m_samples.isEmpty()) {
0265         return imageData;
0266     }
0267 
0268     imageData.m_average = QColor(r / c, g / c, b / c, 255);
0269 
0270     for (int iteration = 0; iteration < 5; ++iteration) {
0271         for (auto &stat : imageData.m_clusters) {
0272             r = 0;
0273             g = 0;
0274             b = 0;
0275             c = 0;
0276 
0277             for (auto color : std::as_const(stat.colors)) {
0278                 c++;
0279                 r += qRed(color);
0280                 g += qGreen(color);
0281                 b += qBlue(color);
0282             }
0283             r = r / c;
0284             g = g / c;
0285             b = b / c;
0286             stat.centroid = qRgb(r, g, b);
0287             stat.ratio = qreal(stat.colors.count()) / qreal(imageData.m_samples.count());
0288             stat.colors = QList<QRgb>({stat.centroid});
0289         }
0290 
0291         for (auto color : std::as_const(imageData.m_samples)) {
0292             positionColor(color, imageData.m_clusters);
0293         }
0294     }
0295 
0296     std::sort(imageData.m_clusters.begin(), imageData.m_clusters.end(), [](const ImageData::colorStat &a, const ImageData::colorStat &b) {
0297         return a.colors.size() > b.colors.size();
0298     });
0299 
0300     // compress blocks that became too similar
0301     auto sourceIt = imageData.m_clusters.end();
0302     QList<QList<ImageData::colorStat>::iterator> itemsToDelete;
0303     while (sourceIt != imageData.m_clusters.begin()) {
0304         sourceIt--;
0305         for (auto destIt = imageData.m_clusters.begin(); destIt != imageData.m_clusters.end() && destIt != sourceIt; destIt++) {
0306             if (squareDistance((*sourceIt).centroid, (*destIt).centroid) < s_minimumSquareDistance) {
0307                 const qreal ratio = (*sourceIt).ratio / (*destIt).ratio;
0308                 const int r = ratio * qreal(qRed((*sourceIt).centroid)) + (1 - ratio) * qreal(qRed((*destIt).centroid));
0309                 const int g = ratio * qreal(qGreen((*sourceIt).centroid)) + (1 - ratio) * qreal(qGreen((*destIt).centroid));
0310                 const int b = ratio * qreal(qBlue((*sourceIt).centroid)) + (1 - ratio) * qreal(qBlue((*destIt).centroid));
0311                 (*destIt).ratio += (*sourceIt).ratio;
0312                 (*destIt).centroid = qRgb(r, g, b);
0313                 itemsToDelete << sourceIt;
0314                 break;
0315             }
0316         }
0317     }
0318     for (const auto &i : std::as_const(itemsToDelete)) {
0319         imageData.m_clusters.erase(i);
0320     }
0321 
0322     imageData.m_highlight = QColor();
0323     imageData.m_dominant = QColor(imageData.m_clusters.first().centroid);
0324     imageData.m_closestToBlack = Qt::white;
0325     imageData.m_closestToWhite = Qt::black;
0326 
0327     imageData.m_palette.clear();
0328 
0329     bool first = true;
0330 
0331     for (const auto &stat : std::as_const(imageData.m_clusters)) {
0332         QVariantMap entry;
0333         const QColor color(stat.centroid);
0334         entry[QStringLiteral("color")] = color;
0335         entry[QStringLiteral("ratio")] = stat.ratio;
0336 
0337         QColor contrast = QColor(255 - color.red(), 255 - color.green(), 255 - color.blue());
0338         contrast.setHsl(contrast.hslHue(), //
0339                         contrast.hslSaturation(), //
0340                         128 + (128 - contrast.lightness()));
0341         QColor tempContrast;
0342         int minimumDistance = 4681800; // max distance: 4*3*2*3*255*255
0343         for (const auto &stat : std::as_const(imageData.m_clusters)) {
0344             const int distance = squareDistance(contrast.rgb(), stat.centroid);
0345 
0346             if (distance < minimumDistance) {
0347                 tempContrast = QColor(stat.centroid);
0348                 minimumDistance = distance;
0349             }
0350         }
0351 
0352         if (imageData.m_clusters.size() <= 3) {
0353             if (qGray(imageData.m_dominant.rgb()) < 120) {
0354                 contrast = QColor(230, 230, 230);
0355             } else {
0356                 contrast = QColor(20, 20, 20);
0357             }
0358             // TODO: replace m_clusters.size() > 3 with entropy calculation
0359         } else if (squareDistance(contrast.rgb(), tempContrast.rgb()) < s_minimumSquareDistance * 1.5) {
0360             contrast = tempContrast;
0361         } else {
0362             contrast = tempContrast;
0363             contrast.setHsl(contrast.hslHue(),
0364                             contrast.hslSaturation(),
0365                             contrast.lightness() > 128 ? qMin(contrast.lightness() + 20, 255) : qMax(0, contrast.lightness() - 20));
0366         }
0367 
0368         entry[QStringLiteral("contrastColor")] = contrast;
0369 
0370         if (first) {
0371             imageData.m_dominantContrast = contrast;
0372             imageData.m_dominant = color;
0373         }
0374         first = false;
0375 
0376         if (!imageData.m_highlight.isValid() || ColorUtils::chroma(color) > ColorUtils::chroma(imageData.m_highlight)) {
0377             imageData.m_highlight = color;
0378         }
0379 
0380         if (qGray(color.rgb()) > qGray(imageData.m_closestToWhite.rgb())) {
0381             imageData.m_closestToWhite = color;
0382         }
0383         if (qGray(color.rgb()) < qGray(imageData.m_closestToBlack.rgb())) {
0384             imageData.m_closestToBlack = color;
0385         }
0386         imageData.m_palette << entry;
0387     }
0388 
0389     return imageData;
0390 }
0391 
0392 QVariantList ImageColors::palette() const
0393 {
0394     if (m_futureImageData) {
0395         qWarning() << m_futureImageData->future().isFinished();
0396     }
0397     return_fallback(m_fallbackPalette) return m_imageData.m_palette;
0398 }
0399 
0400 ColorUtils::Brightness ImageColors::paletteBrightness() const
0401 {
0402     /* clang-format off */
0403     return_fallback(m_fallbackPaletteBrightness)
0404 
0405     return qGray(m_imageData.m_dominant.rgb()) < 128 ? ColorUtils::Dark : ColorUtils::Light;
0406     /* clang-format on */
0407 }
0408 
0409 QColor ImageColors::average() const
0410 {
0411     /* clang-format off */
0412     return_fallback_finally(m_fallbackAverage, linkBackgroundColor)
0413 
0414     return m_imageData.m_average;
0415     /* clang-format on */
0416 }
0417 
0418 QColor ImageColors::dominant() const
0419 {
0420     /* clang-format off */
0421     return_fallback_finally(m_fallbackDominant, linkBackgroundColor)
0422 
0423     return m_imageData.m_dominant;
0424     /* clang-format on */
0425 }
0426 
0427 QColor ImageColors::dominantContrast() const
0428 {
0429     /* clang-format off */
0430     return_fallback_finally(m_fallbackDominantContrasting, linkBackgroundColor)
0431 
0432     return m_imageData.m_dominantContrast;
0433     /* clang-format on */
0434 }
0435 
0436 QColor ImageColors::foreground() const
0437 {
0438     /* clang-format off */
0439     return_fallback_finally(m_fallbackForeground, textColor)
0440 
0441     if (paletteBrightness() == ColorUtils::Dark)
0442     {
0443         if (qGray(m_imageData.m_closestToWhite.rgb()) < 200) {
0444             return QColor(230, 230, 230);
0445         }
0446         return m_imageData.m_closestToWhite;
0447     } else {
0448         if (qGray(m_imageData.m_closestToBlack.rgb()) > 80) {
0449             return QColor(20, 20, 20);
0450         }
0451         return m_imageData.m_closestToBlack;
0452     }
0453     /* clang-format on */
0454 }
0455 
0456 QColor ImageColors::background() const
0457 {
0458     /* clang-format off */
0459     return_fallback_finally(m_fallbackBackground, backgroundColor)
0460 
0461     if (paletteBrightness() == ColorUtils::Dark) {
0462         if (qGray(m_imageData.m_closestToBlack.rgb()) > 80) {
0463             return QColor(20, 20, 20);
0464         }
0465         return m_imageData.m_closestToBlack;
0466     } else {
0467         if (qGray(m_imageData.m_closestToWhite.rgb()) < 200) {
0468             return QColor(230, 230, 230);
0469         }
0470         return m_imageData.m_closestToWhite;
0471     }
0472     /* clang-format on */
0473 }
0474 
0475 QColor ImageColors::highlight() const
0476 {
0477     /* clang-format off */
0478     return_fallback_finally(m_fallbackHighlight, linkColor)
0479 
0480     return m_imageData.m_highlight;
0481     /* clang-format on */
0482 }
0483 
0484 QColor ImageColors::closestToWhite() const
0485 {
0486     /* clang-format off */
0487     return_fallback(Qt::white)
0488     if (qGray(m_imageData.m_closestToWhite.rgb()) < 200) {
0489         return QColor(230, 230, 230);
0490     }
0491     /* clang-format on */
0492 
0493     return m_imageData.m_closestToWhite;
0494 }
0495 
0496 QColor ImageColors::closestToBlack() const
0497 {
0498     /* clang-format off */
0499     return_fallback(Qt::black)
0500     if (qGray(m_imageData.m_closestToBlack.rgb()) > 80) {
0501         return QColor(20, 20, 20);
0502     }
0503     /* clang-format on */
0504     return m_imageData.m_closestToBlack;
0505 }
0506 
0507 #include "moc_imagecolors.cpp"