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 | #pragma once |
9 | |
10 | #include <QColor> |
11 | #include <QFuture> |
12 | #include <QImage> |
13 | #include <QObject> |
14 | #include <QPointer> |
15 | #include <QQuickItem> |
16 | #include <QQuickItemGrabResult> |
17 | #include <QQuickWindow> |
18 | |
19 | #include <platform/colorutils.h> |
20 | |
21 | /*! |
22 | * \qmlvaluetype imageColorsPaletteSwatch |
23 | * \inqmlmodule org.kde.kirigami |
24 | */ |
25 | struct PaletteSwatch { |
26 | Q_GADGET |
27 | QML_VALUE_TYPE(imageColorsPaletteSwatch) |
28 | |
29 | /*! |
30 | * \qmlproperty real imageColorsPaletteSwatch::ratio |
31 | * |
32 | * How dominant the color is in the source image. |
33 | */ |
34 | Q_PROPERTY(qreal ratio READ ratio FINAL) |
35 | |
36 | /*! |
37 | * \qmlproperty color imageColorsPaletteSwatch::color |
38 | * |
39 | * The color of the list item |
40 | */ |
41 | Q_PROPERTY(QColor color READ color FINAL) |
42 | |
43 | /*! |
44 | * \qmlproperty color imageColorsPaletteSwatch::contrastColor |
45 | * |
46 | * The color from the source image that's closest to the inverse of color. |
47 | */ |
48 | Q_PROPERTY(QColor contrastColor READ contrastColor FINAL) |
49 | |
50 | public: |
51 | explicit PaletteSwatch(); |
52 | explicit PaletteSwatch(qreal ratio, const QColor &color, const QColor &contrastColor); |
53 | |
54 | qreal ratio() const; |
55 | const QColor &color() const; |
56 | const QColor &contrastColor() const; |
57 | |
58 | bool operator==(const PaletteSwatch &other) const; |
59 | |
60 | private: |
61 | qreal m_ratio; |
62 | QColor m_color; |
63 | QColor m_contrastColor; |
64 | }; |
65 | |
66 | struct ImageData { |
67 | struct colorStat { |
68 | QList<QRgb> colors; |
69 | QRgb centroid = 0; |
70 | qreal ratio = 0; |
71 | }; |
72 | |
73 | struct colorSet { |
74 | QColor average; |
75 | QColor text; |
76 | QColor background; |
77 | QColor highlight; |
78 | }; |
79 | |
80 | QList<QRgb> m_samples; |
81 | QList<colorStat> m_clusters; |
82 | QList<PaletteSwatch> m_palette; |
83 | |
84 | bool m_darkPalette = true; |
85 | QColor m_dominant = Qt::transparent; |
86 | QColor m_dominantContrast; |
87 | QColor m_average; |
88 | QColor m_highlight; |
89 | |
90 | QColor m_closestToBlack; |
91 | QColor m_closestToWhite; |
92 | }; |
93 | |
94 | /*! |
95 | * \qmltype ImageColors |
96 | * \inqmlmodule org.kde.kirigami |
97 | * |
98 | * \brief Extracts the dominant colors from an element or an image and exports it to a color palette. |
99 | */ |
100 | class ImageColors : public QObject |
101 | { |
102 | Q_OBJECT |
103 | QML_ELEMENT |
104 | /*! |
105 | * \qmlproperty var ImageColors::source |
106 | * |
107 | * The source from which colors should be extracted from. |
108 | * |
109 | * source can be one of the following: |
110 | * \list |
111 | * \li Item |
112 | * \li QImage |
113 | * \li QIcon |
114 | * \li Icon name |
115 | * \endlist |
116 | * |
117 | * Note that an Item's color palette will only be extracted once unless you |
118 | * call update(), regardless of how the item hanges. |
119 | */ |
120 | Q_PROPERTY(QVariant source READ source WRITE setSource NOTIFY sourceChanged FINAL) |
121 | |
122 | /*! |
123 | * \qmlproperty list<imageColorsPaletteSwatch> ImageColors::palette |
124 | * |
125 | * A list of colors and related information about then. |
126 | * |
127 | * Each list item has the following properties: |
128 | * \list |
129 | * \li color: The color of the list item. |
130 | * \li ratio: How dominant the color is in the source image. |
131 | * \li contrastingColor: The color from the source image that's closest to the inverse of color. |
132 | * \endlist |
133 | * |
134 | * The list is sorted by \c ratio; the first element is the most |
135 | * dominant color in the source image and the last element is the |
136 | * least dominant color of the image. |
137 | * |
138 | * \note K-means clustering is used to extract these colors; see \l {https://en.wikipedia.org/wiki/K-means_clustering} {K-Means Clustering (Wikipedia)}. |
139 | */ |
140 | Q_PROPERTY(QList<PaletteSwatch> palette READ palette NOTIFY paletteChanged FINAL) |
141 | |
142 | /*! |
143 | * \qmlproperty int ImageColors::paletteBrightness |
144 | * |
145 | * Information whether the palette is towards a light or dark color |
146 | * scheme, possible values are: |
147 | * \list |
148 | * \li ColorUtils.Light |
149 | * \li ColorUtils.Dark |
150 | * \endlist |
151 | */ |
152 | Q_PROPERTY(ColorUtils::Brightness paletteBrightness READ paletteBrightness NOTIFY paletteChanged FINAL) |
153 | |
154 | /*! |
155 | * \qmlproperty color ImageColors::average |
156 | * |
157 | * The average color of the source image. |
158 | */ |
159 | Q_PROPERTY(QColor average READ average NOTIFY paletteChanged FINAL) |
160 | |
161 | /*! |
162 | * \qmlproperty color ImageColors::dominant |
163 | * |
164 | * The dominant color of the source image. |
165 | * |
166 | * The dominant color of the image is the color of the largest |
167 | * cluster in the image. |
168 | * |
169 | * See \l {https://en.wikipedia.org/wiki/K-means_clustering} {K-Means Clustering (Wikipedia)} |
170 | */ |
171 | Q_PROPERTY(QColor dominant READ dominant NOTIFY paletteChanged FINAL) |
172 | |
173 | /*! |
174 | * \qmlproperty color ImageColors::dominantContrast |
175 | * |
176 | * Suggested "contrasting" color to the dominant one. It's the color in the palette nearest to the negative of the dominant |
177 | */ |
178 | Q_PROPERTY(QColor dominantContrast READ dominantContrast NOTIFY paletteChanged FINAL) |
179 | |
180 | /*! |
181 | * \qmlproperty color ImageColors::highlight |
182 | * |
183 | * An accent color extracted from the source image. |
184 | * |
185 | * The accent color is the color cluster with the highest CIELAB |
186 | * chroma in the source image. |
187 | * |
188 | * See \l {https://en.wikipedia.org/wiki/Colorfulness#Chroma} {Chroma (Wikipedia)} |
189 | */ |
190 | Q_PROPERTY(QColor highlight READ highlight NOTIFY paletteChanged FINAL) |
191 | |
192 | /*! |
193 | * \qmlproperty color ImageColors::foreground |
194 | * |
195 | * A color suitable for rendering text and other foreground |
196 | * over the source image. |
197 | * |
198 | * On dark items, this will be the color closest to white in |
199 | * the image if it's light enough, or a bright gray otherwise. |
200 | * On light items, this will be the color closest to black in |
201 | * the image if it's dark enough, or a dark gray otherwise. |
202 | */ |
203 | Q_PROPERTY(QColor foreground READ foreground NOTIFY paletteChanged FINAL) |
204 | |
205 | /*! |
206 | * \qmlproperty color ImageColors::background |
207 | * |
208 | * A color suitable for rendering a background behind the |
209 | * source image. |
210 | * |
211 | * On dark items, this will be the color closest to black in the |
212 | * image if it's dark enough, or a dark gray otherwise. |
213 | * On light items, this will be the color closest to white |
214 | * in the image if it's light enough, or a bright gray otherwise. |
215 | */ |
216 | Q_PROPERTY(QColor background READ background NOTIFY paletteChanged FINAL) |
217 | |
218 | /*! |
219 | * \qmlproperty color ImageColors::closestToWhite |
220 | * |
221 | * The lightest color of the source image. |
222 | */ |
223 | Q_PROPERTY(QColor closestToWhite READ closestToWhite NOTIFY paletteChanged FINAL) |
224 | |
225 | /*! |
226 | * \qmlproperty color ImageColors::closestToBlack |
227 | * |
228 | * The darkest color of the source image. |
229 | */ |
230 | Q_PROPERTY(QColor closestToBlack READ closestToBlack NOTIFY paletteChanged FINAL) |
231 | |
232 | /*! |
233 | * \qmlproperty list<imageColorsPaletteSwatch> ImageColors::fallbackPalette |
234 | * |
235 | * The value to return when palette is not available, e.g. when |
236 | * ImageColors is still computing it or the source is invalid. |
237 | */ |
238 | Q_PROPERTY(QList<PaletteSwatch> fallbackPalette MEMBER m_fallbackPalette NOTIFY fallbackPaletteChanged FINAL) |
239 | |
240 | /*! |
241 | * \qmlproperty int ImageColors::fallbackPaletteBrightness |
242 | * |
243 | * The value to return when paletteBrightness is not available, e.g. when |
244 | * ImageColors is still computing it or the source is invalid. |
245 | */ |
246 | Q_PROPERTY(ColorUtils::Brightness fallbackPaletteBrightness MEMBER m_fallbackPaletteBrightness NOTIFY fallbackPaletteBrightnessChanged FINAL) |
247 | |
248 | /*! |
249 | * \qmlproperty color ImageColors::fallbackAverage |
250 | * |
251 | * The value to return when average is not available, e.g. when |
252 | * ImageColors is still computing it or the source is invalid. |
253 | */ |
254 | Q_PROPERTY(QColor fallbackAverage MEMBER m_fallbackAverage NOTIFY fallbackAverageChanged FINAL) |
255 | |
256 | /*! |
257 | * \qmlproperty color ImageColors::fallbackDominant |
258 | * |
259 | * The value to return when dominant is not available, e.g. when |
260 | * ImageColors is still computing it or the source is invalid. |
261 | */ |
262 | Q_PROPERTY(QColor fallbackDominant MEMBER m_fallbackDominant NOTIFY fallbackDominantChanged FINAL) |
263 | |
264 | /*! |
265 | * \qmlproperty color ImageColors::fallbackDominantContrasting |
266 | * |
267 | * The value to return when dominantContrasting is not available, e.g. when |
268 | * ImageColors is still computing it or the source is invalid. |
269 | */ |
270 | Q_PROPERTY(QColor fallbackDominantContrasting MEMBER m_fallbackDominantContrasting NOTIFY fallbackDominantContrastingChanged FINAL) |
271 | |
272 | /*! |
273 | * \qmlproperty color ImageColors::fallbackHighlight |
274 | * |
275 | * The value to return when highlight is not available, e.g. when |
276 | * ImageColors is still computing it or the source is invalid. |
277 | */ |
278 | Q_PROPERTY(QColor fallbackHighlight MEMBER m_fallbackHighlight NOTIFY fallbackHighlightChanged FINAL) |
279 | |
280 | /*! |
281 | * \qmlproperty color ImageColors::fallbackForeground |
282 | * |
283 | * The value to return when foreground is not available, e.g. when |
284 | * ImageColors is still computing it or the source is invalid. |
285 | */ |
286 | Q_PROPERTY(QColor fallbackForeground MEMBER m_fallbackForeground NOTIFY fallbackForegroundChanged FINAL) |
287 | |
288 | /*! |
289 | * \qmlproperty color ImageColors::fallbackBackground |
290 | * |
291 | * The value to return when background is not available, e.g. when |
292 | * ImageColors is still computing it or the source is invalid. |
293 | */ |
294 | Q_PROPERTY(QColor fallbackBackground MEMBER m_fallbackBackground NOTIFY fallbackBackgroundChanged FINAL) |
295 | |
296 | public: |
297 | explicit ImageColors(QObject *parent = nullptr); |
298 | ~ImageColors() override; |
299 | |
300 | void setSource(const QVariant &source); |
301 | QVariant source() const; |
302 | |
303 | void setSourceImage(const QImage &image); |
304 | QImage sourceImage() const; |
305 | |
306 | void setSourceItem(QQuickItem *source); |
307 | QQuickItem *sourceItem() const; |
308 | |
309 | /*! |
310 | * \qmlmethod void ImageColors::update() |
311 | * |
312 | * Updates the colors |
313 | */ |
314 | Q_INVOKABLE void update(); |
315 | |
316 | QList<PaletteSwatch> palette() const; |
317 | ColorUtils::Brightness paletteBrightness() const; |
318 | QColor average() const; |
319 | QColor dominant() const; |
320 | QColor dominantContrast() const; |
321 | QColor highlight() const; |
322 | QColor foreground() const; |
323 | QColor background() const; |
324 | QColor closestToWhite() const; |
325 | QColor closestToBlack() const; |
326 | |
327 | Q_SIGNALS: |
328 | void sourceChanged(); |
329 | void paletteChanged(); |
330 | void fallbackPaletteChanged(); |
331 | void fallbackPaletteBrightnessChanged(); |
332 | void fallbackAverageChanged(); |
333 | void fallbackDominantChanged(); |
334 | void fallbackDominantContrastingChanged(); |
335 | void fallbackHighlightChanged(); |
336 | void fallbackForegroundChanged(); |
337 | void fallbackBackgroundChanged(); |
338 | |
339 | private: |
340 | static inline void positionColor(QRgb rgb, QList<ImageData::colorStat> &clusters); |
341 | static void positionColorMP(const decltype(ImageData::m_samples) &samples, decltype(ImageData::m_clusters) &clusters, int numCore = 0); |
342 | static ImageData generatePalette(const QImage &sourceImage); |
343 | |
344 | static double getClusterScore(const ImageData::colorStat &stat); |
345 | void postProcess(ImageData &imageData) const; |
346 | |
347 | // Arbitrary number that seems to work well |
348 | static const int s_minimumSquareDistance = 32000; |
349 | QPointer<QQuickWindow> m_window; |
350 | QVariant m_source; |
351 | QPointer<QQuickItem> m_sourceItem; |
352 | QSharedPointer<QQuickItemGrabResult> m_grabResult; |
353 | QImage m_sourceImage; |
354 | QFutureWatcher<QImage> *m_futureSourceImageData = nullptr; |
355 | |
356 | QFutureWatcher<ImageData> *m_futureImageData = nullptr; |
357 | ImageData m_imageData; |
358 | |
359 | QList<PaletteSwatch> m_fallbackPalette; |
360 | ColorUtils::Brightness m_fallbackPaletteBrightness; |
361 | QColor m_fallbackAverage; |
362 | QColor m_fallbackDominant; |
363 | QColor m_fallbackDominantContrasting; |
364 | QColor m_fallbackHighlight; |
365 | QColor m_fallbackForeground; |
366 | QColor m_fallbackBackground; |
367 | }; |
368 | |