KSvg

svg.cpp
1/*
2 SPDX-FileCopyrightText: 2006-2007 Aaron Seigo <aseigo@kde.org>
3 SPDX-FileCopyrightText: 2008-2010 Marco Martin <notmart@gmail.com>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "svg.h"
9#include "framesvg.h"
10#include "private/imageset_p.h"
11#include "private/svg_p.h"
12
13#include <array>
14#include <cmath>
15#include <mutex>
16
17#include <QBuffer>
18#include <QCoreApplication>
19#include <QDir>
20#include <QPainter>
21#include <QRegularExpression>
22#include <QStringBuilder>
23#include <QXmlStreamReader>
24#include <QXmlStreamWriter>
25
26#include <KCompressionDevice>
27#include <KConfigGroup>
28#include <QDebug>
29
30#include "debug_p.h"
31#include "imageset.h"
32
33size_t qHash(const KSvg::SvgPrivate::CacheId &id, size_t seed)
34{
35 std::array<size_t, 10> parts = {
36 ::qHash(id.width),
37 ::qHash(id.height),
38 ::qHash(id.elementName),
39 ::qHash(id.filePath),
40 ::qHash(id.status),
41 ::qHash(id.scaleFactor),
42 ::qHash(id.colorSet),
43 ::qHash(id.styleSheet),
44 ::qHash(id.extraFlags),
45 ::qHash(id.lastModified),
46 };
47 return qHashRange(parts.begin(), parts.end(), seed);
48}
49
50size_t qHash(const QList<QColor> &colors, size_t seed)
51{
52 std::vector<size_t> parts;
53 for (const QColor &c : std::as_const(colors)) {
54 parts.push_back(::qHash(c.red()));
55 parts.push_back(::qHash(c.green()));
56 parts.push_back(::qHash(c.blue()));
57 parts.push_back(::qHash(c.alpha()));
58 }
59 return qHashRange(parts.begin(), parts.end(), seed);
60}
61
62namespace KSvg
63{
64class SvgRectsCacheSingleton
65{
66public:
67 SvgRectsCache self;
68};
69
70Q_GLOBAL_STATIC(SvgRectsCacheSingleton, privateSvgRectsCacheSelf)
71
72const size_t SvgRectsCache::s_seed = 0x9e3779b9;
73
74SharedSvgRenderer::SharedSvgRenderer(QObject *parent)
75 : QSvgRenderer(parent)
76{
77}
78
79SharedSvgRenderer::SharedSvgRenderer(const QString &filename, const QString &styleSheet, QHash<QString, QRectF> &interestingElements, QObject *parent)
80 : QSvgRenderer(parent)
81{
82 KCompressionDevice file(filename, KCompressionDevice::GZip);
83 if (!file.open(QIODevice::ReadOnly)) {
84 return;
85 }
86 m_filename = filename;
87 m_styleSheet = styleSheet;
88 m_interestingElements = interestingElements;
89 load(file.readAll(), styleSheet, interestingElements);
90}
91
92SharedSvgRenderer::SharedSvgRenderer(const QByteArray &contents, const QString &styleSheet, QHash<QString, QRectF> &interestingElements, QObject *parent)
93 : QSvgRenderer(parent)
94{
95 load(contents, styleSheet, interestingElements);
96}
97
98void SharedSvgRenderer::reload()
99{
100 KCompressionDevice file(m_filename, KCompressionDevice::GZip);
101 if (!file.open(QIODevice::ReadOnly)) {
102 return;
103 }
104
105 load(file.readAll(), m_styleSheet, m_interestingElements);
106}
107
108bool SharedSvgRenderer::load(const QByteArray &contents, const QString &styleSheet, QHash<QString, QRectF> &interestingElements)
109{
110 // Apply the style sheet.
111 if (!styleSheet.isEmpty() && contents.contains("current-color-scheme")) {
112 QByteArray processedContents;
113 processedContents.reserve(contents.size());
114 QXmlStreamReader reader(contents);
115
116 QBuffer buffer(&processedContents);
117 buffer.open(QIODevice::WriteOnly);
118 QXmlStreamWriter writer(&buffer);
119 while (!reader.atEnd()) {
120 if (reader.readNext() == QXmlStreamReader::StartElement && reader.qualifiedName() == QLatin1String("style")
121 && reader.attributes().value(QLatin1String("id")) == QLatin1String("current-color-scheme")) {
122 writer.writeStartElement(QLatin1String("style"));
123 writer.writeAttributes(reader.attributes());
124 writer.writeCharacters(styleSheet);
125 writer.writeEndElement();
126 while (reader.tokenType() != QXmlStreamReader::EndElement) {
127 reader.readNext();
128 }
129 } else if (reader.tokenType() != QXmlStreamReader::Invalid) {
130 writer.writeCurrentToken(reader);
131 }
132 }
133 buffer.close();
134 if (!QSvgRenderer::load(processedContents)) {
135 return false;
136 }
137 } else if (!QSvgRenderer::load(contents)) {
138 return false;
139 }
140
141 // Search the SVG to find and store all ids that contain size hints.
142 const QString contentsAsString(QString::fromLatin1(contents));
143 static const QRegularExpression idExpr(QLatin1String("id\\s*?=\\s*?(['\"])(\\d+?-\\d+?-.*?)\\1"));
144 Q_ASSERT(idExpr.isValid());
145
146 auto matchIt = idExpr.globalMatch(contentsAsString);
147 while (matchIt.hasNext()) {
148 auto match = matchIt.next();
149 QString elementId = match.captured(2);
150
151 QRectF elementRect = boundsOnElement(elementId);
152 if (elementRect.isValid()) {
153 interestingElements.insert(elementId, elementRect);
154 }
155 }
156
157 return true;
158}
159
160SvgRectsCache::SvgRectsCache(QObject *parent)
161 : QObject(parent)
162{
163 const QString svgElementsFile = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1Char('/') + QStringLiteral("ksvg-elements");
164 m_svgElementsCache = KSharedConfig::openConfig(svgElementsFile, KConfig::SimpleConfig);
165
166 m_configSyncTimer = new QTimer(this);
167 m_configSyncTimer->setSingleShot(true);
168 m_configSyncTimer->setInterval(5000);
169 connect(m_configSyncTimer, &QTimer::timeout, this, [this]() {
170 m_svgElementsCache->sync();
171 });
172}
173
174SvgRectsCache *SvgRectsCache::instance()
175{
176 return &privateSvgRectsCacheSelf()->self;
177}
178
179void SvgRectsCache::insert(KSvg::SvgPrivate::CacheId cacheId, const QRectF &rect, unsigned int lastModified)
180{
181 insert(qHash(cacheId, SvgRectsCache::s_seed), cacheId.filePath, rect, lastModified);
182}
183
184void SvgRectsCache::insert(size_t id, const QString &filePath, const QRectF &rect, unsigned int lastModified)
185{
186 const unsigned int savedTime = lastModifiedTimeFromCache(filePath);
187
188 if (savedTime == lastModified && m_localRectCache.contains(id)) {
189 return;
190 }
191
192 m_localRectCache.insert(id, rect);
193
194 KConfigGroup imageGroup(m_svgElementsCache, filePath);
195
196 if (rect.isValid()) {
197 imageGroup.writeEntry(QString::number(id), rect);
198 } else {
199 m_invalidElements[filePath] << id;
200 imageGroup.writeEntry("Invalidelements", m_invalidElements[filePath].values());
201 }
202
203 QMetaObject::invokeMethod(m_configSyncTimer, qOverload<>(&QTimer::start));
204
205 if (savedTime != lastModified) {
206 m_lastModifiedTimes[filePath] = lastModified;
207 imageGroup.writeEntry("LastModified", lastModified);
208 Q_EMIT lastModifiedChanged(filePath, lastModified);
209 }
210}
211
212bool SvgRectsCache::findElementRect(KSvg::SvgPrivate::CacheId cacheId, QRectF &rect)
213{
214 return findElementRect(qHash(cacheId, SvgRectsCache::s_seed), cacheId.filePath, rect);
215}
216
217bool SvgRectsCache::findElementRect(size_t id, QStringView filePath, QRectF &rect)
218{
219 auto it = m_localRectCache.find(id);
220
221 if (it == m_localRectCache.end()) {
222 auto elements = m_invalidElements.value(filePath.toString());
223 if (elements.contains(id)) {
224 rect = QRectF();
225 return true;
226 }
227 return false;
228 }
229
230 rect = *it;
231
232 return true;
233}
234
235bool SvgRectsCache::loadImageFromCache(const QString &path, uint lastModified)
236{
237 if (path.isEmpty()) {
238 return false;
239 }
240
241 KConfigGroup imageGroup(m_svgElementsCache, path);
242
243 unsigned int savedTime = lastModifiedTimeFromCache(path);
244
245 // Reload even if is older, to support downgrades
246 if (lastModified != savedTime) {
247 imageGroup.deleteGroup();
248 QMetaObject::invokeMethod(m_configSyncTimer, qOverload<>(&QTimer::start));
249 return false;
250 }
251
252 auto &elements = m_invalidElements[path];
253 if (elements.isEmpty()) {
254 auto list = imageGroup.readEntry("Invalidelements", QList<unsigned int>());
255 m_invalidElements[path] = QSet<unsigned int>(list.begin(), list.end());
256
257 for (const auto &key : imageGroup.keyList()) {
258 bool ok = false;
259 uint keyUInt = key.toUInt(&ok);
260 if (ok) {
261 const QRectF rect = imageGroup.readEntry(key, QRectF());
262 m_localRectCache.insert(keyUInt, rect);
263 }
264 }
265 }
266 return true;
267}
268
269void SvgRectsCache::dropImageFromCache(const QString &path)
270{
271 KConfigGroup imageGroup(m_svgElementsCache, path);
272 imageGroup.deleteGroup();
273 QMetaObject::invokeMethod(m_configSyncTimer, qOverload<>(&QTimer::start));
274}
275
276QList<QSizeF> SvgRectsCache::sizeHintsForId(const QString &path, const QString &id)
277{
278 const QString pathId = path % id;
279
280 auto it = m_sizeHintsForId.constFind(pathId);
281 if (it == m_sizeHintsForId.constEnd()) {
282 KConfigGroup imageGroup(m_svgElementsCache, path);
283 const QStringList &encoded = imageGroup.readEntry(id, QStringList());
284 QList<QSizeF> sizes;
285 for (const auto &token : encoded) {
286 const auto &parts = token.split(QLatin1Char('x'));
287 if (parts.size() != 2) {
288 continue;
289 }
290 QSize size = QSize(parts[0].toDouble(), parts[1].toDouble());
291 if (!size.isEmpty()) {
292 sizes << size;
293 }
294 }
295 m_sizeHintsForId[pathId] = sizes;
296 return sizes;
297 }
298
299 return *it;
300}
301
302void SvgRectsCache::insertSizeHintForId(const QString &path, const QString &id, const QSizeF &size)
303{
304 // TODO: need to make this more efficient
305 auto sizeListToString = [](const QList<QSizeF> &list) {
306 QString ret;
307 for (const auto &s : list) {
308 ret += QString::number(s.width()) % QLatin1Char('x') % QString::number(s.height()) % QLatin1Char(',');
309 }
310 return ret;
311 };
312 m_sizeHintsForId[path % id].append(size);
313 KConfigGroup imageGroup(m_svgElementsCache, path);
314 imageGroup.writeEntry(id, sizeListToString(m_sizeHintsForId[path % id]));
315 QMetaObject::invokeMethod(m_configSyncTimer, qOverload<>(&QTimer::start));
316}
317
318QString SvgRectsCache::iconThemePath()
319{
320 if (!m_iconThemePath.isEmpty()) {
321 return m_iconThemePath;
322 }
323
324 KConfigGroup imageGroup(m_svgElementsCache, QStringLiteral("General"));
325 m_iconThemePath = imageGroup.readEntry(QStringLiteral("IconThemePath"), QString());
326
327 return m_iconThemePath;
328}
329
330void SvgRectsCache::setIconThemePath(const QString &path)
331{
332 m_iconThemePath = path;
333 KConfigGroup imageGroup(m_svgElementsCache, QStringLiteral("General"));
334 imageGroup.writeEntry(QStringLiteral("IconThemePath"), path);
335 QMetaObject::invokeMethod(m_configSyncTimer, qOverload<>(&QTimer::start));
336}
337
338void SvgRectsCache::setNaturalSize(const QString &path, const QSizeF &size)
339{
340 KConfigGroup imageGroup(m_svgElementsCache, path);
341
342 // FIXME: needs something faster, perhaps even sprintf
343 imageGroup.writeEntry(QStringLiteral("NaturalSize"), size);
344 QMetaObject::invokeMethod(m_configSyncTimer, qOverload<>(&QTimer::start));
345}
346
347QSizeF SvgRectsCache::naturalSize(const QString &path)
348{
349 KConfigGroup imageGroup(m_svgElementsCache, path);
350
351 // FIXME: needs something faster, perhaps even sprintf
352 return imageGroup.readEntry(QStringLiteral("NaturalSize"), QSizeF());
353}
354
355QStringList SvgRectsCache::cachedKeysForPath(const QString &path) const
356{
357 KConfigGroup imageGroup(m_svgElementsCache, path);
358 QStringList list = imageGroup.keyList();
359 QStringList filtered;
360
361 std::copy_if(list.begin(), list.end(), std::back_inserter(filtered), [](const QString element) {
362 bool ok;
363 element.toLong(&ok);
364 return ok;
365 });
366 return filtered;
367}
368
369unsigned int SvgRectsCache::lastModifiedTimeFromCache(const QString &filePath)
370{
371 const auto &i = m_lastModifiedTimes.constFind(filePath);
372 if (i != m_lastModifiedTimes.constEnd()) {
373 return i.value();
374 }
375
376 KConfigGroup imageGroup(m_svgElementsCache, filePath);
377 const unsigned int savedTime = imageGroup.readEntry("LastModified", 0);
378 m_lastModifiedTimes[filePath] = savedTime;
379 return savedTime;
380}
381
382void SvgRectsCache::updateLastModified(const QString &filePath, unsigned int lastModified)
383{
384 KConfigGroup imageGroup(m_svgElementsCache, filePath);
385 const unsigned int savedTime = lastModifiedTimeFromCache(filePath);
386
387 if (savedTime != lastModified) {
388 m_lastModifiedTimes[filePath] = lastModified;
389 imageGroup.writeEntry("LastModified", lastModified);
390 QMetaObject::invokeMethod(m_configSyncTimer, qOverload<>(&QTimer::start));
391 Q_EMIT lastModifiedChanged(filePath, lastModified);
392 }
393}
394
395SvgPrivate::SvgPrivate(Svg *svg)
396 : q(svg)
397 , renderer(nullptr)
398 , styleCrc(0)
399 , lastModified(0)
400 , devicePixelRatio(1.0)
401 , status(Svg::Status::Normal)
402 , multipleImages(false)
403 , themed(false)
404 , fromCurrentImageSet(false)
405 , cacheRendering(true)
406 , themeFailed(false)
407{
408}
409
410SvgPrivate::~SvgPrivate()
411{
412 eraseRenderer();
413}
414
415size_t SvgPrivate::paletteId(const QPalette &palette, const QColor &positive, const QColor &neutral, const QColor &negative) const
416{
417 std::array<size_t, 4> parts = {
418 ::qHash(palette.cacheKey()),
419 ::qHash(positive.rgba()),
420 ::qHash(neutral.rgba()),
421 ::qHash(negative.rgba()),
422 };
423 return qHashRange(parts.begin(), parts.end(), SvgRectsCache::s_seed);
424}
425
426// This function is meant for the rects cache
427SvgPrivate::CacheId SvgPrivate::cacheId(QStringView elementId) const
428{
429 auto idSize = size.isValid() && size != naturalSize ? size : QSizeF{-1.0, -1.0};
430 return CacheId{idSize.width(), idSize.height(), path, elementId.toString(), status, devicePixelRatio, -1, 0, 0, lastModified};
431}
432
433// This function is meant for the pixmap cache
434QString SvgPrivate::cachePath(const QString &id, const QSize &size) const
435{
436 std::vector<size_t> parts;
437 const auto colors = colorOverrides.values();
438 for (const QColor &c : std::as_const(colors)) {
439 parts.push_back(::qHash(c.red()));
440 parts.push_back(::qHash(c.green()));
441 parts.push_back(::qHash(c.blue()));
442 parts.push_back(::qHash(c.alpha()));
443 }
444 const size_t colorsHash = qHashRange(parts.begin(), parts.end(), SvgRectsCache::s_seed);
445
446 auto cacheId = CacheId{double(size.width()), double(size.height()), path, id, status, devicePixelRatio, colorSet, colorsHash, 0, lastModified};
447 return QString::number(qHash(cacheId, SvgRectsCache::s_seed));
448}
449
450bool SvgPrivate::setImagePath(const QString &imagePath)
451{
452 QString actualPath = imagePath;
453 bool isAbsoluteFile = QDir::isAbsolutePath(actualPath);
454 if (imagePath.startsWith(QLatin1String("file://"))) {
455 // length of file://
456 actualPath.remove(0, 7);
457 isAbsoluteFile = true;
458 }
459 // If someone using the QML API uses Qt.resolvedUrl to get the absolute path inside of a QRC,
460 // the URI will look something like qrc:/artwork/file.svg
461 // In order for file IO to work it needs to be reformatted it needs to be :/artwork/file.svg
462 if (imagePath.startsWith(QLatin1String("qrc:/"))) {
463 actualPath.replace(QLatin1String("qrc:/"), QLatin1String(":/"));
464 isAbsoluteFile = true;
465 }
466
467 bool isThemed = !actualPath.isEmpty() && !isAbsoluteFile;
468
469 // lets check to see if we're already set to this file
470 if (isThemed == themed && ((themed && themePath == actualPath) || (!themed && path == actualPath))) {
471 return false;
472 }
473
474 eraseRenderer();
475
476 // if we don't have any path right now and are going to set one,
477 // then lets not schedule a repaint because we are just initializing!
478 bool updateNeeded = true; //! path.isEmpty() || !themePath.isEmpty();
479
480 QObject::disconnect(imageSetChangedConnection);
481
482 themed = isThemed;
483 path.clear();
484 themePath.clear();
485
486 bool oldfromCurrentImageSet = fromCurrentImageSet;
487 fromCurrentImageSet = isThemed && actualImageSet()->currentImageSetHasImage(imagePath);
488
489 if (fromCurrentImageSet != oldfromCurrentImageSet) {
490 Q_EMIT q->fromCurrentImageSetChanged(fromCurrentImageSet);
491 }
492
493 if (themed) {
494 themePath = actualPath;
495 path = actualImageSet()->imagePath(themePath);
496 themeFailed = path.isEmpty();
497 imageSetChangedConnection = QObject::connect(actualImageSet(), &ImageSet::imageSetChanged, q, [this]() {
498 imageSetChanged();
499 });
500 } else if (QFileInfo::exists(actualPath)) {
501 imageSetChangedConnection = QObject::connect(actualImageSet(), &ImageSet::imageSetChanged, q, [this]() {
502 imageSetChanged();
503 });
504 path = actualPath;
505 } else {
506#ifndef NDEBUG
507 // qCDebug(LOG_KSVG) << "file '" << path << "' does not exist!";
508#endif
509 }
510
511 QDateTime lastModifiedDate;
512 if (!path.isEmpty()) {
513 const QFileInfo info(path);
514 lastModifiedDate = info.lastModified();
515
516 lastModified = lastModifiedDate.toSecsSinceEpoch();
517
518 const bool imageWasCached = SvgRectsCache::instance()->loadImageFromCache(path, lastModified);
519
520 if (!imageWasCached) {
521 std::shared_lock lock(s_renderersLock);
522 auto i = s_renderers.constBegin();
523 while (i != s_renderers.constEnd()) {
524 if (i.key().contains(path)) {
525 i.value()->reload();
526 }
527 i++;
528 }
529 }
530 }
531
532 // also images with absolute path needs to have a natural size initialized,
533 // even if looks a bit weird using ImageSet to store non-themed stuff
534 if ((themed && !path.isEmpty() && lastModifiedDate.isValid()) || QFileInfo::exists(actualPath)) {
535 naturalSize = SvgRectsCache::instance()->naturalSize(path);
536 if (naturalSize.isEmpty()) {
537 createRenderer();
538 naturalSize = renderer->defaultSize();
539 SvgRectsCache::instance()->setNaturalSize(path, naturalSize);
540 }
541 }
542
543 q->resize();
544 Q_EMIT q->imagePathChanged();
545
546 return updateNeeded;
547}
548
549ImageSet *SvgPrivate::actualImageSet()
550{
551 if (!theme) {
552 theme = new KSvg::ImageSet(q);
553 }
554
555 return theme.data();
556}
557
558QPixmap SvgPrivate::findInCache(const QString &elementId, qreal ratio, const QSizeF &s)
559{
560 QSize size;
561 QString actualElementId;
562
563 // Look at the size hinted elements and try to find the smallest one with an
564 // identical aspect ratio.
565 if (s.isValid() && !elementId.isEmpty()) {
566 const QList<QSizeF> elementSizeHints = SvgRectsCache::instance()->sizeHintsForId(path, elementId);
567
568 if (!elementSizeHints.isEmpty()) {
569 QSizeF bestFit(-1, -1);
570
571 for (const auto &hint : elementSizeHints) {
572 if (hint.width() >= s.width() * ratio && hint.height() >= s.height() * ratio
573 && (!bestFit.isValid() || (bestFit.width() * bestFit.height()) > (hint.width() * hint.height()))) {
574 bestFit = hint;
575 }
576 }
577
578 if (bestFit.isValid()) {
579 actualElementId = QString::number(bestFit.width()) % QLatin1Char('-') % QString::number(bestFit.height()) % QLatin1Char('-') % elementId;
580 }
581 }
582 }
583
584 if (elementId.isEmpty() || !q->hasElement(actualElementId)) {
585 actualElementId = elementId;
586 }
587
588 if (elementId.isEmpty() || (multipleImages && s.isValid())) {
589 size = s.toSize() * ratio;
590 } else {
591 size = elementRect(actualElementId).size().toSize() * ratio;
592 }
593
594 if (size.isEmpty()) {
595 return QPixmap();
596 }
597
598 const QString id = cachePath(actualElementId, size);
599
600 QPixmap p;
601 if (cacheRendering && lastModified == SvgRectsCache::instance()->lastModifiedTimeFromCache(path) && actualImageSet()->d->findInCache(id, p, lastModified)) {
602 p.setDevicePixelRatio(ratio);
603 // qCDebug(LOG_PLASMA) << "found cached version of " << id << p.size();
604 return p;
605 }
606
607 createRenderer();
608
609 QRectF finalRect = makeUniform(renderer->boundsOnElement(actualElementId), QRect(QPoint(0, 0), size));
610
611 // don't alter the pixmap size or it won't match up properly to, e.g., FrameSvg elements
612 // makeUniform should never change the size so much that it gains or loses a whole pixel
613 p = QPixmap(size);
614
616 QPainter renderPainter(&p);
617
618 if (actualElementId.isEmpty()) {
619 renderer->render(&renderPainter, finalRect);
620 } else {
621 renderer->render(&renderPainter, actualElementId, finalRect);
622 }
623
624 renderPainter.end();
625 p.setDevicePixelRatio(ratio);
626
627 if (cacheRendering) {
628 actualImageSet()->d->insertIntoCache(id, p, QString::number((qint64)q, 16) % QLatin1Char('_') % actualElementId);
629 }
630
631 SvgRectsCache::instance()->updateLastModified(path, lastModified);
632
633 return p;
634}
635
636void SvgPrivate::createRenderer()
637{
638 if (renderer) {
639 return;
640 }
641
642 if (themed && path.isEmpty() && !themeFailed) {
643 if (path.isEmpty()) {
644 path = actualImageSet()->imagePath(themePath);
645 themeFailed = path.isEmpty();
646 if (themeFailed) {
647 qCWarning(LOG_KSVG) << "No image path found for" << themePath;
648 }
649 }
650 }
651
652 QString styleSheet;
653 if (!colorOverrides.isEmpty()) {
654 if (stylesheetOverride.isEmpty()) {
655 stylesheetOverride = actualImageSet()->d->svgStyleSheet(q);
656 }
657 styleSheet = stylesheetOverride;
658 } else {
659 styleSheet = actualImageSet()->d->svgStyleSheet(q);
660 }
661
662 styleCrc = qChecksum(QByteArrayView(styleSheet.toUtf8().constData(), styleSheet.size()));
663
664 {
665 std::shared_lock lock(s_renderersLock);
666 QHash<QString, SharedSvgRenderer::Ptr>::const_iterator it = s_renderers.constFind(styleCrc + path);
667
668 if (it != s_renderers.constEnd()) {
669 renderer = it.value();
670 if (size == QSizeF()) {
671 size = renderer->defaultSize();
672 }
673 return;
674 }
675 }
676
677 if (path.isEmpty()) {
678 renderer = new SharedSvgRenderer();
679 } else {
680 QHash<QString, QRectF> interestingElements;
681 renderer = new SharedSvgRenderer(path, styleSheet, interestingElements);
682
683 // Add interesting elements to the theme's rect cache.
684 QHashIterator<QString, QRectF> i(interestingElements);
685
686 QRegularExpression sizeHintedKeyExpr(QStringLiteral("^(\\d+)-(\\d+)-(.+)$"));
687
688 while (i.hasNext()) {
689 i.next();
690 const QString &elementId = i.key();
691 QString originalId = i.key();
692 const QRectF &elementRect = i.value();
693
694 originalId.replace(sizeHintedKeyExpr, QStringLiteral("\\3"));
695 SvgRectsCache::instance()->insertSizeHintForId(path, originalId, elementRect.size().toSize());
696
697 const CacheId cacheId{.width = -1.0,
698 .height = -1.0,
699 .filePath = path,
700 .elementName = elementId,
701 .status = status,
702 .scaleFactor = devicePixelRatio,
703 .colorSet = -1,
704 .styleSheet = 0,
705 .extraFlags = 0,
706 .lastModified = lastModified};
707 SvgRectsCache::instance()->insert(cacheId, elementRect, lastModified);
708 }
709 }
710
711 {
712 std::unique_lock lock(s_renderersLock);
713 s_renderers[styleCrc + path] = renderer;
714 if (size == QSizeF()) {
715 size = renderer->defaultSize();
716 }
717 }
718}
719
720void SvgPrivate::eraseRenderer()
721{
722 if (renderer && renderer->ref.loadRelaxed() == 2) {
723 // this and the cache reference it
724 std::unique_lock lock(s_renderersLock);
725 s_renderers.erase(s_renderers.find(styleCrc + path));
726 }
727
728 renderer = nullptr;
729 styleCrc = QChar(0);
730}
731
732QRectF SvgPrivate::elementRect(QStringView elementId)
733{
734 if (themed && path.isEmpty()) {
735 if (themeFailed) {
736 return QRectF();
737 }
738
739 path = actualImageSet()->imagePath(themePath);
740 themeFailed = path.isEmpty();
741
742 if (themeFailed) {
743 return QRectF();
744 }
745 }
746
747 if (path.isEmpty()) {
748 return QRectF();
749 }
750
751 QRectF rect;
752 const CacheId cacheId = SvgPrivate::cacheId(elementId);
753 bool found = SvgRectsCache::instance()->findElementRect(cacheId, rect);
754 // This is a corner case where we are *sure* the element is not valid
755 if (!found) {
756 rect = findAndCacheElementRect(elementId);
757 }
758
759 return rect;
760}
761
762QRectF SvgPrivate::findAndCacheElementRect(QStringView elementId)
763{
764 // we need to check the id before createRenderer(), otherwise it may generate a different id compared to the previous cacheId)( call
765 const CacheId cacheId = SvgPrivate::cacheId(elementId);
766
767 createRenderer();
768
769 auto elementIdString = elementId.toString();
770
771 // This code will usually never be run because createRenderer already caches all the boundingRect in the elements in the svg
772 QRectF elementRect = renderer->elementExists(elementIdString)
773 ? renderer->transformForElement(elementIdString).map(renderer->boundsOnElement(elementIdString)).boundingRect()
774 : QRectF();
775
776 naturalSize = renderer->defaultSize();
777
778 qreal dx = size.width() / renderer->defaultSize().width();
779 qreal dy = size.height() / renderer->defaultSize().height();
780
781 elementRect = QRectF(elementRect.x() * dx, elementRect.y() * dy, elementRect.width() * dx, elementRect.height() * dy);
782 SvgRectsCache::instance()->insert(cacheId, elementRect, lastModified);
783
784 return elementRect;
785}
786
787bool Svg::eventFilter(QObject *watched, QEvent *event)
788{
789 return QObject::eventFilter(watched, event);
790}
791
792// Following two are utility functions to snap rendered elements to the pixel grid
793// to and from are always 0 <= val <= 1
794qreal SvgPrivate::closestDistance(qreal to, qreal from)
795{
796 qreal a = to - from;
797 if (qFuzzyCompare(to, from)) {
798 return 0;
799 } else if (to > from) {
800 qreal b = to - from - 1;
801 return (qAbs(a) > qAbs(b)) ? b : a;
802 } else {
803 qreal b = 1 + to - from;
804 return (qAbs(a) > qAbs(b)) ? b : a;
805 }
806}
807
808QRectF SvgPrivate::makeUniform(const QRectF &orig, const QRectF &dst)
809{
810 if (qFuzzyIsNull(orig.x()) || qFuzzyIsNull(orig.y())) {
811 return dst;
812 }
813
814 QRectF res(dst);
815 qreal div_w = dst.width() / orig.width();
816 qreal div_h = dst.height() / orig.height();
817
818 qreal div_x = dst.x() / orig.x();
819 qreal div_y = dst.y() / orig.y();
820
821 // horizontal snap
822 if (!qFuzzyIsNull(div_x) && !qFuzzyCompare(div_w, div_x)) {
823 qreal rem_orig = orig.x() - (floor(orig.x()));
824 qreal rem_dst = dst.x() - (floor(dst.x()));
825 qreal offset = closestDistance(rem_dst, rem_orig);
826 res.translate(offset + offset * div_w, 0);
827 res.setWidth(res.width() + offset);
828 }
829 // vertical snap
830 if (!qFuzzyIsNull(div_y) && !qFuzzyCompare(div_h, div_y)) {
831 qreal rem_orig = orig.y() - (floor(orig.y()));
832 qreal rem_dst = dst.y() - (floor(dst.y()));
833 qreal offset = closestDistance(rem_dst, rem_orig);
834 res.translate(0, offset + offset * div_h);
835 res.setHeight(res.height() + offset);
836 }
837
838 return res;
839}
840
841void SvgPrivate::imageSetChanged()
842{
843 if (q->imagePath().isEmpty()) {
844 return;
845 }
846
847 QString currentPath = themed ? themePath : path;
848 themePath.clear();
849 eraseRenderer();
850 setImagePath(currentPath);
851 q->resize();
852
853 // qCDebug(LOG_KSVG) << themePath << ">>>>>>>>>>>>>>>>>> theme changed";
854 Q_EMIT q->repaintNeeded();
855 Q_EMIT q->imageSetChanged(q->imageSet());
856}
857
858void SvgPrivate::colorsChanged()
859{
860 eraseRenderer();
861 qCDebug(LOG_KSVG) << "repaint needed from colorsChanged";
862
863 Q_EMIT q->repaintNeeded();
864}
865
866std::shared_mutex SvgPrivate::s_renderersLock;
867QHash<QString, SharedSvgRenderer::Ptr> SvgPrivate::s_renderers;
868QPointer<ImageSet> SvgPrivate::s_systemColorsCache;
869
871 : QObject(parent)
872 , d(new SvgPrivate(this))
873{
874 connect(SvgRectsCache::instance(), &SvgRectsCache::lastModifiedChanged, this, [this](const QString &filePath, unsigned int lastModified) {
875 if (d->lastModified != lastModified && filePath == d->path) {
876 d->lastModified = lastModified;
878 }
879 });
880}
881
882Svg::~Svg()
883{
884 delete d;
885}
886
888{
889 if (FrameSvg *f = qobject_cast<FrameSvg *>(this)) {
890 f->clearCache();
891 }
892
893 d->devicePixelRatio = ratio;
894
896}
897
899{
900 return d->devicePixelRatio;
901}
902
903QPixmap Svg::pixmap(const QString &elementID)
904{
905 if (elementID.isNull() || d->multipleImages) {
906 return d->findInCache(elementID, d->devicePixelRatio, size());
907 } else {
908 return d->findInCache(elementID, d->devicePixelRatio);
909 }
910}
911
912QImage Svg::image(const QSize &size, const QString &elementID)
913{
914 QPixmap pix(d->findInCache(elementID, d->devicePixelRatio, size));
915 return pix.toImage();
916}
917
918void Svg::paint(QPainter *painter, const QPointF &point, const QString &elementID)
919{
920 Q_ASSERT(painter->device());
921 const qreal ratio = painter->device()->devicePixelRatio();
922 QPixmap pix((elementID.isNull() || d->multipleImages) ? d->findInCache(elementID, ratio, size()) : d->findInCache(elementID, ratio));
923
924 if (pix.isNull()) {
925 return;
926 }
927
928 painter->drawPixmap(QRectF(point, size()), pix, QRectF(QPointF(0, 0), pix.size()));
929}
930
931void Svg::paint(QPainter *painter, int x, int y, const QString &elementID)
932{
933 paint(painter, QPointF(x, y), elementID);
934}
935
936void Svg::paint(QPainter *painter, const QRectF &rect, const QString &elementID)
937{
938 Q_ASSERT(painter->device());
939 const qreal ratio = painter->device()->devicePixelRatio();
940 QPixmap pix(d->findInCache(elementID, ratio, rect.size()));
941
942 painter->drawPixmap(rect, pix, QRect(QPoint(0, 0), pix.size()));
943}
944
945void Svg::paint(QPainter *painter, int x, int y, int width, int height, const QString &elementID)
946{
947 Q_ASSERT(painter->device());
948 const qreal ratio = painter->device()->devicePixelRatio();
949 QPixmap pix(d->findInCache(elementID, ratio, QSizeF(width, height)));
950 painter->drawPixmap(x, y, pix, 0, 0, pix.size().width(), pix.size().height());
951}
952
953QSizeF Svg::size() const
954{
955 if (d->size.isEmpty()) {
956 d->size = d->naturalSize;
957 }
958
959 return {std::round(d->size.width()), std::round(d->size.height())};
960}
961
962void Svg::resize(qreal width, qreal height)
963{
964 resize(QSize(width, height));
965}
966
967void Svg::resize(const QSizeF &size)
968{
969 if (qFuzzyCompare(size.width(), d->size.width()) && qFuzzyCompare(size.height(), d->size.height())) {
970 return;
971 }
972
973 d->size = size;
975}
976
978{
979 if (qFuzzyCompare(d->naturalSize.width(), d->size.width()) && qFuzzyCompare(d->naturalSize.height(), d->size.height())) {
980 return;
981 }
982
983 d->size = d->naturalSize;
985}
986
987QSizeF Svg::elementSize(const QString &elementId) const
988{
989 const QSizeF s = d->elementRect(elementId).size();
990 return {std::round(s.width()), std::round(s.height())};
991}
992
993QSizeF Svg::elementSize(QStringView elementId) const
994{
995 const QSizeF s = d->elementRect(elementId).size();
996 return {std::round(s.width()), std::round(s.height())};
997}
998
999QRectF Svg::elementRect(const QString &elementId) const
1000{
1001 return d->elementRect(elementId);
1002}
1003
1004QRectF Svg::elementRect(QStringView elementId) const
1005{
1006 return d->elementRect(elementId);
1007}
1008
1009bool Svg::hasElement(const QString &elementId) const
1010{
1011 return hasElement(QStringView(elementId));
1012}
1013
1014bool Svg::hasElement(QStringView elementId) const
1015{
1016 if (elementId.isEmpty() || (d->path.isNull() && d->themePath.isNull())) {
1017 return false;
1018 }
1019
1020 return d->elementRect(elementId).isValid();
1021}
1022
1023bool Svg::isValid() const
1024{
1025 if (d->path.isNull() && d->themePath.isNull()) {
1026 return false;
1027 }
1028
1029 // try very hard to avoid creation of a parser
1030 QSizeF naturalSize = SvgRectsCache::instance()->naturalSize(d->path);
1031 if (!naturalSize.isEmpty()) {
1032 return true;
1033 }
1034
1035 if (d->path.isEmpty() || !QFileInfo::exists(d->path)) {
1036 return false;
1037 }
1038 d->createRenderer();
1039 return d->renderer->isValid();
1040}
1041
1043{
1044 d->multipleImages = multiple;
1045}
1046
1048{
1049 return d->multipleImages;
1050}
1051
1052void Svg::setImagePath(const QString &svgFilePath)
1053{
1054 if (d->setImagePath(svgFilePath)) {
1056 }
1057}
1058
1059QString Svg::imagePath() const
1060{
1061 return d->themed ? d->themePath : d->path;
1062}
1063
1065{
1066 d->cacheRendering = useCache;
1068}
1069
1071{
1072 return d->cacheRendering;
1073}
1074
1075bool Svg::fromCurrentImageSet() const
1076{
1077 return d->fromCurrentImageSet;
1078}
1079
1081{
1082 if (!theme || theme == d->theme.data()) {
1083 return;
1084 }
1085
1086 if (d->theme) {
1087 disconnect(d->theme.data(), nullptr, this, nullptr);
1088 }
1089
1090 d->theme = theme;
1091 connect(theme, SIGNAL(imageSetChanged(QString)), this, SLOT(imageSetChanged()));
1092 d->imageSetChanged();
1093}
1094
1096{
1097 return d->actualImageSet();
1098}
1099
1100void Svg::setStatus(KSvg::Svg::Status status)
1101{
1102 if (status == d->status) {
1103 return;
1104 }
1105
1106 d->status = status;
1107 d->eraseRenderer();
1108 Q_EMIT statusChanged(status);
1110}
1111
1112Svg::Status Svg::status() const
1113{
1114 return d->status;
1115}
1116
1117void Svg::setColorSet(KSvg::Svg::ColorSet colorSet)
1118{
1119 const KColorScheme::ColorSet convertedSet = KColorScheme::ColorSet(colorSet);
1120 if (convertedSet == d->colorSet) {
1121 return;
1122 }
1123
1124 d->colorSet = convertedSet;
1125 d->eraseRenderer();
1126 Q_EMIT colorSetChanged(colorSet);
1128}
1129
1130Svg::ColorSet Svg::colorSet() const
1131{
1132 return Svg::ColorSet(d->colorSet);
1133}
1134
1135QColor Svg::color(StyleSheetColor colorName) const
1136{
1137 auto it = d->colorOverrides.constFind(colorName);
1138 if (it != d->colorOverrides.constEnd()) {
1139 return *it;
1140 }
1141 return d->actualImageSet()->d->namedColor(colorName, this);
1142}
1143
1144void Svg::setColor(StyleSheetColor colorName, const QColor &color)
1145{
1146 if (d->colorOverrides.value(colorName) == color) {
1147 return;
1148 }
1149
1150 if (color.isValid()) {
1151 d->colorOverrides[colorName] = color;
1152 } else {
1153 d->colorOverrides.remove(colorName);
1154 }
1155 d->stylesheetOverride.clear();
1156
1157 d->eraseRenderer();
1159}
1160
1161void Svg::clearColorOverrides()
1162{
1163 d->colorOverrides.clear();
1164 d->stylesheetOverride.clear();
1165 d->eraseRenderer();
1167}
1168
1169} // KSvg namespace
1170
1171#include "moc_svg.cpp"
1172#include "private/moc_svg_p.cpp"
static KSharedConfig::Ptr openConfig(const QString &fileName=QString(), OpenFlags mode=FullConfig, QStandardPaths::StandardLocation type=QStandardPaths::GenericConfigLocation)
Interface to the Svg image set.
Definition imageset.h:41
virtual void setImagePath(const QString &svgFilePath)
This method sets the SVG file to render.
Definition svg.cpp:1052
Q_INVOKABLE QImage image(const QSize &size, const QString &elementID=QString())
This method returns an image of the SVG represented by this object.
Definition svg.cpp:912
void statusChanged(KSvg::Svg::Status status)
This signal is emitted when the status has changed.
void setStatus(Svg::Status status)
This method sets the image in a selected status.
Definition svg.cpp:1100
void setContainsMultipleImages(bool multiple)
This method sets whether the SVG contains a single image or multiple ones.
Definition svg.cpp:1042
Q_INVOKABLE void resize()
This method resizes the rendered image to the natural size of the SVG.
Definition svg.cpp:977
void setColorSet(ColorSet colorSet)
This method sets a color set for the SVG.
Definition svg.cpp:1117
Q_INVOKABLE QPixmap pixmap(const QString &elementID=QString())
This method returns a pixmap of the SVG represented by this object.
Definition svg.cpp:903
qreal devicePixelRatio() const
This method returns the device pixel ratio for this Svg.
Definition svg.cpp:898
bool containsMultipleImages() const
This method returns whether the SVG contains multiple images.
Definition svg.cpp:1047
void setDevicePixelRatio(qreal factor)
This method sets the device pixel ratio for the Svg.
Definition svg.cpp:887
void colorSetChanged(KSvg::Svg::ColorSet colorSet)
This signal is emitted when the color set has changed.
Svg(QObject *parent=nullptr)
This method constructs an SVG object that implicitly shares and caches rendering.
Definition svg.cpp:870
void sizeChanged()
This signal is emitted whenever the size has changed.
Q_INVOKABLE QRectF elementRect(const QString &elementId) const
This method returns the bounding rect of a given element.
Definition svg.cpp:999
Q_INVOKABLE void paint(QPainter *painter, const QPointF &point, const QString &elementID=QString())
This method paints all or part of the SVG represented by this object.
Definition svg.cpp:918
ImageSet * imageSet() const
This method returns the KSvg::ImageSet used by this Svg object.
Definition svg.cpp:1095
void repaintNeeded()
This signal is emitted whenever the SVG data has changed in such a way that a repaint is required.
void imageSetChanged(ImageSet *imageSet)
This signal is emitted when the image set has changed.
bool isUsingRenderingCache() const
Whether the rendering cache is being used.
Definition svg.cpp:1070
Q_INVOKABLE bool hasElement(const QString &elementId) const
This method checks whether an element exists in the loaded SVG.
Definition svg.cpp:1009
Q_INVOKABLE bool isValid() const
This method checks whether this object is backed by a valid SVG file.
Definition svg.cpp:1023
Q_INVOKABLE QSizeF elementSize(const QString &elementId) const
This method returns the size of a given element.
Definition svg.cpp:987
void setUsingRenderingCache(bool useCache)
This method sets whether or not to cache the results of rendering to pixmaps.
Definition svg.cpp:1064
void setImageSet(KSvg::ImageSet *theme)
This method sets the KSvg::ImageSet to use with this Svg object.
Definition svg.cpp:1080
Q_SCRIPTABLE CaptureState status()
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
QAction * hint(const QObject *recvr, const char *slot, QObject *parent)
QAction * load(const QObject *recvr, const char *slot, QObject *parent)
QString path(const QString &relativePath)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
KGuiItem insert()
The KSvg namespace.
KTEXTEDITOR_EXPORT size_t qHash(KTextEditor::Cursor cursor, size_t seed=0) noexcept
const char * constData() const const
bool contains(QByteArrayView bv) const const
void reserve(qsizetype size)
qsizetype size() const const
QRgb rgba() const const
bool isValid() const const
qint64 toSecsSinceEpoch() const const
bool isAbsolutePath(const QString &path)
bool exists(const QString &path)
iterator insert(const Key &key, const T &value)
iterator begin()
iterator end()
bool isEmpty() const const
bool invokeMethod(QObject *context, Functor &&function, FunctorReturnType *ret)
QObject(QObject *parent)
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool disconnect(const QMetaObject::Connection &connection)
virtual bool eventFilter(QObject *watched, QEvent *event)
QObject * parent() const const
T qobject_cast(QObject *object)
qreal devicePixelRatio() const const
QPaintDevice * device() const const
void drawPixmap(const QPoint &point, const QPixmap &pixmap)
qint64 cacheKey() const const
void fill(const QColor &color)
bool isNull() const const
void setDevicePixelRatio(qreal scaleFactor)
QSize size() const const
QImage toImage() const const
qreal height() const const
bool isValid() const const
QSizeF size() const const
qreal width() const const
qreal x() const const
qreal y() const const
int height() const const
bool isEmpty() const const
int width() const const
qreal height() const const
bool isEmpty() const const
bool isValid() const const
QSize toSize() const const
qreal width() const const
QString writableLocation(StandardLocation type)
QString & append(QChar ch)
void clear()
QString fromLatin1(QByteArrayView str)
QString & insert(qsizetype position, QChar ch)
bool isEmpty() const const
bool isNull() const const
QString number(double n, char format, int precision)
QString & remove(QChar ch, Qt::CaseSensitivity cs)
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
qsizetype size() const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QByteArray toUtf8() const const
bool isEmpty() const const
QString toString() const const
bool load(QXmlStreamReader *contents)
transparent
QFuture< typename qValueType< Iterator >::value_type > filtered(Iterator begin, Iterator end, KeepFunctor &&filterFunction)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void start()
void timeout()
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Feb 28 2025 11:48:58 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.