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)
QML_ELEMENTQVariant source
ColorUtils::Brightness paletteBrightness
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)