File indexing completed on 2023-10-01 04:11:46

0001 /*
0002     SPDX-FileCopyrightText: 2006-2007 Aaron Seigo <aseigo@kde.org>
0003     SPDX-FileCopyrightText: 2008-2010 Marco Martin <notmart@gmail.com>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "svg.h"
0009 #include "private/svg_p.h"
0010 #include "private/theme_p.h"
0011 
0012 #include <array>
0013 #include <cmath>
0014 
0015 #include <QBuffer>
0016 #include <QCoreApplication>
0017 #include <QDir>
0018 #include <QPainter>
0019 #include <QRegularExpression>
0020 #include <QStringBuilder>
0021 #include <QXmlStreamReader>
0022 #include <QXmlStreamWriter>
0023 
0024 #include <KColorScheme>
0025 #include <KCompressionDevice>
0026 #include <KConfigGroup>
0027 #include <KIconEffect>
0028 #include <KIconLoader>
0029 #include <KIconTheme>
0030 #include <KPackage/Package>
0031 #include <QDebug>
0032 
0033 #include "applet.h"
0034 #include "debug_p.h"
0035 #include "package.h"
0036 #include "theme.h"
0037 
0038 uint qHash(const Plasma::SvgPrivate::CacheId &id, uint seed)
0039 {
0040     std::array<size_t, 10> parts = {
0041         ::qHash(id.width),
0042         ::qHash(id.height),
0043         ::qHash(id.elementName),
0044         ::qHash(id.filePath),
0045         ::qHash(id.status),
0046         ::qHash(id.devicePixelRatio),
0047         ::qHash(id.scaleFactor),
0048         ::qHash(id.colorGroup),
0049         ::qHash(id.extraFlags),
0050         ::qHash(id.lastModified),
0051     };
0052     return qHashRange(parts.begin(), parts.end(), seed);
0053 }
0054 
0055 namespace Plasma
0056 {
0057 class SvgRectsCacheSingleton
0058 {
0059 public:
0060     SvgRectsCache self;
0061 };
0062 
0063 Q_GLOBAL_STATIC(SvgRectsCacheSingleton, privateSvgRectsCacheSelf)
0064 
0065 const uint SvgRectsCache::s_seed = 0x9e3779b9;
0066 
0067 SharedSvgRenderer::SharedSvgRenderer(QObject *parent)
0068     : QSvgRenderer(parent)
0069 {
0070 }
0071 
0072 SharedSvgRenderer::SharedSvgRenderer(const QString &filename, const QString &styleSheet, QHash<QString, QRectF> &interestingElements, QObject *parent)
0073     : QSvgRenderer(parent)
0074 {
0075     KCompressionDevice file(filename, KCompressionDevice::GZip);
0076     if (!file.open(QIODevice::ReadOnly)) {
0077         return;
0078     }
0079     m_filename = filename;
0080     m_styleSheet = styleSheet;
0081     m_interestingElements = interestingElements;
0082     load(file.readAll(), styleSheet, interestingElements);
0083 }
0084 
0085 SharedSvgRenderer::SharedSvgRenderer(const QByteArray &contents, const QString &styleSheet, QHash<QString, QRectF> &interestingElements, QObject *parent)
0086     : QSvgRenderer(parent)
0087 {
0088     load(contents, styleSheet, interestingElements);
0089 }
0090 
0091 void SharedSvgRenderer::reload()
0092 {
0093     KCompressionDevice file(m_filename, KCompressionDevice::GZip);
0094     if (!file.open(QIODevice::ReadOnly)) {
0095         return;
0096     }
0097 
0098     load(file.readAll(), m_styleSheet, m_interestingElements);
0099 }
0100 
0101 bool SharedSvgRenderer::load(const QByteArray &contents, const QString &styleSheet, QHash<QString, QRectF> &interestingElements)
0102 {
0103     // Apply the style sheet.
0104     if (!styleSheet.isEmpty() && contents.contains("current-color-scheme")) {
0105         QByteArray processedContents;
0106         processedContents.reserve(contents.size());
0107         QXmlStreamReader reader(contents);
0108 
0109         QBuffer buffer(&processedContents);
0110         buffer.open(QIODevice::WriteOnly);
0111         QXmlStreamWriter writer(&buffer);
0112         while (!reader.atEnd()) {
0113             if (reader.readNext() == QXmlStreamReader::StartElement && reader.qualifiedName() == QLatin1String("style")
0114                 && reader.attributes().value(QLatin1String("id")) == QLatin1String("current-color-scheme")) {
0115                 writer.writeStartElement(QLatin1String("style"));
0116                 writer.writeAttributes(reader.attributes());
0117                 writer.writeCharacters(styleSheet);
0118                 writer.writeEndElement();
0119                 while (reader.tokenType() != QXmlStreamReader::EndElement) {
0120                     reader.readNext();
0121                 }
0122             } else if (reader.tokenType() != QXmlStreamReader::Invalid) {
0123                 writer.writeCurrentToken(reader);
0124             }
0125         }
0126         buffer.close();
0127         if (!QSvgRenderer::load(processedContents)) {
0128             return false;
0129         }
0130     } else if (!QSvgRenderer::load(contents)) {
0131         return false;
0132     }
0133 
0134     // Search the SVG to find and store all ids that contain size hints.
0135     const QString contentsAsString(QString::fromLatin1(contents));
0136     static const QRegularExpression idExpr(QLatin1String("id\\s*?=\\s*?(['\"])(\\d+?-\\d+?-.*?)\\1"));
0137     Q_ASSERT(idExpr.isValid());
0138 
0139     auto matchIt = idExpr.globalMatch(contentsAsString);
0140     while (matchIt.hasNext()) {
0141         auto match = matchIt.next();
0142         QString elementId = match.captured(2);
0143 
0144         QRectF elementRect = boundsOnElement(elementId);
0145         if (elementRect.isValid()) {
0146             interestingElements.insert(elementId, elementRect);
0147         }
0148     }
0149 
0150     return true;
0151 }
0152 
0153 SvgRectsCache::SvgRectsCache(QObject *parent)
0154     : QObject(parent)
0155 {
0156     const QString svgElementsFile =
0157         QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1Char('/') + QStringLiteral("plasma-svgelements");
0158     m_svgElementsCache = KSharedConfig::openConfig(svgElementsFile, KConfig::SimpleConfig);
0159 
0160     m_configSyncTimer = new QTimer(this);
0161     m_configSyncTimer->setSingleShot(true);
0162     m_configSyncTimer->setInterval(5000);
0163     connect(m_configSyncTimer, &QTimer::timeout, this, [this]() {
0164         m_svgElementsCache->sync();
0165     });
0166 }
0167 
0168 SvgRectsCache *SvgRectsCache::instance()
0169 {
0170     return &privateSvgRectsCacheSelf()->self;
0171 }
0172 
0173 void SvgRectsCache::insert(Plasma::SvgPrivate::CacheId cacheId, const QRectF &rect, unsigned int lastModified)
0174 {
0175     insert(qHash(cacheId, SvgRectsCache::s_seed), cacheId.filePath, rect, lastModified);
0176 }
0177 
0178 void SvgRectsCache::insert(uint id, const QString &filePath, const QRectF &rect, unsigned int lastModified)
0179 {
0180     const unsigned int savedTime = lastModifiedTimeFromCache(filePath);
0181 
0182     if (savedTime == lastModified && m_localRectCache.contains(id)) {
0183         return;
0184     }
0185 
0186     m_localRectCache.insert(id, rect);
0187 
0188     KConfigGroup imageGroup(m_svgElementsCache, filePath);
0189 
0190     if (rect.isValid()) {
0191         imageGroup.writeEntry(QString::number(id), rect);
0192     } else {
0193         m_invalidElements[filePath] << id;
0194         imageGroup.writeEntry("Invalidelements", m_invalidElements[filePath].values());
0195     }
0196 
0197     QMetaObject::invokeMethod(m_configSyncTimer, qOverload<>(&QTimer::start));
0198 
0199     if (savedTime != lastModified) {
0200         m_lastModifiedTimes[filePath] = lastModified;
0201         imageGroup.writeEntry("LastModified", lastModified);
0202         Q_EMIT lastModifiedChanged(filePath, lastModified);
0203     }
0204 }
0205 
0206 bool SvgRectsCache::findElementRect(Plasma::SvgPrivate::CacheId cacheId, QRectF &rect)
0207 {
0208     return findElementRect(qHash(cacheId, SvgRectsCache::s_seed), cacheId.filePath, rect);
0209 }
0210 
0211 bool SvgRectsCache::findElementRect(uint id, QStringView filePath, QRectF &rect)
0212 {
0213     auto it = m_localRectCache.find(id);
0214 
0215     if (it == m_localRectCache.end()) {
0216         auto elements = m_invalidElements.value(filePath.toString());
0217         if (elements.contains(id)) {
0218             rect = QRectF();
0219             return true;
0220         }
0221         return false;
0222     }
0223 
0224     rect = *it;
0225 
0226     return true;
0227 }
0228 
0229 bool SvgRectsCache::loadImageFromCache(const QString &path, uint lastModified)
0230 {
0231     if (path.isEmpty()) {
0232         return false;
0233     }
0234 
0235     KConfigGroup imageGroup(m_svgElementsCache, path);
0236 
0237     unsigned int savedTime = lastModifiedTimeFromCache(path);
0238 
0239     // Reload even if is older, to support downgrades
0240     if (lastModified != savedTime) {
0241         imageGroup.deleteGroup();
0242         QMetaObject::invokeMethod(m_configSyncTimer, qOverload<>(&QTimer::start));
0243         return false;
0244     }
0245 
0246     auto &elements = m_invalidElements[path];
0247     if (elements.isEmpty()) {
0248         auto list = imageGroup.readEntry("Invalidelements", QList<unsigned int>());
0249         m_invalidElements[path] = QSet<unsigned int>(list.begin(), list.end());
0250 
0251         for (const auto &key : imageGroup.keyList()) {
0252             bool ok = false;
0253             uint keyUInt = key.toUInt(&ok);
0254             if (ok) {
0255                 const QRectF rect = imageGroup.readEntry(key, QRectF());
0256                 m_localRectCache.insert(keyUInt, rect);
0257             }
0258         }
0259     }
0260     return true;
0261 }
0262 
0263 void SvgRectsCache::dropImageFromCache(const QString &path)
0264 {
0265     KConfigGroup imageGroup(m_svgElementsCache, path);
0266     imageGroup.deleteGroup();
0267     QMetaObject::invokeMethod(m_configSyncTimer, qOverload<>(&QTimer::start));
0268 }
0269 
0270 QList<QSize> SvgRectsCache::sizeHintsForId(const QString &path, const QString &id)
0271 {
0272     const QString pathId = path % id;
0273 
0274     auto it = m_sizeHintsForId.constFind(pathId);
0275     if (it == m_sizeHintsForId.constEnd()) {
0276         KConfigGroup imageGroup(m_svgElementsCache, path);
0277         const QStringList &encoded = imageGroup.readEntry(id, QStringList());
0278         QList<QSize> sizes;
0279         for (const auto &token : encoded) {
0280             const auto &parts = token.split(QLatin1Char('x'));
0281             if (parts.size() != 2) {
0282                 continue;
0283             }
0284             QSize size = QSize(parts[0].toDouble(), parts[1].toDouble());
0285             if (!size.isEmpty()) {
0286                 sizes << size;
0287             }
0288         }
0289         m_sizeHintsForId[pathId] = sizes;
0290         return sizes;
0291     }
0292 
0293     return *it;
0294 }
0295 
0296 void SvgRectsCache::insertSizeHintForId(const QString &path, const QString &id, const QSize &size)
0297 {
0298     // TODO: need to make this more efficient
0299     auto sizeListToString = [](const QList<QSize> &list) {
0300         QString ret;
0301         for (const auto &s : list) {
0302             ret += QString::number(s.width()) % QLatin1Char('x') % QString::number(s.height()) % QLatin1Char(',');
0303         }
0304         return ret;
0305     };
0306     m_sizeHintsForId[path % id].append(size);
0307     KConfigGroup imageGroup(m_svgElementsCache, path);
0308     imageGroup.writeEntry(id, sizeListToString(m_sizeHintsForId[path % id]));
0309     QMetaObject::invokeMethod(m_configSyncTimer, qOverload<>(&QTimer::start));
0310 }
0311 
0312 QString SvgRectsCache::iconThemePath()
0313 {
0314     if (!m_iconThemePath.isEmpty()) {
0315         return m_iconThemePath;
0316     }
0317 
0318     KConfigGroup imageGroup(m_svgElementsCache, QStringLiteral("General"));
0319     m_iconThemePath = imageGroup.readEntry(QStringLiteral("IconThemePath"), QString());
0320 
0321     return m_iconThemePath;
0322 }
0323 
0324 void SvgRectsCache::setIconThemePath(const QString &path)
0325 {
0326     m_iconThemePath = path;
0327     KConfigGroup imageGroup(m_svgElementsCache, QStringLiteral("General"));
0328     imageGroup.writeEntry(QStringLiteral("IconThemePath"), path);
0329     QMetaObject::invokeMethod(m_configSyncTimer, qOverload<>(&QTimer::start));
0330 }
0331 
0332 void SvgRectsCache::setNaturalSize(const QString &path, qreal scaleFactor, const QSizeF &size)
0333 {
0334     KConfigGroup imageGroup(m_svgElementsCache, path);
0335 
0336     // FIXME: needs something faster, perhaps even sprintf
0337     imageGroup.writeEntry(QStringLiteral("NaturalSize_") % QString::number(scaleFactor), size);
0338     QMetaObject::invokeMethod(m_configSyncTimer, qOverload<>(&QTimer::start));
0339 }
0340 
0341 QSizeF SvgRectsCache::naturalSize(const QString &path, qreal scaleFactor)
0342 {
0343     KConfigGroup imageGroup(m_svgElementsCache, path);
0344 
0345     // FIXME: needs something faster, perhaps even sprintf
0346     return imageGroup.readEntry(QStringLiteral("NaturalSize_") % QString::number(scaleFactor), QSizeF());
0347 }
0348 
0349 QStringList SvgRectsCache::cachedKeysForPath(const QString &path) const
0350 {
0351     KConfigGroup imageGroup(m_svgElementsCache, path);
0352     QStringList list = imageGroup.keyList();
0353     QStringList filtered;
0354 
0355     std::copy_if(list.begin(), list.end(), std::back_inserter(filtered), [](const QString element) {
0356         bool ok;
0357         element.toLong(&ok);
0358         return ok;
0359     });
0360     return filtered;
0361 }
0362 
0363 unsigned int SvgRectsCache::lastModifiedTimeFromCache(const QString &filePath)
0364 {
0365     const auto &i = m_lastModifiedTimes.constFind(filePath);
0366     if (i != m_lastModifiedTimes.constEnd()) {
0367         return i.value();
0368     }
0369 
0370     KConfigGroup imageGroup(m_svgElementsCache, filePath);
0371     const unsigned int savedTime = imageGroup.readEntry("LastModified", 0);
0372     m_lastModifiedTimes[filePath] = savedTime;
0373     return savedTime;
0374 }
0375 
0376 void SvgRectsCache::updateLastModified(const QString &filePath, unsigned int lastModified)
0377 {
0378     KConfigGroup imageGroup(m_svgElementsCache, filePath);
0379     const unsigned int savedTime = lastModifiedTimeFromCache(filePath);
0380 
0381     if (savedTime != lastModified) {
0382         m_lastModifiedTimes[filePath] = lastModified;
0383         imageGroup.writeEntry("LastModified", lastModified);
0384         QMetaObject::invokeMethod(m_configSyncTimer, qOverload<>(&QTimer::start));
0385         Q_EMIT lastModifiedChanged(filePath, lastModified);
0386     }
0387 }
0388 
0389 SvgPrivate::SvgPrivate(Svg *svg)
0390     : q(svg)
0391     , renderer(nullptr)
0392     , styleCrc(0)
0393     , colorGroup(Plasma::Theme::NormalColorGroup)
0394     , lastModified(0)
0395     , devicePixelRatio(1.0)
0396     , scaleFactor(s_lastScaleFactor)
0397     , status(Svg::Status::Normal)
0398     , multipleImages(false)
0399     , themed(false)
0400     , useSystemColors(false)
0401     , fromCurrentTheme(false)
0402     , applyColors(false)
0403     , usesColors(false)
0404     , cacheRendering(true)
0405     , themeFailed(false)
0406 {
0407 }
0408 
0409 SvgPrivate::~SvgPrivate()
0410 {
0411     eraseRenderer();
0412 }
0413 
0414 // This function is meant for the rects cache
0415 SvgPrivate::CacheId SvgPrivate::cacheId(QStringView elementId) const
0416 {
0417     auto idSize = size.isValid() && size != naturalSize ? size : QSizeF{-1.0, -1.0};
0418     return CacheId{idSize.width(), idSize.height(), path, elementId.toString(), status, devicePixelRatio, scaleFactor, -1, 0, lastModified};
0419 }
0420 
0421 // This function is meant for the pixmap cache
0422 QString SvgPrivate::cachePath(const QString &id, const QSize &size) const
0423 {
0424     auto cacheId = CacheId{double(size.width()), double(size.height()), path, id, status, devicePixelRatio, scaleFactor, colorGroup, 0, lastModified};
0425     return QString::number(qHash(cacheId, SvgRectsCache::s_seed));
0426 }
0427 
0428 bool SvgPrivate::setImagePath(const QString &imagePath)
0429 {
0430     QString actualPath = imagePath;
0431     if (imagePath.startsWith(QLatin1String("file://"))) {
0432         // length of file://
0433         actualPath.remove(0, 7);
0434     }
0435 
0436     bool isThemed = !actualPath.isEmpty() && !QDir::isAbsolutePath(actualPath);
0437     bool inIconTheme = false;
0438 
0439     // an absolute path.. let's try if this actually an *icon* theme
0440     if (!isThemed && !actualPath.isEmpty()) {
0441         const auto *iconTheme = KIconLoader::global()->theme();
0442         isThemed = inIconTheme = iconTheme && actualPath.startsWith(iconTheme->dir());
0443     }
0444     // lets check to see if we're already set to this file
0445     if (isThemed == themed && ((themed && themePath == actualPath) || (!themed && path == actualPath))) {
0446         return false;
0447     }
0448 
0449     eraseRenderer();
0450 
0451     // if we don't have any path right now and are going to set one,
0452     // then lets not schedule a repaint because we are just initializing!
0453     bool updateNeeded = true; //! path.isEmpty() || !themePath.isEmpty();
0454 
0455     QObject::disconnect(actualTheme(), SIGNAL(themeChanged()), q, SLOT(themeChanged()));
0456     if (isThemed && !themed && s_systemColorsCache) {
0457         // catch the case where we weren't themed, but now we are, and the colors cache was set up
0458         // ensure we are not connected to that theme previously
0459         QObject::disconnect(s_systemColorsCache.data(), nullptr, q, nullptr);
0460     }
0461 
0462     themed = isThemed;
0463     path.clear();
0464     themePath.clear();
0465 
0466     bool oldFromCurrentTheme = fromCurrentTheme;
0467     fromCurrentTheme = !inIconTheme && isThemed && actualTheme()->currentThemeHasImage(imagePath);
0468 
0469     if (fromCurrentTheme != oldFromCurrentTheme) {
0470         Q_EMIT q->fromCurrentThemeChanged(fromCurrentTheme);
0471     }
0472 
0473     if (inIconTheme) {
0474         themePath = actualPath;
0475         path = actualPath;
0476         QObject::connect(actualTheme(), SIGNAL(themeChanged()), q, SLOT(themeChanged()));
0477     } else if (themed) {
0478         themePath = actualPath;
0479         path = actualTheme()->imagePath(themePath);
0480         themeFailed = path.isEmpty();
0481         QObject::connect(actualTheme(), SIGNAL(themeChanged()), q, SLOT(themeChanged()));
0482     } else if (QFileInfo::exists(actualPath)) {
0483         QObject::connect(cacheAndColorsTheme(), SIGNAL(themeChanged()), q, SLOT(themeChanged()), Qt::UniqueConnection);
0484         path = actualPath;
0485     } else {
0486 #ifndef NDEBUG
0487         // qCDebug(LOG_PLASMA) << "file '" << path << "' does not exist!";
0488 #endif
0489     }
0490 
0491     QDateTime lastModifiedDate;
0492     if (!path.isEmpty()) {
0493         const QFileInfo info(path);
0494         lastModifiedDate = info.lastModified();
0495 
0496         lastModified = lastModifiedDate.toSecsSinceEpoch();
0497 
0498         const bool imageWasCached = SvgRectsCache::instance()->loadImageFromCache(path, lastModified);
0499 
0500         if (!imageWasCached) {
0501             auto i = s_renderers.constBegin();
0502             while (i != s_renderers.constEnd()) {
0503                 if (i.key().contains(path)) {
0504                     i.value()->reload();
0505                 }
0506                 i++;
0507             }
0508         }
0509     }
0510 
0511     // check if svg wants colorscheme applied
0512     checkColorHints();
0513 
0514     // also images with absolute path needs to have a natural size initialized,
0515     // even if looks a bit weird using Theme to store non-themed stuff
0516     if ((themed && !path.isEmpty() && lastModifiedDate.isValid()) || QFileInfo::exists(actualPath)) {
0517         naturalSize = SvgRectsCache::instance()->naturalSize(path, scaleFactor);
0518         if (naturalSize.isEmpty()) {
0519             createRenderer();
0520             naturalSize = renderer->defaultSize() * scaleFactor;
0521             SvgRectsCache::instance()->setNaturalSize(path, scaleFactor, naturalSize);
0522         }
0523     }
0524 
0525     q->resize();
0526     Q_EMIT q->imagePathChanged();
0527 
0528     return updateNeeded;
0529 }
0530 
0531 Theme *SvgPrivate::actualTheme()
0532 {
0533     if (!theme) {
0534         theme = new Plasma::Theme(q);
0535     }
0536 
0537     return theme.data();
0538 }
0539 
0540 Theme *SvgPrivate::cacheAndColorsTheme()
0541 {
0542     if (themed || !useSystemColors) {
0543         return actualTheme();
0544     } else {
0545         // use a separate cache source for unthemed svg's
0546         if (!s_systemColorsCache) {
0547             // FIXME: reference count this, so that it is deleted when no longer in use
0548             s_systemColorsCache = new Plasma::Theme(QStringLiteral("internal-system-colors"));
0549         }
0550 
0551         return s_systemColorsCache.data();
0552     }
0553 }
0554 
0555 QPixmap SvgPrivate::findInCache(const QString &elementId, qreal ratio, const QSizeF &s)
0556 {
0557     QSize size;
0558     QString actualElementId;
0559 
0560     // Look at the size hinted elements and try to find the smallest one with an
0561     // identical aspect ratio.
0562     if (s.isValid() && !elementId.isEmpty()) {
0563         const QList<QSize> elementSizeHints = SvgRectsCache::instance()->sizeHintsForId(path, elementId);
0564 
0565         if (!elementSizeHints.isEmpty()) {
0566             QSizeF bestFit(-1, -1);
0567 
0568             for (const auto &hint : elementSizeHints) {
0569                 if (hint.width() >= s.width() * ratio && hint.height() >= s.height() * ratio
0570                     && (!bestFit.isValid() || (bestFit.width() * bestFit.height()) > (hint.width() * hint.height()))) {
0571                     bestFit = hint;
0572                 }
0573             }
0574 
0575             if (bestFit.isValid()) {
0576                 actualElementId = QString::number(bestFit.width()) % QLatin1Char('-') % QString::number(bestFit.height()) % QLatin1Char('-') % elementId;
0577             }
0578         }
0579     }
0580 
0581     if (elementId.isEmpty() || !q->hasElement(actualElementId)) {
0582         actualElementId = elementId;
0583     }
0584 
0585     if (elementId.isEmpty() || (multipleImages && s.isValid())) {
0586         size = s.toSize() * ratio;
0587     } else {
0588         size = elementRect(actualElementId).size().toSize() * ratio;
0589     }
0590 
0591     if (size.isEmpty()) {
0592         return QPixmap();
0593     }
0594 
0595     const QString id = cachePath(actualElementId, size);
0596 
0597     QPixmap p;
0598     if (cacheRendering && lastModified == SvgRectsCache::instance()->lastModifiedTimeFromCache(path)
0599         && cacheAndColorsTheme()->findInCache(id, p, lastModified)) {
0600         p.setDevicePixelRatio(ratio);
0601         // qCDebug(LOG_PLASMA) << "found cached version of " << id << p.size();
0602         return p;
0603     }
0604 
0605     createRenderer();
0606 
0607     QRectF finalRect = makeUniform(renderer->boundsOnElement(actualElementId), QRect(QPoint(0, 0), size));
0608 
0609     // don't alter the pixmap size or it won't match up properly to, e.g., FrameSvg elements
0610     // makeUniform should never change the size so much that it gains or loses a whole pixel
0611     p = QPixmap(size);
0612 
0613     p.fill(Qt::transparent);
0614     QPainter renderPainter(&p);
0615 
0616     if (actualElementId.isEmpty()) {
0617         renderer->render(&renderPainter, finalRect);
0618     } else {
0619         renderer->render(&renderPainter, actualElementId, finalRect);
0620     }
0621 
0622     renderPainter.end();
0623     p.setDevicePixelRatio(ratio);
0624 
0625     // Apply current color scheme if the svg asks for it
0626     if (applyColors) {
0627         QImage itmp = p.toImage();
0628         KIconEffect::colorize(itmp, cacheAndColorsTheme()->color(Theme::BackgroundColor), 1.0);
0629         p = p.fromImage(itmp);
0630     }
0631 
0632     if (cacheRendering) {
0633         cacheAndColorsTheme()->insertIntoCache(id, p, QString::number((qint64)q, 16) % QLatin1Char('_') % actualElementId);
0634     }
0635 
0636     SvgRectsCache::instance()->updateLastModified(path, lastModified);
0637 
0638     return p;
0639 }
0640 
0641 void SvgPrivate::createRenderer()
0642 {
0643     if (renderer) {
0644         return;
0645     }
0646 
0647     if (themed && path.isEmpty() && !themeFailed) {
0648         Applet *applet = qobject_cast<Applet *>(q->parent());
0649         // FIXME: this maybe could be more efficient if we knew if the package was empty, e.g. for
0650         // C++; however, I'm not sure this has any real world runtime impact. something to measure
0651         // for.
0652         if (applet) {
0653             path = applet->filePath("images", themePath + QLatin1String(".svg"));
0654 
0655             if (path.isEmpty()) {
0656                 path = applet->filePath("images", themePath + QLatin1String(".svgz"));
0657             }
0658         }
0659 
0660         if (path.isEmpty()) {
0661             path = actualTheme()->imagePath(themePath);
0662             themeFailed = path.isEmpty();
0663             if (themeFailed) {
0664                 qCWarning(LOG_PLASMA) << "No image path found for" << themePath;
0665             }
0666         }
0667     }
0668 
0669     QString styleSheet = cacheAndColorsTheme()->d->svgStyleSheet(colorGroup, status);
0670     styleCrc = qChecksum(styleSheet.toUtf8().constData(), styleSheet.size());
0671 
0672     QHash<QString, SharedSvgRenderer::Ptr>::const_iterator it = s_renderers.constFind(styleCrc + path);
0673 
0674     if (it != s_renderers.constEnd()) {
0675         renderer = it.value();
0676     } else {
0677         if (path.isEmpty()) {
0678             renderer = new SharedSvgRenderer();
0679         } else {
0680             QHash<QString, QRectF> interestingElements;
0681             renderer = new SharedSvgRenderer(path, styleSheet, interestingElements);
0682 
0683             // Add interesting elements to the theme's rect cache.
0684             QHashIterator<QString, QRectF> i(interestingElements);
0685 
0686             QRegularExpression sizeHintedKeyExpr(QStringLiteral("^(\\d+)-(\\d+)-(.+)$"));
0687 
0688             while (i.hasNext()) {
0689                 i.next();
0690                 const QString &elementId = i.key();
0691                 QString originalId = i.key();
0692                 const QRectF &elementRect = i.value();
0693 
0694                 originalId.replace(sizeHintedKeyExpr, QStringLiteral("\\3"));
0695                 SvgRectsCache::instance()->insertSizeHintForId(path, originalId, elementRect.size().toSize());
0696 
0697                 const CacheId cacheId({-1.0, -1.0, path, elementId, status, devicePixelRatio, scaleFactor, -1, 0, lastModified});
0698                 SvgRectsCache::instance()->insert(cacheId, elementRect, lastModified);
0699             }
0700         }
0701 
0702         s_renderers[styleCrc + path] = renderer;
0703     }
0704 
0705     if (size == QSizeF()) {
0706         size = renderer->defaultSize();
0707     }
0708 }
0709 
0710 void SvgPrivate::eraseRenderer()
0711 {
0712     if (renderer && renderer->ref.loadRelaxed() == 2) {
0713         // this and the cache reference it
0714         s_renderers.erase(s_renderers.find(styleCrc + path));
0715     }
0716 
0717     renderer = nullptr;
0718     styleCrc = QChar(0);
0719 }
0720 
0721 QRectF SvgPrivate::elementRect(QStringView elementId)
0722 {
0723     if (themed && path.isEmpty()) {
0724         if (themeFailed) {
0725             return QRectF();
0726         }
0727 
0728         path = actualTheme()->imagePath(themePath);
0729         themeFailed = path.isEmpty();
0730 
0731         if (themeFailed) {
0732             return QRectF();
0733         }
0734     }
0735 
0736     if (path.isEmpty()) {
0737         return QRectF();
0738     }
0739 
0740     QRectF rect;
0741     const CacheId cacheId = SvgPrivate::cacheId(elementId);
0742     bool found = SvgRectsCache::instance()->findElementRect(cacheId, rect);
0743     // This is a corner case where we are *sure* the element is not valid
0744     if (!found) {
0745         rect = findAndCacheElementRect(elementId);
0746     }
0747 
0748     return rect;
0749 }
0750 
0751 QRectF SvgPrivate::findAndCacheElementRect(QStringView elementId)
0752 {
0753     // we need to check the id before createRenderer(), otherwise it may generate a different id compared to the previous cacheId)( call
0754     const CacheId cacheId = SvgPrivate::cacheId(elementId);
0755 
0756     createRenderer();
0757 
0758     auto elementIdString = elementId.toString();
0759 
0760     // This code will usually never be run because createRenderer already caches all the boundingRect in the elements in the svg
0761     QRectF elementRect = renderer->elementExists(elementIdString)
0762         ? renderer->transformForElement(elementIdString).map(renderer->boundsOnElement(elementIdString)).boundingRect()
0763         : QRectF();
0764 
0765     naturalSize = renderer->defaultSize() * scaleFactor;
0766 
0767     qreal dx = size.width() / renderer->defaultSize().width();
0768     qreal dy = size.height() / renderer->defaultSize().height();
0769 
0770     elementRect = QRectF(elementRect.x() * dx, elementRect.y() * dy, elementRect.width() * dx, elementRect.height() * dy);
0771     SvgRectsCache::instance()->insert(cacheId, elementRect, lastModified);
0772 
0773     return elementRect;
0774 }
0775 
0776 void SvgPrivate::checkColorHints()
0777 {
0778     if (elementRect(QStringLiteral("hint-apply-color-scheme")).isValid()) {
0779         applyColors = true;
0780         usesColors = true;
0781     } else if (elementRect(QStringLiteral("current-color-scheme")).isValid()) {
0782         applyColors = false;
0783         usesColors = true;
0784     } else {
0785         applyColors = false;
0786         usesColors = false;
0787     }
0788 
0789     // check to see if we are using colors, but the theme isn't being used or isn't providing
0790     // a colorscheme
0791     if (qGuiApp) {
0792         if (usesColors && (!themed || !actualTheme()->colorScheme())) {
0793             QObject::connect(actualTheme()->d, SIGNAL(applicationPaletteChange()), q, SLOT(colorsChanged()));
0794         } else {
0795             QObject::disconnect(actualTheme()->d, SIGNAL(applicationPaletteChange()), q, SLOT(colorsChanged()));
0796         }
0797     }
0798 }
0799 
0800 bool Svg::eventFilter(QObject *watched, QEvent *event)
0801 {
0802     return QObject::eventFilter(watched, event);
0803 }
0804 
0805 // Following two are utility functions to snap rendered elements to the pixel grid
0806 // to and from are always 0 <= val <= 1
0807 qreal SvgPrivate::closestDistance(qreal to, qreal from)
0808 {
0809     qreal a = to - from;
0810     if (qFuzzyCompare(to, from)) {
0811         return 0;
0812     } else if (to > from) {
0813         qreal b = to - from - 1;
0814         return (qAbs(a) > qAbs(b)) ? b : a;
0815     } else {
0816         qreal b = 1 + to - from;
0817         return (qAbs(a) > qAbs(b)) ? b : a;
0818     }
0819 }
0820 
0821 QRectF SvgPrivate::makeUniform(const QRectF &orig, const QRectF &dst)
0822 {
0823     if (qFuzzyIsNull(orig.x()) || qFuzzyIsNull(orig.y())) {
0824         return dst;
0825     }
0826 
0827     QRectF res(dst);
0828     qreal div_w = dst.width() / orig.width();
0829     qreal div_h = dst.height() / orig.height();
0830 
0831     qreal div_x = dst.x() / orig.x();
0832     qreal div_y = dst.y() / orig.y();
0833 
0834     // horizontal snap
0835     if (!qFuzzyIsNull(div_x) && !qFuzzyCompare(div_w, div_x)) {
0836         qreal rem_orig = orig.x() - (floor(orig.x()));
0837         qreal rem_dst = dst.x() - (floor(dst.x()));
0838         qreal offset = closestDistance(rem_dst, rem_orig);
0839         res.translate(offset + offset * div_w, 0);
0840         res.setWidth(res.width() + offset);
0841     }
0842     // vertical snap
0843     if (!qFuzzyIsNull(div_y) && !qFuzzyCompare(div_h, div_y)) {
0844         qreal rem_orig = orig.y() - (floor(orig.y()));
0845         qreal rem_dst = dst.y() - (floor(dst.y()));
0846         qreal offset = closestDistance(rem_dst, rem_orig);
0847         res.translate(0, offset + offset * div_h);
0848         res.setHeight(res.height() + offset);
0849     }
0850 
0851     return res;
0852 }
0853 
0854 void SvgPrivate::themeChanged()
0855 {
0856     if (q->imagePath().isEmpty()) {
0857         return;
0858     }
0859 
0860     if (themed) {
0861         // check if new theme svg wants colorscheme applied
0862         checkColorHints();
0863     }
0864 
0865     QString currentPath = themed ? themePath : path;
0866     themePath.clear();
0867     eraseRenderer();
0868     setImagePath(currentPath);
0869     q->resize();
0870 
0871     // qCDebug(LOG_PLASMA) << themePath << ">>>>>>>>>>>>>>>>>> theme changed";
0872     Q_EMIT q->repaintNeeded();
0873 }
0874 
0875 void SvgPrivate::colorsChanged()
0876 {
0877     if (!usesColors) {
0878         return;
0879     }
0880 
0881     eraseRenderer();
0882     qCDebug(LOG_PLASMA) << "repaint needed from colorsChanged";
0883 
0884     Q_EMIT q->repaintNeeded();
0885 }
0886 
0887 QHash<QString, SharedSvgRenderer::Ptr> SvgPrivate::s_renderers;
0888 QPointer<Theme> SvgPrivate::s_systemColorsCache;
0889 qreal SvgPrivate::s_lastScaleFactor = 1.0;
0890 
0891 Svg::Svg(QObject *parent)
0892     : QObject(parent)
0893     , d(new SvgPrivate(this))
0894 {
0895     connect(SvgRectsCache::instance(), &SvgRectsCache::lastModifiedChanged, this, [this](const QString &filePath, unsigned int lastModified) {
0896         if (d->lastModified != lastModified && filePath == d->path) {
0897             d->lastModified = lastModified;
0898             Q_EMIT repaintNeeded();
0899         }
0900     });
0901 }
0902 
0903 Svg::~Svg()
0904 {
0905     delete d;
0906 }
0907 
0908 void Svg::setDevicePixelRatio(qreal ratio)
0909 {
0910     // be completely integer for now
0911     // devicepixelratio is always set integer in the svg, so needs at least 192dpi to double up.
0912     //(it needs to be integer to have lines contained inside a svg piece to keep being pixel aligned)
0913     if (floor(d->devicePixelRatio) == floor(ratio)) {
0914         return;
0915     }
0916 
0917     if (FrameSvg *f = qobject_cast<FrameSvg *>(this)) {
0918         f->clearCache();
0919     }
0920 
0921     d->devicePixelRatio = floor(ratio);
0922 
0923     Q_EMIT repaintNeeded();
0924 }
0925 
0926 qreal Svg::devicePixelRatio()
0927 {
0928     return d->devicePixelRatio;
0929 }
0930 
0931 void Svg::setScaleFactor(qreal ratio)
0932 {
0933     // be completely integer for now
0934     // devicepixelratio is always set integer in the svg, so needs at least 192dpi to double up.
0935     //(it needs to be integer to have lines contained inside a svg piece to keep being pixel aligned)
0936     if (floor(d->scaleFactor) == floor(ratio)) {
0937         return;
0938     }
0939 
0940     d->scaleFactor = floor(ratio);
0941     d->s_lastScaleFactor = d->scaleFactor;
0942     // not resize() because we want to do it unconditionally
0943 
0944     d->naturalSize = SvgRectsCache::instance()->naturalSize(d->path, d->scaleFactor);
0945     if (d->naturalSize.isEmpty()) {
0946         d->createRenderer();
0947         d->naturalSize = d->renderer->defaultSize() * d->scaleFactor;
0948     }
0949 
0950     d->size = d->naturalSize;
0951 
0952     Q_EMIT repaintNeeded();
0953     Q_EMIT sizeChanged();
0954 }
0955 
0956 qreal Svg::scaleFactor() const
0957 {
0958     return d->scaleFactor;
0959 }
0960 
0961 void Svg::setColorGroup(Plasma::Theme::ColorGroup group)
0962 {
0963     if (d->colorGroup == group) {
0964         return;
0965     }
0966 
0967     d->colorGroup = group;
0968     d->renderer = nullptr;
0969     Q_EMIT colorGroupChanged();
0970     Q_EMIT repaintNeeded();
0971 }
0972 
0973 Plasma::Theme::ColorGroup Svg::colorGroup() const
0974 {
0975     return d->colorGroup;
0976 }
0977 
0978 QPixmap Svg::pixmap(const QString &elementID)
0979 {
0980     if (elementID.isNull() || d->multipleImages) {
0981         return d->findInCache(elementID, d->devicePixelRatio, size());
0982     } else {
0983         return d->findInCache(elementID, d->devicePixelRatio);
0984     }
0985 }
0986 
0987 QImage Svg::image(const QSize &size, const QString &elementID)
0988 {
0989     QPixmap pix(d->findInCache(elementID, d->devicePixelRatio, size));
0990     return pix.toImage();
0991 }
0992 
0993 void Svg::paint(QPainter *painter, const QPointF &point, const QString &elementID)
0994 {
0995     Q_ASSERT(painter->device());
0996     const int ratio = painter->device()->devicePixelRatio();
0997     QPixmap pix((elementID.isNull() || d->multipleImages) ? d->findInCache(elementID, ratio, size()) : d->findInCache(elementID, ratio));
0998 
0999     if (pix.isNull()) {
1000         return;
1001     }
1002 
1003     painter->drawPixmap(QRectF(point, size()), pix, QRectF(QPointF(0, 0), pix.size()));
1004 }
1005 
1006 void Svg::paint(QPainter *painter, int x, int y, const QString &elementID)
1007 {
1008     paint(painter, QPointF(x, y), elementID);
1009 }
1010 
1011 void Svg::paint(QPainter *painter, const QRectF &rect, const QString &elementID)
1012 {
1013     Q_ASSERT(painter->device());
1014     const int ratio = painter->device()->devicePixelRatio();
1015     QPixmap pix(d->findInCache(elementID, ratio, rect.size()));
1016 
1017     painter->drawPixmap(QRectF(rect.topLeft(), rect.size()), pix, QRectF(QPointF(0, 0), pix.size()));
1018 }
1019 
1020 void Svg::paint(QPainter *painter, int x, int y, int width, int height, const QString &elementID)
1021 {
1022     Q_ASSERT(painter->device());
1023     const int ratio = painter->device()->devicePixelRatio();
1024     QPixmap pix(d->findInCache(elementID, ratio, QSizeF(width, height)));
1025     painter->drawPixmap(x, y, pix, 0, 0, pix.size().width(), pix.size().height());
1026 }
1027 
1028 QSize Svg::size() const
1029 {
1030     if (d->size.isEmpty()) {
1031         d->size = d->naturalSize;
1032     }
1033 
1034     return d->size.toSize();
1035 }
1036 
1037 void Svg::resize(qreal width, qreal height)
1038 {
1039     resize(QSize(width, height));
1040 }
1041 
1042 void Svg::resize(const QSizeF &size)
1043 {
1044     if (qFuzzyCompare(size.width(), d->size.width()) && qFuzzyCompare(size.height(), d->size.height())) {
1045         return;
1046     }
1047 
1048     d->size = size;
1049     Q_EMIT sizeChanged();
1050 }
1051 
1052 void Svg::resize()
1053 {
1054     if (qFuzzyCompare(d->naturalSize.width(), d->size.width()) && qFuzzyCompare(d->naturalSize.height(), d->size.height())) {
1055         return;
1056     }
1057 
1058     d->size = d->naturalSize;
1059     Q_EMIT sizeChanged();
1060 }
1061 
1062 QSize Svg::elementSize(const QString &elementId) const
1063 {
1064     return d->elementRect(elementId).size().toSize();
1065 }
1066 
1067 QSize Svg::elementSize(QStringView elementId) const
1068 {
1069     return d->elementRect(elementId).size().toSize();
1070 }
1071 
1072 QRectF Svg::elementRect(const QString &elementId) const
1073 {
1074     return d->elementRect(elementId);
1075 }
1076 
1077 QRectF Svg::elementRect(QStringView elementId) const
1078 {
1079     return d->elementRect(elementId);
1080 }
1081 
1082 bool Svg::hasElement(const QString &elementId) const
1083 {
1084     return hasElement(QStringView(elementId));
1085 }
1086 
1087 bool Svg::hasElement(QStringView elementId) const
1088 {
1089     if (elementId.isEmpty() || (d->path.isNull() && d->themePath.isNull())) {
1090         return false;
1091     }
1092 
1093     return d->elementRect(elementId).isValid();
1094 }
1095 
1096 bool Svg::isValid() const
1097 {
1098     if (d->path.isNull() && d->themePath.isNull()) {
1099         return false;
1100     }
1101 
1102     // try very hard to avoid creation of a parser
1103     QSizeF naturalSize = SvgRectsCache::instance()->naturalSize(d->path, d->scaleFactor);
1104     if (!naturalSize.isEmpty()) {
1105         return true;
1106     }
1107 
1108     if (d->path.isEmpty() || !QFileInfo::exists(d->path)) {
1109         return false;
1110     }
1111     d->createRenderer();
1112     return d->renderer->isValid();
1113 }
1114 
1115 void Svg::setContainsMultipleImages(bool multiple)
1116 {
1117     d->multipleImages = multiple;
1118 }
1119 
1120 bool Svg::containsMultipleImages() const
1121 {
1122     return d->multipleImages;
1123 }
1124 
1125 void Svg::setImagePath(const QString &svgFilePath)
1126 {
1127     if (d->setImagePath(svgFilePath)) {
1128         Q_EMIT repaintNeeded();
1129     }
1130 }
1131 
1132 QString Svg::imagePath() const
1133 {
1134     return d->themed ? d->themePath : d->path;
1135 }
1136 
1137 void Svg::setUsingRenderingCache(bool useCache)
1138 {
1139     d->cacheRendering = useCache;
1140 }
1141 
1142 bool Svg::isUsingRenderingCache() const
1143 {
1144     return d->cacheRendering;
1145 }
1146 
1147 bool Svg::fromCurrentTheme() const
1148 {
1149     return d->fromCurrentTheme;
1150 }
1151 
1152 void Svg::setUseSystemColors(bool system)
1153 {
1154     if (d->useSystemColors == system) {
1155         return;
1156     }
1157 
1158     d->useSystemColors = system;
1159     Q_EMIT repaintNeeded();
1160 }
1161 
1162 bool Svg::useSystemColors() const
1163 {
1164     return d->useSystemColors;
1165 }
1166 
1167 void Svg::setTheme(Plasma::Theme *theme)
1168 {
1169     if (!theme || theme == d->theme.data()) {
1170         return;
1171     }
1172 
1173     if (d->theme) {
1174         disconnect(d->theme.data(), nullptr, this, nullptr);
1175     }
1176 
1177     d->theme = theme;
1178     connect(theme, SIGNAL(themeChanged()), this, SLOT(themeChanged()));
1179     d->themeChanged();
1180 }
1181 
1182 Theme *Svg::theme() const
1183 {
1184     return d->actualTheme();
1185 }
1186 
1187 void Svg::setStatus(Plasma::Svg::Status status)
1188 {
1189     if (status == d->status) {
1190         return;
1191     }
1192 
1193     d->status = status;
1194     d->eraseRenderer();
1195     Q_EMIT statusChanged(status);
1196     Q_EMIT repaintNeeded();
1197 }
1198 
1199 Svg::Status Svg::status() const
1200 {
1201     return d->status;
1202 }
1203 
1204 } // Plasma namespace
1205 
1206 #include "moc_svg.cpp"
1207 #include "private/moc_svg_p.cpp"