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