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

source code of kirigami/src/imagecolors.cpp