QGIS API Documentation  3.2.0-Bonn (bc43194)
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 a url...
403  if ( !path.contains( QLatin1String( "://" ) ) ) // otherwise short, relative SVG paths might be considered URLs
404  {
405  return mMissingSvg;
406  }
407 
408  QUrl svgUrl( path );
409  if ( !svgUrl.isValid() )
410  {
411  return mMissingSvg;
412  }
413 
414  // check whether it's a url pointing to a local file
415  if ( svgUrl.scheme().compare( QLatin1String( "file" ), Qt::CaseInsensitive ) == 0 )
416  {
417  svgFile.setFileName( svgUrl.toLocalFile() );
418  if ( svgFile.exists() )
419  {
420  if ( svgFile.open( QIODevice::ReadOnly ) )
421  {
422  return svgFile.readAll();
423  }
424  }
425 
426  // not found...
427  return mMissingSvg;
428  }
429 
430  QMutexLocker locker( &mMutex );
431 
432  // already a request in progress for this url
433  if ( mPendingRemoteUrls.contains( path ) )
434  return mFetchingSvg;
435 
436  if ( mRemoteContentCache.contains( path ) )
437  {
438  // already fetched this content - phew. Just return what we already got.
439  return *mRemoteContentCache[ path ];
440  }
441 
442  mPendingRemoteUrls.insert( path );
443  //fire up task to fetch image in background
444  QNetworkRequest request( svgUrl );
445  request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
446  request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
447 
449  connect( task, &QgsNetworkContentFetcherTask::fetched, this, [this, task, path]
450  {
451  QMutexLocker locker( &mMutex );
452 
453  QNetworkReply *reply = task->reply();
454  if ( !reply )
455  {
456  // canceled
457  QMetaObject::invokeMethod( const_cast< QgsSvgCache * >( this ), "onRemoteSvgFetched", Qt::QueuedConnection, Q_ARG( QString, path ), Q_ARG( bool, false ) );
458  return;
459  }
460 
461  if ( reply->error() != QNetworkReply::NoError )
462  {
463  QgsMessageLog::logMessage( tr( "SVG request failed [error: %1 - url: %2]" ).arg( reply->errorString(), path ), tr( "SVG" ) );
464  return;
465  }
466 
467  bool ok = true;
468 
469  QVariant status = reply->attribute( QNetworkRequest::HttpStatusCodeAttribute );
470  if ( !status.isNull() && status.toInt() >= 400 )
471  {
472  QVariant phrase = reply->attribute( QNetworkRequest::HttpReasonPhraseAttribute );
473  QgsMessageLog::logMessage( tr( "SVG request error [status: %1 - reason phrase: %2] for %3" ).arg( status.toInt() ).arg( phrase.toString(), path ), tr( "SVG" ) );
474  mRemoteContentCache.insert( path, new QByteArray( mMissingSvg ) );
475  ok = false;
476  }
477 
478  // we accept both real SVG mime types AND plain text types - because some sites
479  // (notably github) serve up svgs as raw text
480  QString contentType = reply->header( QNetworkRequest::ContentTypeHeader ).toString();
481  if ( !contentType.startsWith( QLatin1String( "image/svg+xml" ), Qt::CaseInsensitive )
482  && !contentType.startsWith( QLatin1String( "text/plain" ), Qt::CaseInsensitive ) )
483  {
484  QgsMessageLog::logMessage( tr( "Unexpected MIME type %1 received for %2" ).arg( contentType, path ), tr( "SVG" ) );
485  mRemoteContentCache.insert( path, new QByteArray( mMissingSvg ) );
486  ok = false;
487  }
488 
489  if ( ok )
490  {
491  // read the image data
492  mRemoteContentCache.insert( path, new QByteArray( reply->readAll() ) );
493  }
494  QMetaObject::invokeMethod( const_cast< QgsSvgCache * >( this ), "onRemoteSvgFetched", Qt::QueuedConnection, Q_ARG( QString, path ), Q_ARG( bool, true ) );
495  } );
496 
498  return mFetchingSvg;
499 }
500 
501 void QgsSvgCache::cacheImage( QgsSvgCacheEntry *entry )
502 {
503  if ( !entry )
504  {
505  return;
506  }
507 
508  entry->image.reset();
509 
510  QSizeF viewBoxSize;
511  QSizeF scaledSize;
512  QSize imageSize = sizeForImage( *entry, viewBoxSize, scaledSize );
513 
514  // cast double image sizes to int for QImage
515  std::unique_ptr< QImage > image = qgis::make_unique< QImage >( imageSize, QImage::Format_ARGB32_Premultiplied );
516  image->fill( 0 ); // transparent background
517 
518  QPainter p( image.get() );
519  QSvgRenderer r( entry->svgContent );
520  if ( qgsDoubleNear( viewBoxSize.width(), viewBoxSize.height() ) )
521  {
522  r.render( &p );
523  }
524  else
525  {
526  QSizeF s( viewBoxSize );
527  s.scale( scaledSize.width(), scaledSize.height(), Qt::KeepAspectRatio );
528  QRectF rect( ( imageSize.width() - s.width() ) / 2, ( imageSize.height() - s.height() ) / 2, s.width(), s.height() );
529  r.render( &p, rect );
530  }
531 
532  mTotalSize += ( image->width() * image->height() * 32 );
533  entry->image = std::move( image );
534 }
535 
536 void QgsSvgCache::cachePicture( QgsSvgCacheEntry *entry, bool forceVectorOutput )
537 {
538  Q_UNUSED( forceVectorOutput );
539  if ( !entry )
540  {
541  return;
542  }
543 
544  entry->picture.reset();
545 
546  bool isFixedAR = entry->fixedAspectRatio > 0;
547 
548  //correct QPictures dpi correction
549  std::unique_ptr< QPicture > picture = qgis::make_unique< QPicture >();
550  QRectF rect;
551  QSvgRenderer r( entry->svgContent );
552  double hwRatio = 1.0;
553  if ( r.viewBoxF().width() > 0 )
554  {
555  if ( isFixedAR )
556  {
557  hwRatio = entry->fixedAspectRatio;
558  }
559  else
560  {
561  hwRatio = r.viewBoxF().height() / r.viewBoxF().width();
562  }
563  }
564 
565  double wSize = entry->size;
566  double hSize = wSize * hwRatio;
567 
568  QSizeF s( r.viewBoxF().size() );
569  s.scale( wSize, hSize, isFixedAR ? Qt::IgnoreAspectRatio : Qt::KeepAspectRatio );
570  rect = QRectF( -s.width() / 2.0, -s.height() / 2.0, s.width(), s.height() );
571 
572  QPainter p( picture.get() );
573  r.render( &p, rect );
574  entry->picture = std::move( picture );
575  mTotalSize += entry->picture->size();
576 }
577 
578 QgsSvgCacheEntry *QgsSvgCache::cacheEntry( const QString &path, double size, const QColor &fill, const QColor &stroke, double strokeWidth,
579  double widthScaleFactor, double fixedAspectRatio )
580 {
581  //search entries in mEntryLookup
582  QgsSvgCacheEntry *currentEntry = nullptr;
583  QList<QgsSvgCacheEntry *> entries = mEntryLookup.values( path );
584  QDateTime modified;
585  QList<QgsSvgCacheEntry *>::iterator entryIt = entries.begin();
586  for ( ; entryIt != entries.end(); ++entryIt )
587  {
588  QgsSvgCacheEntry *cacheEntry = *entryIt;
589  if ( qgsDoubleNear( cacheEntry->size, size ) && cacheEntry->fill == fill && cacheEntry->stroke == stroke &&
590  qgsDoubleNear( cacheEntry->strokeWidth, strokeWidth ) && qgsDoubleNear( cacheEntry->widthScaleFactor, widthScaleFactor ) &&
591  qgsDoubleNear( cacheEntry->fixedAspectRatio, fixedAspectRatio ) )
592  {
593  if ( mFileModifiedCheckTimeout <= 0 || cacheEntry->fileModifiedLastCheckTimer.hasExpired( mFileModifiedCheckTimeout ) )
594  {
595  if ( !modified.isValid() )
596  modified = QFileInfo( path ).lastModified();
597 
598  if ( cacheEntry->fileModified != modified )
599  continue;
600  }
601  currentEntry = cacheEntry;
602  break;
603  }
604  }
605 
606  //if not found: create new entry
607  //cache and replace params in svg content
608  if ( !currentEntry )
609  {
610  currentEntry = insertSvg( path, size, fill, stroke, strokeWidth, widthScaleFactor, fixedAspectRatio );
611  }
612  else
613  {
614  takeEntryFromList( currentEntry );
615  if ( !mMostRecentEntry ) //list is empty
616  {
617  mMostRecentEntry = currentEntry;
618  mLeastRecentEntry = currentEntry;
619  }
620  else
621  {
622  mMostRecentEntry->nextEntry = currentEntry;
623  currentEntry->previousEntry = mMostRecentEntry;
624  currentEntry->nextEntry = nullptr;
625  mMostRecentEntry = currentEntry;
626  }
627  }
628 
629  //debugging
630  //printEntryList();
631 
632  return currentEntry;
633 }
634 
635 void QgsSvgCache::replaceElemParams( QDomElement &elem, const QColor &fill, const QColor &stroke, double strokeWidth )
636 {
637  if ( elem.isNull() )
638  {
639  return;
640  }
641 
642  //go through attributes
643  QDomNamedNodeMap attributes = elem.attributes();
644  int nAttributes = attributes.count();
645  for ( int i = 0; i < nAttributes; ++i )
646  {
647  QDomAttr attribute = attributes.item( i ).toAttr();
648  //e.g. style="fill:param(fill);param(stroke)"
649  if ( attribute.name().compare( QLatin1String( "style" ), Qt::CaseInsensitive ) == 0 )
650  {
651  //entries separated by ';'
652  QString newAttributeString;
653 
654  QStringList entryList = attribute.value().split( ';' );
655  QStringList::const_iterator entryIt = entryList.constBegin();
656  for ( ; entryIt != entryList.constEnd(); ++entryIt )
657  {
658  QStringList keyValueSplit = entryIt->split( ':' );
659  if ( keyValueSplit.size() < 2 )
660  {
661  continue;
662  }
663  QString key = keyValueSplit.at( 0 );
664  QString value = keyValueSplit.at( 1 );
665  if ( value.startsWith( QLatin1String( "param(fill)" ) ) )
666  {
667  value = fill.name();
668  }
669  else if ( value.startsWith( QLatin1String( "param(fill-opacity)" ) ) )
670  {
671  value = fill.alphaF();
672  }
673  else if ( value.startsWith( QLatin1String( "param(outline)" ) ) )
674  {
675  value = stroke.name();
676  }
677  else if ( value.startsWith( QLatin1String( "param(outline-opacity)" ) ) )
678  {
679  value = stroke.alphaF();
680  }
681  else if ( value.startsWith( QLatin1String( "param(outline-width)" ) ) )
682  {
683  value = QString::number( strokeWidth );
684  }
685 
686  if ( entryIt != entryList.constBegin() )
687  {
688  newAttributeString.append( ';' );
689  }
690  newAttributeString.append( key + ':' + value );
691  }
692  elem.setAttribute( attribute.name(), newAttributeString );
693  }
694  else
695  {
696  QString value = attribute.value();
697  if ( value.startsWith( QLatin1String( "param(fill)" ) ) )
698  {
699  elem.setAttribute( attribute.name(), fill.name() );
700  }
701  else if ( value.startsWith( QLatin1String( "param(fill-opacity)" ) ) )
702  {
703  elem.setAttribute( attribute.name(), fill.alphaF() );
704  }
705  else if ( value.startsWith( QLatin1String( "param(outline)" ) ) )
706  {
707  elem.setAttribute( attribute.name(), stroke.name() );
708  }
709  else if ( value.startsWith( QLatin1String( "param(outline-opacity)" ) ) )
710  {
711  elem.setAttribute( attribute.name(), stroke.alphaF() );
712  }
713  else if ( value.startsWith( QLatin1String( "param(outline-width)" ) ) )
714  {
715  elem.setAttribute( attribute.name(), QString::number( strokeWidth ) );
716  }
717  }
718  }
719 
720  QDomNodeList childList = elem.childNodes();
721  int nChildren = childList.count();
722  for ( int i = 0; i < nChildren; ++i )
723  {
724  QDomElement childElem = childList.at( i ).toElement();
725  replaceElemParams( childElem, fill, stroke, strokeWidth );
726  }
727 }
728 
729 void QgsSvgCache::containsElemParams( const QDomElement &elem, bool &hasFillParam, bool &hasDefaultFill, QColor &defaultFill,
730  bool &hasFillOpacityParam, bool &hasDefaultFillOpacity, double &defaultFillOpacity,
731  bool &hasStrokeParam, bool &hasDefaultStroke, QColor &defaultStroke,
732  bool &hasStrokeWidthParam, bool &hasDefaultStrokeWidth, double &defaultStrokeWidth,
733  bool &hasStrokeOpacityParam, bool &hasDefaultStrokeOpacity, double &defaultStrokeOpacity ) const
734 {
735  if ( elem.isNull() )
736  {
737  return;
738  }
739 
740  //we already have all the information, no need to go deeper
741  if ( hasFillParam && hasStrokeParam && hasStrokeWidthParam && hasFillOpacityParam && hasStrokeOpacityParam )
742  {
743  return;
744  }
745 
746  //check this elements attribute
747  QDomNamedNodeMap attributes = elem.attributes();
748  int nAttributes = attributes.count();
749 
750  QStringList valueSplit;
751  for ( int i = 0; i < nAttributes; ++i )
752  {
753  QDomAttr attribute = attributes.item( i ).toAttr();
754  if ( attribute.name().compare( QLatin1String( "style" ), Qt::CaseInsensitive ) == 0 )
755  {
756  //entries separated by ';'
757  QStringList entryList = attribute.value().split( ';' );
758  QStringList::const_iterator entryIt = entryList.constBegin();
759  for ( ; entryIt != entryList.constEnd(); ++entryIt )
760  {
761  QStringList keyValueSplit = entryIt->split( ':' );
762  if ( keyValueSplit.size() < 2 )
763  {
764  continue;
765  }
766  QString value = keyValueSplit.at( 1 );
767  valueSplit = value.split( ' ' );
768  if ( !hasFillParam && value.startsWith( QLatin1String( "param(fill)" ) ) )
769  {
770  hasFillParam = true;
771  if ( valueSplit.size() > 1 )
772  {
773  defaultFill = QColor( valueSplit.at( 1 ) );
774  hasDefaultFill = true;
775  }
776  }
777  else if ( !hasFillOpacityParam && value.startsWith( QLatin1String( "param(fill-opacity)" ) ) )
778  {
779  hasFillOpacityParam = true;
780  if ( valueSplit.size() > 1 )
781  {
782  bool ok;
783  double opacity = valueSplit.at( 1 ).toDouble( &ok );
784  if ( ok )
785  {
786  defaultFillOpacity = opacity;
787  hasDefaultFillOpacity = true;
788  }
789  }
790  }
791  else if ( !hasStrokeParam && value.startsWith( QLatin1String( "param(outline)" ) ) )
792  {
793  hasStrokeParam = true;
794  if ( valueSplit.size() > 1 )
795  {
796  defaultStroke = QColor( valueSplit.at( 1 ) );
797  hasDefaultStroke = true;
798  }
799  }
800  else if ( !hasStrokeWidthParam && value.startsWith( QLatin1String( "param(outline-width)" ) ) )
801  {
802  hasStrokeWidthParam = true;
803  if ( valueSplit.size() > 1 )
804  {
805  defaultStrokeWidth = valueSplit.at( 1 ).toDouble();
806  hasDefaultStrokeWidth = true;
807  }
808  }
809  else if ( !hasStrokeOpacityParam && value.startsWith( QLatin1String( "param(outline-opacity)" ) ) )
810  {
811  hasStrokeOpacityParam = true;
812  if ( valueSplit.size() > 1 )
813  {
814  bool ok;
815  double opacity = valueSplit.at( 1 ).toDouble( &ok );
816  if ( ok )
817  {
818  defaultStrokeOpacity = opacity;
819  hasDefaultStrokeOpacity = true;
820  }
821  }
822  }
823  }
824  }
825  else
826  {
827  QString value = attribute.value();
828  valueSplit = value.split( ' ' );
829  if ( !hasFillParam && value.startsWith( QLatin1String( "param(fill)" ) ) )
830  {
831  hasFillParam = true;
832  if ( valueSplit.size() > 1 )
833  {
834  defaultFill = QColor( valueSplit.at( 1 ) );
835  hasDefaultFill = true;
836  }
837  }
838  else if ( !hasFillOpacityParam && value.startsWith( QLatin1String( "param(fill-opacity)" ) ) )
839  {
840  hasFillOpacityParam = true;
841  if ( valueSplit.size() > 1 )
842  {
843  bool ok;
844  double opacity = valueSplit.at( 1 ).toDouble( &ok );
845  if ( ok )
846  {
847  defaultFillOpacity = opacity;
848  hasDefaultFillOpacity = true;
849  }
850  }
851  }
852  else if ( !hasStrokeParam && value.startsWith( QLatin1String( "param(outline)" ) ) )
853  {
854  hasStrokeParam = true;
855  if ( valueSplit.size() > 1 )
856  {
857  defaultStroke = QColor( valueSplit.at( 1 ) );
858  hasDefaultStroke = true;
859  }
860  }
861  else if ( !hasStrokeWidthParam && value.startsWith( QLatin1String( "param(outline-width)" ) ) )
862  {
863  hasStrokeWidthParam = true;
864  if ( valueSplit.size() > 1 )
865  {
866  defaultStrokeWidth = valueSplit.at( 1 ).toDouble();
867  hasDefaultStrokeWidth = true;
868  }
869  }
870  else if ( !hasStrokeOpacityParam && value.startsWith( QLatin1String( "param(outline-opacity)" ) ) )
871  {
872  hasStrokeOpacityParam = true;
873  if ( valueSplit.size() > 1 )
874  {
875  bool ok;
876  double opacity = valueSplit.at( 1 ).toDouble( &ok );
877  if ( ok )
878  {
879  defaultStrokeOpacity = opacity;
880  hasDefaultStrokeOpacity = true;
881  }
882  }
883  }
884  }
885  }
886 
887  //pass it further to child items
888  QDomNodeList childList = elem.childNodes();
889  int nChildren = childList.count();
890  for ( int i = 0; i < nChildren; ++i )
891  {
892  QDomElement childElem = childList.at( i ).toElement();
893  containsElemParams( childElem, hasFillParam, hasDefaultFill, defaultFill,
894  hasFillOpacityParam, hasDefaultFillOpacity, defaultFillOpacity,
895  hasStrokeParam, hasDefaultStroke, defaultStroke,
896  hasStrokeWidthParam, hasDefaultStrokeWidth, defaultStrokeWidth,
897  hasStrokeOpacityParam, hasDefaultStrokeOpacity, defaultStrokeOpacity );
898  }
899 }
900 
901 void QgsSvgCache::removeCacheEntry( const QString &s, QgsSvgCacheEntry *entry )
902 {
903  delete entry;
904  mEntryLookup.remove( s, entry );
905 }
906 
907 void QgsSvgCache::printEntryList()
908 {
909  QgsDebugMsg( "****************svg cache entry list*************************" );
910  QgsDebugMsg( "Cache size: " + QString::number( mTotalSize ) );
911  QgsSvgCacheEntry *entry = mLeastRecentEntry;
912  while ( entry )
913  {
914  QgsDebugMsg( "***Entry:" );
915  QgsDebugMsg( "File:" + entry->path );
916  QgsDebugMsg( "Size:" + QString::number( entry->size ) );
917  QgsDebugMsg( "Width scale factor" + QString::number( entry->widthScaleFactor ) );
918  entry = entry->nextEntry;
919  }
920 }
921 
922 QSize QgsSvgCache::sizeForImage( const QgsSvgCacheEntry &entry, QSizeF &viewBoxSize, QSizeF &scaledSize ) const
923 {
924  bool isFixedAR = entry.fixedAspectRatio > 0;
925 
926  QSvgRenderer r( entry.svgContent );
927  double hwRatio = 1.0;
928  viewBoxSize = r.viewBoxF().size();
929  if ( viewBoxSize.width() > 0 )
930  {
931  if ( isFixedAR )
932  {
933  hwRatio = entry.fixedAspectRatio;
934  }
935  else
936  {
937  hwRatio = viewBoxSize.height() / viewBoxSize.width();
938  }
939  }
940 
941  // cast double image sizes to int for QImage
942  scaledSize.setWidth( entry.size );
943  int wImgSize = static_cast< int >( scaledSize.width() );
944  if ( wImgSize < 1 )
945  {
946  wImgSize = 1;
947  }
948  scaledSize.setHeight( scaledSize.width() * hwRatio );
949  int hImgSize = static_cast< int >( scaledSize.height() );
950  if ( hImgSize < 1 )
951  {
952  hImgSize = 1;
953  }
954  return QSize( wImgSize, hImgSize );
955 }
956 
957 QImage QgsSvgCache::imageFromCachedPicture( const QgsSvgCacheEntry &entry ) const
958 {
959  QSizeF viewBoxSize;
960  QSizeF scaledSize;
961  QImage image( sizeForImage( entry, viewBoxSize, scaledSize ), QImage::Format_ARGB32_Premultiplied );
962  image.fill( 0 ); // transparent background
963 
964  QPainter p( &image );
965  p.drawPicture( QPoint( 0, 0 ), *entry.picture );
966  return image;
967 }
968 
969 void QgsSvgCache::trimToMaximumSize()
970 {
971  //only one entry in cache
972  if ( mLeastRecentEntry == mMostRecentEntry )
973  {
974  return;
975  }
976  QgsSvgCacheEntry *entry = mLeastRecentEntry;
977  while ( entry && ( mTotalSize > MAXIMUM_SIZE ) )
978  {
979  QgsSvgCacheEntry *bkEntry = entry;
980  entry = entry->nextEntry;
981 
982  takeEntryFromList( bkEntry );
983  mEntryLookup.remove( bkEntry->path, bkEntry );
984  mTotalSize -= bkEntry->dataSize();
985  delete bkEntry;
986  }
987 }
988 
989 void QgsSvgCache::takeEntryFromList( QgsSvgCacheEntry *entry )
990 {
991  if ( !entry )
992  {
993  return;
994  }
995 
996  if ( entry->previousEntry )
997  {
998  entry->previousEntry->nextEntry = entry->nextEntry;
999  }
1000  else
1001  {
1002  mLeastRecentEntry = entry->nextEntry;
1003  }
1004  if ( entry->nextEntry )
1005  {
1006  entry->nextEntry->previousEntry = entry->previousEntry;
1007  }
1008  else
1009  {
1010  mMostRecentEntry = entry->previousEntry;
1011  }
1012 }
1013 
1014 void QgsSvgCache::downloadProgress( qint64 bytesReceived, qint64 bytesTotal )
1015 {
1016  QString msg = tr( "%1 of %2 bytes of svg image downloaded." ).arg( bytesReceived ).arg( bytesTotal < 0 ? QStringLiteral( "unknown number of" ) : QString::number( bytesTotal ) );
1017  QgsDebugMsg( msg );
1018  emit statusChanged( msg );
1019 }
1020 
1021 void QgsSvgCache::onRemoteSvgFetched( const QString &url, bool success )
1022 {
1023  QMutexLocker locker( &mMutex );
1024  mPendingRemoteUrls.remove( url );
1025 
1026  QgsSvgCacheEntry *nextEntry = mLeastRecentEntry;
1027  while ( QgsSvgCacheEntry *entry = nextEntry )
1028  {
1029  nextEntry = entry->nextEntry;
1030  if ( entry->path == url )
1031  {
1032  takeEntryFromList( entry );
1033  mEntryLookup.remove( entry->path, entry );
1034  mTotalSize -= entry->dataSize();
1035  delete entry;
1036  }
1037  }
1038 
1039  if ( success )
1040  emit remoteSvgFetched( url );
1041 }
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:251
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.