File indexing completed on 2024-05-05 04:46:59
0001 /* 0002 * Copyright 2020 Marco Martin <mart@kde.org> 0003 * 0004 * This program is free software; you can redistribute it and/or modify 0005 * it under the terms of the GNU General Public License as published by 0006 * the Free Software Foundation; either version 2 of the License, or 0007 * (at your option) any later version. 0008 * 0009 * This program is distributed in the hope that it will be useful, 0010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 0011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 0012 * GNU General Public License for more details. 0013 * 0014 * You should have received a copy of the GNU General Public License 0015 * along with this program; if not, write to the Free Software 0016 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 2.010-1301, USA. 0017 */ 0018 0019 #include "imagecolors.h" 0020 #include "platformtheme.h" 0021 0022 #include <QDebug> 0023 #include <QTimer> 0024 #include <QtConcurrent> 0025 0026 #include <cmath> 0027 0028 #define return_fallback(value) \ 0029 if (m_imageData.m_samples.size() == 0) { \ 0030 return value; \ 0031 } 0032 0033 #define return_fallback_finally(value, finally) \ 0034 if (m_imageData.m_samples.size() == 0) { \ 0035 return value.isValid() ? value : static_cast<Maui::PlatformTheme *>(qmlAttachedPropertiesObject<Maui::PlatformTheme>(this, true))->finally(); \ 0036 } 0037 0038 ImageColors::ImageColors(QObject *parent) 0039 : QObject(parent) 0040 { 0041 m_imageSyncTimer = new QTimer(this); 0042 m_imageSyncTimer->setSingleShot(true); 0043 m_imageSyncTimer->setInterval(100); 0044 /* connect(m_imageSyncTimer, &QTimer::timeout, this, [this]() { 0045 generatePalette(); 0046 });*/ 0047 } 0048 0049 ImageColors::~ImageColors() 0050 { 0051 } 0052 0053 void ImageColors::setSource(const QVariant &source) 0054 { 0055 if (source.canConvert<QQuickItem *>()) { 0056 qDebug() << "can convert to item"; 0057 setSourceItem(source.value<QQuickItem *>()); 0058 } else if (source.canConvert<QImage>()) { 0059 qDebug() << "can convert to image"; 0060 0061 setSourceImage(source.value<QImage>()); 0062 } else if (source.canConvert<QIcon>()) { 0063 qDebug() << "can convert to icon"; 0064 0065 setSourceImage(source.value<QIcon>().pixmap(128, 128).toImage()); 0066 } else if (source.canConvert<QString>()) { 0067 qDebug() << "can convert to string"; 0068 if(source.toString().isEmpty()) 0069 { 0070 return; 0071 } 0072 0073 if(source.toString().startsWith("qrc:")) 0074 { 0075 qDebug() << "SET IMAGE FROM QRC IMAGE COLORS" << source.toString(); 0076 setSourceImage(QImage(source.toString().replace("qrc", ""))); 0077 }else 0078 { 0079 0080 setSourceImage(QIcon::fromTheme(source.toString()).pixmap(128, 128).toImage()); 0081 } 0082 } else { 0083 return; 0084 } 0085 0086 m_source = source; 0087 Q_EMIT sourceChanged(); 0088 } 0089 0090 QVariant ImageColors::source() const 0091 { 0092 return m_source; 0093 } 0094 0095 void ImageColors::setSourceImage(const QImage &image) 0096 { 0097 if (m_window) { 0098 disconnect(m_window.data(), nullptr, this, nullptr); 0099 } 0100 if (m_sourceItem) { 0101 disconnect(m_sourceItem.data(), nullptr, this, nullptr); 0102 } 0103 if (m_grabResult) { 0104 disconnect(m_grabResult.data(), nullptr, this, nullptr); 0105 m_grabResult.clear(); 0106 } 0107 0108 m_sourceItem.clear(); 0109 0110 m_sourceImage = image; 0111 update(); 0112 } 0113 0114 QImage ImageColors::sourceImage() const 0115 { 0116 return m_sourceImage; 0117 } 0118 0119 void ImageColors::setSourceItem(QQuickItem *source) 0120 { 0121 if (m_sourceItem == source) { 0122 return; 0123 } 0124 0125 if (m_window) { 0126 disconnect(m_window.data(), nullptr, this, nullptr); 0127 } 0128 if (m_sourceItem) { 0129 disconnect(m_sourceItem, nullptr, this, nullptr); 0130 } 0131 m_sourceItem = source; 0132 update(); 0133 0134 if (m_sourceItem) { 0135 auto syncWindow = [this]() { 0136 if (m_window) { 0137 disconnect(m_window.data(), nullptr, this, nullptr); 0138 } 0139 m_window = m_sourceItem->window(); 0140 if (m_window) { 0141 connect(m_window, &QWindow::visibleChanged, this, &ImageColors::update); 0142 } 0143 }; 0144 0145 connect(m_sourceItem, &QQuickItem::windowChanged, this, syncWindow); 0146 syncWindow(); 0147 } 0148 } 0149 0150 QQuickItem *ImageColors::sourceItem() const 0151 { 0152 return m_sourceItem; 0153 } 0154 0155 void ImageColors::update() 0156 { 0157 if (m_futureImageData) { 0158 m_futureImageData->cancel(); 0159 m_futureImageData->deleteLater(); 0160 } 0161 auto runUpdate = [this]() { 0162 QFuture<ImageData> future = QtConcurrent::run([this]() { 0163 return generatePalette(m_sourceImage); 0164 }); 0165 m_futureImageData = new QFutureWatcher<ImageData>(this); 0166 connect(m_futureImageData, &QFutureWatcher<ImageData>::finished, this, [this]() { 0167 if (!m_futureImageData) { 0168 return; 0169 } 0170 m_imageData = m_futureImageData->future().result(); 0171 m_futureImageData->deleteLater(); 0172 m_futureImageData = nullptr; 0173 0174 Q_EMIT paletteChanged(); 0175 }); 0176 m_futureImageData->setFuture(future); 0177 }; 0178 0179 if (!m_sourceItem || !m_window) { 0180 if (!m_sourceImage.isNull()) { 0181 runUpdate(); 0182 } 0183 return; 0184 } 0185 0186 if (m_grabResult) { 0187 disconnect(m_grabResult.data(), nullptr, this, nullptr); 0188 m_grabResult.clear(); 0189 } 0190 0191 m_grabResult = m_sourceItem->grabToImage(QSize(128, 128)); 0192 0193 if (m_grabResult) { 0194 connect(m_grabResult.data(), &QQuickItemGrabResult::ready, this, [this, runUpdate]() { 0195 m_sourceImage = m_grabResult->image(); 0196 m_grabResult.clear(); 0197 runUpdate(); 0198 }); 0199 } 0200 } 0201 0202 inline int squareDistance(QRgb color1, QRgb color2) 0203 { 0204 // https://en.wikipedia.org/wiki/Color_difference 0205 // Using RGB distance for performance, as CIEDE2000 istoo complicated 0206 if (qRed(color1) - qRed(color2) < 128) { 0207 return 2 * pow(qRed(color1) - qRed(color2), 2) // 0208 + 4 * pow(qGreen(color1) - qGreen(color2), 2) // 0209 + 3 * pow(qBlue(color1) - qBlue(color2), 2); 0210 } else { 0211 return 3 * pow(qRed(color1) - qRed(color2), 2) // 0212 + 4 * pow(qGreen(color1) - qGreen(color2), 2) // 0213 + 2 * pow(qBlue(color1) - qBlue(color2), 2); 0214 } 0215 } 0216 0217 void ImageColors::positionColor(QRgb rgb, QList<ImageData::colorStat> &clusters) 0218 { 0219 for (auto &stat : clusters) { 0220 if (squareDistance(rgb, stat.centroid) < s_minimumSquareDistance) { 0221 stat.colors.append(rgb); 0222 return; 0223 } 0224 } 0225 0226 ImageData::colorStat stat; 0227 stat.colors.append(rgb); 0228 stat.centroid = rgb; 0229 clusters << stat; 0230 } 0231 0232 ImageData ImageColors::generatePalette(const QImage &sourceImage) 0233 { 0234 ImageData imageData; 0235 0236 if (sourceImage.isNull() || sourceImage.width() == 0) { 0237 return imageData; 0238 } 0239 0240 imageData.m_clusters.clear(); 0241 imageData.m_samples.clear(); 0242 0243 QColor sampleColor; 0244 int r = 0; 0245 int g = 0; 0246 int b = 0; 0247 int c = 0; 0248 for (int x = 0; x < sourceImage.width(); ++x) { 0249 for (int y = 0; y < sourceImage.height(); ++y) { 0250 sampleColor = sourceImage.pixelColor(x, y); 0251 if (sampleColor.alpha() == 0) { 0252 continue; 0253 } 0254 QRgb rgb = sampleColor.rgb(); 0255 c++; 0256 r += qRed(rgb); 0257 g += qGreen(rgb); 0258 b += qBlue(rgb); 0259 imageData.m_samples << rgb; 0260 positionColor(rgb, imageData.m_clusters); 0261 } 0262 } 0263 0264 if (imageData.m_samples.isEmpty()) { 0265 return imageData; 0266 } 0267 0268 imageData.m_average = QColor(r / c, g / c, b / c, 255); 0269 0270 for (int iteration = 0; iteration < 5; ++iteration) { 0271 for (auto &stat : imageData.m_clusters) { 0272 r = 0; 0273 g = 0; 0274 b = 0; 0275 c = 0; 0276 0277 for (auto color : std::as_const(stat.colors)) { 0278 c++; 0279 r += qRed(color); 0280 g += qGreen(color); 0281 b += qBlue(color); 0282 } 0283 r = r / c; 0284 g = g / c; 0285 b = b / c; 0286 stat.centroid = qRgb(r, g, b); 0287 stat.ratio = qreal(stat.colors.count()) / qreal(imageData.m_samples.count()); 0288 stat.colors = QList<QRgb>({stat.centroid}); 0289 } 0290 0291 for (auto color : std::as_const(imageData.m_samples)) { 0292 positionColor(color, imageData.m_clusters); 0293 } 0294 } 0295 0296 std::sort(imageData.m_clusters.begin(), imageData.m_clusters.end(), [](const ImageData::colorStat &a, const ImageData::colorStat &b) { 0297 return a.colors.size() > b.colors.size(); 0298 }); 0299 0300 // compress blocks that became too similar 0301 auto sourceIt = imageData.m_clusters.end(); 0302 QList<QList<ImageData::colorStat>::iterator> itemsToDelete; 0303 while (sourceIt != imageData.m_clusters.begin()) { 0304 sourceIt--; 0305 for (auto destIt = imageData.m_clusters.begin(); destIt != imageData.m_clusters.end() && destIt != sourceIt; destIt++) { 0306 if (squareDistance((*sourceIt).centroid, (*destIt).centroid) < s_minimumSquareDistance) { 0307 const qreal ratio = (*sourceIt).ratio / (*destIt).ratio; 0308 const int r = ratio * qreal(qRed((*sourceIt).centroid)) + (1 - ratio) * qreal(qRed((*destIt).centroid)); 0309 const int g = ratio * qreal(qGreen((*sourceIt).centroid)) + (1 - ratio) * qreal(qGreen((*destIt).centroid)); 0310 const int b = ratio * qreal(qBlue((*sourceIt).centroid)) + (1 - ratio) * qreal(qBlue((*destIt).centroid)); 0311 (*destIt).ratio += (*sourceIt).ratio; 0312 (*destIt).centroid = qRgb(r, g, b); 0313 itemsToDelete << sourceIt; 0314 break; 0315 } 0316 } 0317 } 0318 for (const auto &i : std::as_const(itemsToDelete)) { 0319 imageData.m_clusters.erase(i); 0320 } 0321 0322 imageData.m_highlight = QColor(); 0323 imageData.m_dominant = QColor(imageData.m_clusters.first().centroid); 0324 imageData.m_closestToBlack = Qt::white; 0325 imageData.m_closestToWhite = Qt::black; 0326 0327 imageData.m_palette.clear(); 0328 0329 bool first = true; 0330 0331 for (const auto &stat : std::as_const(imageData.m_clusters)) { 0332 QVariantMap entry; 0333 const QColor color(stat.centroid); 0334 entry[QStringLiteral("color")] = color; 0335 entry[QStringLiteral("ratio")] = stat.ratio; 0336 0337 QColor contrast = QColor(255 - color.red(), 255 - color.green(), 255 - color.blue()); 0338 contrast.setHsl(contrast.hslHue(), // 0339 contrast.hslSaturation(), // 0340 128 + (128 - contrast.lightness())); 0341 QColor tempContrast; 0342 int minimumDistance = 4681800; // max distance: 4*3*2*3*255*255 0343 for (const auto &stat : std::as_const(imageData.m_clusters)) { 0344 const int distance = squareDistance(contrast.rgb(), stat.centroid); 0345 0346 if (distance < minimumDistance) { 0347 tempContrast = QColor(stat.centroid); 0348 minimumDistance = distance; 0349 } 0350 } 0351 0352 if (imageData.m_clusters.size() <= 3) { 0353 if (qGray(imageData.m_dominant.rgb()) < 120) { 0354 contrast = QColor(230, 230, 230); 0355 } else { 0356 contrast = QColor(20, 20, 20); 0357 } 0358 // TODO: replace m_clusters.size() > 3 with entropy calculation 0359 } else if (squareDistance(contrast.rgb(), tempContrast.rgb()) < s_minimumSquareDistance * 1.5) { 0360 contrast = tempContrast; 0361 } else { 0362 contrast = tempContrast; 0363 contrast.setHsl(contrast.hslHue(), 0364 contrast.hslSaturation(), 0365 contrast.lightness() > 128 ? qMin(contrast.lightness() + 20, 255) : qMax(0, contrast.lightness() - 20)); 0366 } 0367 0368 entry[QStringLiteral("contrastColor")] = contrast; 0369 0370 if (first) { 0371 imageData.m_dominantContrast = contrast; 0372 imageData.m_dominant = color; 0373 } 0374 first = false; 0375 0376 if (!imageData.m_highlight.isValid() || ColorUtils::chroma(color) > ColorUtils::chroma(imageData.m_highlight)) { 0377 imageData.m_highlight = color; 0378 } 0379 0380 if (qGray(color.rgb()) > qGray(imageData.m_closestToWhite.rgb())) { 0381 imageData.m_closestToWhite = color; 0382 } 0383 if (qGray(color.rgb()) < qGray(imageData.m_closestToBlack.rgb())) { 0384 imageData.m_closestToBlack = color; 0385 } 0386 imageData.m_palette << entry; 0387 } 0388 0389 return imageData; 0390 } 0391 0392 QVariantList ImageColors::palette() const 0393 { 0394 if (m_futureImageData) { 0395 qWarning() << m_futureImageData->future().isFinished(); 0396 } 0397 return_fallback(m_fallbackPalette) return m_imageData.m_palette; 0398 } 0399 0400 ColorUtils::Brightness ImageColors::paletteBrightness() const 0401 { 0402 /* clang-format off */ 0403 return_fallback(m_fallbackPaletteBrightness) 0404 0405 return qGray(m_imageData.m_dominant.rgb()) < 128 ? ColorUtils::Dark : ColorUtils::Light; 0406 /* clang-format on */ 0407 } 0408 0409 QColor ImageColors::average() const 0410 { 0411 /* clang-format off */ 0412 return_fallback_finally(m_fallbackAverage, linkBackgroundColor) 0413 0414 return m_imageData.m_average; 0415 /* clang-format on */ 0416 } 0417 0418 QColor ImageColors::dominant() const 0419 { 0420 /* clang-format off */ 0421 return_fallback_finally(m_fallbackDominant, linkBackgroundColor) 0422 0423 return m_imageData.m_dominant; 0424 /* clang-format on */ 0425 } 0426 0427 QColor ImageColors::dominantContrast() const 0428 { 0429 /* clang-format off */ 0430 return_fallback_finally(m_fallbackDominantContrasting, linkBackgroundColor) 0431 0432 return m_imageData.m_dominantContrast; 0433 /* clang-format on */ 0434 } 0435 0436 QColor ImageColors::foreground() const 0437 { 0438 /* clang-format off */ 0439 return_fallback_finally(m_fallbackForeground, textColor) 0440 0441 if (paletteBrightness() == ColorUtils::Dark) 0442 { 0443 if (qGray(m_imageData.m_closestToWhite.rgb()) < 200) { 0444 return QColor(230, 230, 230); 0445 } 0446 return m_imageData.m_closestToWhite; 0447 } else { 0448 if (qGray(m_imageData.m_closestToBlack.rgb()) > 80) { 0449 return QColor(20, 20, 20); 0450 } 0451 return m_imageData.m_closestToBlack; 0452 } 0453 /* clang-format on */ 0454 } 0455 0456 QColor ImageColors::background() const 0457 { 0458 /* clang-format off */ 0459 return_fallback_finally(m_fallbackBackground, backgroundColor) 0460 0461 if (paletteBrightness() == ColorUtils::Dark) { 0462 if (qGray(m_imageData.m_closestToBlack.rgb()) > 80) { 0463 return QColor(20, 20, 20); 0464 } 0465 return m_imageData.m_closestToBlack; 0466 } else { 0467 if (qGray(m_imageData.m_closestToWhite.rgb()) < 200) { 0468 return QColor(230, 230, 230); 0469 } 0470 return m_imageData.m_closestToWhite; 0471 } 0472 /* clang-format on */ 0473 } 0474 0475 QColor ImageColors::highlight() const 0476 { 0477 /* clang-format off */ 0478 return_fallback_finally(m_fallbackHighlight, linkColor) 0479 0480 return m_imageData.m_highlight; 0481 /* clang-format on */ 0482 } 0483 0484 QColor ImageColors::closestToWhite() const 0485 { 0486 /* clang-format off */ 0487 return_fallback(Qt::white) 0488 if (qGray(m_imageData.m_closestToWhite.rgb()) < 200) { 0489 return QColor(230, 230, 230); 0490 } 0491 /* clang-format on */ 0492 0493 return m_imageData.m_closestToWhite; 0494 } 0495 0496 QColor ImageColors::closestToBlack() const 0497 { 0498 /* clang-format off */ 0499 return_fallback(Qt::black) 0500 if (qGray(m_imageData.m_closestToBlack.rgb()) > 80) { 0501 return QColor(20, 20, 20); 0502 } 0503 /* clang-format on */ 0504 return m_imageData.m_closestToBlack; 0505 } 0506 0507 #include "moc_imagecolors.cpp"