8#include "imagecolors.h"
11#include <QFutureWatcher>
12#include <QGuiApplication>
13#include <QtConcurrentRun>
15#include "loggingcategory.h"
19#include "config-OpenMP.h"
24#include "platform/platformtheme.h"
26#define return_fallback(value) \
27 if (m_imageData.m_samples.size() == 0) { \
31#define return_fallback_finally(value, finally) \
32 if (m_imageData.m_samples.size() == 0) { \
33 return value.isValid() \
35 : static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true))->finally(); \
38PaletteSwatch::PaletteSwatch()
42PaletteSwatch::PaletteSwatch(qreal ratio,
const QColor &color,
const QColor &contrastColor)
45 , m_contrastColor(contrastColor)
49qreal PaletteSwatch::ratio()
const
54const QColor &PaletteSwatch::color()
const
59const QColor &PaletteSwatch::contrastColor()
const
61 return m_contrastColor;
64bool PaletteSwatch::operator==(
const PaletteSwatch &other)
const
66 return m_ratio == other.m_ratio
67 && m_color == other.m_color
68 && m_contrastColor == other.m_contrastColor;
71ImageColors::ImageColors(
QObject *parent)
76ImageColors::~ImageColors()
80void ImageColors::setSource(
const QVariant &source)
82 if (m_futureSourceImageData) {
83 m_futureSourceImageData->
cancel();
84 m_futureSourceImageData->deleteLater();
85 m_futureSourceImageData =
nullptr;
102 return QImage(url.toLocalFile());
104 return QImage(sourceString);
108 const QImage image = m_futureSourceImageData->future().result();
109 m_futureSourceImageData->deleteLater();
110 m_futureSourceImageData =
nullptr;
111 setSourceImage(image);
115 m_futureSourceImageData->setFuture(future);
131void ImageColors::setSourceImage(
const QImage &image)
141 m_grabResult.
clear();
144 m_sourceItem.
clear();
146 m_sourceImage = image;
150QImage ImageColors::sourceImage()
const
152 return m_sourceImage;
155void ImageColors::setSourceItem(
QQuickItem *source)
157 if (m_sourceItem ==
source) {
165 disconnect(m_sourceItem,
nullptr,
this,
nullptr);
171 auto syncWindow = [
this]() {
175 m_window = m_sourceItem->window();
192void ImageColors::update()
194 if (m_futureImageData) {
195 m_futureImageData->disconnect(
this,
nullptr);
196 m_futureImageData->
cancel();
197 m_futureImageData->deleteLater();
198 m_futureImageData =
nullptr;
201 auto runUpdate = [
this]() {
202 auto sourceImage{m_sourceImage};
204 return generatePalette(sourceImage);
208 if (!m_futureImageData) {
211 m_imageData = m_futureImageData->future().result();
212 postProcess(m_imageData);
213 m_futureImageData->deleteLater();
214 m_futureImageData =
nullptr;
218 m_futureImageData->setFuture(future);
221 if (!m_sourceItem || !m_sourceItem->window() || !m_sourceItem->window()->isVisible()) {
222 if (!m_sourceImage.
isNull()) {
233 m_grabResult.
clear();
236 m_grabResult = m_sourceItem->grabToImage(
QSize(128, 128));
240 m_sourceImage = m_grabResult->image();
241 m_grabResult.clear();
247static inline int squareDistance(QRgb color1, QRgb color2)
251 if (qRed(color1) - qRed(color2) < 128) {
252 return 2 * pow(qRed(color1) - qRed(color2), 2)
253 + 4 * pow(qGreen(color1) - qGreen(color2), 2)
254 + 3 * pow(qBlue(color1) - qBlue(color2), 2);
256 return 3 * pow(qRed(color1) - qRed(color2), 2)
257 + 4 * pow(qGreen(color1) - qGreen(color2), 2)
258 + 2 * pow(qBlue(color1) - qBlue(color2), 2);
264 for (
auto &stat : clusters) {
265 if (squareDistance(rgb,
stat.centroid) < s_minimumSquareDistance) {
266 stat.colors.append(rgb);
271 ImageData::colorStat
stat;
272 stat.colors.append(rgb);
277void ImageColors::positionColorMP(
const decltype(ImageData::m_samples) &samples,
decltype(ImageData::m_clusters) &clusters,
int numCore)
280 if (samples.size() < 65536 || numCore < 2) {
285 for (
auto color : samples) {
286 positionColor(color, clusters);
292 const int numSamplesPerThread = samples.size() / numCore;
293 std::vector<
decltype(ImageData::m_clusters)> tempClusters(numCore,
decltype(ImageData::m_clusters){});
294#pragma omp parallel for
295 for (
int i = 0; i < numCore; ++i) {
296 const auto beginIt = std::next(samples.begin(), numSamplesPerThread * i);
297 const auto endIt = i < numCore - 1 ? std::next(samples.begin(), numSamplesPerThread * (i + 1)) : samples.
end();
299 for (
auto it = beginIt; it != endIt; it = std::next(it)) {
300 positionColor(*it, tempClusters[omp_get_thread_num()]);
306 for (
const auto &clusterPart : tempClusters) {
307 clusters << clusterPart;
309 for (
int i = 0; i < clusters.size() - 1; ++i) {
310 auto &clusterA = clusters[i];
311 if (clusterA.colors.empty()) {
314 for (
int j = i + 1; j < clusters.size(); ++j) {
315 auto &clusterB = clusters[j];
316 if (clusterB.colors.empty()) {
319 if (squareDistance(clusterA.centroid, clusterB.centroid) < s_minimumSquareDistance) {
321 clusterA.colors.append(clusterB.colors);
322 clusterB.colors.clear();
327 auto removeIt = std::remove_if(clusters.begin(), clusters.end(), [](
const ImageData::colorStat &stat) {
328 return stat.colors.empty();
330 clusters.erase(removeIt, clusters.end());
334ImageData ImageColors::generatePalette(
const QImage &sourceImage)
338 if (sourceImage.
isNull() || sourceImage.
width() == 0) {
342 imageData.m_clusters.
clear();
343 imageData.m_samples.
clear();
346 static const int numCore = std::min(8, omp_get_num_procs());
347 omp_set_num_threads(numCore);
349 constexpr int numCore = 1;
356#pragma omp parallel for collapse(2) reduction(+ : r) reduction(+ : g) reduction(+ : b) reduction(+ : c)
357 for (
int x = 0; x < sourceImage.
width(); ++x) {
358 for (
int y = 0; y < sourceImage.
height(); ++y) {
360 if (sampleColor.
alpha() == 0) {
366 QRgb rgb = sampleColor.
rgb();
372 imageData.m_samples << rgb;
376 if (imageData.m_samples.
isEmpty()) {
380 positionColorMP(imageData.m_samples, imageData.m_clusters, numCore);
382 imageData.m_average =
QColor(r / c, g / c, b / c, 255);
384 for (
int iteration = 0; iteration < 5; ++iteration) {
385#pragma omp parallel for private(r, g, b, c)
386 for (
int i = 0; i < imageData.m_clusters.
size(); ++i) {
387 auto &
stat = imageData.m_clusters[i];
393 for (
auto color : std::as_const(
stat.colors)) {
402 stat.centroid = qRgb(r, g, b);
403 stat.ratio = std::clamp(qreal(
stat.colors.count()) / qreal(imageData.m_samples.
count()), 0.0, 1.0);
407 positionColorMP(imageData.m_samples, imageData.m_clusters, numCore);
410 std::sort(imageData.m_clusters.
begin(), imageData.m_clusters.
end(), [](
const ImageData::colorStat &a,
const ImageData::colorStat &b) {
411 return getClusterScore(a) > getClusterScore(b);
415 auto sourceIt = imageData.m_clusters.
end();
417 std::vector<int> itemsToDelete;
418 while (sourceIt != imageData.m_clusters.
begin()) {
420 for (
auto destIt = imageData.m_clusters.
begin(); destIt != imageData.m_clusters.
end() && destIt != sourceIt; destIt++) {
421 if (squareDistance((*sourceIt).centroid, (*destIt).centroid) < s_minimumSquareDistance) {
422 const qreal ratio = (*sourceIt).ratio / (*destIt).ratio;
423 const int r = ratio * qreal(qRed((*sourceIt).centroid)) + (1 - ratio) * qreal(qRed((*destIt).centroid));
424 const int g = ratio * qreal(qGreen((*sourceIt).centroid)) + (1 - ratio) * qreal(qGreen((*destIt).centroid));
425 const int b = ratio * qreal(qBlue((*sourceIt).centroid)) + (1 - ratio) * qreal(qBlue((*destIt).centroid));
426 (*destIt).ratio += (*sourceIt).ratio;
427 (*destIt).centroid = qRgb(r, g, b);
428 itemsToDelete.push_back(std::distance(imageData.m_clusters.
begin(), sourceIt));
433 for (
auto i : std::as_const(itemsToDelete)) {
437 imageData.m_highlight =
QColor();
438 imageData.m_dominant =
QColor(imageData.m_clusters.
first().centroid);
442 imageData.m_palette.
clear();
446#pragma omp parallel for ordered
447 for (
int i = 0; i < imageData.m_clusters.
size(); ++i) {
448 const auto &
stat = imageData.m_clusters[i];
451 QColor contrast =
QColor(255 - color.red(), 255 - color.green(), 255 - color.blue());
456 int minimumDistance = 4681800;
457 for (
const auto &stat : std::as_const(imageData.m_clusters)) {
460 if (distance < minimumDistance) {
466 if (imageData.m_clusters.
size() <= 3) {
467 if (qGray(imageData.m_dominant.
rgb()) < 120) {
468 contrast =
QColor(230, 230, 230);
470 contrast =
QColor(20, 20, 20);
473 }
else if (squareDistance(contrast.
rgb(), tempContrast.
rgb()) < s_minimumSquareDistance * 1.5) {
474 contrast = tempContrast;
476 contrast = tempContrast;
485 imageData.m_dominantContrast = contrast;
486 imageData.m_dominant = color;
491 imageData.m_highlight = color;
494 if (qGray(color.rgb()) > qGray(imageData.m_closestToWhite.
rgb())) {
495 imageData.m_closestToWhite = color;
497 if (qGray(color.rgb()) < qGray(imageData.m_closestToBlack.
rgb())) {
498 imageData.m_closestToBlack = color;
500 imageData.m_palette << PaletteSwatch(
stat.ratio, color, contrast);
507double ImageColors::getClusterScore(
const ImageData::colorStat &stat)
512void ImageColors::postProcess(ImageData &imageData)
const
514 constexpr short unsigned WCAG_NON_TEXT_CONTRAST_RATIO = 3;
515 constexpr qreal WCAG_TEXT_CONTRAST_RATIO = 4.5;
517 auto platformTheme = qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(
this,
false);
518 if (!platformTheme) {
523 const qreal backgroundLum = ColorUtils::luminance(backgroundColor);
524 qreal lowerLum, upperLum;
526 if (qGray(backgroundColor.
rgb()) < 192) {
528 lowerLum = WCAG_NON_TEXT_CONTRAST_RATIO * (backgroundLum + 0.05) - 0.05;
535 const qreal textLum = ColorUtils::luminance(textColor);
536 lowerLum = WCAG_TEXT_CONTRAST_RATIO * (textLum + 0.05) - 0.05;
537 upperLum = backgroundLum;
540 auto adjustSaturation = [](
QColor &color) {
542 if (color.hsvSaturationF() < 0.5) {
543 const qreal h = color.hsvHueF();
544 const qreal v = color.valueF();
545 color.setHsvF(h, 0.5, v);
548 adjustSaturation(imageData.m_dominant);
549 adjustSaturation(imageData.m_highlight);
550 adjustSaturation(imageData.m_average);
552 auto adjustLightness = [lowerLum, upperLum](
QColor &color) {
553 short unsigned colorOperationCount = 0;
554 const qreal h = color.hslHueF();
555 const qreal s = color.hslSaturationF();
556 const qreal l = color.lightnessF();
557 while (ColorUtils::luminance(color.rgb()) < lowerLum && colorOperationCount++ < 10) {
558 color.setHslF(h, s, std::min(1.0, l + colorOperationCount * 0.03));
560 while (ColorUtils::luminance(color.rgb()) > upperLum && colorOperationCount++ < 10) {
561 color.setHslF(h, s, std::max(0.0, l - colorOperationCount * 0.03));
564 adjustLightness(imageData.m_dominant);
565 adjustLightness(imageData.m_highlight);
566 adjustLightness(imageData.m_average);
571 if (m_futureImageData) {
572 qCWarning(KirigamiLog) << m_futureImageData->future().isFinished();
574 return_fallback(m_fallbackPalette)
return m_imageData.m_palette;
580 return_fallback(m_fallbackPaletteBrightness)
589 return_fallback_finally(m_fallbackAverage, linkBackgroundColor)
591 return m_imageData.m_average;
598 return_fallback_finally(m_fallbackDominant, linkBackgroundColor)
600 return m_imageData.m_dominant;
607 return_fallback_finally(m_fallbackDominantContrasting, linkBackgroundColor)
609 return m_imageData.m_dominantContrast;
616 return_fallback_finally(m_fallbackForeground, textColor)
620 if (qGray(m_imageData.m_closestToWhite.
rgb()) < 200) {
621 return QColor(230, 230, 230);
623 return m_imageData.m_closestToWhite;
625 if (qGray(m_imageData.m_closestToBlack.
rgb()) > 80) {
626 return QColor(20, 20, 20);
628 return m_imageData.m_closestToBlack;
636 return_fallback_finally(m_fallbackBackground, backgroundColor)
639 if (qGray(m_imageData.m_closestToBlack.
rgb()) > 80) {
640 return QColor(20, 20, 20);
642 return m_imageData.m_closestToBlack;
644 if (qGray(m_imageData.m_closestToWhite.
rgb()) < 200) {
645 return QColor(230, 230, 230);
647 return m_imageData.m_closestToWhite;
655 return_fallback_finally(m_fallbackHighlight, linkColor)
657 return m_imageData.m_highlight;
665 if (qGray(m_imageData.m_closestToWhite.
rgb()) < 200) {
666 return QColor(230, 230, 230);
670 return m_imageData.m_closestToWhite;
677 if (qGray(m_imageData.m_closestToBlack.
rgb()) > 80) {
678 return QColor(20, 20, 20);
681 return m_imageData.m_closestToBlack;
684#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.
@ Light
The item is light and requires a dark foreground color to achieve readable contrast.
@ Dark
The item is dark and requires a light foreground color to achieve readable contrast.
QML_ELEMENTQVariant source
The source from which colors should be extracted from.
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.
QColor closestToBlack
The darkest color of the source image.
QList< PaletteSwatch > palette
A list of colors and related information about then.
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)
const QList< QKeySequence > & end()
KOSM_EXPORT double distance(const std::vector< const OSM::Node * > &path, Coordinate coord)
int hslSaturation() const const
bool isValid() const const
int lightness() 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)
bool hasThemeIcon(const QString &name)
bool isNull() const const
QColor pixelColor(const QPoint &position) const const
qsizetype count() const const
bool isEmpty() const const
void removeAt(qsizetype i)
qsizetype size() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool disconnect(const QMetaObject::Connection &connection)
QImage toImage() const const
void windowChanged(QQuickWindow *window)
QFuture< T > run(Function function,...)
bool isLocalFile() const const
void visibleChanged(bool arg)