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"