1/*
2 * Copyright 2020 Marco Martin <mart@kde.org>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 2.010-1301, USA.
17 */
18
19#include "imagecolors.h"
20
21#include <QDebug>
22#include <QFutureWatcher>
23#include <QGuiApplication>
24#include <QTimer>
25#include <QtConcurrentRun>
26
27#include "loggingcategory.h"
28#include <cmath>
29#include <vector>
30
31#include "config-OpenMP.h"
32#if HAVE_OpenMP
33#include <omp.h>
34#endif
35
36#include "platform/platformtheme.h"
37
38#define return_fallback(value) \
39 if (m_imageData.m_samples.size() == 0) { \
40 return value; \
41 }
42
43#define return_fallback_finally(value, finally) \
44 if (m_imageData.m_samples.size() == 0) { \
45 return value.isValid() \
46 ? value \
47 : static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true))->finally(); \
48 }
49
50ImageColors::ImageColors(QObject *parent)
51 : QObject(parent)
52{
53 m_imageSyncTimer = new QTimer(this);
54 m_imageSyncTimer->setSingleShot(true);
55 m_imageSyncTimer->setInterval(100);
56 /* connect(m_imageSyncTimer, &QTimer::timeout, this, [this]() {
57 generatePalette();
58 });*/
59}
60
61ImageColors::~ImageColors()
62{
63}
64
65void ImageColors::setSource(const QVariant &source)
66{
67 if (m_futureSourceImageData) {
68 m_futureSourceImageData->cancel();
69 m_futureSourceImageData->deleteLater();
70 m_futureSourceImageData = nullptr;
71 }
72
73 if (source.canConvert<QQuickItem *>()) {
74 setSourceItem(source.value<QQuickItem *>());
75 } else if (source.canConvert<QImage>()) {
76 setSourceImage(source.value<QImage>());
77 } else if (source.canConvert<QIcon>()) {
78 setSourceImage(source.value<QIcon>().pixmap(w: 128, h: 128).toImage());
79 } else if (source.canConvert<QString>()) {
80 const QString sourceString = source.toString();
81
82 if (QIcon::hasThemeIcon(name: sourceString)) {
83 setSourceImage(QIcon::fromTheme(name: sourceString).pixmap(w: 128, h: 128).toImage());
84 } else {
85 QFuture<QImage> future = QtConcurrent::run(f: [sourceString]() {
86 if (auto url = QUrl(sourceString); url.isLocalFile()) {
87 return QImage(url.toLocalFile());
88 }
89 return QImage(sourceString);
90 });
91 m_futureSourceImageData = new QFutureWatcher<QImage>(this);
92 connect(sender: m_futureSourceImageData, signal: &QFutureWatcher<QImage>::finished, context: this, slot: [this, source]() {
93 const QImage image = m_futureSourceImageData->future().result();
94 m_futureSourceImageData->deleteLater();
95 m_futureSourceImageData = nullptr;
96 setSourceImage(image);
97 m_source = source;
98 Q_EMIT sourceChanged();
99 });
100 m_futureSourceImageData->setFuture(future);
101 return;
102 }
103 } else {
104 return;
105 }
106
107 m_source = source;
108 Q_EMIT sourceChanged();
109}
110
111QVariant ImageColors::source() const
112{
113 return m_source;
114}
115
116void ImageColors::setSourceImage(const QImage &image)
117{
118 if (m_window) {
119 disconnect(sender: m_window.data(), signal: nullptr, receiver: this, member: nullptr);
120 }
121 if (m_sourceItem) {
122 disconnect(sender: m_sourceItem.data(), signal: nullptr, receiver: this, member: nullptr);
123 }
124 if (m_grabResult) {
125 disconnect(sender: m_grabResult.data(), signal: nullptr, receiver: this, member: nullptr);
126 m_grabResult.clear();
127 }
128
129 m_sourceItem.clear();
130
131 m_sourceImage = image;
132 update();
133}
134
135QImage ImageColors::sourceImage() const
136{
137 return m_sourceImage;
138}
139
140void ImageColors::setSourceItem(QQuickItem *source)
141{
142 if (m_sourceItem == source) {
143 return;
144 }
145
146 if (m_window) {
147 disconnect(sender: m_window.data(), signal: nullptr, receiver: this, member: nullptr);
148 }
149 if (m_sourceItem) {
150 disconnect(sender: m_sourceItem, signal: nullptr, receiver: this, member: nullptr);
151 }
152 m_sourceItem = source;
153 update();
154
155 if (m_sourceItem) {
156 auto syncWindow = [this]() {
157 if (m_window) {
158 disconnect(sender: m_window.data(), signal: nullptr, receiver: this, member: nullptr);
159 }
160 m_window = m_sourceItem->window();
161 if (m_window) {
162 connect(sender: m_window, signal: &QWindow::visibleChanged, context: this, slot: &ImageColors::update);
163 }
164 };
165
166 connect(sender: m_sourceItem, signal: &QQuickItem::windowChanged, context: this, slot&: syncWindow);
167 syncWindow();
168 }
169}
170
171QQuickItem *ImageColors::sourceItem() const
172{
173 return m_sourceItem;
174}
175
176void ImageColors::update()
177{
178 if (m_futureImageData) {
179 m_futureImageData->cancel();
180 m_futureImageData->deleteLater();
181 m_futureImageData = nullptr;
182 }
183 auto runUpdate = [this]() {
184 QFuture<ImageData> future = QtConcurrent::run(f: [this]() {
185 return generatePalette(sourceImage: m_sourceImage);
186 });
187 m_futureImageData = new QFutureWatcher<ImageData>(this);
188 connect(sender: m_futureImageData, signal: &QFutureWatcher<ImageData>::finished, context: this, slot: [this]() {
189 if (!m_futureImageData) {
190 return;
191 }
192 m_imageData = m_futureImageData->future().result();
193 postProcess(imageData&: m_imageData);
194 m_futureImageData->deleteLater();
195 m_futureImageData = nullptr;
196
197 Q_EMIT paletteChanged();
198 });
199 m_futureImageData->setFuture(future);
200 };
201
202 if (!m_sourceItem) {
203 if (!m_sourceImage.isNull()) {
204 runUpdate();
205 } else {
206 m_imageData = {};
207 Q_EMIT paletteChanged();
208 }
209 return;
210 }
211
212 if (m_grabResult) {
213 disconnect(sender: m_grabResult.data(), signal: nullptr, receiver: this, member: nullptr);
214 m_grabResult.clear();
215 }
216
217 m_grabResult = m_sourceItem->grabToImage(targetSize: QSize(128, 128));
218
219 if (m_grabResult) {
220 connect(sender: m_grabResult.data(), signal: &QQuickItemGrabResult::ready, context: this, slot: [this, runUpdate]() {
221 m_sourceImage = m_grabResult->image();
222 m_grabResult.clear();
223 runUpdate();
224 });
225 }
226}
227
228inline int squareDistance(QRgb color1, QRgb color2)
229{
230 // https://en.wikipedia.org/wiki/Color_difference
231 // Using RGB distance for performance, as CIEDE2000 is too complicated
232 if (qRed(rgb: color1) - qRed(rgb: color2) < 128) {
233 return 2 * pow(x: qRed(rgb: color1) - qRed(rgb: color2), y: 2) //
234 + 4 * pow(x: qGreen(rgb: color1) - qGreen(rgb: color2), y: 2) //
235 + 3 * pow(x: qBlue(rgb: color1) - qBlue(rgb: color2), y: 2);
236 } else {
237 return 3 * pow(x: qRed(rgb: color1) - qRed(rgb: color2), y: 2) //
238 + 4 * pow(x: qGreen(rgb: color1) - qGreen(rgb: color2), y: 2) //
239 + 2 * pow(x: qBlue(rgb: color1) - qBlue(rgb: color2), y: 2);
240 }
241}
242
243void ImageColors::positionColor(QRgb rgb, QList<ImageData::colorStat> &clusters)
244{
245 for (auto &stat : clusters) {
246 if (squareDistance(color1: rgb, color2: stat.centroid) < s_minimumSquareDistance) {
247 stat.colors.append(t: rgb);
248 return;
249 }
250 }
251
252 ImageData::colorStat stat;
253 stat.colors.append(t: rgb);
254 stat.centroid = rgb;
255 clusters << stat;
256}
257
258void ImageColors::positionColorMP(const decltype(ImageData::m_samples) &samples, decltype(ImageData::m_clusters) &clusters, int numCore)
259{
260#if HAVE_OpenMP
261 if (samples.size() < 65536 /* 256^2 */ || numCore < 2) {
262#else
263 if (true) {
264#endif
265 // Fall back to single thread
266 for (auto color : samples) {
267 positionColor(rgb: color, clusters);
268 }
269 return;
270 }
271#if HAVE_OpenMP
272 // Split the whole samples into multiple parts
273 const int numSamplesPerThread = samples.size() / numCore;
274 std::vector<decltype(ImageData::m_clusters)> tempClusters(numCore, decltype(ImageData::m_clusters){});
275#pragma omp parallel for
276 for (int i = 0; i < numCore; ++i) {
277 decltype(ImageData::m_samples) samplePart;
278 const auto beginIt = std::next(x: samples.begin(), n: numSamplesPerThread * i);
279 const auto endIt = i < numCore - 1 ? std::next(x: samples.begin(), n: numSamplesPerThread * (i + 1)) : samples.end();
280
281 for (auto it = beginIt; it != endIt; it = std::next(x: it)) {
282 positionColor(rgb: *it, clusters&: tempClusters[omp_get_thread_num()]);
283 }
284 } // END omp parallel for
285
286 // Restore clusters
287 // Don't use std::as_const as memory will grow significantly
288 for (const auto &clusterPart : tempClusters) {
289 clusters << clusterPart;
290 }
291 for (int i = 0; i < clusters.size() - 1; ++i) {
292 auto &clusterA = clusters[i];
293 if (clusterA.colors.empty()) {
294 continue; // Already merged
295 }
296 for (int j = i + 1; j < clusters.size(); ++j) {
297 auto &clusterB = clusters[j];
298 if (clusterB.colors.empty()) {
299 continue; // Already merged
300 }
301 if (squareDistance(color1: clusterA.centroid, color2: clusterB.centroid) < s_minimumSquareDistance) {
302 // Merge colors in clusterB into clusterA
303 clusterA.colors.append(l: clusterB.colors);
304 clusterB.colors.clear();
305 }
306 }
307 }
308
309 auto removeIt = std::remove_if(first: clusters.begin(), last: clusters.end(), pred: [](const ImageData::colorStat &stat) {
310 return stat.colors.empty();
311 });
312 clusters.erase(abegin: removeIt, aend: clusters.end());
313#endif
314}
315
316ImageData ImageColors::generatePalette(const QImage &sourceImage) const
317{
318 ImageData imageData;
319
320 if (sourceImage.isNull() || sourceImage.width() == 0) {
321 return imageData;
322 }
323
324 imageData.m_clusters.clear();
325 imageData.m_samples.clear();
326
327#if HAVE_OpenMP
328 static const int numCore = std::min(8, omp_get_num_procs());
329 omp_set_num_threads(numCore);
330#else
331 constexpr int numCore = 1;
332#endif
333 int r = 0;
334 int g = 0;
335 int b = 0;
336 int c = 0;
337
338#pragma omp parallel for collapse(2) reduction(+ : r) reduction(+ : g) reduction(+ : b) reduction(+ : c)
339 for (int x = 0; x < sourceImage.width(); ++x) {
340 for (int y = 0; y < sourceImage.height(); ++y) {
341 const QColor sampleColor = sourceImage.pixelColor(x, y);
342 if (sampleColor.alpha() == 0) {
343 continue;
344 }
345 if (ColorUtils::chroma(color: sampleColor) < 20) {
346 continue;
347 }
348 QRgb rgb = sampleColor.rgb();
349 ++c;
350 r += qRed(rgb);
351 g += qGreen(rgb);
352 b += qBlue(rgb);
353#pragma omp critical
354 imageData.m_samples << rgb;
355 }
356 } // END omp parallel for
357
358 if (imageData.m_samples.isEmpty()) {
359 return imageData;
360 }
361
362 positionColorMP(samples: imageData.m_samples, clusters&: imageData.m_clusters, numCore);
363
364 imageData.m_average = QColor(r / c, g / c, b / c, 255);
365
366 for (int iteration = 0; iteration < 5; ++iteration) {
367#pragma omp parallel for private(r, g, b, c)
368 for (int i = 0; i < imageData.m_clusters.size(); ++i) {
369 auto &stat = imageData.m_clusters[i];
370 r = 0;
371 g = 0;
372 b = 0;
373 c = 0;
374
375 for (auto color : std::as_const(t&: stat.colors)) {
376 c++;
377 r += qRed(rgb: color);
378 g += qGreen(rgb: color);
379 b += qBlue(rgb: color);
380 }
381 r = r / c;
382 g = g / c;
383 b = b / c;
384 stat.centroid = qRgb(r, g, b);
385 stat.ratio = qreal(stat.colors.count()) / qreal(imageData.m_samples.count());
386 stat.colors = QList<QRgb>({stat.centroid});
387 } // END omp parallel for
388
389 positionColorMP(samples: imageData.m_samples, clusters&: imageData.m_clusters, numCore);
390 }
391
392 std::sort(first: imageData.m_clusters.begin(), last: imageData.m_clusters.end(), comp: [this](const ImageData::colorStat &a, const ImageData::colorStat &b) {
393 return getClusterScore(stat: a) > getClusterScore(stat: b);
394 });
395
396 // compress blocks that became too similar
397 auto sourceIt = imageData.m_clusters.end();
398 // Use index instead of iterator, because QList::erase may invalidate iterator.
399 std::vector<int> itemsToDelete;
400 while (sourceIt != imageData.m_clusters.begin()) {
401 sourceIt--;
402 for (auto destIt = imageData.m_clusters.begin(); destIt != imageData.m_clusters.end() && destIt != sourceIt; destIt++) {
403 if (squareDistance(color1: (*sourceIt).centroid, color2: (*destIt).centroid) < s_minimumSquareDistance) {
404 const qreal ratio = (*sourceIt).ratio / (*destIt).ratio;
405 const int r = ratio * qreal(qRed(rgb: (*sourceIt).centroid)) + (1 - ratio) * qreal(qRed(rgb: (*destIt).centroid));
406 const int g = ratio * qreal(qGreen(rgb: (*sourceIt).centroid)) + (1 - ratio) * qreal(qGreen(rgb: (*destIt).centroid));
407 const int b = ratio * qreal(qBlue(rgb: (*sourceIt).centroid)) + (1 - ratio) * qreal(qBlue(rgb: (*destIt).centroid));
408 (*destIt).ratio += (*sourceIt).ratio;
409 (*destIt).centroid = qRgb(r, g, b);
410 itemsToDelete.push_back(x: std::distance(first: imageData.m_clusters.begin(), last: sourceIt));
411 break;
412 }
413 }
414 }
415 for (auto i : std::as_const(t&: itemsToDelete)) {
416 imageData.m_clusters.removeAt(i);
417 }
418
419 imageData.m_highlight = QColor();
420 imageData.m_dominant = QColor(imageData.m_clusters.first().centroid);
421 imageData.m_closestToBlack = Qt::white;
422 imageData.m_closestToWhite = Qt::black;
423
424 imageData.m_palette.clear();
425
426 bool first = true;
427
428#pragma omp parallel for ordered
429 for (int i = 0; i < imageData.m_clusters.size(); ++i) {
430 const auto &stat = imageData.m_clusters[i];
431 QVariantMap entry;
432 const QColor color(stat.centroid);
433 entry[QStringLiteral("color")] = color;
434 entry[QStringLiteral("ratio")] = stat.ratio;
435
436 QColor contrast = QColor(255 - color.red(), 255 - color.green(), 255 - color.blue());
437 contrast.setHsl(h: contrast.hslHue(), //
438 s: contrast.hslSaturation(), //
439 l: 128 + (128 - contrast.lightness()));
440 QColor tempContrast;
441 int minimumDistance = 4681800; // max distance: 4*3*2*3*255*255
442 for (const auto &stat : std::as_const(t&: imageData.m_clusters)) {
443 const int distance = squareDistance(color1: contrast.rgb(), color2: stat.centroid);
444
445 if (distance < minimumDistance) {
446 tempContrast = QColor(stat.centroid);
447 minimumDistance = distance;
448 }
449 }
450
451 if (imageData.m_clusters.size() <= 3) {
452 if (qGray(rgb: imageData.m_dominant.rgb()) < 120) {
453 contrast = QColor(230, 230, 230);
454 } else {
455 contrast = QColor(20, 20, 20);
456 }
457 // TODO: replace m_clusters.size() > 3 with entropy calculation
458 } else if (squareDistance(color1: contrast.rgb(), color2: tempContrast.rgb()) < s_minimumSquareDistance * 1.5) {
459 contrast = tempContrast;
460 } else {
461 contrast = tempContrast;
462 contrast.setHsl(h: contrast.hslHue(),
463 s: contrast.hslSaturation(),
464 l: contrast.lightness() > 128 ? qMin(a: contrast.lightness() + 20, b: 255) : qMax(a: 0, b: contrast.lightness() - 20));
465 }
466
467 entry[QStringLiteral("contrastColor")] = contrast;
468#pragma omp ordered
469 { // BEGIN omp ordered
470 if (first) {
471 imageData.m_dominantContrast = contrast;
472 imageData.m_dominant = color;
473 }
474 first = false;
475
476 if (!imageData.m_highlight.isValid() || ColorUtils::chroma(color) > ColorUtils::chroma(color: imageData.m_highlight)) {
477 imageData.m_highlight = color;
478 }
479
480 if (qGray(rgb: color.rgb()) > qGray(rgb: imageData.m_closestToWhite.rgb())) {
481 imageData.m_closestToWhite = color;
482 }
483 if (qGray(rgb: color.rgb()) < qGray(rgb: imageData.m_closestToBlack.rgb())) {
484 imageData.m_closestToBlack = color;
485 }
486 imageData.m_palette << entry;
487 } // END omp ordered
488 }
489
490 return imageData;
491}
492
493double ImageColors::getClusterScore(const ImageData::colorStat &stat) const
494{
495 return stat.ratio * ColorUtils::chroma(color: QColor(stat.centroid));
496}
497
498void ImageColors::postProcess(ImageData &imageData) const
499{
500 constexpr short unsigned WCAG_NON_TEXT_CONTRAST_RATIO = 3;
501 constexpr qreal WCAG_TEXT_CONTRAST_RATIO = 4.5;
502
503 auto platformTheme = qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(obj: this, create: false);
504 if (!platformTheme) {
505 return;
506 }
507
508 const QColor backgroundColor = static_cast<Kirigami::Platform::PlatformTheme *>(platformTheme)->backgroundColor();
509 const qreal backgroundLum = ColorUtils::luminance(color: backgroundColor);
510 qreal lowerLum, upperLum;
511 // 192 is from kcm_colors
512 if (qGray(rgb: backgroundColor.rgb()) < 192) {
513 // (lowerLum + 0.05) / (backgroundLum + 0.05) >= 3
514 lowerLum = WCAG_NON_TEXT_CONTRAST_RATIO * (backgroundLum + 0.05) - 0.05;
515 upperLum = 0.95;
516 } else {
517 // For light themes, still prefer lighter colors
518 // (lowerLum + 0.05) / (textLum + 0.05) >= 4.5
519 const QColor textColor =
520 static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(obj: this, create: true))->textColor();
521 const qreal textLum = ColorUtils::luminance(color: textColor);
522 lowerLum = WCAG_TEXT_CONTRAST_RATIO * (textLum + 0.05) - 0.05;
523 upperLum = backgroundLum;
524 }
525
526 auto adjustSaturation = [](QColor &color) {
527 // Adjust saturation to make the color more vibrant
528 if (color.hsvSaturationF() < 0.5) {
529 const qreal h = color.hsvHueF();
530 const qreal v = color.valueF();
531 color.setHsvF(h, s: 0.5, v);
532 }
533 };
534 adjustSaturation(imageData.m_dominant);
535 adjustSaturation(imageData.m_highlight);
536 adjustSaturation(imageData.m_average);
537
538 auto adjustLightness = [lowerLum, upperLum](QColor &color) {
539 short unsigned colorOperationCount = 0;
540 const qreal h = color.hslHueF();
541 const qreal s = color.hslSaturationF();
542 const qreal l = color.lightnessF();
543 while (ColorUtils::luminance(color: color.rgb()) < lowerLum && colorOperationCount++ < 10) {
544 color.setHslF(h, s, l: std::min(a: 1.0, b: l + colorOperationCount * 0.03));
545 }
546 while (ColorUtils::luminance(color: color.rgb()) > upperLum && colorOperationCount++ < 10) {
547 color.setHslF(h, s, l: std::max(a: 0.0, b: l - colorOperationCount * 0.03));
548 }
549 };
550 adjustLightness(imageData.m_dominant);
551 adjustLightness(imageData.m_highlight);
552 adjustLightness(imageData.m_average);
553}
554
555QVariantList ImageColors::palette() const
556{
557 if (m_futureImageData) {
558 qCWarning(KirigamiLog) << m_futureImageData->future().isFinished();
559 }
560 return_fallback(m_fallbackPalette) return m_imageData.m_palette;
561}
562
563ColorUtils::Brightness ImageColors::paletteBrightness() const
564{
565 /* clang-format off */
566 return_fallback(m_fallbackPaletteBrightness)
567
568 return qGray(rgb: m_imageData.m_dominant.rgb()) < 128 ? ColorUtils::Dark : ColorUtils::Light;
569 /* clang-format on */
570}
571
572QColor ImageColors::average() const
573{
574 /* clang-format off */
575 return_fallback_finally(m_fallbackAverage, linkBackgroundColor)
576
577 return m_imageData.m_average;
578 /* clang-format on */
579}
580
581QColor ImageColors::dominant() const
582{
583 /* clang-format off */
584 return_fallback_finally(m_fallbackDominant, linkBackgroundColor)
585
586 return m_imageData.m_dominant;
587 /* clang-format on */
588}
589
590QColor ImageColors::dominantContrast() const
591{
592 /* clang-format off */
593 return_fallback_finally(m_fallbackDominantContrasting, linkBackgroundColor)
594
595 return m_imageData.m_dominantContrast;
596 /* clang-format on */
597}
598
599QColor ImageColors::foreground() const
600{
601 /* clang-format off */
602 return_fallback_finally(m_fallbackForeground, textColor)
603
604 if (paletteBrightness() == ColorUtils::Dark)
605 {
606 if (qGray(rgb: m_imageData.m_closestToWhite.rgb()) < 200) {
607 return QColor(230, 230, 230);
608 }
609 return m_imageData.m_closestToWhite;
610 } else {
611 if (qGray(rgb: m_imageData.m_closestToBlack.rgb()) > 80) {
612 return QColor(20, 20, 20);
613 }
614 return m_imageData.m_closestToBlack;
615 }
616 /* clang-format on */
617}
618
619QColor ImageColors::background() const
620{
621 /* clang-format off */
622 return_fallback_finally(m_fallbackBackground, backgroundColor)
623
624 if (paletteBrightness() == ColorUtils::Dark) {
625 if (qGray(rgb: m_imageData.m_closestToBlack.rgb()) > 80) {
626 return QColor(20, 20, 20);
627 }
628 return m_imageData.m_closestToBlack;
629 } else {
630 if (qGray(rgb: m_imageData.m_closestToWhite.rgb()) < 200) {
631 return QColor(230, 230, 230);
632 }
633 return m_imageData.m_closestToWhite;
634 }
635 /* clang-format on */
636}
637
638QColor ImageColors::highlight() const
639{
640 /* clang-format off */
641 return_fallback_finally(m_fallbackHighlight, linkColor)
642
643 return m_imageData.m_highlight;
644 /* clang-format on */
645}
646
647QColor ImageColors::closestToWhite() const
648{
649 /* clang-format off */
650 return_fallback(Qt::white)
651 if (qGray(rgb: m_imageData.m_closestToWhite.rgb()) < 200) {
652 return QColor(230, 230, 230);
653 }
654 /* clang-format on */
655
656 return m_imageData.m_closestToWhite;
657}
658
659QColor ImageColors::closestToBlack() const
660{
661 /* clang-format off */
662 return_fallback(Qt::black)
663 if (qGray(rgb: m_imageData.m_closestToBlack.rgb()) > 80) {
664 return QColor(20, 20, 20);
665 }
666 /* clang-format on */
667 return m_imageData.m_closestToBlack;
668}
669
670#include "moc_imagecolors.cpp"
671

source code of kirigami/src/imagecolors.cpp