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

KDE's Doxygen guidelines are available online.