QGIS API Documentation  3.4.3-Madeira (2f64a3c)
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( 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  mEntryLookup.insert( path, entry );
222 
223  //insert to most recent place in entry list
224  if ( !mMostRecentEntry ) //inserting first entry
225  {
226  mLeastRecentEntry = entry;
227  mMostRecentEntry = entry;
228  entry->previousEntry = nullptr;
229  entry->nextEntry = nullptr;
230  }
231  else
232  {
233  entry->previousEntry = mMostRecentEntry;
234  entry->nextEntry = nullptr;
235  mMostRecentEntry->nextEntry = entry;
236  mMostRecentEntry = entry;
237  }
238 
239  trimToMaximumSize();
240  return entry;
241 }
242 
243 void QgsSvgCache::containsParams( const QString &path, bool &hasFillParam, QColor &defaultFillColor, bool &hasStrokeParam, QColor &defaultStrokeColor,
244  bool &hasStrokeWidthParam, double &defaultStrokeWidth ) const
245 {
246  bool hasDefaultFillColor = false;
247  bool hasFillOpacityParam = false;
248  bool hasDefaultFillOpacity = false;
249  double defaultFillOpacity = 1.0;
250  bool hasDefaultStrokeColor = false;
251  bool hasDefaultStrokeWidth = false;
252  bool hasStrokeOpacityParam = false;
253  bool hasDefaultStrokeOpacity = false;
254  double defaultStrokeOpacity = 1.0;
255 
256  containsParams( path, hasFillParam, hasDefaultFillColor, defaultFillColor,
257  hasFillOpacityParam, hasDefaultFillOpacity, defaultFillOpacity,
258  hasStrokeParam, hasDefaultStrokeColor, defaultStrokeColor,
259  hasStrokeWidthParam, hasDefaultStrokeWidth, defaultStrokeWidth,
260  hasStrokeOpacityParam, hasDefaultStrokeOpacity, defaultStrokeOpacity );
261 }
262 
263 void QgsSvgCache::containsParams( const QString &path,
264  bool &hasFillParam, bool &hasDefaultFillParam, QColor &defaultFillColor,
265  bool &hasFillOpacityParam, bool &hasDefaultFillOpacity, double &defaultFillOpacity,
266  bool &hasStrokeParam, bool &hasDefaultStrokeColor, QColor &defaultStrokeColor,
267  bool &hasStrokeWidthParam, bool &hasDefaultStrokeWidth, double &defaultStrokeWidth,
268  bool &hasStrokeOpacityParam, bool &hasDefaultStrokeOpacity, double &defaultStrokeOpacity ) const
269 {
270  hasFillParam = false;
271  hasFillOpacityParam = false;
272  hasStrokeParam = false;
273  hasStrokeWidthParam = false;
274  hasStrokeOpacityParam = false;
275  defaultFillColor = QColor( Qt::white );
276  defaultFillOpacity = 1.0;
277  defaultStrokeColor = QColor( Qt::black );
278  defaultStrokeWidth = 0.2;
279  defaultStrokeOpacity = 1.0;
280 
281  hasDefaultFillParam = false;
282  hasDefaultFillOpacity = false;
283  hasDefaultStrokeColor = false;
284  hasDefaultStrokeWidth = false;
285  hasDefaultStrokeOpacity = false;
286 
287  QDomDocument svgDoc;
288  if ( !svgDoc.setContent( getImageData( path ) ) )
289  {
290  return;
291  }
292 
293  QDomElement docElem = svgDoc.documentElement();
294  containsElemParams( docElem, hasFillParam, hasDefaultFillParam, defaultFillColor,
295  hasFillOpacityParam, hasDefaultFillOpacity, defaultFillOpacity,
296  hasStrokeParam, hasDefaultStrokeColor, defaultStrokeColor,
297  hasStrokeWidthParam, hasDefaultStrokeWidth, defaultStrokeWidth,
298  hasStrokeOpacityParam, hasDefaultStrokeOpacity, defaultStrokeOpacity );
299 }
300 
301 void QgsSvgCache::replaceParamsAndCacheSvg( QgsSvgCacheEntry *entry )
302 {
303  if ( !entry )
304  {
305  return;
306  }
307 
308  QDomDocument svgDoc;
309  if ( !svgDoc.setContent( getImageData( entry->path ) ) )
310  {
311  return;
312  }
313 
314  //replace fill color, stroke color, stroke with in all nodes
315  QDomElement docElem = svgDoc.documentElement();
316 
317  QSizeF viewboxSize;
318  double sizeScaleFactor = calcSizeScaleFactor( entry, docElem, viewboxSize );
319  entry->viewboxSize = viewboxSize;
320  replaceElemParams( docElem, entry->fill, entry->stroke, entry->strokeWidth * sizeScaleFactor );
321 
322  entry->svgContent = svgDoc.toByteArray( 0 );
323 
324  // toByteArray screws up tspans inside text by adding new lines before and after each span... this should help, at the
325  // risk of potentially breaking some svgs where the newline is desired
326  entry->svgContent.replace( "\n<tspan", "<tspan" );
327  entry->svgContent.replace( "</tspan>\n", "</tspan>" );
328 
329  mTotalSize += entry->svgContent.size();
330 }
331 
332 double QgsSvgCache::calcSizeScaleFactor( QgsSvgCacheEntry *entry, const QDomElement &docElem, QSizeF &viewboxSize ) const
333 {
334  QString viewBox;
335 
336  //bad size
337  if ( !entry || qgsDoubleNear( entry->size, 0.0 ) )
338  return 1.0;
339 
340  //find svg viewbox attribute
341  //first check if docElem is svg element
342  if ( docElem.tagName() == QLatin1String( "svg" ) && docElem.hasAttribute( QStringLiteral( "viewBox" ) ) )
343  {
344  viewBox = docElem.attribute( QStringLiteral( "viewBox" ), QString() );
345  }
346  else if ( docElem.tagName() == QLatin1String( "svg" ) && docElem.hasAttribute( QStringLiteral( "viewbox" ) ) )
347  {
348  viewBox = docElem.attribute( QStringLiteral( "viewbox" ), QString() );
349  }
350  else
351  {
352  QDomElement svgElem = docElem.firstChildElement( QStringLiteral( "svg" ) );
353  if ( !svgElem.isNull() )
354  {
355  if ( svgElem.hasAttribute( QStringLiteral( "viewBox" ) ) )
356  viewBox = svgElem.attribute( QStringLiteral( "viewBox" ), QString() );
357  else if ( svgElem.hasAttribute( QStringLiteral( "viewbox" ) ) )
358  viewBox = svgElem.attribute( QStringLiteral( "viewbox" ), QString() );
359  }
360  }
361 
362  //could not find valid viewbox attribute
363  if ( viewBox.isEmpty() )
364  return 1.0;
365 
366  //width should be 3rd element in a 4 part space delimited string
367  QStringList parts = viewBox.split( ' ' );
368  if ( parts.count() != 4 )
369  return 1.0;
370 
371  bool heightOk = false;
372  double height = parts.at( 3 ).toDouble( &heightOk );
373 
374  bool widthOk = false;
375  double width = parts.at( 2 ).toDouble( &widthOk );
376  if ( widthOk )
377  {
378  if ( heightOk )
379  viewboxSize = QSizeF( width, height );
380  return width / entry->size;
381  }
382 
383  return 1.0;
384 }
385 
386 QByteArray QgsSvgCache::getImageData( const QString &path ) const
387 {
388  // is it a path to local file?
389  QFile svgFile( path );
390  if ( svgFile.exists() )
391  {
392  if ( svgFile.open( QIODevice::ReadOnly ) )
393  {
394  return svgFile.readAll();
395  }
396  else
397  {
398  return mMissingSvg;
399  }
400  }
401 
402  // maybe it's an embedded base64 string
403  if ( path.startsWith( QLatin1String( "base64:" ), Qt::CaseInsensitive ) )
404  {
405  QByteArray base64 = path.mid( 7 ).toLocal8Bit(); // strip 'base64:' prefix
406  return QByteArray::fromBase64( base64, QByteArray::OmitTrailingEquals );
407  }
408 
409  // maybe it's a url...
410  if ( !path.contains( QLatin1String( "://" ) ) ) // otherwise short, relative SVG paths might be considered URLs
411  {
412  return mMissingSvg;
413  }
414 
415  QUrl svgUrl( path );
416  if ( !svgUrl.isValid() )
417  {
418  return mMissingSvg;
419  }
420 
421  // check whether it's a url pointing to a local file
422  if ( svgUrl.scheme().compare( QLatin1String( "file" ), Qt::CaseInsensitive ) == 0 )
423  {
424  svgFile.setFileName( svgUrl.toLocalFile() );
425  if ( svgFile.exists() )
426  {
427  if ( svgFile.open( QIODevice::ReadOnly ) )
428  {
429  return svgFile.readAll();
430  }
431  }
432 
433  // not found...
434  return mMissingSvg;
435  }
436 
437  QMutexLocker locker( &mMutex );
438 
439  // already a request in progress for this url
440  if ( mPendingRemoteUrls.contains( path ) )
441  return mFetchingSvg;
442 
443  if ( mRemoteContentCache.contains( path ) )
444  {
445  // already fetched this content - phew. Just return what we already got.
446  return *mRemoteContentCache[ path ];
447  }
448 
449  mPendingRemoteUrls.insert( path );
450  //fire up task to fetch image in background
451  QNetworkRequest request( svgUrl );
452  request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
453  request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
454 
456  connect( task, &QgsNetworkContentFetcherTask::fetched, this, [this, task, path]
457  {
458  QMutexLocker locker( &mMutex );
459 
460  QNetworkReply *reply = task->reply();
461  if ( !reply )
462  {
463  // canceled
464  QMetaObject::invokeMethod( const_cast< QgsSvgCache * >( this ), "onRemoteSvgFetched", Qt::QueuedConnection, Q_ARG( QString, path ), Q_ARG( bool, false ) );
465  return;
466  }
467 
468  if ( reply->error() != QNetworkReply::NoError )
469  {
470  QgsMessageLog::logMessage( tr( "SVG request failed [error: %1 - url: %2]" ).arg( reply->errorString(), path ), tr( "SVG" ) );
471  return;
472  }
473 
474  bool ok = true;
475 
476  QVariant status = reply->attribute( QNetworkRequest::HttpStatusCodeAttribute );
477  if ( !status.isNull() && status.toInt() >= 400 )
478  {
479  QVariant phrase = reply->attribute( QNetworkRequest::HttpReasonPhraseAttribute );
480  QgsMessageLog::logMessage( tr( "SVG request error [status: %1 - reason phrase: %2] for %3" ).arg( status.toInt() ).arg( phrase.toString(), path ), tr( "SVG" ) );
481  mRemoteContentCache.insert( path, new QByteArray( mMissingSvg ) );
482  ok = false;
483  }
484 
485  // we accept both real SVG mime types AND plain text types - because some sites
486  // (notably github) serve up svgs as raw text
487  QString contentType = reply->header( QNetworkRequest::ContentTypeHeader ).toString();
488  if ( !contentType.startsWith( QLatin1String( "image/svg+xml" ), Qt::CaseInsensitive )
489  && !contentType.startsWith( QLatin1String( "text/plain" ), Qt::CaseInsensitive ) )
490  {
491  QgsMessageLog::logMessage( tr( "Unexpected MIME type %1 received for %2" ).arg( contentType, path ), tr( "SVG" ) );
492  mRemoteContentCache.insert( path, new QByteArray( mMissingSvg ) );
493  ok = false;
494  }
495 
496  if ( ok )
497  {
498  // read the image data
499  mRemoteContentCache.insert( path, new QByteArray( reply->readAll() ) );
500  }
501  QMetaObject::invokeMethod( const_cast< QgsSvgCache * >( this ), "onRemoteSvgFetched", Qt::QueuedConnection, Q_ARG( QString, path ), Q_ARG( bool, true ) );
502  } );
503 
505  return mFetchingSvg;
506 }
507 
508 void QgsSvgCache::cacheImage( QgsSvgCacheEntry *entry )
509 {
510  if ( !entry )
511  {
512  return;
513  }
514 
515  entry->image.reset();
516 
517  QSizeF viewBoxSize;
518  QSizeF scaledSize;
519  QSize imageSize = sizeForImage( *entry, viewBoxSize, scaledSize );
520 
521  // cast double image sizes to int for QImage
522  std::unique_ptr< QImage > image = qgis::make_unique< QImage >( imageSize, QImage::Format_ARGB32_Premultiplied );
523  image->fill( 0 ); // transparent background
524 
525  QPainter p( image.get() );
526  QSvgRenderer r( entry->svgContent );
527  if ( qgsDoubleNear( viewBoxSize.width(), viewBoxSize.height() ) )
528  {
529  r.render( &p );
530  }
531  else
532  {
533  QSizeF s( viewBoxSize );
534  s.scale( scaledSize.width(), scaledSize.height(), Qt::KeepAspectRatio );
535  QRectF rect( ( imageSize.width() - s.width() ) / 2, ( imageSize.height() - s.height() ) / 2, s.width(), s.height() );
536  r.render( &p, rect );
537  }
538 
539  mTotalSize += ( image->width() * image->height() * 32 );
540  entry->image = std::move( image );
541 }
542 
543 void QgsSvgCache::cachePicture( QgsSvgCacheEntry *entry, bool forceVectorOutput )
544 {
545  Q_UNUSED( forceVectorOutput );
546  if ( !entry )
547  {
548  return;
549  }
550 
551  entry->picture.reset();
552 
553  bool isFixedAR = entry->fixedAspectRatio > 0;
554 
555  //correct QPictures dpi correction
556  std::unique_ptr< QPicture > picture = qgis::make_unique< QPicture >();
557  QRectF rect;
558  QSvgRenderer r( entry->svgContent );
559  double hwRatio = 1.0;
560  if ( r.viewBoxF().width() > 0 )
561  {
562  if ( isFixedAR )
563  {
564  hwRatio = entry->fixedAspectRatio;
565  }
566  else
567  {
568  hwRatio = r.viewBoxF().height() / r.viewBoxF().width();
569  }
570  }
571 
572  double wSize = entry->size;
573  double hSize = wSize * hwRatio;
574 
575  QSizeF s( r.viewBoxF().size() );
576  s.scale( wSize, hSize, isFixedAR ? Qt::IgnoreAspectRatio : Qt::KeepAspectRatio );
577  rect = QRectF( -s.width() / 2.0, -s.height() / 2.0, s.width(), s.height() );
578 
579  QPainter p( picture.get() );
580  r.render( &p, rect );
581  entry->picture = std::move( picture );
582  mTotalSize += entry->picture->size();
583 }
584 
585 QgsSvgCacheEntry *QgsSvgCache::cacheEntry( const QString &path, double size, const QColor &fill, const QColor &stroke, double strokeWidth,
586  double widthScaleFactor, double fixedAspectRatio )
587 {
588  //search entries in mEntryLookup
589  QgsSvgCacheEntry *currentEntry = nullptr;
590  QList<QgsSvgCacheEntry *> entries = mEntryLookup.values( path );
591  QDateTime modified;
592  QList<QgsSvgCacheEntry *>::iterator entryIt = entries.begin();
593  for ( ; entryIt != entries.end(); ++entryIt )
594  {
595  QgsSvgCacheEntry *cacheEntry = *entryIt;
596  if ( qgsDoubleNear( cacheEntry->size, size ) && cacheEntry->fill == fill && cacheEntry->stroke == stroke &&
597  qgsDoubleNear( cacheEntry->strokeWidth, strokeWidth ) && qgsDoubleNear( cacheEntry->widthScaleFactor, widthScaleFactor ) &&
598  qgsDoubleNear( cacheEntry->fixedAspectRatio, fixedAspectRatio ) )
599  {
600  if ( mFileModifiedCheckTimeout <= 0 || cacheEntry->fileModifiedLastCheckTimer.hasExpired( mFileModifiedCheckTimeout ) )
601  {
602  if ( !modified.isValid() )
603  modified = QFileInfo( path ).lastModified();
604 
605  if ( cacheEntry->fileModified != modified )
606  continue;
607  }
608  currentEntry = cacheEntry;
609  break;
610  }
611  }
612 
613  //if not found: create new entry
614  //cache and replace params in svg content
615  if ( !currentEntry )
616  {
617  currentEntry = insertSvg( path, size, fill, stroke, strokeWidth, widthScaleFactor, fixedAspectRatio );
618  }
619  else
620  {
621  takeEntryFromList( currentEntry );
622  if ( !mMostRecentEntry ) //list is empty
623  {
624  mMostRecentEntry = currentEntry;
625  mLeastRecentEntry = currentEntry;
626  }
627  else
628  {
629  mMostRecentEntry->nextEntry = currentEntry;
630  currentEntry->previousEntry = mMostRecentEntry;
631  currentEntry->nextEntry = nullptr;
632  mMostRecentEntry = currentEntry;
633  }
634  }
635 
636  //debugging
637  //printEntryList();
638 
639  return currentEntry;
640 }
641 
642 void QgsSvgCache::replaceElemParams( QDomElement &elem, const QColor &fill, const QColor &stroke, double strokeWidth )
643 {
644  if ( elem.isNull() )
645  {
646  return;
647  }
648 
649  //go through attributes
650  QDomNamedNodeMap attributes = elem.attributes();
651  int nAttributes = attributes.count();
652  for ( int i = 0; i < nAttributes; ++i )
653  {
654  QDomAttr attribute = attributes.item( i ).toAttr();
655  //e.g. style="fill:param(fill);param(stroke)"
656  if ( attribute.name().compare( QLatin1String( "style" ), Qt::CaseInsensitive ) == 0 )
657  {
658  //entries separated by ';'
659  QString newAttributeString;
660 
661  QStringList entryList = attribute.value().split( ';' );
662  QStringList::const_iterator entryIt = entryList.constBegin();
663  for ( ; entryIt != entryList.constEnd(); ++entryIt )
664  {
665  QStringList keyValueSplit = entryIt->split( ':' );
666  if ( keyValueSplit.size() < 2 )
667  {
668  continue;
669  }
670  QString key = keyValueSplit.at( 0 );
671  QString value = keyValueSplit.at( 1 );
672  if ( value.startsWith( QLatin1String( "param(fill)" ) ) )
673  {
674  value = fill.name();
675  }
676  else if ( value.startsWith( QLatin1String( "param(fill-opacity)" ) ) )
677  {
678  value = fill.alphaF();
679  }
680  else if ( value.startsWith( QLatin1String( "param(outline)" ) ) )
681  {
682  value = stroke.name();
683  }
684  else if ( value.startsWith( QLatin1String( "param(outline-opacity)" ) ) )
685  {
686  value = stroke.alphaF();
687  }
688  else if ( value.startsWith( QLatin1String( "param(outline-width)" ) ) )
689  {
690  value = QString::number( strokeWidth );
691  }
692 
693  if ( entryIt != entryList.constBegin() )
694  {
695  newAttributeString.append( ';' );
696  }
697  newAttributeString.append( key + ':' + value );
698  }
699  elem.setAttribute( attribute.name(), newAttributeString );
700  }
701  else
702  {
703  QString value = attribute.value();
704  if ( value.startsWith( QLatin1String( "param(fill)" ) ) )
705  {
706  elem.setAttribute( attribute.name(), fill.name() );
707  }
708  else if ( value.startsWith( QLatin1String( "param(fill-opacity)" ) ) )
709  {
710  elem.setAttribute( attribute.name(), fill.alphaF() );
711  }
712  else if ( value.startsWith( QLatin1String( "param(outline)" ) ) )
713  {
714  elem.setAttribute( attribute.name(), stroke.name() );
715  }
716  else if ( value.startsWith( QLatin1String( "param(outline-opacity)" ) ) )
717  {
718  elem.setAttribute( attribute.name(), stroke.alphaF() );
719  }
720  else if ( value.startsWith( QLatin1String( "param(outline-width)" ) ) )
721  {
722  elem.setAttribute( attribute.name(), QString::number( strokeWidth ) );
723  }
724  }
725  }
726 
727  QDomNodeList childList = elem.childNodes();
728  int nChildren = childList.count();
729  for ( int i = 0; i < nChildren; ++i )
730  {
731  QDomElement childElem = childList.at( i ).toElement();
732  replaceElemParams( childElem, fill, stroke, strokeWidth );
733  }
734 }
735 
736 void QgsSvgCache::containsElemParams( const QDomElement &elem, bool &hasFillParam, bool &hasDefaultFill, QColor &defaultFill,
737  bool &hasFillOpacityParam, bool &hasDefaultFillOpacity, double &defaultFillOpacity,
738  bool &hasStrokeParam, bool &hasDefaultStroke, QColor &defaultStroke,
739  bool &hasStrokeWidthParam, bool &hasDefaultStrokeWidth, double &defaultStrokeWidth,
740  bool &hasStrokeOpacityParam, bool &hasDefaultStrokeOpacity, double &defaultStrokeOpacity ) const
741 {
742  if ( elem.isNull() )
743  {
744  return;
745  }
746 
747  //we already have all the information, no need to go deeper
748  if ( hasFillParam && hasStrokeParam && hasStrokeWidthParam && hasFillOpacityParam && hasStrokeOpacityParam )
749  {
750  return;
751  }
752 
753  //check this elements attribute
754  QDomNamedNodeMap attributes = elem.attributes();
755  int nAttributes = attributes.count();
756 
757  QStringList valueSplit;
758  for ( int i = 0; i < nAttributes; ++i )
759  {
760  QDomAttr attribute = attributes.item( i ).toAttr();
761  if ( attribute.name().compare( QLatin1String( "style" ), Qt::CaseInsensitive ) == 0 )
762  {
763  //entries separated by ';'
764  QStringList entryList = attribute.value().split( ';' );
765  QStringList::const_iterator entryIt = entryList.constBegin();
766  for ( ; entryIt != entryList.constEnd(); ++entryIt )
767  {
768  QStringList keyValueSplit = entryIt->split( ':' );
769  if ( keyValueSplit.size() < 2 )
770  {
771  continue;
772  }
773  QString value = keyValueSplit.at( 1 );
774  valueSplit = value.split( ' ' );
775  if ( !hasFillParam && value.startsWith( QLatin1String( "param(fill)" ) ) )
776  {
777  hasFillParam = true;
778  if ( valueSplit.size() > 1 )
779  {
780  defaultFill = QColor( valueSplit.at( 1 ) );
781  hasDefaultFill = true;
782  }
783  }
784  else if ( !hasFillOpacityParam && value.startsWith( QLatin1String( "param(fill-opacity)" ) ) )
785  {
786  hasFillOpacityParam = true;
787  if ( valueSplit.size() > 1 )
788  {
789  bool ok;
790  double opacity = valueSplit.at( 1 ).toDouble( &ok );
791  if ( ok )
792  {
793  defaultFillOpacity = opacity;
794  hasDefaultFillOpacity = true;
795  }
796  }
797  }
798  else if ( !hasStrokeParam && value.startsWith( QLatin1String( "param(outline)" ) ) )
799  {
800  hasStrokeParam = true;
801  if ( valueSplit.size() > 1 )
802  {
803  defaultStroke = QColor( valueSplit.at( 1 ) );
804  hasDefaultStroke = true;
805  }
806  }
807  else if ( !hasStrokeWidthParam && value.startsWith( QLatin1String( "param(outline-width)" ) ) )
808  {
809  hasStrokeWidthParam = true;
810  if ( valueSplit.size() > 1 )
811  {
812  defaultStrokeWidth = valueSplit.at( 1 ).toDouble();
813  hasDefaultStrokeWidth = true;
814  }
815  }
816  else if ( !hasStrokeOpacityParam && value.startsWith( QLatin1String( "param(outline-opacity)" ) ) )
817  {
818  hasStrokeOpacityParam = true;
819  if ( valueSplit.size() > 1 )
820  {
821  bool ok;
822  double opacity = valueSplit.at( 1 ).toDouble( &ok );
823  if ( ok )
824  {
825  defaultStrokeOpacity = opacity;
826  hasDefaultStrokeOpacity = true;
827  }
828  }
829  }
830  }
831  }
832  else
833  {
834  QString value = attribute.value();
835  valueSplit = value.split( ' ' );
836  if ( !hasFillParam && value.startsWith( QLatin1String( "param(fill)" ) ) )
837  {
838  hasFillParam = true;
839  if ( valueSplit.size() > 1 )
840  {
841  defaultFill = QColor( valueSplit.at( 1 ) );
842  hasDefaultFill = true;
843  }
844  }
845  else if ( !hasFillOpacityParam && value.startsWith( QLatin1String( "param(fill-opacity)" ) ) )
846  {
847  hasFillOpacityParam = true;
848  if ( valueSplit.size() > 1 )
849  {
850  bool ok;
851  double opacity = valueSplit.at( 1 ).toDouble( &ok );
852  if ( ok )
853  {
854  defaultFillOpacity = opacity;
855  hasDefaultFillOpacity = true;
856  }
857  }
858  }
859  else if ( !hasStrokeParam && value.startsWith( QLatin1String( "param(outline)" ) ) )
860  {
861  hasStrokeParam = true;
862  if ( valueSplit.size() > 1 )
863  {
864  defaultStroke = QColor( valueSplit.at( 1 ) );
865  hasDefaultStroke = true;
866  }
867  }
868  else if ( !hasStrokeWidthParam && value.startsWith( QLatin1String( "param(outline-width)" ) ) )
869  {
870  hasStrokeWidthParam = true;
871  if ( valueSplit.size() > 1 )
872  {
873  defaultStrokeWidth = valueSplit.at( 1 ).toDouble();
874  hasDefaultStrokeWidth = true;
875  }
876  }
877  else if ( !hasStrokeOpacityParam && value.startsWith( QLatin1String( "param(outline-opacity)" ) ) )
878  {
879  hasStrokeOpacityParam = true;
880  if ( valueSplit.size() > 1 )
881  {
882  bool ok;
883  double opacity = valueSplit.at( 1 ).toDouble( &ok );
884  if ( ok )
885  {
886  defaultStrokeOpacity = opacity;
887  hasDefaultStrokeOpacity = true;
888  }
889  }
890  }
891  }
892  }
893 
894  //pass it further to child items
895  QDomNodeList childList = elem.childNodes();
896  int nChildren = childList.count();
897  for ( int i = 0; i < nChildren; ++i )
898  {
899  QDomElement childElem = childList.at( i ).toElement();
900  containsElemParams( childElem, hasFillParam, hasDefaultFill, defaultFill,
901  hasFillOpacityParam, hasDefaultFillOpacity, defaultFillOpacity,
902  hasStrokeParam, hasDefaultStroke, defaultStroke,
903  hasStrokeWidthParam, hasDefaultStrokeWidth, defaultStrokeWidth,
904  hasStrokeOpacityParam, hasDefaultStrokeOpacity, defaultStrokeOpacity );
905  }
906 }
907 
908 void QgsSvgCache::removeCacheEntry( const QString &s, QgsSvgCacheEntry *entry )
909 {
910  delete entry;
911  mEntryLookup.remove( s, entry );
912 }
913 
914 void QgsSvgCache::printEntryList()
915 {
916  QgsDebugMsg( QStringLiteral( "****************svg cache entry list*************************" ) );
917  QgsDebugMsg( "Cache size: " + QString::number( mTotalSize ) );
918  QgsSvgCacheEntry *entry = mLeastRecentEntry;
919  while ( entry )
920  {
921  QgsDebugMsg( QStringLiteral( "***Entry:" ) );
922  QgsDebugMsg( "File:" + entry->path );
923  QgsDebugMsg( "Size:" + QString::number( entry->size ) );
924  QgsDebugMsg( "Width scale factor" + QString::number( entry->widthScaleFactor ) );
925  entry = entry->nextEntry;
926  }
927 }
928 
929 QSize QgsSvgCache::sizeForImage( const QgsSvgCacheEntry &entry, QSizeF &viewBoxSize, QSizeF &scaledSize ) const
930 {
931  bool isFixedAR = entry.fixedAspectRatio > 0;
932 
933  QSvgRenderer r( entry.svgContent );
934  double hwRatio = 1.0;
935  viewBoxSize = r.viewBoxF().size();
936  if ( viewBoxSize.width() > 0 )
937  {
938  if ( isFixedAR )
939  {
940  hwRatio = entry.fixedAspectRatio;
941  }
942  else
943  {
944  hwRatio = viewBoxSize.height() / viewBoxSize.width();
945  }
946  }
947 
948  // cast double image sizes to int for QImage
949  scaledSize.setWidth( entry.size );
950  int wImgSize = static_cast< int >( scaledSize.width() );
951  if ( wImgSize < 1 )
952  {
953  wImgSize = 1;
954  }
955  scaledSize.setHeight( scaledSize.width() * hwRatio );
956  int hImgSize = static_cast< int >( scaledSize.height() );
957  if ( hImgSize < 1 )
958  {
959  hImgSize = 1;
960  }
961  return QSize( wImgSize, hImgSize );
962 }
963 
964 QImage QgsSvgCache::imageFromCachedPicture( const QgsSvgCacheEntry &entry ) const
965 {
966  QSizeF viewBoxSize;
967  QSizeF scaledSize;
968  QImage image( sizeForImage( entry, viewBoxSize, scaledSize ), QImage::Format_ARGB32_Premultiplied );
969  image.fill( 0 ); // transparent background
970 
971  QPainter p( &image );
972  p.drawPicture( QPoint( 0, 0 ), *entry.picture );
973  return image;
974 }
975 
976 void QgsSvgCache::trimToMaximumSize()
977 {
978  //only one entry in cache
979  if ( mLeastRecentEntry == mMostRecentEntry )
980  {
981  return;
982  }
983  QgsSvgCacheEntry *entry = mLeastRecentEntry;
984  while ( entry && ( mTotalSize > MAXIMUM_SIZE ) )
985  {
986  QgsSvgCacheEntry *bkEntry = entry;
987  entry = entry->nextEntry;
988 
989  takeEntryFromList( bkEntry );
990  mEntryLookup.remove( bkEntry->path, bkEntry );
991  mTotalSize -= bkEntry->dataSize();
992  delete bkEntry;
993  }
994 }
995 
996 void QgsSvgCache::takeEntryFromList( QgsSvgCacheEntry *entry )
997 {
998  if ( !entry )
999  {
1000  return;
1001  }
1002 
1003  if ( entry->previousEntry )
1004  {
1005  entry->previousEntry->nextEntry = entry->nextEntry;
1006  }
1007  else
1008  {
1009  mLeastRecentEntry = entry->nextEntry;
1010  }
1011  if ( entry->nextEntry )
1012  {
1013  entry->nextEntry->previousEntry = entry->previousEntry;
1014  }
1015  else
1016  {
1017  mMostRecentEntry = entry->previousEntry;
1018  }
1019 }
1020 
1021 void QgsSvgCache::downloadProgress( qint64 bytesReceived, qint64 bytesTotal )
1022 {
1023  QString msg = tr( "%1 of %2 bytes of svg image downloaded." ).arg( bytesReceived ).arg( bytesTotal < 0 ? QStringLiteral( "unknown number of" ) : QString::number( bytesTotal ) );
1024  QgsDebugMsg( msg );
1025  emit statusChanged( msg );
1026 }
1027 
1028 void QgsSvgCache::onRemoteSvgFetched( const QString &url, bool success )
1029 {
1030  QMutexLocker locker( &mMutex );
1031  mPendingRemoteUrls.remove( url );
1032 
1033  QgsSvgCacheEntry *nextEntry = mLeastRecentEntry;
1034  while ( QgsSvgCacheEntry *entry = nextEntry )
1035  {
1036  nextEntry = entry->nextEntry;
1037  if ( entry->path == url )
1038  {
1039  takeEntryFromList( entry );
1040  mEntryLookup.remove( entry->path, entry );
1041  mTotalSize -= entry->dataSize();
1042  delete entry;
1043  }
1044  }
1045 
1046  if ( success )
1047  emit remoteSvgFetched( url );
1048 }
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
QByteArray getImageData(const QString &path) const
Gets image data.
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...
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.