File indexing completed on 2024-04-28 15:27:42

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 <QFutureWatcher>
0024 #include <QGuiApplication>
0025 #include <QTimer>
0026 #include <QtConcurrentRun>
0027 
0028 #include "loggingcategory.h"
0029 #include <cmath>
0030 #include <vector>
0031 
0032 #include "config-OpenMP.h"
0033 #if HAVE_OpenMP
0034 #include <omp.h>
0035 #endif
0036 
0037 #define return_fallback(value)                                                                                                                                 \
0038     if (m_imageData.m_samples.size() == 0) {                                                                                                                   \
0039         return value;                                                                                                                                          \
0040     }
0041 
0042 #define return_fallback_finally(value, finally)                                                                                                                \
0043     if (m_imageData.m_samples.size() == 0) {                                                                                                                   \
0044         return value.isValid() ? value : static_cast<Kirigami::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::PlatformTheme>(this, true))->finally();  \
0045     }
0046 
0047 ImageColors::ImageColors(QObject *parent)
0048     : QObject(parent)
0049 {
0050     m_imageSyncTimer = new QTimer(this);
0051     m_imageSyncTimer->setSingleShot(true);
0052     m_imageSyncTimer->setInterval(100);
0053     /* connect(m_imageSyncTimer, &QTimer::timeout, this, [this]() {
0054         generatePalette();
0055      });*/
0056 }
0057 
0058 ImageColors::~ImageColors()
0059 {
0060 }
0061 
0062 void ImageColors::setSource(const QVariant &source)
0063 {
0064     if (m_futureSourceImageData) {
0065         m_futureSourceImageData->cancel();
0066         m_futureSourceImageData->deleteLater();
0067         m_futureSourceImageData = nullptr;
0068     }
0069 
0070     if (source.canConvert<QQuickItem *>()) {
0071         setSourceItem(source.value<QQuickItem *>());
0072     } else if (source.canConvert<QImage>()) {
0073         setSourceImage(source.value<QImage>());
0074     } else if (source.canConvert<QIcon>()) {
0075         setSourceImage(source.value<QIcon>().pixmap(128, 128).toImage());
0076     } else if (source.canConvert<QString>()) {
0077         const QString sourceString = source.toString();
0078 
0079         if (QIcon::hasThemeIcon(sourceString)) {
0080             setSourceImage(QIcon::fromTheme(sourceString).pixmap(128, 128).toImage());
0081         } else {
0082             QFuture<QImage> future = QtConcurrent::run([sourceString]() {
0083                 if (auto url = QUrl(sourceString); url.isLocalFile()) {
0084                     return QImage(url.toLocalFile());
0085                 }
0086                 return QImage(sourceString);
0087             });
0088             m_futureSourceImageData = new QFutureWatcher<QImage>(this);
0089             connect(m_futureSourceImageData, &QFutureWatcher<QImage>::finished, this, [this, source]() {
0090                 const QImage image = m_futureSourceImageData->future().result();
0091                 m_futureSourceImageData->deleteLater();
0092                 m_futureSourceImageData = nullptr;
0093                 setSourceImage(image);
0094                 m_source = source;
0095                 Q_EMIT sourceChanged();
0096             });
0097             m_futureSourceImageData->setFuture(future);
0098             return;
0099         }
0100     } else {
0101         return;
0102     }
0103 
0104     m_source = source;
0105     Q_EMIT sourceChanged();
0106 }
0107 
0108 QVariant ImageColors::source() const
0109 {
0110     return m_source;
0111 }
0112 
0113 void ImageColors::setSourceImage(const QImage &image)
0114 {
0115     if (m_window) {
0116         disconnect(m_window.data(), nullptr, this, nullptr);
0117     }
0118     if (m_sourceItem) {
0119         disconnect(m_sourceItem.data(), nullptr, this, nullptr);
0120     }
0121     if (m_grabResult) {
0122         disconnect(m_grabResult.data(), nullptr, this, nullptr);
0123         m_grabResult.clear();
0124     }
0125 
0126     m_sourceItem.clear();
0127 
0128     m_sourceImage = image;
0129     update();
0130 }
0131 
0132 QImage ImageColors::sourceImage() const
0133 {
0134     return m_sourceImage;
0135 }
0136 
0137 void ImageColors::setSourceItem(QQuickItem *source)
0138 {
0139     if (m_sourceItem == source) {
0140         return;
0141     }
0142 
0143     if (m_window) {
0144         disconnect(m_window.data(), nullptr, this, nullptr);
0145     }
0146     if (m_sourceItem) {
0147         disconnect(m_sourceItem, nullptr, this, nullptr);
0148     }
0149     m_sourceItem = source;
0150     update();
0151 
0152     if (m_sourceItem) {
0153         auto syncWindow = [this]() {
0154             if (m_window) {
0155                 disconnect(m_window.data(), nullptr, this, nullptr);
0156             }
0157             m_window = m_sourceItem->window();
0158             if (m_window) {
0159                 connect(m_window, &QWindow::visibleChanged, this, &ImageColors::update);
0160             }
0161         };
0162 
0163         connect(m_sourceItem, &QQuickItem::windowChanged, this, syncWindow);
0164         syncWindow();
0165     }
0166 }
0167 
0168 QQuickItem *ImageColors::sourceItem() const
0169 {
0170     return m_sourceItem;
0171 }
0172 
0173 void ImageColors::update()
0174 {
0175     if (m_futureImageData) {
0176         m_futureImageData->cancel();
0177         m_futureImageData->deleteLater();
0178         m_futureImageData = nullptr;
0179     }
0180     auto runUpdate = [this]() {
0181         QFuture<ImageData> future = QtConcurrent::run([this]() {
0182             return generatePalette(m_sourceImage);
0183         });
0184         m_futureImageData = new QFutureWatcher<ImageData>(this);
0185         connect(m_futureImageData, &QFutureWatcher<ImageData>::finished, this, [this]() {
0186             if (!m_futureImageData) {
0187                 return;
0188             }
0189             m_imageData = m_futureImageData->future().result();
0190             m_futureImageData->deleteLater();
0191             m_futureImageData = nullptr;
0192 
0193             Q_EMIT paletteChanged();
0194         });
0195         m_futureImageData->setFuture(future);
0196     };
0197 
0198     if (!m_sourceItem) {
0199         if (!m_sourceImage.isNull()) {
0200             runUpdate();
0201         } else {
0202             m_imageData = {};
0203             Q_EMIT paletteChanged();
0204         }
0205         return;
0206     }
0207 
0208     if (m_grabResult) {
0209         disconnect(m_grabResult.data(), nullptr, this, nullptr);
0210         m_grabResult.clear();
0211     }
0212 
0213     m_grabResult = m_sourceItem->grabToImage(QSize(128, 128));
0214 
0215     if (m_grabResult) {
0216         connect(m_grabResult.data(), &QQuickItemGrabResult::ready, this, [this, runUpdate]() {
0217             m_sourceImage = m_grabResult->image();
0218             m_grabResult.clear();
0219             runUpdate();
0220         });
0221     }
0222 }
0223 
0224 inline int squareDistance(QRgb color1, QRgb color2)
0225 {
0226     // https://en.wikipedia.org/wiki/Color_difference
0227     // Using RGB distance for performance, as CIEDE2000 istoo complicated
0228     if (qRed(color1) - qRed(color2) < 128) {
0229         return 2 * pow(qRed(color1) - qRed(color2), 2) //
0230             + 4 * pow(qGreen(color1) - qGreen(color2), 2) //
0231             + 3 * pow(qBlue(color1) - qBlue(color2), 2);
0232     } else {
0233         return 3 * pow(qRed(color1) - qRed(color2), 2) //
0234             + 4 * pow(qGreen(color1) - qGreen(color2), 2) //
0235             + 2 * pow(qBlue(color1) - qBlue(color2), 2);
0236     }
0237 }
0238 
0239 void ImageColors::positionColor(QRgb rgb, QList<ImageData::colorStat> &clusters)
0240 {
0241     for (auto &stat : clusters) {
0242         if (squareDistance(rgb, stat.centroid) < s_minimumSquareDistance) {
0243             stat.colors.append(rgb);
0244             return;
0245         }
0246     }
0247 
0248     ImageData::colorStat stat;
0249     stat.colors.append(rgb);
0250     stat.centroid = rgb;
0251     clusters << stat;
0252 }
0253 
0254 ImageData ImageColors::generatePalette(const QImage &sourceImage) const
0255 {
0256     ImageData imageData;
0257 
0258     if (sourceImage.isNull() || sourceImage.width() == 0) {
0259         return imageData;
0260     }
0261 
0262     imageData.m_clusters.clear();
0263     imageData.m_samples.clear();
0264 
0265 #if HAVE_OpenMP
0266     static const int numCore = std::min(8, omp_get_num_procs());
0267     omp_set_num_threads(numCore);
0268     std::vector<decltype(imageData.m_samples)> tempSamples(numCore, decltype(imageData.m_samples){});
0269 #endif
0270     int r = 0;
0271     int g = 0;
0272     int b = 0;
0273     int c = 0;
0274 
0275 #pragma omp parallel for collapse(2) reduction(+ : r) reduction(+ : g) reduction(+ : b) reduction(+ : c)
0276     for (int x = 0; x < sourceImage.width(); ++x) {
0277         for (int y = 0; y < sourceImage.height(); ++y) {
0278             const QColor sampleColor = sourceImage.pixelColor(x, y);
0279             if (sampleColor.alpha() == 0) {
0280                 continue;
0281             }
0282             if (ColorUtils::chroma(sampleColor) < 20) {
0283                 continue;
0284             }
0285             QRgb rgb = sampleColor.rgb();
0286             ++c;
0287             r += qRed(rgb);
0288             g += qGreen(rgb);
0289             b += qBlue(rgb);
0290 #if HAVE_OpenMP
0291             tempSamples[omp_get_thread_num()] << rgb;
0292 #else
0293             imageData.m_samples << rgb;
0294 #endif
0295         }
0296     } // END omp parallel for
0297 
0298 #if HAVE_OpenMP
0299     for (auto &s : tempSamples) {
0300         imageData.m_samples << std::move(s);
0301     }
0302 #endif
0303 
0304     if (imageData.m_samples.isEmpty()) {
0305         return imageData;
0306     }
0307 
0308     for (QRgb rgb : std::as_const(imageData.m_samples)) {
0309         positionColor(rgb, imageData.m_clusters);
0310     }
0311 
0312     imageData.m_average = QColor(r / c, g / c, b / c, 255);
0313 
0314     for (int iteration = 0; iteration < 5; ++iteration) {
0315 #pragma omp parallel for private(r, g, b, c)
0316         for (int i = 0; i < imageData.m_clusters.size(); ++i) {
0317             auto &stat = imageData.m_clusters[i];
0318             r = 0;
0319             g = 0;
0320             b = 0;
0321             c = 0;
0322 
0323             for (auto color : std::as_const(stat.colors)) {
0324                 c++;
0325                 r += qRed(color);
0326                 g += qGreen(color);
0327                 b += qBlue(color);
0328             }
0329             r = r / c;
0330             g = g / c;
0331             b = b / c;
0332             stat.centroid = qRgb(r, g, b);
0333             stat.ratio = qreal(stat.colors.count()) / qreal(imageData.m_samples.count());
0334             stat.colors = QList<QRgb>({stat.centroid});
0335         } // END omp parallel for
0336 
0337         for (auto color : std::as_const(imageData.m_samples)) {
0338             positionColor(color, imageData.m_clusters);
0339         }
0340     }
0341 
0342     std::sort(imageData.m_clusters.begin(), imageData.m_clusters.end(), [this](const ImageData::colorStat &a, const ImageData::colorStat &b) {
0343         return getClusterScore(a) > getClusterScore(b);
0344     });
0345 
0346     // compress blocks that became too similar
0347     auto sourceIt = imageData.m_clusters.end();
0348     // Use index instead of iterator, because QList::erase may invalidate iterator.
0349     std::vector<int> itemsToDelete;
0350     while (sourceIt != imageData.m_clusters.begin()) {
0351         sourceIt--;
0352         for (auto destIt = imageData.m_clusters.begin(); destIt != imageData.m_clusters.end() && destIt != sourceIt; destIt++) {
0353             if (squareDistance((*sourceIt).centroid, (*destIt).centroid) < s_minimumSquareDistance) {
0354                 const qreal ratio = (*sourceIt).ratio / (*destIt).ratio;
0355                 const int r = ratio * qreal(qRed((*sourceIt).centroid)) + (1 - ratio) * qreal(qRed((*destIt).centroid));
0356                 const int g = ratio * qreal(qGreen((*sourceIt).centroid)) + (1 - ratio) * qreal(qGreen((*destIt).centroid));
0357                 const int b = ratio * qreal(qBlue((*sourceIt).centroid)) + (1 - ratio) * qreal(qBlue((*destIt).centroid));
0358                 (*destIt).ratio += (*sourceIt).ratio;
0359                 (*destIt).centroid = qRgb(r, g, b);
0360                 itemsToDelete.push_back(std::distance(imageData.m_clusters.begin(), sourceIt));
0361                 break;
0362             }
0363         }
0364     }
0365     for (auto i : std::as_const(itemsToDelete)) {
0366         imageData.m_clusters.removeAt(i);
0367     }
0368 
0369     imageData.m_highlight = QColor();
0370     imageData.m_dominant = QColor(imageData.m_clusters.first().centroid);
0371     imageData.m_closestToBlack = Qt::white;
0372     imageData.m_closestToWhite = Qt::black;
0373 
0374     imageData.m_palette.clear();
0375 
0376     bool first = true;
0377 
0378     for (const auto &stat : std::as_const(imageData.m_clusters)) {
0379         QVariantMap entry;
0380         const QColor color(stat.centroid);
0381         entry[QStringLiteral("color")] = color;
0382         entry[QStringLiteral("ratio")] = stat.ratio;
0383 
0384         QColor contrast = QColor(255 - color.red(), 255 - color.green(), 255 - color.blue());
0385         contrast.setHsl(contrast.hslHue(), //
0386                         contrast.hslSaturation(), //
0387                         128 + (128 - contrast.lightness()));
0388         QColor tempContrast;
0389         int minimumDistance = 4681800; // max distance: 4*3*2*3*255*255
0390         for (const auto &stat : std::as_const(imageData.m_clusters)) {
0391             const int distance = squareDistance(contrast.rgb(), stat.centroid);
0392 
0393             if (distance < minimumDistance) {
0394                 tempContrast = QColor(stat.centroid);
0395                 minimumDistance = distance;
0396             }
0397         }
0398 
0399         if (imageData.m_clusters.size() <= 3) {
0400             if (qGray(imageData.m_dominant.rgb()) < 120) {
0401                 contrast = QColor(230, 230, 230);
0402             } else {
0403                 contrast = QColor(20, 20, 20);
0404             }
0405             // TODO: replace m_clusters.size() > 3 with entropy calculation
0406         } else if (squareDistance(contrast.rgb(), tempContrast.rgb()) < s_minimumSquareDistance * 1.5) {
0407             contrast = tempContrast;
0408         } else {
0409             contrast = tempContrast;
0410             contrast.setHsl(contrast.hslHue(),
0411                             contrast.hslSaturation(),
0412                             contrast.lightness() > 128 ? qMin(contrast.lightness() + 20, 255) : qMax(0, contrast.lightness() - 20));
0413         }
0414 
0415         entry[QStringLiteral("contrastColor")] = contrast;
0416 
0417         if (first) {
0418             imageData.m_dominantContrast = contrast;
0419             imageData.m_dominant = color;
0420         }
0421         first = false;
0422 
0423         if (!imageData.m_highlight.isValid() || ColorUtils::chroma(color) > ColorUtils::chroma(imageData.m_highlight)) {
0424             imageData.m_highlight = color;
0425         }
0426 
0427         if (qGray(color.rgb()) > qGray(imageData.m_closestToWhite.rgb())) {
0428             imageData.m_closestToWhite = color;
0429         }
0430         if (qGray(color.rgb()) < qGray(imageData.m_closestToBlack.rgb())) {
0431             imageData.m_closestToBlack = color;
0432         }
0433         imageData.m_palette << entry;
0434     }
0435 
0436     postProcess(imageData);
0437 
0438     return imageData;
0439 }
0440 
0441 double ImageColors::getClusterScore(const ImageData::colorStat &stat) const
0442 {
0443     return stat.ratio * ColorUtils::chroma(QColor(stat.centroid));
0444 }
0445 
0446 void ImageColors::postProcess(ImageData &imageData) const
0447 {
0448     constexpr short unsigned WCAG_NON_TEXT_CONTRAST_RATIO = 3;
0449     constexpr qreal WCAG_TEXT_CONTRAST_RATIO = 4.5;
0450     const QColor backgroundColor = static_cast<Kirigami::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::PlatformTheme>(this, true))->backgroundColor();
0451     const qreal backgroundLum = ColorUtils::luminance(backgroundColor);
0452     qreal lowerLum, upperLum;
0453     // 192 is from kcm_colors
0454     if (qGray(backgroundColor.rgb()) < 192) {
0455         // (lowerLum + 0.05) / (backgroundLum + 0.05) >= 3
0456         lowerLum = WCAG_NON_TEXT_CONTRAST_RATIO * (backgroundLum + 0.05) - 0.05;
0457         upperLum = 0.95;
0458     } else {
0459         // For light themes, still prefer lighter colors
0460         // (lowerLum + 0.05) / (textLum + 0.05) >= 4.5
0461         const QColor textColor = static_cast<Kirigami::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::PlatformTheme>(this, true))->textColor();
0462         const qreal textLum = ColorUtils::luminance(textColor);
0463         lowerLum = WCAG_TEXT_CONTRAST_RATIO * (textLum + 0.05) - 0.05;
0464         upperLum = backgroundLum;
0465     }
0466 
0467     auto adjustSaturation = [](QColor &color) {
0468         // Adjust saturation to make the color more vibrant
0469         if (color.hsvSaturationF() < 0.5) {
0470             const qreal h = color.hsvHueF();
0471             const qreal v = color.valueF();
0472             color.setHsvF(h, 0.5, v);
0473         }
0474     };
0475     adjustSaturation(imageData.m_dominant);
0476     adjustSaturation(imageData.m_highlight);
0477     adjustSaturation(imageData.m_average);
0478 
0479     auto adjustLightness = [lowerLum, upperLum](QColor &color) {
0480         short unsigned colorOperationCount = 0;
0481         const qreal h = color.hslHueF();
0482         const qreal s = color.hslSaturationF();
0483         const qreal l = color.lightnessF();
0484         while (ColorUtils::luminance(color.rgb()) < lowerLum && colorOperationCount++ < 10) {
0485             color.setHslF(h, s, std::min(1.0, l + colorOperationCount * 0.03));
0486         }
0487         while (ColorUtils::luminance(color.rgb()) > upperLum && colorOperationCount++ < 10) {
0488             color.setHslF(h, s, std::max(0.0, l - colorOperationCount * 0.03));
0489         }
0490     };
0491     adjustLightness(imageData.m_dominant);
0492     adjustLightness(imageData.m_highlight);
0493     adjustLightness(imageData.m_average);
0494 }
0495 
0496 QVariantList ImageColors::palette() const
0497 {
0498     if (m_futureImageData) {
0499         qCWarning(KirigamiLog) << m_futureImageData->future().isFinished();
0500     }
0501     return_fallback(m_fallbackPalette) return m_imageData.m_palette;
0502 }
0503 
0504 ColorUtils::Brightness ImageColors::paletteBrightness() const
0505 {
0506     /* clang-format off */
0507     return_fallback(m_fallbackPaletteBrightness)
0508 
0509     return qGray(m_imageData.m_dominant.rgb()) < 128 ? ColorUtils::Dark : ColorUtils::Light;
0510     /* clang-format on */
0511 }
0512 
0513 QColor ImageColors::average() const
0514 {
0515     /* clang-format off */
0516     return_fallback_finally(m_fallbackAverage, linkBackgroundColor)
0517 
0518     return m_imageData.m_average;
0519     /* clang-format on */
0520 }
0521 
0522 QColor ImageColors::dominant() const
0523 {
0524     /* clang-format off */
0525     return_fallback_finally(m_fallbackDominant, linkBackgroundColor)
0526 
0527     return m_imageData.m_dominant;
0528     /* clang-format on */
0529 }
0530 
0531 QColor ImageColors::dominantContrast() const
0532 {
0533     /* clang-format off */
0534     return_fallback_finally(m_fallbackDominantContrasting, linkBackgroundColor)
0535 
0536     return m_imageData.m_dominantContrast;
0537     /* clang-format on */
0538 }
0539 
0540 QColor ImageColors::foreground() const
0541 {
0542     /* clang-format off */
0543     return_fallback_finally(m_fallbackForeground, textColor)
0544 
0545     if (paletteBrightness() == ColorUtils::Dark)
0546     {
0547         if (qGray(m_imageData.m_closestToWhite.rgb()) < 200) {
0548             return QColor(230, 230, 230);
0549         }
0550         return m_imageData.m_closestToWhite;
0551     } else {
0552         if (qGray(m_imageData.m_closestToBlack.rgb()) > 80) {
0553             return QColor(20, 20, 20);
0554         }
0555         return m_imageData.m_closestToBlack;
0556     }
0557     /* clang-format on */
0558 }
0559 
0560 QColor ImageColors::background() const
0561 {
0562     /* clang-format off */
0563     return_fallback_finally(m_fallbackBackground, backgroundColor)
0564 
0565     if (paletteBrightness() == ColorUtils::Dark) {
0566         if (qGray(m_imageData.m_closestToBlack.rgb()) > 80) {
0567             return QColor(20, 20, 20);
0568         }
0569         return m_imageData.m_closestToBlack;
0570     } else {
0571         if (qGray(m_imageData.m_closestToWhite.rgb()) < 200) {
0572             return QColor(230, 230, 230);
0573         }
0574         return m_imageData.m_closestToWhite;
0575     }
0576     /* clang-format on */
0577 }
0578 
0579 QColor ImageColors::highlight() const
0580 {
0581     /* clang-format off */
0582     return_fallback_finally(m_fallbackHighlight, linkColor)
0583 
0584     return m_imageData.m_highlight;
0585     /* clang-format on */
0586 }
0587 
0588 QColor ImageColors::closestToWhite() const
0589 {
0590     /* clang-format off */
0591     return_fallback(Qt::white)
0592     if (qGray(m_imageData.m_closestToWhite.rgb()) < 200) {
0593         return QColor(230, 230, 230);
0594     }
0595     /* clang-format on */
0596 
0597     return m_imageData.m_closestToWhite;
0598 }
0599 
0600 QColor ImageColors::closestToBlack() const
0601 {
0602     /* clang-format off */
0603     return_fallback(Qt::black)
0604     if (qGray(m_imageData.m_closestToBlack.rgb()) > 80) {
0605         return QColor(20, 20, 20);
0606     }
0607     /* clang-format on */
0608     return m_imageData.m_closestToBlack;
0609 }
0610 
0611 #include "moc_imagecolors.cpp"