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 | |
50 | ImageColors::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 | |
61 | ImageColors::~ImageColors() |
62 | { |
63 | } |
64 | |
65 | void 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 | |
111 | QVariant ImageColors::source() const |
112 | { |
113 | return m_source; |
114 | } |
115 | |
116 | void 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 | |
135 | QImage ImageColors::sourceImage() const |
136 | { |
137 | return m_sourceImage; |
138 | } |
139 | |
140 | void 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 | |
171 | QQuickItem *ImageColors::sourceItem() const |
172 | { |
173 | return m_sourceItem; |
174 | } |
175 | |
176 | void 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 | |
228 | inline 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 | |
243 | void 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 | |
258 | void 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 | |
316 | ImageData 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 | |
493 | double ImageColors::getClusterScore(const ImageData::colorStat &stat) const |
494 | { |
495 | return stat.ratio * ColorUtils::chroma(color: QColor(stat.centroid)); |
496 | } |
497 | |
498 | void 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 | |
555 | QVariantList 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 | |
563 | ColorUtils::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 | |
572 | QColor 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 | |
581 | QColor 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 | |
590 | QColor 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 | |
599 | QColor 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 | |
619 | QColor 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 | |
638 | QColor 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 | |
647 | QColor 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 | |
659 | QColor 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 | |