Plasma

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

KDE's Doxygen guidelines are available online.