QGIS API Documentation  3.21.0-Master (5b68dc587e)
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 
void remoteContentFetched(const QString &url)
Emitted when the cache has finished retrieving content from a remote url.
Base class for entries in a QgsAbstractContentCache.
Abstract base class for file content caches, such as SVG or raster image caches.
QgsSvgCacheEntry * findExistingEntry(QgsSvgCacheEntry *entryTemplate)
Returns the existing entry from the cache which matches entryTemplate (deleting entryTemplate when do...
QByteArray getContent(const QString &path, const QByteArray &missingContent, const QByteArray &fetchingContent, bool blocking=false) const
Gets the file content corresponding to the given path.
long mTotalSize
Estimated total size of all cached content.
void trimToMaximumSize()
Removes the least used cache entries until the maximum cache size is under the predefined size limit.
static QString defaultThemePath()
Returns the path to the default theme directory.
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).
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.
QByteArray getImageData(const QString &path, bool blocking=false) const
Gets the SVG content corresponding to the given path.
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.
QgsSvgCache(QObject *parent=nullptr)
Constructor for QgsSvgCache.
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...
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.
void remoteSvgFetched(const QString &url)
Emitted when the cache has finished retrieving an SVG file from a remote url.
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.
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.
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:1234
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39