19#include "imagecolors.h"
22#include <QFutureWatcher>
23#include <QGuiApplication>
25#include <QtConcurrentRun>
27#include "loggingcategory.h"
31#include "config-OpenMP.h"
36#include "platform/platformtheme.h"
38#define return_fallback(value) \
39 if (m_imageData.m_samples.size() == 0) { \
43#define return_fallback_finally(value, finally) \
44 if (m_imageData.m_samples.size() == 0) { \
45 return value.isValid() \
47 : static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true))->finally(); \
50ImageColors::ImageColors(
QObject *parent)
53 m_imageSyncTimer =
new QTimer(
this);
54 m_imageSyncTimer->setSingleShot(
true);
55 m_imageSyncTimer->setInterval(100);
61ImageColors::~ImageColors()
65void ImageColors::setSource(
const QVariant &source)
67 if (m_futureSourceImageData) {
68 m_futureSourceImageData->
cancel();
69 m_futureSourceImageData->deleteLater();
70 m_futureSourceImageData =
nullptr;
87 return QImage(url.toLocalFile());
89 return QImage(sourceString);
93 const QImage image = m_futureSourceImageData->future().result();
94 m_futureSourceImageData->deleteLater();
95 m_futureSourceImageData =
nullptr;
96 setSourceImage(image);
100 m_futureSourceImageData->setFuture(future);
116void ImageColors::setSourceImage(
const QImage &image)
126 m_grabResult.
clear();
129 m_sourceItem.
clear();
131 m_sourceImage = image;
135QImage ImageColors::sourceImage()
const
137 return m_sourceImage;
140void ImageColors::setSourceItem(
QQuickItem *source)
142 if (m_sourceItem ==
source) {
150 disconnect(m_sourceItem,
nullptr,
this,
nullptr);
156 auto syncWindow = [
this]() {
160 m_window = m_sourceItem->window();
176void ImageColors::update()
178 if (m_futureImageData) {
179 m_futureImageData->
cancel();
180 m_futureImageData->deleteLater();
181 m_futureImageData =
nullptr;
183 auto runUpdate = [
this]() {
185 return generatePalette(m_sourceImage);
189 if (!m_futureImageData) {
192 m_imageData = m_futureImageData->future().result();
193 m_futureImageData->deleteLater();
194 m_futureImageData =
nullptr;
198 m_futureImageData->setFuture(future);
202 if (!m_sourceImage.
isNull()) {
213 m_grabResult.
clear();
216 m_grabResult = m_sourceItem->grabToImage(
QSize(128, 128));
220 m_sourceImage = m_grabResult->image();
221 m_grabResult.clear();
227inline int squareDistance(QRgb color1, QRgb color2)
231 if (qRed(color1) - qRed(color2) < 128) {
232 return 2 * pow(qRed(color1) - qRed(color2), 2)
233 + 4 * pow(qGreen(color1) - qGreen(color2), 2)
234 + 3 * pow(qBlue(color1) - qBlue(color2), 2);
236 return 3 * pow(qRed(color1) - qRed(color2), 2)
237 + 4 * pow(qGreen(color1) - qGreen(color2), 2)
238 + 2 * pow(qBlue(color1) - qBlue(color2), 2);
244 for (
auto &stat : clusters) {
245 if (squareDistance(rgb,
stat.centroid) < s_minimumSquareDistance) {
246 stat.colors.append(rgb);
251 ImageData::colorStat
stat;
252 stat.colors.append(rgb);
257void ImageColors::positionColorMP(
const decltype(ImageData::m_samples) &samples,
decltype(ImageData::m_clusters) &clusters,
int numCore)
260 if (samples.size() < 65536 || numCore < 2) {
265 for (
auto color : samples) {
266 positionColor(color, clusters);
272 const int numSamplesPerThread = samples.size() / numCore;
273 std::vector<
decltype(ImageData::m_clusters)> tempClusters(numCore,
decltype(ImageData::m_clusters){});
274#pragma omp parallel for
275 for (
int i = 0; i < numCore; ++i) {
276 decltype(ImageData::m_samples) samplePart;
277 const auto beginIt = std::next(samples.begin(), numSamplesPerThread * i);
278 const auto endIt = i < numCore - 1 ? std::next(samples.begin(), numSamplesPerThread * (i + 1)) : samples.
end();
280 for (
auto it = beginIt; it != endIt; it = std::next(it)) {
281 positionColor(*it, tempClusters[omp_get_thread_num()]);
287 for (
const auto &clusterPart : tempClusters) {
288 clusters << clusterPart;
290 for (
int i = 0; i < clusters.size() - 1; ++i) {
291 auto &clusterA = clusters[i];
292 if (clusterA.colors.empty()) {
295 for (
int j = i + 1; j < clusters.size(); ++j) {
296 auto &clusterB = clusters[j];
297 if (clusterB.colors.empty()) {
300 if (squareDistance(clusterA.centroid, clusterB.centroid) < s_minimumSquareDistance) {
302 clusterA.colors.append(clusterB.colors);
303 clusterB.colors.clear();
308 auto removeIt = std::remove_if(clusters.begin(), clusters.end(), [](
const ImageData::colorStat &stat) {
309 return stat.colors.empty();
311 clusters.erase(removeIt, clusters.end());
315ImageData ImageColors::generatePalette(
const QImage &sourceImage)
const
319 if (sourceImage.
isNull() || sourceImage.
width() == 0) {
323 imageData.m_clusters.
clear();
324 imageData.m_samples.
clear();
327 static const int numCore = std::min(8, omp_get_num_procs());
328 omp_set_num_threads(numCore);
330 constexpr int numCore = 1;
337#pragma omp parallel for collapse(2) reduction(+ : r) reduction(+ : g) reduction(+ : b) reduction(+ : c)
338 for (
int x = 0; x < sourceImage.
width(); ++x) {
339 for (
int y = 0; y < sourceImage.
height(); ++y) {
341 if (sampleColor.
alpha() == 0) {
347 QRgb rgb = sampleColor.
rgb();
353 imageData.m_samples << rgb;
357 if (imageData.m_samples.
isEmpty()) {
361 positionColorMP(imageData.m_samples, imageData.m_clusters, numCore);
363 imageData.m_average =
QColor(r / c, g / c, b / c, 255);
365 for (
int iteration = 0; iteration < 5; ++iteration) {
366#pragma omp parallel for private(r, g, b, c)
367 for (
int i = 0; i < imageData.m_clusters.
size(); ++i) {
368 auto &
stat = imageData.m_clusters[i];
374 for (
auto color :
std::as_const(
stat.colors)) {
383 stat.centroid = qRgb(r, g, b);
384 stat.ratio = qreal(
stat.colors.count()) / qreal(imageData.m_samples.
count());
388 positionColorMP(imageData.m_samples, imageData.m_clusters, numCore);
391 std::sort(imageData.m_clusters.
begin(), imageData.m_clusters.
end(), [
this](
const ImageData::colorStat &a,
const ImageData::colorStat &b) {
392 return getClusterScore(a) > getClusterScore(b);
396 auto sourceIt = imageData.m_clusters.
end();
398 std::vector<int> itemsToDelete;
399 while (sourceIt != imageData.m_clusters.
begin()) {
401 for (
auto destIt = imageData.m_clusters.
begin(); destIt != imageData.m_clusters.
end() && destIt != sourceIt; destIt++) {
402 if (squareDistance((*sourceIt).centroid, (*destIt).centroid) < s_minimumSquareDistance) {
403 const qreal ratio = (*sourceIt).ratio / (*destIt).ratio;
404 const int r = ratio * qreal(qRed((*sourceIt).centroid)) + (1 - ratio) * qreal(qRed((*destIt).centroid));
405 const int g = ratio * qreal(qGreen((*sourceIt).centroid)) + (1 - ratio) * qreal(qGreen((*destIt).centroid));
406 const int b = ratio * qreal(qBlue((*sourceIt).centroid)) + (1 - ratio) * qreal(qBlue((*destIt).centroid));
407 (*destIt).ratio += (*sourceIt).ratio;
408 (*destIt).centroid = qRgb(r, g, b);
409 itemsToDelete.push_back(std::distance(imageData.m_clusters.
begin(), sourceIt));
414 for (
auto i :
std::as_const(itemsToDelete)) {
418 imageData.m_highlight =
QColor();
419 imageData.m_dominant =
QColor(imageData.m_clusters.
first().centroid);
423 imageData.m_palette.clear();
427#pragma omp parallel for ordered
428 for (
int i = 0; i < imageData.m_clusters.
size(); ++i) {
429 const auto &
stat = imageData.m_clusters[i];
432 entry[QStringLiteral(
"color")] = color;
433 entry[QStringLiteral(
"ratio")] =
stat.ratio;
435 QColor contrast =
QColor(255 - color.red(), 255 - color.green(), 255 - color.blue());
440 int minimumDistance = 4681800;
441 for (
const auto &stat :
std::as_const(imageData.m_clusters)) {
444 if (distance < minimumDistance) {
450 if (imageData.m_clusters.
size() <= 3) {
451 if (qGray(imageData.m_dominant.
rgb()) < 120) {
452 contrast =
QColor(230, 230, 230);
454 contrast =
QColor(20, 20, 20);
457 }
else if (squareDistance(contrast.
rgb(), tempContrast.
rgb()) < s_minimumSquareDistance * 1.5) {
458 contrast = tempContrast;
460 contrast = tempContrast;
463 contrast.
lightness() > 128 ? qMin(contrast.
lightness() + 20, 255) : qMax(0, contrast.lightness() - 20));
466 entry[QStringLiteral(
"contrastColor")] = contrast;
470 imageData.m_dominantContrast = contrast;
471 imageData.m_dominant = color;
476 imageData.m_highlight = color;
479 if (qGray(color.rgb()) > qGray(imageData.m_closestToWhite.
rgb())) {
480 imageData.m_closestToWhite = color;
482 if (qGray(color.rgb()) < qGray(imageData.m_closestToBlack.
rgb())) {
483 imageData.m_closestToBlack = color;
485 imageData.m_palette << entry;
489 postProcess(imageData);
494double ImageColors::getClusterScore(
const ImageData::colorStat &stat)
const
499void ImageColors::postProcess(ImageData &imageData)
const
501 constexpr short unsigned WCAG_NON_TEXT_CONTRAST_RATIO = 3;
502 constexpr qreal WCAG_TEXT_CONTRAST_RATIO = 4.5;
504 auto platformTheme = qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(
this,
false);
505 if (!platformTheme) {
510 const qreal backgroundLum = ColorUtils::luminance(backgroundColor);
511 qreal lowerLum, upperLum;
513 if (qGray(backgroundColor.
rgb()) < 192) {
515 lowerLum = WCAG_NON_TEXT_CONTRAST_RATIO * (backgroundLum + 0.05) - 0.05;
522 const qreal textLum = ColorUtils::luminance(textColor);
523 lowerLum = WCAG_TEXT_CONTRAST_RATIO * (textLum + 0.05) - 0.05;
524 upperLum = backgroundLum;
527 auto adjustSaturation = [](
QColor &color) {
529 if (color.hsvSaturationF() < 0.5) {
530 const qreal h = color.hsvHueF();
531 const qreal v = color.valueF();
532 color.setHsvF(h, 0.5, v);
535 adjustSaturation(imageData.m_dominant);
536 adjustSaturation(imageData.m_highlight);
537 adjustSaturation(imageData.m_average);
539 auto adjustLightness = [lowerLum, upperLum](
QColor &color) {
540 short unsigned colorOperationCount = 0;
541 const qreal h = color.hslHueF();
542 const qreal s = color.hslSaturationF();
543 const qreal l = color.lightnessF();
544 while (ColorUtils::luminance(color.rgb()) < lowerLum && colorOperationCount++ < 10) {
545 color.setHslF(h, s, std::min(1.0, l + colorOperationCount * 0.03));
547 while (ColorUtils::luminance(color.rgb()) > upperLum && colorOperationCount++ < 10) {
548 color.setHslF(h, s, std::max(0.0, l - colorOperationCount * 0.03));
551 adjustLightness(imageData.m_dominant);
552 adjustLightness(imageData.m_highlight);
553 adjustLightness(imageData.m_average);
558 if (m_futureImageData) {
559 qCWarning(KirigamiLog) << m_futureImageData->future().isFinished();
561 return_fallback(m_fallbackPalette)
return m_imageData.m_palette;
567 return_fallback(m_fallbackPaletteBrightness)
576 return_fallback_finally(m_fallbackAverage, linkBackgroundColor)
578 return m_imageData.m_average;
585 return_fallback_finally(m_fallbackDominant, linkBackgroundColor)
587 return m_imageData.m_dominant;
594 return_fallback_finally(m_fallbackDominantContrasting, linkBackgroundColor)
596 return m_imageData.m_dominantContrast;
603 return_fallback_finally(m_fallbackForeground, textColor)
607 if (qGray(m_imageData.m_closestToWhite.
rgb()) < 200) {
608 return QColor(230, 230, 230);
610 return m_imageData.m_closestToWhite;
612 if (qGray(m_imageData.m_closestToBlack.
rgb()) > 80) {
613 return QColor(20, 20, 20);
615 return m_imageData.m_closestToBlack;
623 return_fallback_finally(m_fallbackBackground, backgroundColor)
626 if (qGray(m_imageData.m_closestToBlack.
rgb()) > 80) {
627 return QColor(20, 20, 20);
629 return m_imageData.m_closestToBlack;
631 if (qGray(m_imageData.m_closestToWhite.
rgb()) < 200) {
632 return QColor(230, 230, 230);
634 return m_imageData.m_closestToWhite;
642 return_fallback_finally(m_fallbackHighlight, linkColor)
644 return m_imageData.m_highlight;
652 if (qGray(m_imageData.m_closestToWhite.
rgb()) < 200) {
653 return QColor(230, 230, 230);
657 return m_imageData.m_closestToWhite;
664 if (qGray(m_imageData.m_closestToBlack.
rgb()) > 80) {
665 return QColor(20, 20, 20);
668 return m_imageData.m_closestToBlack;
671#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)