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<QSizeF> 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<QSizeF> 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 QSizeF &size)
302{
303 // TODO: need to make this more efficient
304 auto sizeListToString = [](const QList<QSizeF> &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
481 themed = isThemed;
482 path.clear();
483 themePath.clear();
484
485 bool oldfromCurrentImageSet = fromCurrentImageSet;
486 fromCurrentImageSet = isThemed && actualImageSet()->currentImageSetHasImage(imagePath);
487
488 if (fromCurrentImageSet != oldfromCurrentImageSet) {
489 Q_EMIT q->fromCurrentImageSetChanged(fromCurrentImageSet);
490 }
491
492 if (themed) {
493 themePath = actualPath;
494 path = actualImageSet()->imagePath(themePath);
495 themeFailed = path.isEmpty();
496 imageSetChangedConnection = QObject::connect(actualImageSet(), &ImageSet::imageSetChanged, q, [this]() {
497 imageSetChanged();
498 });
499 } else if (QFileInfo::exists(actualPath)) {
500 imageSetChangedConnection = QObject::connect(actualImageSet(), &ImageSet::imageSetChanged, q, [this]() {
501 imageSetChanged();
502 });
503 path = actualPath;
504 } else {
505#ifndef NDEBUG
506 // qCDebug(LOG_KSVG) << "file '" << path << "' does not exist!";
507#endif
508 }
509
510 QDateTime lastModifiedDate;
511 if (!path.isEmpty()) {
512 const QFileInfo info(path);
513 lastModifiedDate = info.lastModified();
514
515 lastModified = lastModifiedDate.toSecsSinceEpoch();
516
517 const bool imageWasCached = SvgRectsCache::instance()->loadImageFromCache(path, lastModified);
518
519 if (!imageWasCached) {
520 auto i = s_renderers.constBegin();
521 while (i != s_renderers.constEnd()) {
522 if (i.key().contains(path)) {
523 i.value()->reload();
524 }
525 i++;
526 }
527 }
528 }
529
530 // also images with absolute path needs to have a natural size initialized,
531 // even if looks a bit weird using ImageSet to store non-themed stuff
532 if ((themed && !path.isEmpty() && lastModifiedDate.isValid()) || QFileInfo::exists(actualPath)) {
533 naturalSize = SvgRectsCache::instance()->naturalSize(path);
534 if (naturalSize.isEmpty()) {
535 createRenderer();
536 naturalSize = renderer->defaultSize();
537 SvgRectsCache::instance()->setNaturalSize(path, naturalSize);
538 }
539 }
540
541 q->resize();
542 Q_EMIT q->imagePathChanged();
543
544 return updateNeeded;
545}
546
547ImageSet *SvgPrivate::actualImageSet()
548{
549 if (!theme) {
550 theme = new KSvg::ImageSet(q);
551 }
552
553 return theme.data();
554}
555
556QPixmap SvgPrivate::findInCache(const QString &elementId, qreal ratio, const QSizeF &s)
557{
558 QSize size;
559 QString actualElementId;
560
561 // Look at the size hinted elements and try to find the smallest one with an
562 // identical aspect ratio.
563 if (s.isValid() && !elementId.isEmpty()) {
564 const QList<QSizeF> elementSizeHints = SvgRectsCache::instance()->sizeHintsForId(path, elementId);
565
566 if (!elementSizeHints.isEmpty()) {
567 QSizeF bestFit(-1, -1);
568
569 for (const auto &hint : elementSizeHints) {
570 if (hint.width() >= s.width() * ratio && hint.height() >= s.height() * ratio
571 && (!bestFit.isValid() || (bestFit.width() * bestFit.height()) > (hint.width() * hint.height()))) {
572 bestFit = hint;
573 }
574 }
575
576 if (bestFit.isValid()) {
577 actualElementId = QString::number(bestFit.width()) % QLatin1Char('-') % QString::number(bestFit.height()) % QLatin1Char('-') % elementId;
578 }
579 }
580 }
581
582 if (elementId.isEmpty() || !q->hasElement(actualElementId)) {
583 actualElementId = elementId;
584 }
585
586 if (elementId.isEmpty() || (multipleImages && s.isValid())) {
587 size = s.toSize() * ratio;
588 } else {
589 size = elementRect(actualElementId).size().toSize() * ratio;
590 }
591
592 if (size.isEmpty()) {
593 return QPixmap();
594 }
595
596 const QString id = cachePath(actualElementId, size);
597
598 QPixmap p;
599 if (cacheRendering && lastModified == SvgRectsCache::instance()->lastModifiedTimeFromCache(path) && actualImageSet()->d->findInCache(id, p, lastModified)) {
600 p.setDevicePixelRatio(ratio);
601 // qCDebug(LOG_PLASMA) << "found cached version of " << id << p.size();
602 return p;
603 }
604
605 createRenderer();
606
607 QRectF finalRect = makeUniform(renderer->boundsOnElement(actualElementId), QRect(QPoint(0, 0), size));
608
609 // don't alter the pixmap size or it won't match up properly to, e.g., FrameSvg elements
610 // makeUniform should never change the size so much that it gains or loses a whole pixel
611 p = QPixmap(size);
612
614 QPainter renderPainter(&p);
615
616 if (actualElementId.isEmpty()) {
617 renderer->render(&renderPainter, finalRect);
618 } else {
619 renderer->render(&renderPainter, actualElementId, finalRect);
620 }
621
622 renderPainter.end();
623 p.setDevicePixelRatio(ratio);
624
625 if (cacheRendering) {
626 actualImageSet()->d->insertIntoCache(id, p, QString::number((qint64)q, 16) % QLatin1Char('_') % actualElementId);
627 }
628
629 SvgRectsCache::instance()->updateLastModified(path, lastModified);
630
631 return p;
632}
633
634void SvgPrivate::createRenderer()
635{
636 if (renderer) {
637 return;
638 }
639
640 if (themed && path.isEmpty() && !themeFailed) {
641 if (path.isEmpty()) {
642 path = actualImageSet()->imagePath(themePath);
643 themeFailed = path.isEmpty();
644 if (themeFailed) {
645 qCWarning(LOG_KSVG) << "No image path found for" << themePath;
646 }
647 }
648 }
649
650 QString styleSheet;
651 if (!colorOverrides.isEmpty()) {
652 if (stylesheetOverride.isEmpty()) {
653 stylesheetOverride = actualImageSet()->d->svgStyleSheet(q);
654 }
655 styleSheet = stylesheetOverride;
656 } else {
657 styleSheet = actualImageSet()->d->svgStyleSheet(q);
658 }
659
660 styleCrc = qChecksum(QByteArrayView(styleSheet.toUtf8().constData(), styleSheet.size()));
661
662 QHash<QString, SharedSvgRenderer::Ptr>::const_iterator it = s_renderers.constFind(styleCrc + path);
663
664 if (it != s_renderers.constEnd()) {
665 renderer = it.value();
666 } else {
667 if (path.isEmpty()) {
668 renderer = new SharedSvgRenderer();
669 } else {
670 QHash<QString, QRectF> interestingElements;
671 renderer = new SharedSvgRenderer(path, styleSheet, interestingElements);
672
673 // Add interesting elements to the theme's rect cache.
674 QHashIterator<QString, QRectF> i(interestingElements);
675
676 QRegularExpression sizeHintedKeyExpr(QStringLiteral("^(\\d+)-(\\d+)-(.+)$"));
677
678 while (i.hasNext()) {
679 i.next();
680 const QString &elementId = i.key();
681 QString originalId = i.key();
682 const QRectF &elementRect = i.value();
683
684 originalId.replace(sizeHintedKeyExpr, QStringLiteral("\\3"));
685 SvgRectsCache::instance()->insertSizeHintForId(path, originalId, elementRect.size().toSize());
686
687 const CacheId cacheId{.width = -1.0,
688 .height = -1.0,
689 .filePath = path,
690 .elementName = elementId,
691 .status = status,
692 .scaleFactor = devicePixelRatio,
693 .colorSet = -1,
694 .styleSheet = 0,
695 .extraFlags = 0,
696 .lastModified = lastModified};
697 SvgRectsCache::instance()->insert(cacheId, elementRect, lastModified);
698 }
699 }
700
701 s_renderers[styleCrc + path] = renderer;
702 }
703
704 if (size == QSizeF()) {
705 size = renderer->defaultSize();
706 }
707}
708
709void SvgPrivate::eraseRenderer()
710{
711 if (renderer && renderer->ref.loadRelaxed() == 2) {
712 // this and the cache reference it
713 s_renderers.erase(s_renderers.find(styleCrc + path));
714 }
715
716 renderer = nullptr;
717 styleCrc = QChar(0);
718}
719
720QRectF SvgPrivate::elementRect(QStringView elementId)
721{
722 if (themed && path.isEmpty()) {
723 if (themeFailed) {
724 return QRectF();
725 }
726
727 path = actualImageSet()->imagePath(themePath);
728 themeFailed = path.isEmpty();
729
730 if (themeFailed) {
731 return QRectF();
732 }
733 }
734
735 if (path.isEmpty()) {
736 return QRectF();
737 }
738
739 QRectF rect;
740 const CacheId cacheId = SvgPrivate::cacheId(elementId);
741 bool found = SvgRectsCache::instance()->findElementRect(cacheId, rect);
742 // This is a corner case where we are *sure* the element is not valid
743 if (!found) {
744 rect = findAndCacheElementRect(elementId);
745 }
746
747 return rect;
748}
749
750QRectF SvgPrivate::findAndCacheElementRect(QStringView elementId)
751{
752 // we need to check the id before createRenderer(), otherwise it may generate a different id compared to the previous cacheId)( call
753 const CacheId cacheId = SvgPrivate::cacheId(elementId);
754
755 createRenderer();
756
757 auto elementIdString = elementId.toString();
758
759 // This code will usually never be run because createRenderer already caches all the boundingRect in the elements in the svg
760 QRectF elementRect = renderer->elementExists(elementIdString)
761 ? renderer->transformForElement(elementIdString).map(renderer->boundsOnElement(elementIdString)).boundingRect()
762 : QRectF();
763
764 naturalSize = renderer->defaultSize();
765
766 qreal dx = size.width() / renderer->defaultSize().width();
767 qreal dy = size.height() / renderer->defaultSize().height();
768
769 elementRect = QRectF(elementRect.x() * dx, elementRect.y() * dy, elementRect.width() * dx, elementRect.height() * dy);
770 SvgRectsCache::instance()->insert(cacheId, elementRect, lastModified);
771
772 return elementRect;
773}
774
775bool Svg::eventFilter(QObject *watched, QEvent *event)
776{
777 return QObject::eventFilter(watched, event);
778}
779
780// Following two are utility functions to snap rendered elements to the pixel grid
781// to and from are always 0 <= val <= 1
782qreal SvgPrivate::closestDistance(qreal to, qreal from)
783{
784 qreal a = to - from;
785 if (qFuzzyCompare(to, from)) {
786 return 0;
787 } else if (to > from) {
788 qreal b = to - from - 1;
789 return (qAbs(a) > qAbs(b)) ? b : a;
790 } else {
791 qreal b = 1 + to - from;
792 return (qAbs(a) > qAbs(b)) ? b : a;
793 }
794}
795
796QRectF SvgPrivate::makeUniform(const QRectF &orig, const QRectF &dst)
797{
798 if (qFuzzyIsNull(orig.x()) || qFuzzyIsNull(orig.y())) {
799 return dst;
800 }
801
802 QRectF res(dst);
803 qreal div_w = dst.width() / orig.width();
804 qreal div_h = dst.height() / orig.height();
805
806 qreal div_x = dst.x() / orig.x();
807 qreal div_y = dst.y() / orig.y();
808
809 // horizontal snap
810 if (!qFuzzyIsNull(div_x) && !qFuzzyCompare(div_w, div_x)) {
811 qreal rem_orig = orig.x() - (floor(orig.x()));
812 qreal rem_dst = dst.x() - (floor(dst.x()));
813 qreal offset = closestDistance(rem_dst, rem_orig);
814 res.translate(offset + offset * div_w, 0);
815 res.setWidth(res.width() + offset);
816 }
817 // vertical snap
818 if (!qFuzzyIsNull(div_y) && !qFuzzyCompare(div_h, div_y)) {
819 qreal rem_orig = orig.y() - (floor(orig.y()));
820 qreal rem_dst = dst.y() - (floor(dst.y()));
821 qreal offset = closestDistance(rem_dst, rem_orig);
822 res.translate(0, offset + offset * div_h);
823 res.setHeight(res.height() + offset);
824 }
825
826 return res;
827}
828
829void SvgPrivate::imageSetChanged()
830{
831 if (q->imagePath().isEmpty()) {
832 return;
833 }
834
835 QString currentPath = themed ? themePath : path;
836 themePath.clear();
837 eraseRenderer();
838 setImagePath(currentPath);
839 q->resize();
840
841 // qCDebug(LOG_KSVG) << themePath << ">>>>>>>>>>>>>>>>>> theme changed";
842 Q_EMIT q->repaintNeeded();
843 Q_EMIT q->imageSetChanged(q->imageSet());
844}
845
846void SvgPrivate::colorsChanged()
847{
848 eraseRenderer();
849 qCDebug(LOG_KSVG) << "repaint needed from colorsChanged";
850
851 Q_EMIT q->repaintNeeded();
852}
853
854QHash<QString, SharedSvgRenderer::Ptr> SvgPrivate::s_renderers;
855QPointer<ImageSet> SvgPrivate::s_systemColorsCache;
856
857Svg::Svg(QObject *parent)
858 : QObject(parent)
859 , d(new SvgPrivate(this))
860{
861 connect(SvgRectsCache::instance(), &SvgRectsCache::lastModifiedChanged, this, [this](const QString &filePath, unsigned int lastModified) {
862 if (d->lastModified != lastModified && filePath == d->path) {
863 d->lastModified = lastModified;
865 }
866 });
867}
868
869Svg::~Svg()
870{
871 delete d;
872}
873
875{
876 if (FrameSvg *f = qobject_cast<FrameSvg *>(this)) {
877 f->clearCache();
878 }
879
880 d->devicePixelRatio = ratio;
881
883}
884
886{
887 return d->devicePixelRatio;
888}
889
890QPixmap Svg::pixmap(const QString &elementID)
891{
892 if (elementID.isNull() || d->multipleImages) {
893 return d->findInCache(elementID, d->devicePixelRatio, size());
894 } else {
895 return d->findInCache(elementID, d->devicePixelRatio);
896 }
897}
898
899QImage Svg::image(const QSize &size, const QString &elementID)
900{
901 QPixmap pix(d->findInCache(elementID, d->devicePixelRatio, size));
902 return pix.toImage();
903}
904
905void Svg::paint(QPainter *painter, const QPointF &point, const QString &elementID)
906{
907 Q_ASSERT(painter->device());
908 const qreal ratio = painter->device()->devicePixelRatio();
909 QPixmap pix((elementID.isNull() || d->multipleImages) ? d->findInCache(elementID, ratio, size()) : d->findInCache(elementID, ratio));
910
911 if (pix.isNull()) {
912 return;
913 }
914
915 painter->drawPixmap(QRectF(point, size()), pix, QRectF(QPointF(0, 0), pix.size()));
916}
917
918void Svg::paint(QPainter *painter, int x, int y, const QString &elementID)
919{
920 paint(painter, QPointF(x, y), elementID);
921}
922
923void Svg::paint(QPainter *painter, const QRectF &rect, const QString &elementID)
924{
925 Q_ASSERT(painter->device());
926 const qreal ratio = painter->device()->devicePixelRatio();
927 QPixmap pix(d->findInCache(elementID, ratio, rect.size()));
928
929 painter->drawPixmap(rect, pix, QRect(QPoint(0, 0), pix.size()));
930}
931
932void Svg::paint(QPainter *painter, int x, int y, int width, int height, const QString &elementID)
933{
934 Q_ASSERT(painter->device());
935 const qreal ratio = painter->device()->devicePixelRatio();
936 QPixmap pix(d->findInCache(elementID, ratio, QSizeF(width, height)));
937 painter->drawPixmap(x, y, pix, 0, 0, pix.size().width(), pix.size().height());
938}
939
940QSizeF Svg::size() const
941{
942 if (d->size.isEmpty()) {
943 d->size = d->naturalSize;
944 }
945
946 return {std::round(d->size.width()), std::round(d->size.height())};
947}
948
949void Svg::resize(qreal width, qreal height)
950{
951 resize(QSize(width, height));
952}
953
954void Svg::resize(const QSizeF &size)
955{
956 if (qFuzzyCompare(size.width(), d->size.width()) && qFuzzyCompare(size.height(), d->size.height())) {
957 return;
958 }
959
960 d->size = size;
962}
963
965{
966 if (qFuzzyCompare(d->naturalSize.width(), d->size.width()) && qFuzzyCompare(d->naturalSize.height(), d->size.height())) {
967 return;
968 }
969
970 d->size = d->naturalSize;
972}
973
974QSizeF Svg::elementSize(const QString &elementId) const
975{
976 const QSizeF s = d->elementRect(elementId).size();
977 return {std::round(s.width()), std::round(s.height())};
978}
979
980QSizeF Svg::elementSize(QStringView elementId) const
981{
982 const QSizeF s = d->elementRect(elementId).size();
983 return {std::round(s.width()), std::round(s.height())};
984}
985
986QRectF Svg::elementRect(const QString &elementId) const
987{
988 return d->elementRect(elementId);
989}
990
991QRectF Svg::elementRect(QStringView elementId) const
992{
993 return d->elementRect(elementId);
994}
995
996bool Svg::hasElement(const QString &elementId) const
997{
998 return hasElement(QStringView(elementId));
999}
1000
1001bool Svg::hasElement(QStringView elementId) const
1002{
1003 if (elementId.isEmpty() || (d->path.isNull() && d->themePath.isNull())) {
1004 return false;
1005 }
1006
1007 return d->elementRect(elementId).isValid();
1008}
1009
1010bool Svg::isValid() const
1011{
1012 if (d->path.isNull() && d->themePath.isNull()) {
1013 return false;
1014 }
1015
1016 // try very hard to avoid creation of a parser
1017 QSizeF naturalSize = SvgRectsCache::instance()->naturalSize(d->path);
1018 if (!naturalSize.isEmpty()) {
1019 return true;
1020 }
1021
1022 if (d->path.isEmpty() || !QFileInfo::exists(d->path)) {
1023 return false;
1024 }
1025 d->createRenderer();
1026 return d->renderer->isValid();
1027}
1028
1030{
1031 d->multipleImages = multiple;
1032}
1033
1035{
1036 return d->multipleImages;
1037}
1038
1039void Svg::setImagePath(const QString &svgFilePath)
1040{
1041 if (d->setImagePath(svgFilePath)) {
1043 }
1044}
1045
1046QString Svg::imagePath() const
1047{
1048 return d->themed ? d->themePath : d->path;
1049}
1050
1052{
1053 d->cacheRendering = useCache;
1055}
1056
1058{
1059 return d->cacheRendering;
1060}
1061
1062bool Svg::fromCurrentImageSet() const
1063{
1064 return d->fromCurrentImageSet;
1065}
1066
1068{
1069 if (!theme || theme == d->theme.data()) {
1070 return;
1071 }
1072
1073 if (d->theme) {
1074 disconnect(d->theme.data(), nullptr, this, nullptr);
1075 }
1076
1077 d->theme = theme;
1078 connect(theme, SIGNAL(imageSetChanged(QString)), this, SLOT(imageSetChanged()));
1079 d->imageSetChanged();
1080}
1081
1083{
1084 return d->actualImageSet();
1085}
1086
1087void Svg::setStatus(KSvg::Svg::Status status)
1088{
1089 if (status == d->status) {
1090 return;
1091 }
1092
1093 d->status = status;
1094 d->eraseRenderer();
1095 Q_EMIT statusChanged(status);
1097}
1098
1099Svg::Status Svg::status() const
1100{
1101 return d->status;
1102}
1103
1104void Svg::setColorSet(KSvg::Svg::ColorSet colorSet)
1105{
1106 const KColorScheme::ColorSet convertedSet = KColorScheme::ColorSet(colorSet);
1107 if (convertedSet == d->colorSet) {
1108 return;
1109 }
1110
1111 d->colorSet = convertedSet;
1112 d->eraseRenderer();
1113 Q_EMIT colorSetChanged(colorSet);
1115}
1116
1117Svg::ColorSet Svg::colorSet() const
1118{
1119 return Svg::ColorSet(d->colorSet);
1120}
1121
1122QColor Svg::color(StyleSheetColor colorName) const
1123{
1124 auto it = d->colorOverrides.constFind(colorName);
1125 if (it != d->colorOverrides.constEnd()) {
1126 return *it;
1127 }
1128 return d->actualImageSet()->d->namedColor(colorName, this);
1129}
1130
1131void Svg::setColor(StyleSheetColor colorName, const QColor &color)
1132{
1133 if (d->colorOverrides.value(colorName) == color) {
1134 return;
1135 }
1136
1137 if (color.isValid()) {
1138 d->colorOverrides[colorName] = color;
1139 } else {
1140 d->colorOverrides.remove(colorName);
1141 }
1142 d->stylesheetOverride.clear();
1143
1144 d->eraseRenderer();
1146}
1147
1148void Svg::clearColorOverrides()
1149{
1150 d->colorOverrides.clear();
1151 d->stylesheetOverride.clear();
1152 d->eraseRenderer();
1154}
1155
1156} // KSvg namespace
1157
1158#include "moc_svg.cpp"
1159#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:1039
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:899
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:1087
void setContainsMultipleImages(bool multiple)
This method sets whether the SVG contains a single image or multiple ones.
Definition svg.cpp:1029
Q_INVOKABLE void resize()
This method resizes the rendered image to the natural size of the SVG.
Definition svg.cpp:964
void setColorSet(ColorSet colorSet)
This method sets a color set for the SVG.
Definition svg.cpp:1104
Q_INVOKABLE QPixmap pixmap(const QString &elementID=QString())
This method returns a pixmap of the SVG represented by this object.
Definition svg.cpp:890
qreal devicePixelRatio() const
This method returns the device pixel ratio for this Svg.
Definition svg.cpp:885
bool containsMultipleImages() const
This method returns whether the SVG contains multiple images.
Definition svg.cpp:1034
void setDevicePixelRatio(qreal factor)
This method sets the device pixel ratio for the Svg.
Definition svg.cpp:874
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:986
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:905
ImageSet * imageSet() const
This method returns the KSvg::ImageSet used by this Svg object.
Definition svg.cpp:1082
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:1057
Q_INVOKABLE bool hasElement(const QString &elementId) const
This method checks whether an element exists in the loaded SVG.
Definition svg.cpp:996
Q_INVOKABLE bool isValid() const
This method checks whether this object is backed by a valid SVG file.
Definition svg.cpp:1010
Q_INVOKABLE QSizeF elementSize(const QString &elementId) const
This method returns the size of a given element.
Definition svg.cpp:974
void setUsingRenderingCache(bool useCache)
This method sets whether or not to cache the results of rendering to pixmaps.
Definition svg.cpp:1051
void setImageSet(KSvg::ImageSet *theme)
This method sets the KSvg::ImageSet to use with this Svg object.
Definition svg.cpp:1067
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
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
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)
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-2024 The KDE developers.
Generated on Fri Oct 11 2024 12:07:44 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.