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