| 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 | |