19 #include "imagecolors.h"
20 #include "platformtheme.h"
23 #include <QGuiApplication>
25 #include <QtConcurrent>
27 #include "loggingcategory.h"
31 #include "config-OpenMP.h"
36 #define return_fallback(value) \
37 if (m_imageData.m_samples.size() == 0) { \
41 #define return_fallback_finally(value, finally) \
42 if (m_imageData.m_samples.size() == 0) { \
43 return value.isValid() ? value : static_cast<Kirigami::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::PlatformTheme>(this, true))->finally(); \
46 ImageColors::ImageColors(
QObject *parent)
49 m_imageSyncTimer =
new QTimer(
this);
50 m_imageSyncTimer->setSingleShot(
true);
51 m_imageSyncTimer->setInterval(100);
57 ImageColors::~ImageColors()
61 void ImageColors::setSource(
const QVariant &source)
63 if (m_futureSourceImageData) {
64 m_futureSourceImageData->cancel();
65 m_futureSourceImageData->deleteLater();
66 m_futureSourceImageData =
nullptr;
83 return QImage(url.toLocalFile());
85 return QImage(sourceString);
89 const QImage image = m_futureSourceImageData->future().result();
90 m_futureSourceImageData->deleteLater();
91 m_futureSourceImageData =
nullptr;
92 setSourceImage(image);
94 Q_EMIT sourceChanged();
96 m_futureSourceImageData->setFuture(future);
104 Q_EMIT sourceChanged();
112 void ImageColors::setSourceImage(
const QImage &image)
115 disconnect(m_window.data(),
nullptr,
this,
nullptr);
118 disconnect(m_sourceItem.data(),
nullptr,
this,
nullptr);
121 disconnect(m_grabResult.data(),
nullptr,
this,
nullptr);
122 m_grabResult.clear();
125 m_sourceItem.clear();
127 m_sourceImage = image;
131 QImage ImageColors::sourceImage()
const
133 return m_sourceImage;
136 void ImageColors::setSourceItem(
QQuickItem *source)
138 if (m_sourceItem == source) {
143 disconnect(m_window.data(),
nullptr,
this,
nullptr);
146 disconnect(m_sourceItem,
nullptr,
this,
nullptr);
148 m_sourceItem = source;
152 auto syncWindow = [
this]() {
154 disconnect(m_window.data(),
nullptr,
this,
nullptr);
156 m_window = m_sourceItem->window();
172 void ImageColors::update()
174 if (m_futureImageData) {
175 m_futureImageData->cancel();
176 m_futureImageData->deleteLater();
177 m_futureImageData =
nullptr;
179 auto runUpdate = [
this]() {
181 return generatePalette(m_sourceImage);
185 if (!m_futureImageData) {
188 m_imageData = m_futureImageData->
future().result();
189 m_futureImageData->deleteLater();
190 m_futureImageData =
nullptr;
192 Q_EMIT paletteChanged();
194 m_futureImageData->setFuture(future);
198 if (!m_sourceImage.isNull()) {
202 Q_EMIT paletteChanged();
208 disconnect(m_grabResult.data(),
nullptr,
this,
nullptr);
209 m_grabResult.clear();
212 m_grabResult = m_sourceItem->grabToImage(
QSize(128, 128));
216 m_sourceImage = m_grabResult->image();
217 m_grabResult.clear();
227 if (qRed(color1) - qRed(color2) < 128) {
228 return 2 * pow(qRed(color1) - qRed(color2), 2)
229 + 4 * pow(qGreen(color1) - qGreen(color2), 2)
230 + 3 * pow(qBlue(color1) - qBlue(color2), 2);
232 return 3 * pow(qRed(color1) - qRed(color2), 2)
233 + 4 * pow(qGreen(color1) - qGreen(color2), 2)
234 + 2 * pow(qBlue(color1) - qBlue(color2), 2);
240 for (
auto &stat : clusters) {
242 stat.colors.append(rgb);
247 ImageData::colorStat
stat;
248 stat.colors.append(rgb);
253 ImageData ImageColors::generatePalette(
const QImage &sourceImage)
const
257 if (sourceImage.
isNull() || sourceImage.
width() == 0) {
261 imageData.m_clusters.clear();
262 imageData.m_samples.clear();
265 static const int numCore = std::min(8, omp_get_num_procs());
266 omp_set_num_threads(numCore);
267 std::vector<decltype(imageData.m_samples)> tempSamples(numCore, decltype(imageData.m_samples){});
274 #pragma omp parallel for collapse(2) reduction(+ : r) reduction(+ : g) reduction(+ : b) reduction(+ : c)
275 for (
int x = 0; x < sourceImage.
width(); ++x) {
276 for (
int y = 0; y < sourceImage.
height(); ++y) {
278 if (sampleColor.
alpha() == 0) {
284 QRgb rgb = sampleColor.
rgb();
290 tempSamples[omp_get_thread_num()] << rgb;
292 imageData.m_samples << rgb;
298 for (
auto &s : tempSamples) {
299 imageData.m_samples << std::move(s);
303 if (imageData.m_samples.isEmpty()) {
307 for (QRgb rgb : std::as_const(imageData.m_samples)) {
308 positionColor(rgb, imageData.m_clusters);
311 imageData.m_average =
QColor(r / c, g / c, b / c, 255);
313 for (
int iteration = 0; iteration < 5; ++iteration) {
314 #pragma omp parallel for private(r, g, b, c)
315 for (
int i = 0; i < imageData.m_clusters.size(); ++i) {
316 auto &
stat = imageData.m_clusters[i];
322 for (
auto color : std::as_const(
stat.colors)) {
331 stat.centroid = qRgb(r, g, b);
332 stat.ratio = qreal(
stat.colors.count()) / qreal(imageData.m_samples.count());
336 for (
auto color : std::as_const(imageData.m_samples)) {
337 positionColor(color, imageData.m_clusters);
341 std::sort(imageData.m_clusters.begin(), imageData.m_clusters.end(), [
this](
const ImageData::colorStat &a,
const ImageData::colorStat &b) {
342 return getClusterScore(a) > getClusterScore(b);
346 auto sourceIt = imageData.m_clusters.end();
348 std::vector<int> itemsToDelete;
349 while (sourceIt != imageData.m_clusters.begin()) {
351 for (
auto destIt = imageData.m_clusters.begin(); destIt != imageData.m_clusters.end() && destIt != sourceIt; destIt++) {
352 if (
squareDistance((*sourceIt).centroid, (*destIt).centroid) < s_minimumSquareDistance) {
353 const qreal ratio = (*sourceIt).ratio / (*destIt).ratio;
354 const int r = ratio * qreal(qRed((*sourceIt).centroid)) + (1 - ratio) * qreal(qRed((*destIt).centroid));
355 const int g = ratio * qreal(qGreen((*sourceIt).centroid)) + (1 - ratio) * qreal(qGreen((*destIt).centroid));
356 const int b = ratio * qreal(qBlue((*sourceIt).centroid)) + (1 - ratio) * qreal(qBlue((*destIt).centroid));
357 (*destIt).ratio += (*sourceIt).ratio;
358 (*destIt).centroid = qRgb(r, g, b);
359 itemsToDelete.push_back(std::distance(imageData.m_clusters.begin(), sourceIt));
364 for (
auto i : std::as_const(itemsToDelete)) {
365 imageData.m_clusters.removeAt(i);
368 imageData.m_highlight =
QColor();
369 imageData.m_dominant =
QColor(imageData.m_clusters.first().centroid);
373 imageData.m_palette.clear();
377 for (
const auto &stat : std::as_const(imageData.m_clusters)) {
380 entry[QStringLiteral(
"color")] = color;
381 entry[QStringLiteral(
"ratio")] =
stat.ratio;
383 QColor contrast =
QColor(255 - color.red(), 255 - color.green(), 255 - color.blue());
388 int minimumDistance = 4681800;
389 for (
const auto &stat : std::as_const(imageData.m_clusters)) {
392 if (distance < minimumDistance) {
398 if (imageData.m_clusters.size() <= 3) {
399 if (qGray(imageData.m_dominant.rgb()) < 120) {
400 contrast =
QColor(230, 230, 230);
402 contrast =
QColor(20, 20, 20);
405 }
else if (
squareDistance(contrast.
rgb(), tempContrast.
rgb()) < s_minimumSquareDistance * 1.5) {
406 contrast = tempContrast;
408 contrast = tempContrast;
414 entry[QStringLiteral(
"contrastColor")] = contrast;
417 imageData.m_dominantContrast = contrast;
418 imageData.m_dominant = color;
423 imageData.m_highlight = color;
426 if (qGray(color.rgb()) > qGray(imageData.m_closestToWhite.rgb())) {
427 imageData.m_closestToWhite = color;
429 if (qGray(color.rgb()) < qGray(imageData.m_closestToBlack.rgb())) {
430 imageData.m_closestToBlack = color;
432 imageData.m_palette << entry;
435 postProcess(imageData);
440 double ImageColors::getClusterScore(
const ImageData::colorStat &stat)
const
445 void ImageColors::postProcess(ImageData &imageData)
const
447 constexpr
short unsigned WCAG_NON_TEXT_CONTRAST_RATIO = 3;
448 constexpr qreal WCAG_TEXT_CONTRAST_RATIO = 4.5;
449 const QColor backgroundColor =
static_cast<Kirigami::PlatformTheme *
>(qmlAttachedPropertiesObject<Kirigami::PlatformTheme>(
this,
true))->backgroundColor();
450 const qreal backgroundLum = ColorUtils::luminance(backgroundColor);
451 qreal lowerLum, upperLum;
453 if (qGray(backgroundColor.
rgb()) < 192) {
455 lowerLum = WCAG_NON_TEXT_CONTRAST_RATIO * (backgroundLum + 0.05) - 0.05;
460 const QColor textColor =
static_cast<Kirigami::PlatformTheme *
>(qmlAttachedPropertiesObject<Kirigami::PlatformTheme>(
this,
true))->textColor();
461 const qreal textLum = ColorUtils::luminance(textColor);
462 lowerLum = WCAG_TEXT_CONTRAST_RATIO * (textLum + 0.05) - 0.05;
463 upperLum = backgroundLum;
466 auto adjustSaturation = [](
QColor &color) {
468 if (color.hsvSaturationF() < 0.5) {
469 const qreal h = color.hsvHueF();
470 const qreal v = color.valueF();
471 color.setHsvF(h, 0.5, v);
474 adjustSaturation(imageData.m_dominant);
475 adjustSaturation(imageData.m_highlight);
476 adjustSaturation(imageData.m_average);
478 auto adjustLightness = [lowerLum, upperLum](
QColor &color) {
479 short unsigned colorOperationCount = 0;
480 const qreal h = color.hslHueF();
481 const qreal s = color.hslSaturationF();
482 const qreal l = color.lightnessF();
483 while (ColorUtils::luminance(color.rgb()) < lowerLum && colorOperationCount++ < 10) {
484 color.setHslF(h, s, std::min(1.0, l + colorOperationCount * 0.03));
486 while (ColorUtils::luminance(color.rgb()) > upperLum && colorOperationCount++ < 10) {
487 color.setHslF(h, s, std::max(0.0, l - colorOperationCount * 0.03));
490 adjustLightness(imageData.m_dominant);
491 adjustLightness(imageData.m_highlight);
492 adjustLightness(imageData.m_average);
497 if (m_futureImageData) {
498 qCWarning(KirigamiLog) << m_futureImageData->future().isFinished();
500 return_fallback(m_fallbackPalette)
return m_imageData.m_palette;
506 return_fallback(m_fallbackPaletteBrightness)
515 return_fallback_finally(m_fallbackAverage, linkBackgroundColor)
517 return m_imageData.m_average;
524 return_fallback_finally(m_fallbackDominant, linkBackgroundColor)
526 return m_imageData.m_dominant;
533 return_fallback_finally(m_fallbackDominantContrasting, linkBackgroundColor)
535 return m_imageData.m_dominantContrast;
542 return_fallback_finally(m_fallbackForeground, textColor)
546 if (qGray(m_imageData.m_closestToWhite.rgb()) < 200) {
547 return QColor(230, 230, 230);
549 return m_imageData.m_closestToWhite;
551 if (qGray(m_imageData.m_closestToBlack.rgb()) > 80) {
552 return QColor(20, 20, 20);
554 return m_imageData.m_closestToBlack;
562 return_fallback_finally(m_fallbackBackground, backgroundColor)
565 if (qGray(m_imageData.m_closestToBlack.rgb()) > 80) {
566 return QColor(20, 20, 20);
568 return m_imageData.m_closestToBlack;
570 if (qGray(m_imageData.m_closestToWhite.rgb()) < 200) {
571 return QColor(230, 230, 230);
573 return m_imageData.m_closestToWhite;
581 return_fallback_finally(m_fallbackHighlight, linkColor)
583 return m_imageData.m_highlight;
591 if (qGray(m_imageData.m_closestToWhite.rgb()) < 200) {
592 return QColor(230, 230, 230);
596 return m_imageData.m_closestToWhite;
603 if (qGray(m_imageData.m_closestToBlack.rgb()) > 80) {
604 return QColor(20, 20, 20);
607 return m_imageData.m_closestToBlack;
610 #include "moc_imagecolors.cpp"