Kirigami2

imagecolors.cpp
1 /*
2  * Copyright 2020 Marco Martin <[email protected]>
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 #include "platformtheme.h"
21 
22 #include <QDebug>
23 #include <QGuiApplication>
24 #include <QTimer>
25 #include <QtConcurrent>
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 #define return_fallback(value) \
37  if (m_imageData.m_samples.size() == 0) { \
38  return value; \
39  }
40 
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(); \
44  }
45 
46 ImageColors::ImageColors(QObject *parent)
47  : QObject(parent)
48 {
49  m_imageSyncTimer = new QTimer(this);
50  m_imageSyncTimer->setSingleShot(true);
51  m_imageSyncTimer->setInterval(100);
52  /* connect(m_imageSyncTimer, &QTimer::timeout, this, [this]() {
53  generatePalette();
54  });*/
55 }
56 
57 ImageColors::~ImageColors()
58 {
59 }
60 
61 void ImageColors::setSource(const QVariant &source)
62 {
63  if (m_futureSourceImageData) {
64  m_futureSourceImageData->cancel();
65  m_futureSourceImageData->deleteLater();
66  m_futureSourceImageData = nullptr;
67  }
68 
69  if (source.canConvert<QQuickItem *>()) {
70  setSourceItem(source.value<QQuickItem *>());
71  } else if (source.canConvert<QImage>()) {
72  setSourceImage(source.value<QImage>());
73  } else if (source.canConvert<QIcon>()) {
74  setSourceImage(source.value<QIcon>().pixmap(128, 128).toImage());
75  } else if (source.canConvert<QString>()) {
76  const QString sourceString = source.toString();
77 
78  if (QIcon::hasThemeIcon(sourceString)) {
79  setSourceImage(QIcon::fromTheme(sourceString).pixmap(128, 128).toImage());
80  } else {
81  QFuture<QImage> future = QtConcurrent::run([sourceString]() {
82  if (auto url = QUrl(sourceString); url.isLocalFile()) {
83  return QImage(url.toLocalFile());
84  }
85  return QImage(sourceString);
86  });
87  m_futureSourceImageData = new QFutureWatcher<QImage>(this);
88  connect(m_futureSourceImageData, &QFutureWatcher<QImage>::finished, this, [this, source]() {
89  const QImage image = m_futureSourceImageData->future().result();
90  m_futureSourceImageData->deleteLater();
91  m_futureSourceImageData = nullptr;
92  setSourceImage(image);
93  m_source = source;
94  Q_EMIT sourceChanged();
95  });
96  m_futureSourceImageData->setFuture(future);
97  return;
98  }
99  } else {
100  return;
101  }
102 
103  m_source = source;
104  Q_EMIT sourceChanged();
105 }
106 
108 {
109  return m_source;
110 }
111 
112 void ImageColors::setSourceImage(const QImage &image)
113 {
114  if (m_window) {
115  disconnect(m_window.data(), nullptr, this, nullptr);
116  }
117  if (m_sourceItem) {
118  disconnect(m_sourceItem.data(), nullptr, this, nullptr);
119  }
120  if (m_grabResult) {
121  disconnect(m_grabResult.data(), nullptr, this, nullptr);
122  m_grabResult.clear();
123  }
124 
125  m_sourceItem.clear();
126 
127  m_sourceImage = image;
128  update();
129 }
130 
131 QImage ImageColors::sourceImage() const
132 {
133  return m_sourceImage;
134 }
135 
136 void ImageColors::setSourceItem(QQuickItem *source)
137 {
138  if (m_sourceItem == source) {
139  return;
140  }
141 
142  if (m_window) {
143  disconnect(m_window.data(), nullptr, this, nullptr);
144  }
145  if (m_sourceItem) {
146  disconnect(m_sourceItem, nullptr, this, nullptr);
147  }
148  m_sourceItem = source;
149  update();
150 
151  if (m_sourceItem) {
152  auto syncWindow = [this]() {
153  if (m_window) {
154  disconnect(m_window.data(), nullptr, this, nullptr);
155  }
156  m_window = m_sourceItem->window();
157  if (m_window) {
158  connect(m_window, &QWindow::visibleChanged, this, &ImageColors::update);
159  }
160  };
161 
162  connect(m_sourceItem, &QQuickItem::windowChanged, this, syncWindow);
163  syncWindow();
164  }
165 }
166 
167 QQuickItem *ImageColors::sourceItem() const
168 {
169  return m_sourceItem;
170 }
171 
172 void ImageColors::update()
173 {
174  if (m_futureImageData) {
175  m_futureImageData->cancel();
176  m_futureImageData->deleteLater();
177  m_futureImageData = nullptr;
178  }
179  auto runUpdate = [this]() {
180  QFuture<ImageData> future = QtConcurrent::run([this]() {
181  return generatePalette(m_sourceImage);
182  });
183  m_futureImageData = new QFutureWatcher<ImageData>(this);
184  connect(m_futureImageData, &QFutureWatcher<ImageData>::finished, this, [this]() {
185  if (!m_futureImageData) {
186  return;
187  }
188  m_imageData = m_futureImageData->future().result();
189  m_futureImageData->deleteLater();
190  m_futureImageData = nullptr;
191 
192  Q_EMIT paletteChanged();
193  });
194  m_futureImageData->setFuture(future);
195  };
196 
197  if (!m_sourceItem) {
198  if (!m_sourceImage.isNull()) {
199  runUpdate();
200  } else {
201  m_imageData = {};
202  Q_EMIT paletteChanged();
203  }
204  return;
205  }
206 
207  if (m_grabResult) {
208  disconnect(m_grabResult.data(), nullptr, this, nullptr);
209  m_grabResult.clear();
210  }
211 
212  m_grabResult = m_sourceItem->grabToImage(QSize(128, 128));
213 
214  if (m_grabResult) {
215  connect(m_grabResult.data(), &QQuickItemGrabResult::ready, this, [this, runUpdate]() {
216  m_sourceImage = m_grabResult->image();
217  m_grabResult.clear();
218  runUpdate();
219  });
220  }
221 }
222 
223 inline int squareDistance(QRgb color1, QRgb color2)
224 {
225  // https://en.wikipedia.org/wiki/Color_difference
226  // Using RGB distance for performance, as CIEDE2000 istoo complicated
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);
231  } else {
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);
235  }
236 }
237 
238 void ImageColors::positionColor(QRgb rgb, QList<ImageData::colorStat> &clusters)
239 {
240  for (auto &stat : clusters) {
241  if (squareDistance(rgb, stat.centroid) < s_minimumSquareDistance) {
242  stat.colors.append(rgb);
243  return;
244  }
245  }
246 
247  ImageData::colorStat stat;
248  stat.colors.append(rgb);
249  stat.centroid = rgb;
250  clusters << stat;
251 }
252 
253 ImageData ImageColors::generatePalette(const QImage &sourceImage) const
254 {
255  ImageData imageData;
256 
257  if (sourceImage.isNull() || sourceImage.width() == 0) {
258  return imageData;
259  }
260 
261  imageData.m_clusters.clear();
262  imageData.m_samples.clear();
263 
264 #if HAVE_OpenMP
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){});
268 #endif
269  int r = 0;
270  int g = 0;
271  int b = 0;
272  int c = 0;
273 
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) {
277  const QColor sampleColor = sourceImage.pixelColor(x, y);
278  if (sampleColor.alpha() == 0) {
279  continue;
280  }
281  if (ColorUtils::chroma(sampleColor) < 20) {
282  continue;
283  }
284  QRgb rgb = sampleColor.rgb();
285  ++c;
286  r += qRed(rgb);
287  g += qGreen(rgb);
288  b += qBlue(rgb);
289 #if HAVE_OpenMP
290  tempSamples[omp_get_thread_num()] << rgb;
291 #else
292  imageData.m_samples << rgb;
293 #endif
294  }
295  } // END omp parallel for
296 
297 #if HAVE_OpenMP
298  for (auto &s : tempSamples) {
299  imageData.m_samples << std::move(s);
300  }
301 #endif
302 
303  if (imageData.m_samples.isEmpty()) {
304  return imageData;
305  }
306 
307  for (QRgb rgb : std::as_const(imageData.m_samples)) {
308  positionColor(rgb, imageData.m_clusters);
309  }
310 
311  imageData.m_average = QColor(r / c, g / c, b / c, 255);
312 
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];
317  r = 0;
318  g = 0;
319  b = 0;
320  c = 0;
321 
322  for (auto color : std::as_const(stat.colors)) {
323  c++;
324  r += qRed(color);
325  g += qGreen(color);
326  b += qBlue(color);
327  }
328  r = r / c;
329  g = g / c;
330  b = b / c;
331  stat.centroid = qRgb(r, g, b);
332  stat.ratio = qreal(stat.colors.count()) / qreal(imageData.m_samples.count());
333  stat.colors = QList<QRgb>({stat.centroid});
334  } // END omp parallel for
335 
336  for (auto color : std::as_const(imageData.m_samples)) {
337  positionColor(color, imageData.m_clusters);
338  }
339  }
340 
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);
343  });
344 
345  // compress blocks that became too similar
346  auto sourceIt = imageData.m_clusters.end();
347  // Use index instead of iterator, because QList::erase may invalidate iterator.
348  std::vector<int> itemsToDelete;
349  while (sourceIt != imageData.m_clusters.begin()) {
350  sourceIt--;
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));
360  break;
361  }
362  }
363  }
364  for (auto i : std::as_const(itemsToDelete)) {
365  imageData.m_clusters.removeAt(i);
366  }
367 
368  imageData.m_highlight = QColor();
369  imageData.m_dominant = QColor(imageData.m_clusters.first().centroid);
370  imageData.m_closestToBlack = Qt::white;
371  imageData.m_closestToWhite = Qt::black;
372 
373  imageData.m_palette.clear();
374 
375  bool first = true;
376 
377  for (const auto &stat : std::as_const(imageData.m_clusters)) {
378  QVariantMap entry;
379  const QColor color(stat.centroid);
380  entry[QStringLiteral("color")] = color;
381  entry[QStringLiteral("ratio")] = stat.ratio;
382 
383  QColor contrast = QColor(255 - color.red(), 255 - color.green(), 255 - color.blue());
384  contrast.setHsl(contrast.hslHue(), //
385  contrast.hslSaturation(), //
386  128 + (128 - contrast.lightness()));
387  QColor tempContrast;
388  int minimumDistance = 4681800; // max distance: 4*3*2*3*255*255
389  for (const auto &stat : std::as_const(imageData.m_clusters)) {
390  const int distance = squareDistance(contrast.rgb(), stat.centroid);
391 
392  if (distance < minimumDistance) {
393  tempContrast = QColor(stat.centroid);
394  minimumDistance = distance;
395  }
396  }
397 
398  if (imageData.m_clusters.size() <= 3) {
399  if (qGray(imageData.m_dominant.rgb()) < 120) {
400  contrast = QColor(230, 230, 230);
401  } else {
402  contrast = QColor(20, 20, 20);
403  }
404  // TODO: replace m_clusters.size() > 3 with entropy calculation
405  } else if (squareDistance(contrast.rgb(), tempContrast.rgb()) < s_minimumSquareDistance * 1.5) {
406  contrast = tempContrast;
407  } else {
408  contrast = tempContrast;
409  contrast.setHsl(contrast.hslHue(),
410  contrast.hslSaturation(),
411  contrast.lightness() > 128 ? qMin(contrast.lightness() + 20, 255) : qMax(0, contrast.lightness() - 20));
412  }
413 
414  entry[QStringLiteral("contrastColor")] = contrast;
415 
416  if (first) {
417  imageData.m_dominantContrast = contrast;
418  imageData.m_dominant = color;
419  }
420  first = false;
421 
422  if (!imageData.m_highlight.isValid() || ColorUtils::chroma(color) > ColorUtils::chroma(imageData.m_highlight)) {
423  imageData.m_highlight = color;
424  }
425 
426  if (qGray(color.rgb()) > qGray(imageData.m_closestToWhite.rgb())) {
427  imageData.m_closestToWhite = color;
428  }
429  if (qGray(color.rgb()) < qGray(imageData.m_closestToBlack.rgb())) {
430  imageData.m_closestToBlack = color;
431  }
432  imageData.m_palette << entry;
433  }
434 
435  postProcess(imageData);
436 
437  return imageData;
438 }
439 
440 double ImageColors::getClusterScore(const ImageData::colorStat &stat) const
441 {
442  return stat.ratio * ColorUtils::chroma(QColor(stat.centroid));
443 }
444 
445 void ImageColors::postProcess(ImageData &imageData) const
446 {
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;
452  // 192 is from kcm_colors
453  if (qGray(backgroundColor.rgb()) < 192) {
454  // (lowerLum + 0.05) / (backgroundLum + 0.05) >= 3
455  lowerLum = WCAG_NON_TEXT_CONTRAST_RATIO * (backgroundLum + 0.05) - 0.05;
456  upperLum = 0.95;
457  } else {
458  // For light themes, still prefer lighter colors
459  // (lowerLum + 0.05) / (textLum + 0.05) >= 4.5
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;
464  }
465 
466  auto adjustSaturation = [](QColor &color) {
467  // Adjust saturation to make the color more vibrant
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);
472  }
473  };
474  adjustSaturation(imageData.m_dominant);
475  adjustSaturation(imageData.m_highlight);
476  adjustSaturation(imageData.m_average);
477 
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));
485  }
486  while (ColorUtils::luminance(color.rgb()) > upperLum && colorOperationCount++ < 10) {
487  color.setHslF(h, s, std::max(0.0, l - colorOperationCount * 0.03));
488  }
489  };
490  adjustLightness(imageData.m_dominant);
491  adjustLightness(imageData.m_highlight);
492  adjustLightness(imageData.m_average);
493 }
494 
495 QVariantList ImageColors::palette() const
496 {
497  if (m_futureImageData) {
498  qCWarning(KirigamiLog) << m_futureImageData->future().isFinished();
499  }
500  return_fallback(m_fallbackPalette) return m_imageData.m_palette;
501 }
502 
504 {
505  /* clang-format off */
506  return_fallback(m_fallbackPaletteBrightness)
507 
508  return qGray(m_imageData.m_dominant.rgb()) < 128 ? ColorUtils::Dark : ColorUtils::Light;
509  /* clang-format on */
510 }
511 
513 {
514  /* clang-format off */
515  return_fallback_finally(m_fallbackAverage, linkBackgroundColor)
516 
517  return m_imageData.m_average;
518  /* clang-format on */
519 }
520 
522 {
523  /* clang-format off */
524  return_fallback_finally(m_fallbackDominant, linkBackgroundColor)
525 
526  return m_imageData.m_dominant;
527  /* clang-format on */
528 }
529 
531 {
532  /* clang-format off */
533  return_fallback_finally(m_fallbackDominantContrasting, linkBackgroundColor)
534 
535  return m_imageData.m_dominantContrast;
536  /* clang-format on */
537 }
538 
540 {
541  /* clang-format off */
542  return_fallback_finally(m_fallbackForeground, textColor)
543 
544  if (paletteBrightness() == ColorUtils::Dark)
545  {
546  if (qGray(m_imageData.m_closestToWhite.rgb()) < 200) {
547  return QColor(230, 230, 230);
548  }
549  return m_imageData.m_closestToWhite;
550  } else {
551  if (qGray(m_imageData.m_closestToBlack.rgb()) > 80) {
552  return QColor(20, 20, 20);
553  }
554  return m_imageData.m_closestToBlack;
555  }
556  /* clang-format on */
557 }
558 
560 {
561  /* clang-format off */
562  return_fallback_finally(m_fallbackBackground, backgroundColor)
563 
564  if (paletteBrightness() == ColorUtils::Dark) {
565  if (qGray(m_imageData.m_closestToBlack.rgb()) > 80) {
566  return QColor(20, 20, 20);
567  }
568  return m_imageData.m_closestToBlack;
569  } else {
570  if (qGray(m_imageData.m_closestToWhite.rgb()) < 200) {
571  return QColor(230, 230, 230);
572  }
573  return m_imageData.m_closestToWhite;
574  }
575  /* clang-format on */
576 }
577 
579 {
580  /* clang-format off */
581  return_fallback_finally(m_fallbackHighlight, linkColor)
582 
583  return m_imageData.m_highlight;
584  /* clang-format on */
585 }
586 
588 {
589  /* clang-format off */
590  return_fallback(Qt::white)
591  if (qGray(m_imageData.m_closestToWhite.rgb()) < 200) {
592  return QColor(230, 230, 230);
593  }
594  /* clang-format on */
595 
596  return m_imageData.m_closestToWhite;
597 }
598 
600 {
601  /* clang-format off */
602  return_fallback(Qt::black)
603  if (qGray(m_imageData.m_closestToBlack.rgb()) > 80) {
604  return QColor(20, 20, 20);
605  }
606  /* clang-format on */
607  return m_imageData.m_closestToBlack;
608 }
609 
610 #include "moc_imagecolors.cpp"
QColor dominant
The dominant color of the source image.
Definition: imagecolors.h:118
QColor closestToWhite
The lightest color of the source image.
Definition: imagecolors.h:160
QFuture< T > run(Function function,...)
QColor closestToBlack
The darkest color of the source image.
Definition: imagecolors.h:165
int height() const const
QFuture< T > future() const const
T value() const const
QRgb rgb() const const
void visibleChanged(bool arg)
QIcon fromTheme(const QString &name)
@ Light
The item is light and requires a dark foreground color to achieve readable contrast.
Definition: colorutils.h:27
void setHsl(int h, int s, int l, int a)
void windowChanged(QQuickWindow *window)
KOSM_EXPORT double distance(const std::vector< const OSM::Node * > &path, Coordinate coord)
@ Dark
The item is dark and requires a light foreground color to achieve readable contrast.
Definition: colorutils.h:26
QColor average
The average color of the source image.
Definition: imagecolors.h:108
bool isNull() const const
QColor dominantContrast
Suggested "contrasting" color to the dominant one.
Definition: imagecolors.h:123
int alpha() const const
bool hasThemeIcon(const QString &name)
QColor background
A color suitable for rendering a background behind the source image.
Definition: imagecolors.h:155
QImage toImage() const const
bool canConvert(int targetTypeId) const const
QColor foreground
A color suitable for rendering text and other foreground over the source image.
Definition: imagecolors.h:144
int hslSaturation() const const
static Q_INVOKABLE qreal chroma(const QColor &color)
Returns the CIELAB chroma of the given color.
Definition: colorutils.cpp:301
QVariant source
The source from which colors should be extracted from.
Definition: imagecolors.h:79
int lightness() const const
QPixmap pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) const const
QColor highlight
An accent color extracted from the source image.
Definition: imagecolors.h:133
ColorUtils::Brightness paletteBrightness
Information whether the palette is towards a light or dark color scheme, possible values are:
Definition: imagecolors.h:103
bool isLocalFile() const const
void update(Part *part, const QByteArray &data, qint64 dataSize)
int hslHue() const const
QColor pixelColor(int x, int y) const const
QVariantList palette
A list of colors and related information about then.
Definition: imagecolors.h:95
Eigen::MatrixXd squareDistance(const Eigen::MatrixXd &a)
Brightness
Describes the contrast of an item.
Definition: colorutils.h:25
QString toString() const const
int stat(const QString &path, KDE_struct_stat *buf)
int width() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Sun Jan 29 2023 04:11:03 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.