QGIS API Documentation  3.4.15-Madeira (e83d02e274)
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 <QSvgRenderer>
36 #include <QFileInfo>
37 #include <QNetworkReply>
38 #include <QNetworkRequest>
39 
41 
42 QgsSvgCacheEntry::QgsSvgCacheEntry( const QString &p, double s, double ow, double wsf, const QColor &fi, const QColor &ou, double far )
43  : path( p )
44  , fileModified( p.startsWith( QLatin1String( "base64:", Qt::CaseInsensitive ) ) ? QDateTime() : QFileInfo( p ).lastModified() )
45  , size( s )
46  , strokeWidth( ow )
47  , widthScaleFactor( wsf )
48  , fixedAspectRatio( far )
49  , fill( fi )
50  , stroke( ou )
51 {
52  fileModifiedLastCheckTimer.start();
53 }
54 
55 bool QgsSvgCacheEntry::operator==( const QgsSvgCacheEntry &other ) const
56 {
57  bool equal = other.path == path && qgsDoubleNear( other.size, size ) && qgsDoubleNear( other.strokeWidth, strokeWidth ) && qgsDoubleNear( other.widthScaleFactor, widthScaleFactor )
58  && other.fixedAspectRatio == fixedAspectRatio && other.fill == fill && other.stroke == stroke;
59 
60  if ( equal && ( mFileModifiedCheckTimeout <= 0 || fileModifiedLastCheckTimer.hasExpired( mFileModifiedCheckTimeout ) ) )
61  equal = other.fileModified == fileModified;
62 
63  return equal;
64 }
65 
66 int QgsSvgCacheEntry::dataSize() const
67 {
68  int size = svgContent.size();
69  if ( picture )
70  {
71  size += picture->size();
72  }
73  if ( image )
74  {
75  size += ( image->width() * image->height() * 32 );
76  }
77  return size;
78 }
80 
81 
82 QgsSvgCache::QgsSvgCache( QObject *parent )
83  : QObject( parent )
84  , mMutex( QMutex::Recursive )
85 {
86  mMissingSvg = QStringLiteral( "<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>?</text></svg>" ).toLatin1();
87 
88  const QString downloadingSvgPath = QgsApplication::defaultThemePath() + QStringLiteral( "downloading_svg.svg" );
89  if ( QFile::exists( downloadingSvgPath ) )
90  {
91  QFile file( downloadingSvgPath );
92  if ( file.open( QIODevice::ReadOnly ) )
93  {
94  mFetchingSvg = file.readAll();
95  }
96  }
97 
98  if ( mFetchingSvg.isEmpty() )
99  {
100  mFetchingSvg = QStringLiteral( "<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>?</text></svg>" ).toLatin1();
101  }
102 }
103 
105 {
106  qDeleteAll( mEntryLookup );
107 }
108 
109 
110 QImage QgsSvgCache::svgAsImage( const QString &file, double size, const QColor &fill, const QColor &stroke, double strokeWidth,
111  double widthScaleFactor, bool &fitsInCache, double fixedAspectRatio )
112 {
113  QMutexLocker locker( &mMutex );
114 
115  fitsInCache = true;
116  QgsSvgCacheEntry *currentEntry = cacheEntry( file, size, fill, stroke, strokeWidth, widthScaleFactor, fixedAspectRatio );
117 
118  QImage result;
119 
120  //if current entry image is 0: cache image for entry
121  // checks to see if image will fit into cache
122  //update stats for memory usage
123  if ( !currentEntry->image )
124  {
125  QSvgRenderer r( currentEntry->svgContent );
126  double hwRatio = 1.0;
127  if ( r.viewBoxF().width() > 0 )
128  {
129  if ( currentEntry->fixedAspectRatio > 0 )
130  {
131  hwRatio = currentEntry->fixedAspectRatio;
132  }
133  else
134  {
135  hwRatio = r.viewBoxF().height() / r.viewBoxF().width();
136  }
137  }
138  long cachedDataSize = 0;
139  cachedDataSize += currentEntry->svgContent.size();
140  cachedDataSize += static_cast< int >( currentEntry->size * currentEntry->size * hwRatio * 32 );
141  if ( cachedDataSize > MAXIMUM_SIZE / 2 )
142  {
143  fitsInCache = false;
144  currentEntry->image.reset();
145 
146  // instead cache picture
147  if ( !currentEntry->picture )
148  {
149  cachePicture( currentEntry, false );
150  }
151 
152  // ...and render cached picture to result image
153  result = imageFromCachedPicture( *currentEntry );
154  }
155  else
156  {
157  cacheImage( currentEntry );
158  result = *( currentEntry->image );
159  }
160  trimToMaximumSize();
161  }
162  else
163  {
164  result = *( currentEntry->image );
165  }
166 
167  return result;
168 }
169 
170 QPicture QgsSvgCache::svgAsPicture( const QString &path, double size, const QColor &fill, const QColor &stroke, double strokeWidth,
171  double widthScaleFactor, bool forceVectorOutput, double fixedAspectRatio )
172 {
173  QMutexLocker locker( &mMutex );
174 
175  QgsSvgCacheEntry *currentEntry = cacheEntry( path, size, fill, stroke, strokeWidth, widthScaleFactor, fixedAspectRatio );
176 
177  //if current entry picture is 0: cache picture for entry
178  //update stats for memory usage
179  if ( !currentEntry->picture )
180  {
181  cachePicture( currentEntry, forceVectorOutput );
182  trimToMaximumSize();
183  }
184 
185  QPicture p;
186  // For some reason p.detach() doesn't seem to always work as intended, at
187  // least with QT 5.5 on Ubuntu 16.04
188  // Serialization/deserialization is a safe way to be ensured we don't
189  // share a copy.
190  p.setData( currentEntry->picture->data(), currentEntry->picture->size() );
191  return p;
192 }
193 
194 QByteArray QgsSvgCache::svgContent( const QString &path, double size, const QColor &fill, const QColor &stroke, double strokeWidth,
195  double widthScaleFactor, double fixedAspectRatio )
196 {
197  QMutexLocker locker( &mMutex );
198 
199  QgsSvgCacheEntry *currentEntry = cacheEntry( path, size, fill, stroke, strokeWidth, widthScaleFactor, fixedAspectRatio );
200 
201  return currentEntry->svgContent;
202 }
203 
204 QSizeF QgsSvgCache::svgViewboxSize( const QString &path, double size, const QColor &fill, const QColor &stroke, double strokeWidth, double widthScaleFactor, double fixedAspectRatio )
205 {
206  QMutexLocker locker( &mMutex );
207 
208  QgsSvgCacheEntry *currentEntry = cacheEntry( path, size, fill, stroke, strokeWidth, widthScaleFactor, fixedAspectRatio );
209 
210  return currentEntry->viewboxSize;
211 }
212 
213 QgsSvgCacheEntry *QgsSvgCache::insertSvg( const QString &path, double size, const QColor &fill, const QColor &stroke, double strokeWidth,
214  double widthScaleFactor, double fixedAspectRatio )
215 {
216  QgsSvgCacheEntry *entry = new QgsSvgCacheEntry( path, size, strokeWidth, widthScaleFactor, fill, stroke, fixedAspectRatio );
217  entry->mFileModifiedCheckTimeout = mFileModifiedCheckTimeout;
218 
219  replaceParamsAndCacheSvg( entry );
220 
221  if ( !entry->path.startsWith( QStringLiteral( "base64:" ) ) )
222  {
223  entry->fileModified = QFileInfo( entry->path ).lastModified();
224  entry->fileModifiedLastCheckTimer.start();
225  }
226 
227  mEntryLookup.insert( path, entry );
228 
229  //insert to most recent place in entry list
230  if ( !mMostRecentEntry ) //inserting first entry
231  {
232  mLeastRecentEntry = entry;
233  mMostRecentEntry = entry;
234  entry->previousEntry = nullptr;
235  entry->nextEntry = nullptr;
236  }
237  else
238  {
239  entry->previousEntry = mMostRecentEntry;
240  entry->nextEntry = nullptr;
241  mMostRecentEntry->nextEntry = entry;
242  mMostRecentEntry = entry;
243  }
244 
245  trimToMaximumSize();
246  return entry;
247 }
248 
249 void QgsSvgCache::containsParams( const QString &path, bool &hasFillParam, QColor &defaultFillColor, bool &hasStrokeParam, QColor &defaultStrokeColor,
250  bool &hasStrokeWidthParam, double &defaultStrokeWidth ) const
251 {
252  bool hasDefaultFillColor = false;
253  bool hasFillOpacityParam = false;
254  bool hasDefaultFillOpacity = false;
255  double defaultFillOpacity = 1.0;
256  bool hasDefaultStrokeColor = false;
257  bool hasDefaultStrokeWidth = false;
258  bool hasStrokeOpacityParam = false;
259  bool hasDefaultStrokeOpacity = false;
260  double defaultStrokeOpacity = 1.0;
261 
262  containsParams( path, hasFillParam, hasDefaultFillColor, defaultFillColor,
263  hasFillOpacityParam, hasDefaultFillOpacity, defaultFillOpacity,
264  hasStrokeParam, hasDefaultStrokeColor, defaultStrokeColor,
265  hasStrokeWidthParam, hasDefaultStrokeWidth, defaultStrokeWidth,
266  hasStrokeOpacityParam, hasDefaultStrokeOpacity, defaultStrokeOpacity );
267 }
268 
269 void QgsSvgCache::containsParams( const QString &path,
270  bool &hasFillParam, bool &hasDefaultFillParam, QColor &defaultFillColor,
271  bool &hasFillOpacityParam, bool &hasDefaultFillOpacity, double &defaultFillOpacity,
272  bool &hasStrokeParam, bool &hasDefaultStrokeColor, QColor &defaultStrokeColor,
273  bool &hasStrokeWidthParam, bool &hasDefaultStrokeWidth, double &defaultStrokeWidth,
274  bool &hasStrokeOpacityParam, bool &hasDefaultStrokeOpacity, double &defaultStrokeOpacity ) const
275 {
276  hasFillParam = false;
277  hasFillOpacityParam = false;
278  hasStrokeParam = false;
279  hasStrokeWidthParam = false;
280  hasStrokeOpacityParam = false;
281  defaultFillColor = QColor( Qt::white );
282  defaultFillOpacity = 1.0;
283  defaultStrokeColor = QColor( Qt::black );
284  defaultStrokeWidth = 0.2;
285  defaultStrokeOpacity = 1.0;
286 
287  hasDefaultFillParam = false;
288  hasDefaultFillOpacity = false;
289  hasDefaultStrokeColor = false;
290  hasDefaultStrokeWidth = false;
291  hasDefaultStrokeOpacity = false;
292 
293  QDomDocument svgDoc;
294  if ( !svgDoc.setContent( getImageData( path ) ) )
295  {
296  return;
297  }
298 
299  QDomElement docElem = svgDoc.documentElement();
300  containsElemParams( docElem, hasFillParam, hasDefaultFillParam, defaultFillColor,
301  hasFillOpacityParam, hasDefaultFillOpacity, defaultFillOpacity,
302  hasStrokeParam, hasDefaultStrokeColor, defaultStrokeColor,
303  hasStrokeWidthParam, hasDefaultStrokeWidth, defaultStrokeWidth,
304  hasStrokeOpacityParam, hasDefaultStrokeOpacity, defaultStrokeOpacity );
305 }
306 
307 void QgsSvgCache::replaceParamsAndCacheSvg( QgsSvgCacheEntry *entry )
308 {
309  if ( !entry )
310  {
311  return;
312  }
313 
314  QDomDocument svgDoc;
315  if ( !svgDoc.setContent( getImageData( entry->path ) ) )
316  {
317  return;
318  }
319 
320  //replace fill color, stroke color, stroke with in all nodes
321  QDomElement docElem = svgDoc.documentElement();
322 
323  QSizeF viewboxSize;
324  double sizeScaleFactor = calcSizeScaleFactor( entry, docElem, viewboxSize );
325  entry->viewboxSize = viewboxSize;
326  replaceElemParams( docElem, entry->fill, entry->stroke, entry->strokeWidth * sizeScaleFactor );
327 
328  entry->svgContent = svgDoc.toByteArray( 0 );
329 
330  // toByteArray screws up tspans inside text by adding new lines before and after each span... this should help, at the
331  // risk of potentially breaking some svgs where the newline is desired
332  entry->svgContent.replace( "\n<tspan", "<tspan" );
333  entry->svgContent.replace( "</tspan>\n", "</tspan>" );
334 
335  mTotalSize += entry->svgContent.size();
336 }
337 
338 double QgsSvgCache::calcSizeScaleFactor( QgsSvgCacheEntry *entry, const QDomElement &docElem, QSizeF &viewboxSize ) const
339 {
340  QString viewBox;
341 
342  //bad size
343  if ( !entry || qgsDoubleNear( entry->size, 0.0 ) )
344  return 1.0;
345 
346  //find svg viewbox attribute
347  //first check if docElem is svg element
348  if ( docElem.tagName() == QLatin1String( "svg" ) && docElem.hasAttribute( QStringLiteral( "viewBox" ) ) )
349  {
350  viewBox = docElem.attribute( QStringLiteral( "viewBox" ), QString() );
351  }
352  else if ( docElem.tagName() == QLatin1String( "svg" ) && docElem.hasAttribute( QStringLiteral( "viewbox" ) ) )
353  {
354  viewBox = docElem.attribute( QStringLiteral( "viewbox" ), QString() );
355  }
356  else
357  {
358  QDomElement svgElem = docElem.firstChildElement( QStringLiteral( "svg" ) );
359  if ( !svgElem.isNull() )
360  {
361  if ( svgElem.hasAttribute( QStringLiteral( "viewBox" ) ) )
362  viewBox = svgElem.attribute( QStringLiteral( "viewBox" ), QString() );
363  else if ( svgElem.hasAttribute( QStringLiteral( "viewbox" ) ) )
364  viewBox = svgElem.attribute( QStringLiteral( "viewbox" ), QString() );
365  }
366  }
367 
368  //could not find valid viewbox attribute
369  if ( viewBox.isEmpty() )
370  return 1.0;
371 
372  //width should be 3rd element in a 4 part space delimited string
373  QStringList parts = viewBox.split( ' ' );
374  if ( parts.count() != 4 )
375  return 1.0;
376 
377  bool heightOk = false;
378  double height = parts.at( 3 ).toDouble( &heightOk );
379 
380  bool widthOk = false;
381  double width = parts.at( 2 ).toDouble( &widthOk );
382  if ( widthOk )
383  {
384  if ( heightOk )
385  viewboxSize = QSizeF( width, height );
386  return width / entry->size;
387  }
388 
389  return 1.0;
390 }
391 
392 QByteArray QgsSvgCache::getImageData( const QString &path ) const
393 {
394  // maybe it's an embedded base64 string
395  if ( path.startsWith( QLatin1String( "base64:" ), Qt::CaseInsensitive ) )
396  {
397  QByteArray base64 = path.mid( 7 ).toLocal8Bit(); // strip 'base64:' prefix
398  return QByteArray::fromBase64( base64, QByteArray::OmitTrailingEquals );
399  }
400 
401  // is it a path to local file?
402  QFile svgFile( path );
403  if ( svgFile.exists() )
404  {
405  if ( svgFile.open( QIODevice::ReadOnly ) )
406  {
407  return svgFile.readAll();
408  }
409  else
410  {
411  return mMissingSvg;
412  }
413  }
414 
415  // maybe it's a url...
416  if ( !path.contains( QLatin1String( "://" ) ) ) // otherwise short, relative SVG paths might be considered URLs
417  {
418  return mMissingSvg;
419  }
420 
421  QUrl svgUrl( path );
422  if ( !svgUrl.isValid() )
423  {
424  return mMissingSvg;
425  }
426 
427  // check whether it's a url pointing to a local file
428  if ( svgUrl.scheme().compare( QLatin1String( "file" ), Qt::CaseInsensitive ) == 0 )
429  {
430  svgFile.setFileName( svgUrl.toLocalFile() );
431  if ( svgFile.exists() )
432  {
433  if ( svgFile.open( QIODevice::ReadOnly ) )
434  {
435  return svgFile.readAll();
436  }
437  }
438 
439  // not found...
440  return mMissingSvg;
441  }
442 
443  QMutexLocker locker( &mMutex );
444 
445  // already a request in progress for this url
446  if ( mPendingRemoteUrls.contains( path ) )
447  return mFetchingSvg;
448 
449  if ( mRemoteContentCache.contains( path ) )
450  {
451  // already fetched this content - phew. Just return what we already got.
452  return *mRemoteContentCache[ path ];
453  }
454 
455  mPendingRemoteUrls.insert( path );
456  //fire up task to fetch image in background
457  QNetworkRequest request( svgUrl );
458  request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
459  request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
460 
462  connect( task, &QgsNetworkContentFetcherTask::fetched, this, [this, task, path]
463  {
464  QMutexLocker locker( &mMutex );
465 
466  QNetworkReply *reply = task->reply();
467  if ( !reply )
468  {
469  // canceled
470  QMetaObject::invokeMethod( const_cast< QgsSvgCache * >( this ), "onRemoteSvgFetched", Qt::QueuedConnection, Q_ARG( QString, path ), Q_ARG( bool, false ) );
471  return;
472  }
473 
474  if ( reply->error() != QNetworkReply::NoError )
475  {
476  QgsMessageLog::logMessage( tr( "SVG request failed [error: %1 - url: %2]" ).arg( reply->errorString(), path ), tr( "SVG" ) );
477  return;
478  }
479 
480  bool ok = true;
481 
482  QVariant status = reply->attribute( QNetworkRequest::HttpStatusCodeAttribute );
483  if ( !status.isNull() && status.toInt() >= 400 )
484  {
485  QVariant phrase = reply->attribute( QNetworkRequest::HttpReasonPhraseAttribute );
486  QgsMessageLog::logMessage( tr( "SVG request error [status: %1 - reason phrase: %2] for %3" ).arg( status.toInt() ).arg( phrase.toString(), path ), tr( "SVG" ) );
487  mRemoteContentCache.insert( path, new QByteArray( mMissingSvg ) );
488  ok = false;
489  }
490 
491  // we accept both real SVG mime types AND plain text types - because some sites
492  // (notably github) serve up svgs as raw text
493  QString contentType = reply->header( QNetworkRequest::ContentTypeHeader ).toString();
494  if ( !contentType.startsWith( QLatin1String( "image/svg+xml" ), Qt::CaseInsensitive )
495  && !contentType.startsWith( QLatin1String( "text/plain" ), Qt::CaseInsensitive ) )
496  {
497  QgsMessageLog::logMessage( tr( "Unexpected MIME type %1 received for %2" ).arg( contentType, path ), tr( "SVG" ) );
498  mRemoteContentCache.insert( path, new QByteArray( mMissingSvg ) );
499  ok = false;
500  }
501 
502  if ( ok )
503  {
504  // read the image data
505  mRemoteContentCache.insert( path, new QByteArray( reply->readAll() ) );
506  }
507  QMetaObject::invokeMethod( const_cast< QgsSvgCache * >( this ), "onRemoteSvgFetched", Qt::QueuedConnection, Q_ARG( QString, path ), Q_ARG( bool, true ) );
508  } );
509 
511  return mFetchingSvg;
512 }
513 
514 void QgsSvgCache::cacheImage( QgsSvgCacheEntry *entry )
515 {
516  if ( !entry )
517  {
518  return;
519  }
520 
521  entry->image.reset();
522 
523  QSizeF viewBoxSize;
524  QSizeF scaledSize;
525  QSize imageSize = sizeForImage( *entry, viewBoxSize, scaledSize );
526 
527  // cast double image sizes to int for QImage
528  std::unique_ptr< QImage > image = qgis::make_unique< QImage >( imageSize, QImage::Format_ARGB32_Premultiplied );
529  image->fill( 0 ); // transparent background
530 
531  QPainter p( image.get() );
532  QSvgRenderer r( entry->svgContent );
533  if ( qgsDoubleNear( viewBoxSize.width(), viewBoxSize.height() ) )
534  {
535  r.render( &p );
536  }
537  else
538  {
539  QSizeF s( viewBoxSize );
540  s.scale( scaledSize.width(), scaledSize.height(), Qt::KeepAspectRatio );
541  QRectF rect( ( imageSize.width() - s.width() ) / 2, ( imageSize.height() - s.height() ) / 2, s.width(), s.height() );
542  r.render( &p, rect );
543  }
544 
545  mTotalSize += ( image->width() * image->height() * 32 );
546  entry->image = std::move( image );
547 }
548 
549 void QgsSvgCache::cachePicture( QgsSvgCacheEntry *entry, bool forceVectorOutput )
550 {
551  Q_UNUSED( forceVectorOutput );
552  if ( !entry )
553  {
554  return;
555  }
556 
557  entry->picture.reset();
558 
559  bool isFixedAR = entry->fixedAspectRatio > 0;
560 
561  //correct QPictures dpi correction
562  std::unique_ptr< QPicture > picture = qgis::make_unique< QPicture >();
563  QRectF rect;
564  QSvgRenderer r( entry->svgContent );
565  double hwRatio = 1.0;
566  if ( r.viewBoxF().width() > 0 )
567  {
568  if ( isFixedAR )
569  {
570  hwRatio = entry->fixedAspectRatio;
571  }
572  else
573  {
574  hwRatio = r.viewBoxF().height() / r.viewBoxF().width();
575  }
576  }
577 
578  double wSize = entry->size;
579  double hSize = wSize * hwRatio;
580 
581  QSizeF s( r.viewBoxF().size() );
582  s.scale( wSize, hSize, isFixedAR ? Qt::IgnoreAspectRatio : Qt::KeepAspectRatio );
583  rect = QRectF( -s.width() / 2.0, -s.height() / 2.0, s.width(), s.height() );
584 
585  QPainter p( picture.get() );
586  r.render( &p, rect );
587  entry->picture = std::move( picture );
588  mTotalSize += entry->picture->size();
589 }
590 
591 QgsSvgCacheEntry *QgsSvgCache::cacheEntry( const QString &path, double size, const QColor &fill, const QColor &stroke, double strokeWidth,
592  double widthScaleFactor, double fixedAspectRatio )
593 {
594  //search entries in mEntryLookup
595  QgsSvgCacheEntry *currentEntry = nullptr;
596  QList<QgsSvgCacheEntry *> entries = mEntryLookup.values( path );
597  QDateTime modified;
598  QList<QgsSvgCacheEntry *>::iterator entryIt = entries.begin();
599  for ( ; entryIt != entries.end(); ++entryIt )
600  {
601  QgsSvgCacheEntry *cacheEntry = *entryIt;
602  if ( qgsDoubleNear( cacheEntry->size, size ) && cacheEntry->fill == fill && cacheEntry->stroke == stroke &&
603  qgsDoubleNear( cacheEntry->strokeWidth, strokeWidth ) && qgsDoubleNear( cacheEntry->widthScaleFactor, widthScaleFactor ) &&
604  qgsDoubleNear( cacheEntry->fixedAspectRatio, fixedAspectRatio ) )
605  {
606  if ( mFileModifiedCheckTimeout <= 0 || cacheEntry->fileModifiedLastCheckTimer.hasExpired( mFileModifiedCheckTimeout ) )
607  {
608  if ( !modified.isValid() )
609  modified = QFileInfo( path ).lastModified();
610 
611  if ( cacheEntry->fileModified != modified )
612  continue;
613  else
614  cacheEntry->fileModifiedLastCheckTimer.restart();
615  }
616  currentEntry = cacheEntry;
617  break;
618  }
619  }
620 
621  //if not found: create new entry
622  //cache and replace params in svg content
623  if ( !currentEntry )
624  {
625  currentEntry = insertSvg( path, size, fill, stroke, strokeWidth, widthScaleFactor, fixedAspectRatio );
626  }
627  else
628  {
629  takeEntryFromList( currentEntry );
630  if ( !mMostRecentEntry ) //list is empty
631  {
632  mMostRecentEntry = currentEntry;
633  mLeastRecentEntry = currentEntry;
634  }
635  else
636  {
637  mMostRecentEntry->nextEntry = currentEntry;
638  currentEntry->previousEntry = mMostRecentEntry;
639  currentEntry->nextEntry = nullptr;
640  mMostRecentEntry = currentEntry;
641  }
642  }
643 
644  //debugging
645  //printEntryList();
646 
647  return currentEntry;
648 }
649 
650 void QgsSvgCache::replaceElemParams( QDomElement &elem, const QColor &fill, const QColor &stroke, double strokeWidth )
651 {
652  if ( elem.isNull() )
653  {
654  return;
655  }
656 
657  //go through attributes
658  QDomNamedNodeMap attributes = elem.attributes();
659  int nAttributes = attributes.count();
660  for ( int i = 0; i < nAttributes; ++i )
661  {
662  QDomAttr attribute = attributes.item( i ).toAttr();
663  //e.g. style="fill:param(fill);param(stroke)"
664  if ( attribute.name().compare( QLatin1String( "style" ), Qt::CaseInsensitive ) == 0 )
665  {
666  //entries separated by ';'
667  QString newAttributeString;
668 
669  QStringList entryList = attribute.value().split( ';' );
670  QStringList::const_iterator entryIt = entryList.constBegin();
671  for ( ; entryIt != entryList.constEnd(); ++entryIt )
672  {
673  QStringList keyValueSplit = entryIt->split( ':' );
674  if ( keyValueSplit.size() < 2 )
675  {
676  continue;
677  }
678  const QString key = keyValueSplit.at( 0 );
679  QString value = keyValueSplit.at( 1 );
680  QString newValue = value;
681  value = value.trimmed().toLower();
682 
683  if ( value.startsWith( QLatin1String( "param(fill)" ) ) )
684  {
685  newValue = fill.name();
686  }
687  else if ( value.startsWith( QLatin1String( "param(fill-opacity)" ) ) )
688  {
689  newValue = QString::number( fill.alphaF() );
690  }
691  else if ( value.startsWith( QLatin1String( "param(outline)" ) ) )
692  {
693  newValue = stroke.name();
694  }
695  else if ( value.startsWith( QLatin1String( "param(outline-opacity)" ) ) )
696  {
697  newValue = QString::number( stroke.alphaF() );
698  }
699  else if ( value.startsWith( QLatin1String( "param(outline-width)" ) ) )
700  {
701  newValue = QString::number( strokeWidth );
702  }
703 
704  if ( entryIt != entryList.constBegin() )
705  {
706  newAttributeString.append( ';' );
707  }
708  newAttributeString.append( key + ':' + newValue );
709  }
710  elem.setAttribute( attribute.name(), newAttributeString );
711  }
712  else
713  {
714  const QString value = attribute.value().trimmed().toLower();
715  if ( value.startsWith( QLatin1String( "param(fill)" ) ) )
716  {
717  elem.setAttribute( attribute.name(), fill.name() );
718  }
719  else if ( value.startsWith( QLatin1String( "param(fill-opacity)" ) ) )
720  {
721  elem.setAttribute( attribute.name(), fill.alphaF() );
722  }
723  else if ( value.startsWith( QLatin1String( "param(outline)" ) ) )
724  {
725  elem.setAttribute( attribute.name(), stroke.name() );
726  }
727  else if ( value.startsWith( QLatin1String( "param(outline-opacity)" ) ) )
728  {
729  elem.setAttribute( attribute.name(), stroke.alphaF() );
730  }
731  else if ( value.startsWith( QLatin1String( "param(outline-width)" ) ) )
732  {
733  elem.setAttribute( attribute.name(), QString::number( strokeWidth ) );
734  }
735  }
736  }
737 
738  QDomNodeList childList = elem.childNodes();
739  int nChildren = childList.count();
740  for ( int i = 0; i < nChildren; ++i )
741  {
742  QDomElement childElem = childList.at( i ).toElement();
743  replaceElemParams( childElem, fill, stroke, strokeWidth );
744  }
745 }
746 
747 void QgsSvgCache::containsElemParams( const QDomElement &elem, bool &hasFillParam, bool &hasDefaultFill, QColor &defaultFill,
748  bool &hasFillOpacityParam, bool &hasDefaultFillOpacity, double &defaultFillOpacity,
749  bool &hasStrokeParam, bool &hasDefaultStroke, QColor &defaultStroke,
750  bool &hasStrokeWidthParam, bool &hasDefaultStrokeWidth, double &defaultStrokeWidth,
751  bool &hasStrokeOpacityParam, bool &hasDefaultStrokeOpacity, double &defaultStrokeOpacity ) const
752 {
753  if ( elem.isNull() )
754  {
755  return;
756  }
757 
758  //we already have all the information, no need to go deeper
759  if ( hasFillParam && hasStrokeParam && hasStrokeWidthParam && hasFillOpacityParam && hasStrokeOpacityParam )
760  {
761  return;
762  }
763 
764  //check this elements attribute
765  QDomNamedNodeMap attributes = elem.attributes();
766  int nAttributes = attributes.count();
767 
768  QStringList valueSplit;
769  for ( int i = 0; i < nAttributes; ++i )
770  {
771  QDomAttr attribute = attributes.item( i ).toAttr();
772  if ( attribute.name().compare( QLatin1String( "style" ), Qt::CaseInsensitive ) == 0 )
773  {
774  //entries separated by ';'
775  QStringList entryList = attribute.value().split( ';' );
776  QStringList::const_iterator entryIt = entryList.constBegin();
777  for ( ; entryIt != entryList.constEnd(); ++entryIt )
778  {
779  QStringList keyValueSplit = entryIt->split( ':' );
780  if ( keyValueSplit.size() < 2 )
781  {
782  continue;
783  }
784  QString value = keyValueSplit.at( 1 );
785  valueSplit = value.split( ' ' );
786  if ( !hasFillParam && value.startsWith( QLatin1String( "param(fill)" ) ) )
787  {
788  hasFillParam = true;
789  if ( valueSplit.size() > 1 )
790  {
791  defaultFill = QColor( valueSplit.at( 1 ) );
792  hasDefaultFill = true;
793  }
794  }
795  else if ( !hasFillOpacityParam && value.startsWith( QLatin1String( "param(fill-opacity)" ) ) )
796  {
797  hasFillOpacityParam = true;
798  if ( valueSplit.size() > 1 )
799  {
800  bool ok;
801  double opacity = valueSplit.at( 1 ).toDouble( &ok );
802  if ( ok )
803  {
804  defaultFillOpacity = opacity;
805  hasDefaultFillOpacity = true;
806  }
807  }
808  }
809  else if ( !hasStrokeParam && value.startsWith( QLatin1String( "param(outline)" ) ) )
810  {
811  hasStrokeParam = true;
812  if ( valueSplit.size() > 1 )
813  {
814  defaultStroke = QColor( valueSplit.at( 1 ) );
815  hasDefaultStroke = true;
816  }
817  }
818  else if ( !hasStrokeWidthParam && value.startsWith( QLatin1String( "param(outline-width)" ) ) )
819  {
820  hasStrokeWidthParam = true;
821  if ( valueSplit.size() > 1 )
822  {
823  defaultStrokeWidth = valueSplit.at( 1 ).toDouble();
824  hasDefaultStrokeWidth = true;
825  }
826  }
827  else if ( !hasStrokeOpacityParam && value.startsWith( QLatin1String( "param(outline-opacity)" ) ) )
828  {
829  hasStrokeOpacityParam = true;
830  if ( valueSplit.size() > 1 )
831  {
832  bool ok;
833  double opacity = valueSplit.at( 1 ).toDouble( &ok );
834  if ( ok )
835  {
836  defaultStrokeOpacity = opacity;
837  hasDefaultStrokeOpacity = true;
838  }
839  }
840  }
841  }
842  }
843  else
844  {
845  QString value = attribute.value();
846  valueSplit = value.split( ' ' );
847  if ( !hasFillParam && value.startsWith( QLatin1String( "param(fill)" ) ) )
848  {
849  hasFillParam = true;
850  if ( valueSplit.size() > 1 )
851  {
852  defaultFill = QColor( valueSplit.at( 1 ) );
853  hasDefaultFill = true;
854  }
855  }
856  else if ( !hasFillOpacityParam && value.startsWith( QLatin1String( "param(fill-opacity)" ) ) )
857  {
858  hasFillOpacityParam = true;
859  if ( valueSplit.size() > 1 )
860  {
861  bool ok;
862  double opacity = valueSplit.at( 1 ).toDouble( &ok );
863  if ( ok )
864  {
865  defaultFillOpacity = opacity;
866  hasDefaultFillOpacity = true;
867  }
868  }
869  }
870  else if ( !hasStrokeParam && value.startsWith( QLatin1String( "param(outline)" ) ) )
871  {
872  hasStrokeParam = true;
873  if ( valueSplit.size() > 1 )
874  {
875  defaultStroke = QColor( valueSplit.at( 1 ) );
876  hasDefaultStroke = true;
877  }
878  }
879  else if ( !hasStrokeWidthParam && value.startsWith( QLatin1String( "param(outline-width)" ) ) )
880  {
881  hasStrokeWidthParam = true;
882  if ( valueSplit.size() > 1 )
883  {
884  defaultStrokeWidth = valueSplit.at( 1 ).toDouble();
885  hasDefaultStrokeWidth = true;
886  }
887  }
888  else if ( !hasStrokeOpacityParam && value.startsWith( QLatin1String( "param(outline-opacity)" ) ) )
889  {
890  hasStrokeOpacityParam = true;
891  if ( valueSplit.size() > 1 )
892  {
893  bool ok;
894  double opacity = valueSplit.at( 1 ).toDouble( &ok );
895  if ( ok )
896  {
897  defaultStrokeOpacity = opacity;
898  hasDefaultStrokeOpacity = true;
899  }
900  }
901  }
902  }
903  }
904 
905  //pass it further to child items
906  QDomNodeList childList = elem.childNodes();
907  int nChildren = childList.count();
908  for ( int i = 0; i < nChildren; ++i )
909  {
910  QDomElement childElem = childList.at( i ).toElement();
911  containsElemParams( childElem, hasFillParam, hasDefaultFill, defaultFill,
912  hasFillOpacityParam, hasDefaultFillOpacity, defaultFillOpacity,
913  hasStrokeParam, hasDefaultStroke, defaultStroke,
914  hasStrokeWidthParam, hasDefaultStrokeWidth, defaultStrokeWidth,
915  hasStrokeOpacityParam, hasDefaultStrokeOpacity, defaultStrokeOpacity );
916  }
917 }
918 
919 void QgsSvgCache::removeCacheEntry( const QString &s, QgsSvgCacheEntry *entry )
920 {
921  delete entry;
922  mEntryLookup.remove( s, entry );
923 }
924 
925 void QgsSvgCache::printEntryList()
926 {
927  QgsDebugMsg( QStringLiteral( "****************svg cache entry list*************************" ) );
928  QgsDebugMsg( "Cache size: " + QString::number( mTotalSize ) );
929  QgsSvgCacheEntry *entry = mLeastRecentEntry;
930  while ( entry )
931  {
932  QgsDebugMsg( QStringLiteral( "***Entry:" ) );
933  QgsDebugMsg( "File:" + entry->path );
934  QgsDebugMsg( "Size:" + QString::number( entry->size ) );
935  QgsDebugMsg( "Width scale factor" + QString::number( entry->widthScaleFactor ) );
936  entry = entry->nextEntry;
937  }
938 }
939 
940 QSize QgsSvgCache::sizeForImage( const QgsSvgCacheEntry &entry, QSizeF &viewBoxSize, QSizeF &scaledSize ) const
941 {
942  bool isFixedAR = entry.fixedAspectRatio > 0;
943 
944  QSvgRenderer r( entry.svgContent );
945  double hwRatio = 1.0;
946  viewBoxSize = r.viewBoxF().size();
947  if ( viewBoxSize.width() > 0 )
948  {
949  if ( isFixedAR )
950  {
951  hwRatio = entry.fixedAspectRatio;
952  }
953  else
954  {
955  hwRatio = viewBoxSize.height() / viewBoxSize.width();
956  }
957  }
958 
959  // cast double image sizes to int for QImage
960  scaledSize.setWidth( entry.size );
961  int wImgSize = static_cast< int >( scaledSize.width() );
962  if ( wImgSize < 1 )
963  {
964  wImgSize = 1;
965  }
966  scaledSize.setHeight( scaledSize.width() * hwRatio );
967  int hImgSize = static_cast< int >( scaledSize.height() );
968  if ( hImgSize < 1 )
969  {
970  hImgSize = 1;
971  }
972  return QSize( wImgSize, hImgSize );
973 }
974 
975 QImage QgsSvgCache::imageFromCachedPicture( const QgsSvgCacheEntry &entry ) const
976 {
977  QSizeF viewBoxSize;
978  QSizeF scaledSize;
979  QImage image( sizeForImage( entry, viewBoxSize, scaledSize ), QImage::Format_ARGB32_Premultiplied );
980  image.fill( 0 ); // transparent background
981 
982  QPainter p( &image );
983  p.drawPicture( QPoint( 0, 0 ), *entry.picture );
984  return image;
985 }
986 
987 void QgsSvgCache::trimToMaximumSize()
988 {
989  //only one entry in cache
990  if ( mLeastRecentEntry == mMostRecentEntry )
991  {
992  return;
993  }
994  QgsSvgCacheEntry *entry = mLeastRecentEntry;
995  while ( entry && ( mTotalSize > MAXIMUM_SIZE ) )
996  {
997  QgsSvgCacheEntry *bkEntry = entry;
998  entry = entry->nextEntry;
999 
1000  takeEntryFromList( bkEntry );
1001  mEntryLookup.remove( bkEntry->path, bkEntry );
1002  mTotalSize -= bkEntry->dataSize();
1003  delete bkEntry;
1004  }
1005 }
1006 
1007 void QgsSvgCache::takeEntryFromList( QgsSvgCacheEntry *entry )
1008 {
1009  if ( !entry )
1010  {
1011  return;
1012  }
1013 
1014  if ( entry->previousEntry )
1015  {
1016  entry->previousEntry->nextEntry = entry->nextEntry;
1017  }
1018  else
1019  {
1020  mLeastRecentEntry = entry->nextEntry;
1021  }
1022  if ( entry->nextEntry )
1023  {
1024  entry->nextEntry->previousEntry = entry->previousEntry;
1025  }
1026  else
1027  {
1028  mMostRecentEntry = entry->previousEntry;
1029  }
1030 }
1031 
1032 void QgsSvgCache::downloadProgress( qint64 bytesReceived, qint64 bytesTotal )
1033 {
1034  QString msg = tr( "%1 of %2 bytes of svg image downloaded." ).arg( bytesReceived ).arg( bytesTotal < 0 ? QStringLiteral( "unknown number of" ) : QString::number( bytesTotal ) );
1035  QgsDebugMsg( msg );
1036  emit statusChanged( msg );
1037 }
1038 
1039 void QgsSvgCache::onRemoteSvgFetched( const QString &url, bool success )
1040 {
1041  QMutexLocker locker( &mMutex );
1042  mPendingRemoteUrls.remove( url );
1043 
1044  QgsSvgCacheEntry *nextEntry = mLeastRecentEntry;
1045  while ( QgsSvgCacheEntry *entry = nextEntry )
1046  {
1047  nextEntry = entry->nextEntry;
1048  if ( entry->path == url )
1049  {
1050  takeEntryFromList( entry );
1051  mEntryLookup.remove( entry->path, entry );
1052  mTotalSize -= entry->dataSize();
1053  delete entry;
1054  }
1055  }
1056 
1057  if ( success )
1058  emit remoteSvgFetched( url );
1059 }
QPicture svgAsPicture(const QString &path, double size, const QColor &fill, const QColor &stroke, double strokeWidth, double widthScaleFactor, bool forceVectorOutput=false, double fixedAspectRatio=0)
Gets SVG as QPicture&.
~QgsSvgCache() override
static QString defaultThemePath()
Returns the path to the default theme directory.
void fetched()
Emitted when the network content has been fetched, regardless of whether the fetch was successful or ...
QImage svgAsImage(const QString &path, double size, const QColor &fill, const QColor &stroke, double strokeWidth, double widthScaleFactor, bool &fitsInCache, double fixedAspectRatio=0)
Gets SVG as QImage.
bool operator==(const QgsFeatureIterator &fi1, const QgsFeatureIterator &fi2)
#define QgsDebugMsg(str)
Definition: qgslogger.h:38
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:278
QNetworkReply * reply()
Returns the network reply.
Handles HTTP network content fetching in a background task.
static QgsTaskManager * taskManager()
Returns the application&#39;s task manager, used for managing application wide background task handling...
QByteArray getImageData(const QString &path) const
Gets image data.
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::Warning, bool notifyUser=true)
Adds a message to the log instance (and creates it if necessary).
long addTask(QgsTask *task, int priority=0)
Adds a task to the manager.
QgsSvgCache(QObject *parent=nullptr)
Constructor for QgsSvgCache.
Definition: qgssvgcache.cpp:82
void containsParams(const QString &path, bool &hasFillParam, QColor &defaultFillColor, bool &hasStrokeParam, QColor &defaultStrokeColor, bool &hasStrokeWidthParam, double &defaultStrokeWidth) const
Tests if an svg file contains parameters for fill, stroke color, stroke width.
QByteArray svgContent(const QString &path, double size, const QColor &fill, const QColor &stroke, double strokeWidth, double widthScaleFactor, double fixedAspectRatio=0)
Gets SVG content.
void remoteSvgFetched(const QString &url)
Emitted when the cache has finished retrieving an SVG file from a remote url.
void statusChanged(const QString &statusQString)
Emit a signal to be caught by qgisapp and display a msg on status bar.
QSizeF svgViewboxSize(const QString &path, double size, const QColor &fill, const QColor &stroke, double strokeWidth, double widthScaleFactor, double fixedAspectRatio=0)
Calculates the viewbox size of a (possibly cached) SVG file.