MauiKit Controls

imagecolors.cpp
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#include "platformtheme.h"
21
22#include <QDebug>
23#include <QTimer>
24#include <QtConcurrent>
25
26#include <cmath>
27
28#define return_fallback(value) \
29 if (m_imageData.m_samples.size() == 0) { \
30 return value; \
31 }
32
33#define return_fallback_finally(value, finally) \
34 if (m_imageData.m_samples.size() == 0) { \
35 return value.isValid() ? value : static_cast<Maui::PlatformTheme *>(qmlAttachedPropertiesObject<Maui::PlatformTheme>(this, true))->finally(); \
36 }
37
38ImageColors::ImageColors(QObject *parent)
39 : QObject(parent)
40{
41 m_imageSyncTimer = new QTimer(this);
42 m_imageSyncTimer->setSingleShot(true);
43 m_imageSyncTimer->setInterval(100);
44 /* connect(m_imageSyncTimer, &QTimer::timeout, this, [this]() {
45 generatePalette();
46 });*/
47}
48
49ImageColors::~ImageColors()
50{
51}
52
53void ImageColors::setSource(const QVariant &source)
54{
55 if (source.canConvert<QQuickItem *>()) {
56 qDebug() << "can convert to item";
57 setSourceItem(source.value<QQuickItem *>());
58 } else if (source.canConvert<QImage>()) {
59 qDebug() << "can convert to image";
60
61 setSourceImage(source.value<QImage>());
62 } else if (source.canConvert<QIcon>()) {
63 qDebug() << "can convert to icon";
64
65 setSourceImage(source.value<QIcon>().pixmap(128, 128).toImage());
66 } else if (source.canConvert<QString>()) {
67 qDebug() << "can convert to string";
68 if(source.toString().isEmpty())
69 {
70 return;
71 }
72
73 if(source.toString().startsWith("qrc:"))
74 {
75 qDebug() << "SET IMAGE FROM QRC IMAGE COLORS" << source.toString();
76 setSourceImage(QImage(source.toString().replace("qrc", "")));
77 }else
78 {
79
80 setSourceImage(QIcon::fromTheme(source.toString()).pixmap(128, 128).toImage());
81 }
82 } else {
83 return;
84 }
85
86 m_source = source;
87 Q_EMIT sourceChanged();
88}
89
91{
92 return m_source;
93}
94
95void ImageColors::setSourceImage(const QImage &image)
96{
97 if (m_window) {
98 disconnect(m_window.data(), nullptr, this, nullptr);
99 }
100 if (m_sourceItem) {
101 disconnect(m_sourceItem.data(), nullptr, this, nullptr);
102 }
103 if (m_grabResult) {
104 disconnect(m_grabResult.data(), nullptr, this, nullptr);
105 m_grabResult.clear();
106 }
107
108 m_sourceItem.clear();
109
110 m_sourceImage = image;
111 update();
112}
113
114QImage ImageColors::sourceImage() const
115{
116 return m_sourceImage;
117}
118
119void ImageColors::setSourceItem(QQuickItem *source)
120{
121 if (m_sourceItem == source) {
122 return;
123 }
124
125 if (m_window) {
126 disconnect(m_window.data(), nullptr, this, nullptr);
127 }
128 if (m_sourceItem) {
129 disconnect(m_sourceItem, nullptr, this, nullptr);
130 }
131 m_sourceItem = source;
132 update();
133
134 if (m_sourceItem) {
135 auto syncWindow = [this]() {
136 if (m_window) {
137 disconnect(m_window.data(), nullptr, this, nullptr);
138 }
139 m_window = m_sourceItem->window();
140 if (m_window) {
141 connect(m_window, &QWindow::visibleChanged, this, &ImageColors::update);
142 }
143 };
144
145 connect(m_sourceItem, &QQuickItem::windowChanged, this, syncWindow);
146 syncWindow();
147 }
148}
149
150QQuickItem *ImageColors::sourceItem() const
151{
152 return m_sourceItem;
153}
154
155void ImageColors::update()
156{
157 if (m_futureImageData) {
158 m_futureImageData->cancel();
159 m_futureImageData->deleteLater();
160 }
161 auto runUpdate = [this]() {
162 QFuture<ImageData> future = QtConcurrent::run([this]() {
163 return generatePalette(m_sourceImage);
164 });
165 m_futureImageData = new QFutureWatcher<ImageData>(this);
166 connect(m_futureImageData, &QFutureWatcher<ImageData>::finished, this, [this]() {
167 if (!m_futureImageData) {
168 return;
169 }
170 m_imageData = m_futureImageData->future().result();
171 m_futureImageData->deleteLater();
172 m_futureImageData = nullptr;
173
174 Q_EMIT paletteChanged();
175 });
176 m_futureImageData->setFuture(future);
177 };
178
179 if (!m_sourceItem || !m_window) {
180 if (!m_sourceImage.isNull()) {
181 runUpdate();
182 }
183 return;
184 }
185
186 if (m_grabResult) {
187 disconnect(m_grabResult.data(), nullptr, this, nullptr);
188 m_grabResult.clear();
189 }
190
191 m_grabResult = m_sourceItem->grabToImage(QSize(128, 128));
192
193 if (m_grabResult) {
194 connect(m_grabResult.data(), &QQuickItemGrabResult::ready, this, [this, runUpdate]() {
195 m_sourceImage = m_grabResult->image();
196 m_grabResult.clear();
197 runUpdate();
198 });
199 }
200}
201
202inline int squareDistance(QRgb color1, QRgb color2)
203{
204 // https://en.wikipedia.org/wiki/Color_difference
205 // Using RGB distance for performance, as CIEDE2000 istoo complicated
206 if (qRed(color1) - qRed(color2) < 128) {
207 return 2 * pow(qRed(color1) - qRed(color2), 2) //
208 + 4 * pow(qGreen(color1) - qGreen(color2), 2) //
209 + 3 * pow(qBlue(color1) - qBlue(color2), 2);
210 } else {
211 return 3 * pow(qRed(color1) - qRed(color2), 2) //
212 + 4 * pow(qGreen(color1) - qGreen(color2), 2) //
213 + 2 * pow(qBlue(color1) - qBlue(color2), 2);
214 }
215}
216
217void ImageColors::positionColor(QRgb rgb, QList<ImageData::colorStat> &clusters)
218{
219 for (auto &stat : clusters) {
220 if (squareDistance(rgb, stat.centroid) < s_minimumSquareDistance) {
221 stat.colors.append(rgb);
222 return;
223 }
224 }
225
226 ImageData::colorStat stat;
227 stat.colors.append(rgb);
228 stat.centroid = rgb;
229 clusters << stat;
230}
231
232ImageData ImageColors::generatePalette(const QImage &sourceImage)
233{
234 ImageData imageData;
235
236 if (sourceImage.isNull() || sourceImage.width() == 0) {
237 return imageData;
238 }
239
240 imageData.m_clusters.clear();
241 imageData.m_samples.clear();
242
243 QColor sampleColor;
244 int r = 0;
245 int g = 0;
246 int b = 0;
247 int c = 0;
248 for (int x = 0; x < sourceImage.width(); ++x) {
249 for (int y = 0; y < sourceImage.height(); ++y) {
250 sampleColor = sourceImage.pixelColor(x, y);
251 if (sampleColor.alpha() == 0) {
252 continue;
253 }
254 QRgb rgb = sampleColor.rgb();
255 c++;
256 r += qRed(rgb);
257 g += qGreen(rgb);
258 b += qBlue(rgb);
259 imageData.m_samples << rgb;
260 positionColor(rgb, imageData.m_clusters);
261 }
262 }
263
264 if (imageData.m_samples.isEmpty()) {
265 return imageData;
266 }
267
268 imageData.m_average = QColor(r / c, g / c, b / c, 255);
269
270 for (int iteration = 0; iteration < 5; ++iteration) {
271 for (auto &stat : imageData.m_clusters) {
272 r = 0;
273 g = 0;
274 b = 0;
275 c = 0;
276
277 for (auto color : std::as_const(stat.colors)) {
278 c++;
279 r += qRed(color);
280 g += qGreen(color);
281 b += qBlue(color);
282 }
283 r = r / c;
284 g = g / c;
285 b = b / c;
286 stat.centroid = qRgb(r, g, b);
287 stat.ratio = qreal(stat.colors.count()) / qreal(imageData.m_samples.count());
288 stat.colors = QList<QRgb>({stat.centroid});
289 }
290
291 for (auto color : std::as_const(imageData.m_samples)) {
292 positionColor(color, imageData.m_clusters);
293 }
294 }
295
296 std::sort(imageData.m_clusters.begin(), imageData.m_clusters.end(), [](const ImageData::colorStat &a, const ImageData::colorStat &b) {
297 return a.colors.size() > b.colors.size();
298 });
299
300 // compress blocks that became too similar
301 auto sourceIt = imageData.m_clusters.end();
302 QList<QList<ImageData::colorStat>::iterator> itemsToDelete;
303 while (sourceIt != imageData.m_clusters.begin()) {
304 sourceIt--;
305 for (auto destIt = imageData.m_clusters.begin(); destIt != imageData.m_clusters.end() && destIt != sourceIt; destIt++) {
306 if (squareDistance((*sourceIt).centroid, (*destIt).centroid) < s_minimumSquareDistance) {
307 const qreal ratio = (*sourceIt).ratio / (*destIt).ratio;
308 const int r = ratio * qreal(qRed((*sourceIt).centroid)) + (1 - ratio) * qreal(qRed((*destIt).centroid));
309 const int g = ratio * qreal(qGreen((*sourceIt).centroid)) + (1 - ratio) * qreal(qGreen((*destIt).centroid));
310 const int b = ratio * qreal(qBlue((*sourceIt).centroid)) + (1 - ratio) * qreal(qBlue((*destIt).centroid));
311 (*destIt).ratio += (*sourceIt).ratio;
312 (*destIt).centroid = qRgb(r, g, b);
313 itemsToDelete << sourceIt;
314 break;
315 }
316 }
317 }
318 for (const auto &i : std::as_const(itemsToDelete)) {
319 imageData.m_clusters.erase(i);
320 }
321
322 imageData.m_highlight = QColor();
323 imageData.m_dominant = QColor(imageData.m_clusters.first().centroid);
324 imageData.m_closestToBlack = Qt::white;
325 imageData.m_closestToWhite = Qt::black;
326
327 imageData.m_palette.clear();
328
329 bool first = true;
330
331 for (const auto &stat : std::as_const(imageData.m_clusters)) {
332 QVariantMap entry;
333 const QColor color(stat.centroid);
334 entry[QStringLiteral("color")] = color;
335 entry[QStringLiteral("ratio")] = stat.ratio;
336
337 QColor contrast = QColor(255 - color.red(), 255 - color.green(), 255 - color.blue());
338 contrast.setHsl(contrast.hslHue(), //
339 contrast.hslSaturation(), //
340 128 + (128 - contrast.lightness()));
341 QColor tempContrast;
342 int minimumDistance = 4681800; // max distance: 4*3*2*3*255*255
343 for (const auto &stat : std::as_const(imageData.m_clusters)) {
344 const int distance = squareDistance(contrast.rgb(), stat.centroid);
345
346 if (distance < minimumDistance) {
347 tempContrast = QColor(stat.centroid);
348 minimumDistance = distance;
349 }
350 }
351
352 if (imageData.m_clusters.size() <= 3) {
353 if (qGray(imageData.m_dominant.rgb()) < 120) {
354 contrast = QColor(230, 230, 230);
355 } else {
356 contrast = QColor(20, 20, 20);
357 }
358 // TODO: replace m_clusters.size() > 3 with entropy calculation
359 } else if (squareDistance(contrast.rgb(), tempContrast.rgb()) < s_minimumSquareDistance * 1.5) {
360 contrast = tempContrast;
361 } else {
362 contrast = tempContrast;
363 contrast.setHsl(contrast.hslHue(),
364 contrast.hslSaturation(),
365 contrast.lightness() > 128 ? qMin(contrast.lightness() + 20, 255) : qMax(0, contrast.lightness() - 20));
366 }
367
368 entry[QStringLiteral("contrastColor")] = contrast;
369
370 if (first) {
371 imageData.m_dominantContrast = contrast;
372 imageData.m_dominant = color;
373 }
374 first = false;
375
376 if (!imageData.m_highlight.isValid() || ColorUtils::chroma(color) > ColorUtils::chroma(imageData.m_highlight)) {
377 imageData.m_highlight = color;
378 }
379
380 if (qGray(color.rgb()) > qGray(imageData.m_closestToWhite.rgb())) {
381 imageData.m_closestToWhite = color;
382 }
383 if (qGray(color.rgb()) < qGray(imageData.m_closestToBlack.rgb())) {
384 imageData.m_closestToBlack = color;
385 }
386 imageData.m_palette << entry;
387 }
388
389 return imageData;
390}
391
392QVariantList ImageColors::palette() const
393{
394 if (m_futureImageData) {
395 qWarning() << m_futureImageData->future().isFinished();
396 }
397 return_fallback(m_fallbackPalette) return m_imageData.m_palette;
398}
399
401{
402 /* clang-format off */
403 return_fallback(m_fallbackPaletteBrightness)
404
405 return qGray(m_imageData.m_dominant.rgb()) < 128 ? ColorUtils::Dark : ColorUtils::Light;
406 /* clang-format on */
407}
408
410{
411 /* clang-format off */
412 return_fallback_finally(m_fallbackAverage, linkBackgroundColor)
413
414 return m_imageData.m_average;
415 /* clang-format on */
416}
417
419{
420 /* clang-format off */
421 return_fallback_finally(m_fallbackDominant, linkBackgroundColor)
422
423 return m_imageData.m_dominant;
424 /* clang-format on */
425}
426
428{
429 /* clang-format off */
430 return_fallback_finally(m_fallbackDominantContrasting, linkBackgroundColor)
431
432 return m_imageData.m_dominantContrast;
433 /* clang-format on */
434}
435
437{
438 /* clang-format off */
439 return_fallback_finally(m_fallbackForeground, textColor)
440
442 {
443 if (qGray(m_imageData.m_closestToWhite.rgb()) < 200) {
444 return QColor(230, 230, 230);
445 }
446 return m_imageData.m_closestToWhite;
447 } else {
448 if (qGray(m_imageData.m_closestToBlack.rgb()) > 80) {
449 return QColor(20, 20, 20);
450 }
451 return m_imageData.m_closestToBlack;
452 }
453 /* clang-format on */
454}
455
457{
458 /* clang-format off */
459 return_fallback_finally(m_fallbackBackground, backgroundColor)
460
462 if (qGray(m_imageData.m_closestToBlack.rgb()) > 80) {
463 return QColor(20, 20, 20);
464 }
465 return m_imageData.m_closestToBlack;
466 } else {
467 if (qGray(m_imageData.m_closestToWhite.rgb()) < 200) {
468 return QColor(230, 230, 230);
469 }
470 return m_imageData.m_closestToWhite;
471 }
472 /* clang-format on */
473}
474
476{
477 /* clang-format off */
478 return_fallback_finally(m_fallbackHighlight, linkColor)
479
480 return m_imageData.m_highlight;
481 /* clang-format on */
482}
483
485{
486 /* clang-format off */
487 return_fallback(Qt::white)
488 if (qGray(m_imageData.m_closestToWhite.rgb()) < 200) {
489 return QColor(230, 230, 230);
490 }
491 /* clang-format on */
492
493 return m_imageData.m_closestToWhite;
494}
495
497{
498 /* clang-format off */
499 return_fallback(Qt::black)
500 if (qGray(m_imageData.m_closestToBlack.rgb()) > 80) {
501 return QColor(20, 20, 20);
502 }
503 /* clang-format on */
504 return m_imageData.m_closestToBlack;
505}
506
507#include "moc_imagecolors.cpp"
static Q_INVOKABLE qreal chroma(const QColor &color)
Returns the CIELAB chroma of the given color.
Brightness
Describes the contrast of an item.
Definition colorutils.h:27
@ Light
The item is light and requires a dark foreground color to achieve readable contrast.
Definition colorutils.h:29
@ Dark
The item is dark and requires a light foreground color to achieve readable contrast.
Definition colorutils.h:28
QML_ELEMENTQVariant source
The source from which colors should be extracted from.
Definition imagecolors.h:80
QColor foreground
A color suitable for rendering text and other foreground over the source image.
QColor closestToWhite
The lightest color of the source image.
QColor dominant
The dominant color of the source image.
QVariantList palette
A list of colors and related information about then.
Definition imagecolors.h:96
QColor closestToBlack
The darkest color of the source image.
QColor dominantContrast
Suggested "contrasting" color to the dominant one.
ColorUtils::Brightness paletteBrightness
Information whether the palette is towards a light or dark color scheme, possible values are:
QColor average
The average color of the source image.
QColor background
A color suitable for rendering a background behind the source image.
QColor highlight
An accent color extracted from the source image.
KIOCORE_EXPORT StatJob * stat(const QUrl &url, JobFlags flags=DefaultFlags)
KOSM_EXPORT double distance(const std::vector< const OSM::Node * > &path, Coordinate coord)
int alpha() const const
int hslHue() const const
int hslSaturation() const const
bool isValid() const const
int lightness() const const
QRgb rgb() const const
void setHsl(int h, int s, int l, int a)
QPixmap pixmap(QWindow *window, const QSize &size, Mode mode, State state) const const
QIcon fromTheme(const QString &name)
iterator begin()
void clear()
qsizetype count() const const
iterator end()
iterator erase(const_iterator begin, const_iterator end)
T & first()
bool isEmpty() const const
qsizetype size() const const
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool disconnect(const QMetaObject::Connection &connection)
QImage toImage() const const
QSharedPointer< QQuickItemGrabResult > grabToImage(const QSize &targetSize)
void windowChanged(QQuickWindow *window)
QFuture< T > run(Function function,...)
void clear()
void visibleChanged(bool arg)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri May 2 2025 11:57:11 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.