QGIS API Documentation  3.26.3-Buenos Aires (65e4edfdad)
qgssvgcache.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgssvgcache.h
3  ------------------------------
4  begin : 2011
5  copyright : (C) 2011 by Marco Hugentobler
6  email : marco dot hugentobler at sourcepole dot ch
7  ***************************************************************************/
8 
9 /***************************************************************************
10  * *
11  * This program is free software; you can redistribute it and/or modify *
12  * it under the terms of the GNU General Public License as published by *
13  * the Free Software Foundation; either version 2 of the License, or *
14  * (at your option) any later version. *
15  * *
16  ***************************************************************************/
17 
18 #include "qgssvgcache.h"
19 #include "qgis.h"
20 #include "qgslogger.h"
22 #include "qgsmessagelog.h"
23 #include "qgssymbollayerutils.h"
25 
26 #include <QApplication>
27 #include <QCoreApplication>
28 #include <QCursor>
29 #include <QDomDocument>
30 #include <QDomElement>
31 #include <QFile>
32 #include <QImage>
33 #include <QPainter>
34 #include <QPicture>
35 #include <QRegularExpression>
36 #include <QSvgRenderer>
37 #include <QFileInfo>
38 #include <QNetworkReply>
39 #include <QNetworkRequest>
40 
42 
43 //
44 // QgsSvgCacheEntry
45 //
46 
47 QgsSvgCacheEntry::QgsSvgCacheEntry( const QString &path, double size, double strokeWidth, double widthScaleFactor, const QColor &fill, const QColor &stroke, double fixedAspectRatio, const QMap<QString, QString> &parameters )
49  , size( size )
50  , strokeWidth( strokeWidth )
51  , widthScaleFactor( widthScaleFactor )
52  , fixedAspectRatio( fixedAspectRatio )
53  , fill( fill )
54  , stroke( stroke )
55  , parameters( parameters )
56 {
57 }
58 
59 bool QgsSvgCacheEntry::isEqual( const QgsAbstractContentCacheEntry *other ) const
60 {
61  const QgsSvgCacheEntry *otherSvg = dynamic_cast< const QgsSvgCacheEntry * >( other );
62  // cheapest checks first!
63  if ( !otherSvg
64  || !qgsDoubleNear( otherSvg->fixedAspectRatio, fixedAspectRatio )
65  || !qgsDoubleNear( otherSvg->size, size )
66  || !qgsDoubleNear( otherSvg->strokeWidth, strokeWidth )
67  || !qgsDoubleNear( otherSvg->widthScaleFactor, widthScaleFactor )
68  || otherSvg->fill != fill
69  || otherSvg->stroke != stroke
70  || otherSvg->path != path
71  || otherSvg->parameters != parameters )
72  return false;
73 
74  return true;
75 }
76 
77 int QgsSvgCacheEntry::dataSize() const
78 {
79  int size = svgContent.size();
80  if ( picture )
81  {
82  size += picture->size();
83  }
84  if ( image )
85  {
86  size += ( image->width() * image->height() * 32 );
87  }
88  return size;
89 }
90 
91 void QgsSvgCacheEntry::dump() const
92 {
93  QgsDebugMsgLevel( QStringLiteral( "path: %1, size %2, width scale factor %3" ).arg( path ).arg( size ).arg( widthScaleFactor ), 4 );
94 }
96 
97 
98 //
99 // QgsSvgCache
100 //
101 
102 QgsSvgCache::QgsSvgCache( QObject *parent )
103  : QgsAbstractContentCache< QgsSvgCacheEntry >( parent, QObject::tr( "SVG" ) )
104 {
105  mMissingSvg = QStringLiteral( "<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>?</text></svg>" ).toLatin1();
106 
107  const QString downloadingSvgPath = QgsApplication::defaultThemePath() + QStringLiteral( "downloading_svg.svg" );
108  if ( QFile::exists( downloadingSvgPath ) )
109  {
110  QFile file( downloadingSvgPath );
111  if ( file.open( QIODevice::ReadOnly ) )
112  {
113  mFetchingSvg = file.readAll();
114  }
115  }
116 
117  if ( mFetchingSvg.isEmpty() )
118  {
119  mFetchingSvg = QStringLiteral( "<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>?</text></svg>" ).toLatin1();
120  }
121 
123 }
124 
125 QImage QgsSvgCache::svgAsImage( const QString &file, double size, const QColor &fill, const QColor &stroke, double strokeWidth,
126  double widthScaleFactor, bool &fitsInCache, double fixedAspectRatio, bool blocking, const QMap<QString, QString> &parameters )
127 {
128  const QMutexLocker locker( &mMutex );
129 
130  fitsInCache = true;
131  QgsSvgCacheEntry *currentEntry = cacheEntry( file, size, fill, stroke, strokeWidth, widthScaleFactor, fixedAspectRatio, parameters, blocking );
132 
133  QImage result;
134 
135  //if current entry image is 0: cache image for entry
136  // checks to see if image will fit into cache
137  //update stats for memory usage
138  if ( !currentEntry->image )
139  {
140  const QSvgRenderer r( currentEntry->svgContent );
141  double hwRatio = 1.0;
142  if ( r.viewBoxF().width() > 0 )
143  {
144  if ( currentEntry->fixedAspectRatio > 0 )
145  {
146  hwRatio = currentEntry->fixedAspectRatio;
147  }
148  else
149  {
150  hwRatio = r.viewBoxF().height() / r.viewBoxF().width();
151  }
152  }
153  long cachedDataSize = 0;
154  cachedDataSize += currentEntry->svgContent.size();
155  cachedDataSize += static_cast< int >( currentEntry->size * currentEntry->size * hwRatio * 32 );
156  if ( cachedDataSize > mMaxCacheSize / 2 )
157  {
158  fitsInCache = false;
159  currentEntry->image.reset();
160 
161  // instead cache picture
162  if ( !currentEntry->picture )
163  {
164  cachePicture( currentEntry, false );
165  }
166 
167  // ...and render cached picture to result image
168  result = imageFromCachedPicture( *currentEntry );
169  }
170  else
171  {
172  cacheImage( currentEntry );
173  result = *( currentEntry->image );
174  }
176  }
177  else
178  {
179  result = *( currentEntry->image );
180  }
181 
182  return result;
183 }
184 
185 QPicture QgsSvgCache::svgAsPicture( const QString &path, double size, const QColor &fill, const QColor &stroke, double strokeWidth,
186  double widthScaleFactor, bool forceVectorOutput, double fixedAspectRatio, bool blocking, const QMap<QString, QString> &parameters )
187 {
188  const QMutexLocker locker( &mMutex );
189 
190  QgsSvgCacheEntry *currentEntry = cacheEntry( path, size, fill, stroke, strokeWidth, widthScaleFactor, fixedAspectRatio, parameters, blocking );
191 
192  //if current entry picture is 0: cache picture for entry
193  //update stats for memory usage
194  if ( !currentEntry->picture )
195  {
196  cachePicture( currentEntry, forceVectorOutput );
198  }
199 
200  QPicture p;
201  // For some reason p.detach() doesn't seem to always work as intended, at
202  // least with QT 5.5 on Ubuntu 16.04
203  // Serialization/deserialization is a safe way to be ensured we don't
204  // share a copy.
205  p.setData( currentEntry->picture->data(), currentEntry->picture->size() );
206  return p;
207 }
208 
209 QByteArray QgsSvgCache::svgContent( const QString &path, double size, const QColor &fill, const QColor &stroke, double strokeWidth,
210  double widthScaleFactor, double fixedAspectRatio, bool blocking, const QMap<QString, QString> &parameters, bool *isMissingImage )
211 {
212  const QMutexLocker locker( &mMutex );
213 
214  QgsSvgCacheEntry *currentEntry = cacheEntry( path, size, fill, stroke, strokeWidth, widthScaleFactor, fixedAspectRatio, parameters, blocking, isMissingImage );
215 
216  return currentEntry->svgContent;
217 }
218 
219 QSizeF QgsSvgCache::svgViewboxSize( const QString &path, double size, const QColor &fill, const QColor &stroke, double strokeWidth,
220  double widthScaleFactor, double fixedAspectRatio, bool blocking, const QMap<QString, QString> &parameters )
221 {
222  const QMutexLocker locker( &mMutex );
223 
224  QgsSvgCacheEntry *currentEntry = cacheEntry( path, size, fill, stroke, strokeWidth, widthScaleFactor, fixedAspectRatio, parameters, blocking );
225  return currentEntry->viewboxSize;
226 }
227 
228 void QgsSvgCache::containsParams( const QString &path, bool &hasFillParam, QColor &defaultFillColor, bool &hasStrokeParam, QColor &defaultStrokeColor,
229  bool &hasStrokeWidthParam, double &defaultStrokeWidth, bool blocking ) const
230 {
231  bool hasDefaultFillColor = false;
232  bool hasFillOpacityParam = false;
233  bool hasDefaultFillOpacity = false;
234  double defaultFillOpacity = 1.0;
235  bool hasDefaultStrokeColor = false;
236  bool hasDefaultStrokeWidth = false;
237  bool hasStrokeOpacityParam = false;
238  bool hasDefaultStrokeOpacity = false;
239  double defaultStrokeOpacity = 1.0;
240 
241  containsParams( path, hasFillParam, hasDefaultFillColor, defaultFillColor,
242  hasFillOpacityParam, hasDefaultFillOpacity, defaultFillOpacity,
243  hasStrokeParam, hasDefaultStrokeColor, defaultStrokeColor,
244  hasStrokeWidthParam, hasDefaultStrokeWidth, defaultStrokeWidth,
245  hasStrokeOpacityParam, hasDefaultStrokeOpacity, defaultStrokeOpacity,
246  blocking );
247 }
248 
249 void QgsSvgCache::containsParams( const QString &path,
250  bool &hasFillParam, bool &hasDefaultFillParam, QColor &defaultFillColor,
251  bool &hasFillOpacityParam, bool &hasDefaultFillOpacity, double &defaultFillOpacity,
252  bool &hasStrokeParam, bool &hasDefaultStrokeColor, QColor &defaultStrokeColor,
253  bool &hasStrokeWidthParam, bool &hasDefaultStrokeWidth, double &defaultStrokeWidth,
254  bool &hasStrokeOpacityParam, bool &hasDefaultStrokeOpacity, double &defaultStrokeOpacity,
255  bool blocking ) const
256 {
257  hasFillParam = false;
258  hasFillOpacityParam = false;
259  hasStrokeParam = false;
260  hasStrokeWidthParam = false;
261  hasStrokeOpacityParam = false;
262  defaultFillColor = QColor( Qt::white );
263  defaultFillOpacity = 1.0;
264  defaultStrokeColor = QColor( Qt::black );
265  defaultStrokeWidth = 0.2;
266  defaultStrokeOpacity = 1.0;
267 
268  hasDefaultFillParam = false;
269  hasDefaultFillOpacity = false;
270  hasDefaultStrokeColor = false;
271  hasDefaultStrokeWidth = false;
272  hasDefaultStrokeOpacity = false;
273 
274  QDomDocument svgDoc;
275  if ( !svgDoc.setContent( getContent( path, mMissingSvg, mFetchingSvg, blocking ) ) )
276  {
277  return;
278  }
279 
280  const QDomElement docElem = svgDoc.documentElement();
281  containsElemParams( docElem, hasFillParam, hasDefaultFillParam, defaultFillColor,
282  hasFillOpacityParam, hasDefaultFillOpacity, defaultFillOpacity,
283  hasStrokeParam, hasDefaultStrokeColor, defaultStrokeColor,
284  hasStrokeWidthParam, hasDefaultStrokeWidth, defaultStrokeWidth,
285  hasStrokeOpacityParam, hasDefaultStrokeOpacity, defaultStrokeOpacity );
286 }
287 
288 void QgsSvgCache::replaceParamsAndCacheSvg( QgsSvgCacheEntry *entry, bool blocking )
289 {
290  if ( !entry )
291  {
292  return;
293  }
294 
295  const QByteArray content = getContent( entry->path, mMissingSvg, mFetchingSvg, blocking ) ;
296  entry->isMissingImage = content == mMissingSvg;
297  QDomDocument svgDoc;
298  if ( !svgDoc.setContent( content ) )
299  {
300  return;
301  }
302 
303  //replace fill color, stroke color, stroke with in all nodes
304  QDomElement docElem = svgDoc.documentElement();
305 
306  QSizeF viewboxSize;
307  const double sizeScaleFactor = calcSizeScaleFactor( entry, docElem, viewboxSize );
308  entry->viewboxSize = viewboxSize;
309  replaceElemParams( docElem, entry->fill, entry->stroke, entry->strokeWidth * sizeScaleFactor, entry->parameters );
310 
311  entry->svgContent = svgDoc.toByteArray( 0 );
312 
313 
314  // toByteArray screws up tspans inside text by adding new lines before and after each span... this should help, at the
315  // risk of potentially breaking some svgs where the newline is desired
316  entry->svgContent.replace( "\n<tspan", "<tspan" );
317  entry->svgContent.replace( "</tspan>\n", "</tspan>" );
318 
319  mTotalSize += entry->svgContent.size();
320 }
321 
322 double QgsSvgCache::calcSizeScaleFactor( QgsSvgCacheEntry *entry, const QDomElement &docElem, QSizeF &viewboxSize ) const
323 {
324  QString viewBox;
325 
326  //bad size
327  if ( !entry || qgsDoubleNear( entry->size, 0.0 ) )
328  return 1.0;
329 
330  //find svg viewbox attribute
331  //first check if docElem is svg element
332  if ( docElem.tagName() == QLatin1String( "svg" ) && docElem.hasAttribute( QStringLiteral( "viewBox" ) ) )
333  {
334  viewBox = docElem.attribute( QStringLiteral( "viewBox" ), QString() );
335  }
336  else if ( docElem.tagName() == QLatin1String( "svg" ) && docElem.hasAttribute( QStringLiteral( "viewbox" ) ) )
337  {
338  viewBox = docElem.attribute( QStringLiteral( "viewbox" ), QString() );
339  }
340  else
341  {
342  const QDomElement svgElem = docElem.firstChildElement( QStringLiteral( "svg" ) );
343  if ( !svgElem.isNull() )
344  {
345  if ( svgElem.hasAttribute( QStringLiteral( "viewBox" ) ) )
346  viewBox = svgElem.attribute( QStringLiteral( "viewBox" ), QString() );
347  else if ( svgElem.hasAttribute( QStringLiteral( "viewbox" ) ) )
348  viewBox = svgElem.attribute( QStringLiteral( "viewbox" ), QString() );
349  }
350  }
351 
352  //could not find valid viewbox attribute
353  if ( viewBox.isEmpty() )
354  {
355  // trying looking for width/height and use them as a fallback
356  if ( docElem.tagName() == QLatin1String( "svg" ) && docElem.hasAttribute( QStringLiteral( "width" ) ) )
357  {
358  const QString widthString = docElem.attribute( QStringLiteral( "width" ) );
359  const QRegularExpression measureRegEx( QStringLiteral( "([\\d\\.]+).*?$" ) );
360  const QRegularExpressionMatch widthMatch = measureRegEx.match( widthString );
361  if ( widthMatch.hasMatch() )
362  {
363  const double width = widthMatch.captured( 1 ).toDouble();
364  const QString heightString = docElem.attribute( QStringLiteral( "height" ) );
365 
366  const QRegularExpressionMatch heightMatch = measureRegEx.match( heightString );
367  if ( heightMatch.hasMatch() )
368  {
369  const double height = heightMatch.captured( 1 ).toDouble();
370  viewboxSize = QSizeF( width, height );
371  return width / entry->size;
372  }
373  }
374  }
375 
376  return 1.0;
377  }
378 
379 
380  //width should be 3rd element in a 4 part space delimited string
381  const QStringList parts = viewBox.split( ' ' );
382  if ( parts.count() != 4 )
383  return 1.0;
384 
385  bool heightOk = false;
386  const double height = parts.at( 3 ).toDouble( &heightOk );
387 
388  bool widthOk = false;
389  const double width = parts.at( 2 ).toDouble( &widthOk );
390  if ( widthOk )
391  {
392  if ( heightOk )
393  viewboxSize = QSizeF( width, height );
394  return width / entry->size;
395  }
396 
397  return 1.0;
398 }
399 
400 
401 QByteArray QgsSvgCache::getImageData( const QString &path, bool blocking ) const
402 {
403  return getContent( path, mMissingSvg, mFetchingSvg, blocking );
404 };
405 
406 bool QgsSvgCache::checkReply( QNetworkReply *reply, const QString &path ) const
407 {
408  // we accept both real SVG mime types AND plain text types - because some sites
409  // (notably github) serve up svgs as raw text
410  const QString contentType = reply->header( QNetworkRequest::ContentTypeHeader ).toString();
411  if ( !contentType.startsWith( QLatin1String( "image/svg+xml" ), Qt::CaseInsensitive )
412  && !contentType.startsWith( QLatin1String( "text/plain" ), Qt::CaseInsensitive ) )
413  {
414  QgsMessageLog::logMessage( tr( "Unexpected MIME type %1 received for %2" ).arg( contentType, path ), tr( "SVG" ) );
415  return false;
416  }
417  return true;
418 }
419 
420 void QgsSvgCache::cacheImage( QgsSvgCacheEntry *entry )
421 {
422  if ( !entry )
423  {
424  return;
425  }
426 
427  entry->image.reset();
428 
429  QSizeF viewBoxSize;
430  QSizeF scaledSize;
431  const QSize imageSize = sizeForImage( *entry, viewBoxSize, scaledSize );
432 
433  // cast double image sizes to int for QImage
434  std::unique_ptr< QImage > image = std::make_unique< QImage >( imageSize, QImage::Format_ARGB32_Premultiplied );
435  image->fill( 0 ); // transparent background
436 
437  const bool isFixedAR = entry->fixedAspectRatio > 0;
438 
439  QPainter p( image.get() );
440  QSvgRenderer r( entry->svgContent );
441  if ( qgsDoubleNear( viewBoxSize.width(), viewBoxSize.height() ) )
442  {
443  r.render( &p );
444  }
445  else
446  {
447  QSizeF s( viewBoxSize );
448  s.scale( scaledSize.width(), scaledSize.height(), isFixedAR ? Qt::IgnoreAspectRatio : Qt::KeepAspectRatio );
449  const QRectF rect( ( imageSize.width() - s.width() ) / 2, ( imageSize.height() - s.height() ) / 2, s.width(), s.height() );
450  r.render( &p, rect );
451  }
452 
453  mTotalSize += ( image->width() * image->height() * 32 );
454  entry->image = std::move( image );
455 }
456 
457 void QgsSvgCache::cachePicture( QgsSvgCacheEntry *entry, bool forceVectorOutput )
458 {
459  Q_UNUSED( forceVectorOutput )
460  if ( !entry )
461  {
462  return;
463  }
464 
465  entry->picture.reset();
466 
467  const bool isFixedAR = entry->fixedAspectRatio > 0;
468 
469  //correct QPictures dpi correction
470  std::unique_ptr< QPicture > picture = std::make_unique< QPicture >();
471  QRectF rect;
472  QSvgRenderer r( entry->svgContent );
473  double hwRatio = 1.0;
474  if ( r.viewBoxF().width() > 0 )
475  {
476  if ( isFixedAR )
477  {
478  hwRatio = entry->fixedAspectRatio;
479  }
480  else
481  {
482  hwRatio = r.viewBoxF().height() / r.viewBoxF().width();
483  }
484  }
485 
486  const double wSize = entry->size;
487  const double hSize = wSize * hwRatio;
488 
489  QSizeF s( r.viewBoxF().size() );
490  s.scale( wSize, hSize, isFixedAR ? Qt::IgnoreAspectRatio : Qt::KeepAspectRatio );
491  rect = QRectF( -s.width() / 2.0, -s.height() / 2.0, s.width(), s.height() );
492 
493  QPainter p( picture.get() );
494  r.render( &p, rect );
495  entry->picture = std::move( picture );
496  mTotalSize += entry->picture->size();
497 }
498 
499 QgsSvgCacheEntry *QgsSvgCache::cacheEntry( const QString &path, double size, const QColor &fill, const QColor &stroke, double strokeWidth,
500  double widthScaleFactor, double fixedAspectRatio, const QMap<QString, QString> &parameters, bool blocking, bool *isMissingImage )
501 {
502  QgsSvgCacheEntry *currentEntry = findExistingEntry( new QgsSvgCacheEntry( path, size, strokeWidth, widthScaleFactor, fill, stroke, fixedAspectRatio, parameters ) );
503 
504  if ( currentEntry->svgContent.isEmpty() )
505  {
506  replaceParamsAndCacheSvg( currentEntry, blocking );
507  }
508 
509  if ( isMissingImage )
510  *isMissingImage = currentEntry->isMissingImage;
511 
512  return currentEntry;
513 }
514 
515 
516 void QgsSvgCache::replaceElemParams( QDomElement &elem, const QColor &fill, const QColor &stroke, double strokeWidth, const QMap<QString, QString> &parameters )
517 {
518  if ( elem.isNull() )
519  {
520  return;
521  }
522 
523  //go through attributes
524  const QDomNamedNodeMap attributes = elem.attributes();
525  const int nAttributes = attributes.count();
526  for ( int i = 0; i < nAttributes; ++i )
527  {
528  const QDomAttr attribute = attributes.item( i ).toAttr();
529  //e.g. style="fill:param(fill);param(stroke)"
530  if ( attribute.name().compare( QLatin1String( "style" ), Qt::CaseInsensitive ) == 0 )
531  {
532  //entries separated by ';'
533  QString newAttributeString;
534 
535  const QStringList entryList = attribute.value().split( ';' );
536  QStringList::const_iterator entryIt = entryList.constBegin();
537  for ( ; entryIt != entryList.constEnd(); ++entryIt )
538  {
539  const QStringList keyValueSplit = entryIt->split( ':' );
540  if ( keyValueSplit.size() < 2 )
541  {
542  continue;
543  }
544  const QString key = keyValueSplit.at( 0 );
545  QString value = keyValueSplit.at( 1 );
546  QString newValue = value;
547  value = value.trimmed().toLower();
548 
549  if ( value.startsWith( QLatin1String( "param(fill)" ) ) )
550  {
551  newValue = fill.name();
552  }
553  else if ( value.startsWith( QLatin1String( "param(fill-opacity)" ) ) )
554  {
555  newValue = QString::number( fill.alphaF() );
556  }
557  else if ( value.startsWith( QLatin1String( "param(outline)" ) ) )
558  {
559  newValue = stroke.name();
560  }
561  else if ( value.startsWith( QLatin1String( "param(outline-opacity)" ) ) )
562  {
563  newValue = QString::number( stroke.alphaF() );
564  }
565  else if ( value.startsWith( QLatin1String( "param(outline-width)" ) ) )
566  {
567  newValue = QString::number( strokeWidth );
568  }
569 
570  if ( entryIt != entryList.constBegin() )
571  {
572  newAttributeString.append( ';' );
573  }
574  newAttributeString.append( key + ':' + newValue );
575  }
576  elem.setAttribute( attribute.name(), newAttributeString );
577  }
578  else
579  {
580  const QString value = attribute.value().trimmed().toLower();
581  if ( value.startsWith( QLatin1String( "param(fill)" ) ) )
582  {
583  elem.setAttribute( attribute.name(), fill.name() );
584  }
585  else if ( value.startsWith( QLatin1String( "param(fill-opacity)" ) ) )
586  {
587  elem.setAttribute( attribute.name(), fill.alphaF() );
588  }
589  else if ( value.startsWith( QLatin1String( "param(outline)" ) ) )
590  {
591  elem.setAttribute( attribute.name(), stroke.name() );
592  }
593  else if ( value.startsWith( QLatin1String( "param(outline-opacity)" ) ) )
594  {
595  elem.setAttribute( attribute.name(), stroke.alphaF() );
596  }
597  else if ( value.startsWith( QLatin1String( "param(outline-width)" ) ) )
598  {
599  elem.setAttribute( attribute.name(), QString::number( strokeWidth ) );
600  }
601  else
602  {
603  QMap<QString, QString>::const_iterator paramIt = parameters.constBegin();
604  for ( ; paramIt != parameters.constEnd(); ++paramIt )
605  {
606  if ( value.startsWith( QString( QLatin1String( "param(%1)" ) ).arg( paramIt.key() ) ) )
607  {
608  elem.setAttribute( attribute.name(), paramIt.value() );
609  break;
610  }
611  }
612  }
613  }
614  }
615 
616  QDomNode child = elem.firstChild();
617  if ( child.isText() && child.nodeValue().startsWith( "param(" ) )
618  {
619  QMap<QString, QString>::const_iterator paramIt = parameters.constBegin();
620  for ( ; paramIt != parameters.constEnd(); ++paramIt )
621  {
622  if ( child.toText().data().startsWith( QString( QLatin1String( "param(%1)" ) ).arg( paramIt.key() ) ) )
623  {
624  child.setNodeValue( paramIt.value() );
625  break;
626  }
627  }
628  }
629 
630  const QDomNodeList childList = elem.childNodes();
631  const int nChildren = childList.count();
632  for ( int i = 0; i < nChildren; ++i )
633  {
634  QDomElement childElem = childList.at( i ).toElement();
635  replaceElemParams( childElem, fill, stroke, strokeWidth, parameters );
636  }
637 }
638 
639 void QgsSvgCache::containsElemParams( const QDomElement &elem, bool &hasFillParam, bool &hasDefaultFill, QColor &defaultFill,
640  bool &hasFillOpacityParam, bool &hasDefaultFillOpacity, double &defaultFillOpacity,
641  bool &hasStrokeParam, bool &hasDefaultStroke, QColor &defaultStroke,
642  bool &hasStrokeWidthParam, bool &hasDefaultStrokeWidth, double &defaultStrokeWidth,
643  bool &hasStrokeOpacityParam, bool &hasDefaultStrokeOpacity, double &defaultStrokeOpacity ) const
644 {
645  if ( elem.isNull() )
646  {
647  return;
648  }
649 
650  //we already have all the information, no need to go deeper
651  if ( hasFillParam && hasStrokeParam && hasStrokeWidthParam && hasFillOpacityParam && hasStrokeOpacityParam )
652  {
653  return;
654  }
655 
656  //check this elements attribute
657  const QDomNamedNodeMap attributes = elem.attributes();
658  const int nAttributes = attributes.count();
659 
660  QStringList valueSplit;
661  for ( int i = 0; i < nAttributes; ++i )
662  {
663  const QDomAttr attribute = attributes.item( i ).toAttr();
664  if ( attribute.name().compare( QLatin1String( "style" ), Qt::CaseInsensitive ) == 0 )
665  {
666  //entries separated by ';'
667  const QStringList entryList = attribute.value().split( ';' );
668  QStringList::const_iterator entryIt = entryList.constBegin();
669  for ( ; entryIt != entryList.constEnd(); ++entryIt )
670  {
671  const QStringList keyValueSplit = entryIt->split( ':' );
672  if ( keyValueSplit.size() < 2 )
673  {
674  continue;
675  }
676  const QString value = keyValueSplit.at( 1 );
677  valueSplit = value.split( ' ' );
678  if ( !hasFillParam && value.startsWith( QLatin1String( "param(fill)" ) ) )
679  {
680  hasFillParam = true;
681  if ( valueSplit.size() > 1 )
682  {
683  defaultFill = QColor( valueSplit.at( 1 ) );
684  hasDefaultFill = true;
685  }
686  }
687  else if ( !hasFillOpacityParam && value.startsWith( QLatin1String( "param(fill-opacity)" ) ) )
688  {
689  hasFillOpacityParam = true;
690  if ( valueSplit.size() > 1 )
691  {
692  bool ok;
693  const double opacity = valueSplit.at( 1 ).toDouble( &ok );
694  if ( ok )
695  {
696  defaultFillOpacity = opacity;
697  hasDefaultFillOpacity = true;
698  }
699  }
700  }
701  else if ( !hasStrokeParam && value.startsWith( QLatin1String( "param(outline)" ) ) )
702  {
703  hasStrokeParam = true;
704  if ( valueSplit.size() > 1 )
705  {
706  defaultStroke = QColor( valueSplit.at( 1 ) );
707  hasDefaultStroke = true;
708  }
709  }
710  else if ( !hasStrokeWidthParam && value.startsWith( QLatin1String( "param(outline-width)" ) ) )
711  {
712  hasStrokeWidthParam = true;
713  if ( valueSplit.size() > 1 )
714  {
715  defaultStrokeWidth = valueSplit.at( 1 ).toDouble();
716  hasDefaultStrokeWidth = true;
717  }
718  }
719  else if ( !hasStrokeOpacityParam && value.startsWith( QLatin1String( "param(outline-opacity)" ) ) )
720  {
721  hasStrokeOpacityParam = true;
722  if ( valueSplit.size() > 1 )
723  {
724  bool ok;
725  const double opacity = valueSplit.at( 1 ).toDouble( &ok );
726  if ( ok )
727  {
728  defaultStrokeOpacity = opacity;
729  hasDefaultStrokeOpacity = true;
730  }
731  }
732  }
733  }
734  }
735  else
736  {
737  const QString value = attribute.value();
738  valueSplit = value.split( ' ' );
739  if ( !hasFillParam && value.startsWith( QLatin1String( "param(fill)" ) ) )
740  {
741  hasFillParam = true;
742  if ( valueSplit.size() > 1 )
743  {
744  defaultFill = QColor( valueSplit.at( 1 ) );
745  hasDefaultFill = true;
746  }
747  }
748  else if ( !hasFillOpacityParam && value.startsWith( QLatin1String( "param(fill-opacity)" ) ) )
749  {
750  hasFillOpacityParam = true;
751  if ( valueSplit.size() > 1 )
752  {
753  bool ok;
754  const double opacity = valueSplit.at( 1 ).toDouble( &ok );
755  if ( ok )
756  {
757  defaultFillOpacity = opacity;
758  hasDefaultFillOpacity = true;
759  }
760  }
761  }
762  else if ( !hasStrokeParam && value.startsWith( QLatin1String( "param(outline)" ) ) )
763  {
764  hasStrokeParam = true;
765  if ( valueSplit.size() > 1 )
766  {
767  defaultStroke = QColor( valueSplit.at( 1 ) );
768  hasDefaultStroke = true;
769  }
770  }
771  else if ( !hasStrokeWidthParam && value.startsWith( QLatin1String( "param(outline-width)" ) ) )
772  {
773  hasStrokeWidthParam = true;
774  if ( valueSplit.size() > 1 )
775  {
776  defaultStrokeWidth = valueSplit.at( 1 ).toDouble();
777  hasDefaultStrokeWidth = true;
778  }
779  }
780  else if ( !hasStrokeOpacityParam && value.startsWith( QLatin1String( "param(outline-opacity)" ) ) )
781  {
782  hasStrokeOpacityParam = true;
783  if ( valueSplit.size() > 1 )
784  {
785  bool ok;
786  const double opacity = valueSplit.at( 1 ).toDouble( &ok );
787  if ( ok )
788  {
789  defaultStrokeOpacity = opacity;
790  hasDefaultStrokeOpacity = true;
791  }
792  }
793  }
794  }
795  }
796 
797  //pass it further to child items
798  const QDomNodeList childList = elem.childNodes();
799  const int nChildren = childList.count();
800  for ( int i = 0; i < nChildren; ++i )
801  {
802  const QDomElement childElem = childList.at( i ).toElement();
803  containsElemParams( childElem, hasFillParam, hasDefaultFill, defaultFill,
804  hasFillOpacityParam, hasDefaultFillOpacity, defaultFillOpacity,
805  hasStrokeParam, hasDefaultStroke, defaultStroke,
806  hasStrokeWidthParam, hasDefaultStrokeWidth, defaultStrokeWidth,
807  hasStrokeOpacityParam, hasDefaultStrokeOpacity, defaultStrokeOpacity );
808  }
809 }
810 
811 QSize QgsSvgCache::sizeForImage( const QgsSvgCacheEntry &entry, QSizeF &viewBoxSize, QSizeF &scaledSize ) const
812 {
813  const bool isFixedAR = entry.fixedAspectRatio > 0;
814 
815  const QSvgRenderer r( entry.svgContent );
816  double hwRatio = 1.0;
817  viewBoxSize = r.viewBoxF().size();
818  if ( viewBoxSize.width() > 0 )
819  {
820  if ( isFixedAR )
821  {
822  hwRatio = entry.fixedAspectRatio;
823  }
824  else
825  {
826  hwRatio = viewBoxSize.height() / viewBoxSize.width();
827  }
828  }
829 
830  // cast double image sizes to int for QImage
831  scaledSize.setWidth( entry.size );
832  int wImgSize = static_cast< int >( scaledSize.width() );
833  if ( wImgSize < 1 )
834  {
835  wImgSize = 1;
836  }
837  scaledSize.setHeight( scaledSize.width() * hwRatio );
838  int hImgSize = static_cast< int >( scaledSize.height() );
839  if ( hImgSize < 1 )
840  {
841  hImgSize = 1;
842  }
843  return QSize( wImgSize, hImgSize );
844 }
845 
846 QImage QgsSvgCache::imageFromCachedPicture( const QgsSvgCacheEntry &entry ) const
847 {
848  QSizeF viewBoxSize;
849  QSizeF scaledSize;
850  QImage image( sizeForImage( entry, viewBoxSize, scaledSize ), QImage::Format_ARGB32_Premultiplied );
851  image.fill( 0 ); // transparent background
852 
853  QPainter p( &image );
854  p.drawPicture( QPoint( 0, 0 ), *entry.picture );
855  return image;
856 }
857 
qgssvgcache.h
QgsDebugMsgLevel
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
QgsSvgCache::checkReply
bool checkReply(QNetworkReply *reply, const QString &path) const override
Runs additional checks on a network reply to ensure that the reply content is consistent with that re...
Definition: qgssvgcache.cpp:406
QgsSvgCache::svgContent
QByteArray svgContent(const QString &path, double size, const QColor &fill, const QColor &stroke, double strokeWidth, double widthScaleFactor, double fixedAspectRatio=0, bool blocking=false, const QMap< QString, QString > &parameters=QMap< QString, QString >(), bool *isMissingImage=nullptr)
Gets the SVG content corresponding to the given path.
Definition: qgssvgcache.cpp:209
qgssymbollayerutils.h
qgis.h
qgsnetworkcontentfetchertask.h
QgsSvgCache::containsParams
void containsParams(const QString &path, bool &hasFillParam, QColor &defaultFillColor, bool &hasStrokeParam, QColor &defaultStrokeColor, bool &hasStrokeWidthParam, double &defaultStrokeWidth, bool blocking=false) const
Tests if an SVG file contains parameters for fill, stroke color, stroke width.
Definition: qgssvgcache.cpp:228
QgsSvgCache::svgViewboxSize
QSizeF svgViewboxSize(const QString &path, double size, const QColor &fill, const QColor &stroke, double strokeWidth, double widthScaleFactor, double fixedAspectRatio=0, bool blocking=false, const QMap< QString, QString > &parameters=QMap< QString, QString >())
Calculates the viewbox size of a (possibly cached) SVG file.
Definition: qgssvgcache.cpp:219
QgsSvgCache::QgsSvgCache
QgsSvgCache(QObject *parent=nullptr)
Constructor for QgsSvgCache.
Definition: qgssvgcache.cpp:102
QgsSvgCache::svgAsPicture
QPicture svgAsPicture(const QString &path, double size, const QColor &fill, const QColor &stroke, double strokeWidth, double widthScaleFactor, bool forceVectorOutput=false, double fixedAspectRatio=0, bool blocking=false, const QMap< QString, QString > &parameters=QMap< QString, QString >())
Returns an SVG drawing as a QPicture.
Definition: qgssvgcache.cpp:185
qgsDoubleNear
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:2265
qgsnetworkaccessmanager.h
QgsMessageLog::logMessage
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::MessageLevel::Warning, bool notifyUser=true)
Adds a message to the log instance (and creates it if necessary).
Definition: qgsmessagelog.cpp:27
QgsSvgCache::getImageData
QByteArray getImageData(const QString &path, bool blocking=false) const
Gets the SVG content corresponding to the given path.
Definition: qgssvgcache.cpp:401
QgsAbstractContentCacheEntry
Base class for entries in a QgsAbstractContentCache.
Definition: qgsabstractcontentcache.h:54
QgsAbstractContentCache< QgsSvgCacheEntry >::findExistingEntry
QgsSvgCacheEntry * findExistingEntry(QgsSvgCacheEntry *entryTemplate)
Returns the existing entry from the cache which matches entryTemplate (deleting entryTemplate when do...
Definition: qgsabstractcontentcache.h:501
QgsAbstractContentCache< QgsSvgCacheEntry >::mMutex
QRecursiveMutex mMutex
Definition: qgsabstractcontentcache.h:560
QgsAbstractContentCacheBase::remoteContentFetched
void remoteContentFetched(const QString &url)
Emitted when the cache has finished retrieving content from a remote url.
qgslogger.h
QgsAbstractContentCache< QgsSvgCacheEntry >::trimToMaximumSize
void trimToMaximumSize()
Removes the least used cache entries until the maximum cache size is under the predefined size limit.
Definition: qgsabstractcontentcache.h:236
QgsAbstractContentCache
Abstract base class for file content caches, such as SVG or raster image caches.
Definition: qgsabstractcontentcache.h:196
QgsApplication::defaultThemePath
static QString defaultThemePath()
Returns the path to the default theme directory.
Definition: qgsapplication.cpp:653
QgsSvgCache::svgAsImage
QImage svgAsImage(const QString &path, double size, const QColor &fill, const QColor &stroke, double strokeWidth, double widthScaleFactor, bool &fitsInCache, double fixedAspectRatio=0, bool blocking=false, const QMap< QString, QString > &parameters=QMap< QString, QString >())
Returns an SVG drawing as a QImage.
Definition: qgssvgcache.cpp:125
QgsAbstractContentCache< QgsSvgCacheEntry >::mTotalSize
long mTotalSize
Estimated total size of all cached content.
Definition: qgsabstractcontentcache.h:563
qgsmessagelog.h
QgsAbstractContentCache< QgsSvgCacheEntry >::getContent
QByteArray getContent(const QString &path, const QByteArray &missingContent, const QByteArray &fetchingContent, bool blocking=false) const
Gets the file content corresponding to the given path.
Definition: qgsabstractcontentcache.h:269
QgsSvgCache::remoteSvgFetched
void remoteSvgFetched(const QString &url)
Emitted when the cache has finished retrieving an SVG file from a remote url.
QgsAbstractContentCache< QgsSvgCacheEntry >::mMaxCacheSize
long mMaxCacheSize
Maximum cache size.
Definition: qgsabstractcontentcache.h:566