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"