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 | |
38 | PaletteSwatch::PaletteSwatch() |
39 | { |
40 | } |
41 | |
42 | PaletteSwatch::PaletteSwatch(qreal ratio, const QColor &color, const QColor &contrastColor) |
43 | : m_ratio(ratio) |
44 | , m_color(color) |
45 | , m_contrastColor(contrastColor) |
46 | { |
47 | } |
48 | |
49 | qreal PaletteSwatch::ratio() const |
50 | { |
51 | return m_ratio; |
52 | } |
53 | |
54 | const QColor &PaletteSwatch::color() const |
55 | { |
56 | return m_color; |
57 | } |
58 | |
59 | const QColor &PaletteSwatch::contrastColor() const |
60 | { |
61 | return m_contrastColor; |
62 | } |
63 | |
64 | bool 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 | |
71 | ImageColors::ImageColors(QObject *parent) |
72 | : QObject(parent) |
73 | { |
74 | } |
75 | |
76 | ImageColors::~ImageColors() |
77 | { |
78 | } |
79 | |
80 | void 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 | |
126 | QVariant ImageColors::source() const |
127 | { |
128 | return m_source; |
129 | } |
130 | |
131 | void 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 | |
150 | QImage ImageColors::sourceImage() const |
151 | { |
152 | return m_sourceImage; |
153 | } |
154 | |
155 | void 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 | |
187 | QQuickItem *ImageColors::sourceItem() const |
188 | { |
189 | return m_sourceItem; |
190 | } |
191 | |
192 | void 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 | |
247 | static 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 | |
262 | void 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 | |
277 | void 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 | |
334 | ImageData 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 | |
507 | double ImageColors::getClusterScore(const ImageData::colorStat &stat) |
508 | { |
509 | return stat.ratio * ColorUtils::chroma(color: QColor(stat.centroid)); |
510 | } |
511 | |
512 | void 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 | |
569 | QList<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 | |
577 | ColorUtils::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 | |
586 | QColor 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 | |
595 | QColor 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 | |
604 | QColor 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 | |
613 | QColor 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 | |
633 | QColor 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 | |
652 | QColor 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 | |
661 | QColor 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 | |
673 | QColor 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 | |