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