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"
21 #include "qgsnetworkaccessmanager.h"
22 #include "qgsmessagelog.h"
23 #include "qgssymbollayerutils.h"
24 #include "qgsnetworkcontentfetchertask.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 
40 ///@cond PRIVATE
41 
42 //
43 // QgsSvgCacheEntry
44 //
45 
QgsSvgCacheEntry(const QString & path,double size,double strokeWidth,double widthScaleFactor,const QColor & fill,const QColor & stroke,double fixedAspectRatio)46 QgsSvgCacheEntry::QgsSvgCacheEntry( const QString &path, double size, double strokeWidth, double widthScaleFactor, const QColor &fill, const QColor &stroke, double fixedAspectRatio )
47   : QgsAbstractContentCacheEntry( path )
48   , size( size )
49   , strokeWidth( strokeWidth )
50   , widthScaleFactor( widthScaleFactor )
51   , fixedAspectRatio( fixedAspectRatio )
52   , fill( fill )
53   , stroke( stroke )
54 {
55 }
56 
isEqual(const QgsAbstractContentCacheEntry * other) const57 bool QgsSvgCacheEntry::isEqual( const QgsAbstractContentCacheEntry *other ) const
58 {
59   const QgsSvgCacheEntry *otherSvg = dynamic_cast< const QgsSvgCacheEntry * >( other );
60   // cheapest checks first!
61   if ( !otherSvg
62        || !qgsDoubleNear( otherSvg->fixedAspectRatio, fixedAspectRatio )
63        || !qgsDoubleNear( otherSvg->size, size )
64        || !qgsDoubleNear( otherSvg->strokeWidth, strokeWidth )
65        || !qgsDoubleNear( otherSvg->widthScaleFactor, widthScaleFactor )
66        || otherSvg->fill != fill
67        || otherSvg->stroke != stroke
68        || otherSvg->path != path )
69     return false;
70 
71   return true;
72 }
73 
dataSize() const74 int QgsSvgCacheEntry::dataSize() const
75 {
76   int size = svgContent.size();
77   if ( picture )
78   {
79     size += picture->size();
80   }
81   if ( image )
82   {
83     size += ( image->width() * image->height() * 32 );
84   }
85   return size;
86 }
87 
dump() const88 void QgsSvgCacheEntry::dump() const
89 {
90   QgsDebugMsgLevel( QStringLiteral( "path: %1, size %2, width scale factor %3" ).arg( path ).arg( size ).arg( widthScaleFactor ), 4 );
91 }
92 ///@endcond
93 
94 
95 //
96 // QgsSvgCache
97 //
98 
QgsSvgCache(QObject * parent)99 QgsSvgCache::QgsSvgCache( QObject *parent )
100   : QgsAbstractContentCache< QgsSvgCacheEntry >( parent, QObject::tr( "SVG" ) )
101 {
102   mMissingSvg = QStringLiteral( "<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>?</text></svg>" ).toLatin1();
103 
104   const QString downloadingSvgPath = QgsApplication::defaultThemePath() + QStringLiteral( "downloading_svg.svg" );
105   if ( QFile::exists( downloadingSvgPath ) )
106   {
107     QFile file( downloadingSvgPath );
108     if ( file.open( QIODevice::ReadOnly ) )
109     {
110       mFetchingSvg = file.readAll();
111     }
112   }
113 
114   if ( mFetchingSvg.isEmpty() )
115   {
116     mFetchingSvg = QStringLiteral( "<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>?</text></svg>" ).toLatin1();
117   }
118 
119   connect( this, &QgsAbstractContentCacheBase::remoteContentFetched, this, &QgsSvgCache::remoteSvgFetched );
120 }
121 
svgAsImage(const QString & file,double size,const QColor & fill,const QColor & stroke,double strokeWidth,double widthScaleFactor,bool & fitsInCache,double fixedAspectRatio,bool blocking)122 QImage QgsSvgCache::svgAsImage( const QString &file, double size, const QColor &fill, const QColor &stroke, double strokeWidth,
123                                 double widthScaleFactor, bool &fitsInCache, double fixedAspectRatio, bool blocking )
124 {
125   QMutexLocker locker( &mMutex );
126 
127   fitsInCache = true;
128   QgsSvgCacheEntry *currentEntry = cacheEntry( file, size, fill, stroke, strokeWidth, widthScaleFactor, fixedAspectRatio, blocking );
129 
130   QImage result;
131 
132   //if current entry image is 0: cache image for entry
133   // checks to see if image will fit into cache
134   //update stats for memory usage
135   if ( !currentEntry->image )
136   {
137     QSvgRenderer r( currentEntry->svgContent );
138     double hwRatio = 1.0;
139     if ( r.viewBoxF().width() > 0 )
140     {
141       if ( currentEntry->fixedAspectRatio > 0 )
142       {
143         hwRatio = currentEntry->fixedAspectRatio;
144       }
145       else
146       {
147         hwRatio = r.viewBoxF().height() / r.viewBoxF().width();
148       }
149     }
150     long cachedDataSize = 0;
151     cachedDataSize += currentEntry->svgContent.size();
152     cachedDataSize += static_cast< int >( currentEntry->size * currentEntry->size * hwRatio * 32 );
153     if ( cachedDataSize > mMaxCacheSize / 2 )
154     {
155       fitsInCache = false;
156       currentEntry->image.reset();
157 
158       // instead cache picture
159       if ( !currentEntry->picture )
160       {
161         cachePicture( currentEntry, false );
162       }
163 
164       // ...and render cached picture to result image
165       result = imageFromCachedPicture( *currentEntry );
166     }
167     else
168     {
169       cacheImage( currentEntry );
170       result = *( currentEntry->image );
171     }
172     trimToMaximumSize();
173   }
174   else
175   {
176     result = *( currentEntry->image );
177   }
178 
179   return result;
180 }
181 
svgAsPicture(const QString & path,double size,const QColor & fill,const QColor & stroke,double strokeWidth,double widthScaleFactor,bool forceVectorOutput,double fixedAspectRatio,bool blocking)182 QPicture QgsSvgCache::svgAsPicture( const QString &path, double size, const QColor &fill, const QColor &stroke, double strokeWidth,
183                                     double widthScaleFactor, bool forceVectorOutput, double fixedAspectRatio, bool blocking )
184 {
185   QMutexLocker locker( &mMutex );
186 
187   QgsSvgCacheEntry *currentEntry = cacheEntry( path, size, fill, stroke, strokeWidth, widthScaleFactor, fixedAspectRatio, blocking );
188 
189   //if current entry picture is 0: cache picture for entry
190   //update stats for memory usage
191   if ( !currentEntry->picture )
192   {
193     cachePicture( currentEntry, forceVectorOutput );
194     trimToMaximumSize();
195   }
196 
197   QPicture p;
198   // For some reason p.detach() doesn't seem to always work as intended, at
199   // least with QT 5.5 on Ubuntu 16.04
200   // Serialization/deserialization is a safe way to be ensured we don't
201   // share a copy.
202   p.setData( currentEntry->picture->data(), currentEntry->picture->size() );
203   return p;
204 }
205 
svgContent(const QString & path,double size,const QColor & fill,const QColor & stroke,double strokeWidth,double widthScaleFactor,double fixedAspectRatio,bool blocking,bool * isMissingImage)206 QByteArray QgsSvgCache::svgContent( const QString &path, double size, const QColor &fill, const QColor &stroke, double strokeWidth,
207                                     double widthScaleFactor, double fixedAspectRatio, bool blocking, bool *isMissingImage )
208 {
209   QMutexLocker locker( &mMutex );
210 
211   QgsSvgCacheEntry *currentEntry = cacheEntry( path, size, fill, stroke, strokeWidth, widthScaleFactor, fixedAspectRatio, blocking, isMissingImage );
212 
213   return currentEntry->svgContent;
214 }
215 
svgViewboxSize(const QString & path,double size,const QColor & fill,const QColor & stroke,double strokeWidth,double widthScaleFactor,double fixedAspectRatio,bool blocking)216 QSizeF QgsSvgCache::svgViewboxSize( const QString &path, double size, const QColor &fill, const QColor &stroke, double strokeWidth,
217                                     double widthScaleFactor, double fixedAspectRatio, bool blocking )
218 {
219   QMutexLocker locker( &mMutex );
220 
221   QgsSvgCacheEntry *currentEntry = cacheEntry( path, size, fill, stroke, strokeWidth, widthScaleFactor, fixedAspectRatio, blocking );
222   return currentEntry->viewboxSize;
223 }
224 
containsParams(const QString & path,bool & hasFillParam,QColor & defaultFillColor,bool & hasStrokeParam,QColor & defaultStrokeColor,bool & hasStrokeWidthParam,double & defaultStrokeWidth,bool blocking) const225 void QgsSvgCache::containsParams( const QString &path, bool &hasFillParam, QColor &defaultFillColor, bool &hasStrokeParam, QColor &defaultStrokeColor,
226                                   bool &hasStrokeWidthParam, double &defaultStrokeWidth, bool blocking ) const
227 {
228   bool hasDefaultFillColor = false;
229   bool hasFillOpacityParam = false;
230   bool hasDefaultFillOpacity = false;
231   double defaultFillOpacity = 1.0;
232   bool hasDefaultStrokeColor = false;
233   bool hasDefaultStrokeWidth = false;
234   bool hasStrokeOpacityParam = false;
235   bool hasDefaultStrokeOpacity = false;
236   double defaultStrokeOpacity = 1.0;
237 
238   containsParams( path, hasFillParam, hasDefaultFillColor, defaultFillColor,
239                   hasFillOpacityParam, hasDefaultFillOpacity, defaultFillOpacity,
240                   hasStrokeParam, hasDefaultStrokeColor, defaultStrokeColor,
241                   hasStrokeWidthParam, hasDefaultStrokeWidth, defaultStrokeWidth,
242                   hasStrokeOpacityParam, hasDefaultStrokeOpacity, defaultStrokeOpacity,
243                   blocking );
244 }
245 
containsParams(const QString & path,bool & hasFillParam,bool & hasDefaultFillParam,QColor & defaultFillColor,bool & hasFillOpacityParam,bool & hasDefaultFillOpacity,double & defaultFillOpacity,bool & hasStrokeParam,bool & hasDefaultStrokeColor,QColor & defaultStrokeColor,bool & hasStrokeWidthParam,bool & hasDefaultStrokeWidth,double & defaultStrokeWidth,bool & hasStrokeOpacityParam,bool & hasDefaultStrokeOpacity,double & defaultStrokeOpacity,bool blocking) const246 void QgsSvgCache::containsParams( const QString &path,
247                                   bool &hasFillParam, bool &hasDefaultFillParam, QColor &defaultFillColor,
248                                   bool &hasFillOpacityParam, bool &hasDefaultFillOpacity, double &defaultFillOpacity,
249                                   bool &hasStrokeParam, bool &hasDefaultStrokeColor, QColor &defaultStrokeColor,
250                                   bool &hasStrokeWidthParam, bool &hasDefaultStrokeWidth, double &defaultStrokeWidth,
251                                   bool &hasStrokeOpacityParam, bool &hasDefaultStrokeOpacity, double &defaultStrokeOpacity,
252                                   bool blocking ) const
253 {
254   hasFillParam = false;
255   hasFillOpacityParam = false;
256   hasStrokeParam = false;
257   hasStrokeWidthParam = false;
258   hasStrokeOpacityParam = false;
259   defaultFillColor = QColor( Qt::white );
260   defaultFillOpacity = 1.0;
261   defaultStrokeColor = QColor( Qt::black );
262   defaultStrokeWidth = 0.2;
263   defaultStrokeOpacity = 1.0;
264 
265   hasDefaultFillParam = false;
266   hasDefaultFillOpacity = false;
267   hasDefaultStrokeColor = false;
268   hasDefaultStrokeWidth = false;
269   hasDefaultStrokeOpacity = false;
270 
271   QDomDocument svgDoc;
272   if ( !svgDoc.setContent( getContent( path, mMissingSvg, mFetchingSvg, blocking ) ) )
273   {
274     return;
275   }
276 
277   QDomElement docElem = svgDoc.documentElement();
278   containsElemParams( docElem, hasFillParam, hasDefaultFillParam, defaultFillColor,
279                       hasFillOpacityParam, hasDefaultFillOpacity, defaultFillOpacity,
280                       hasStrokeParam, hasDefaultStrokeColor, defaultStrokeColor,
281                       hasStrokeWidthParam, hasDefaultStrokeWidth, defaultStrokeWidth,
282                       hasStrokeOpacityParam, hasDefaultStrokeOpacity, defaultStrokeOpacity );
283 }
284 
replaceParamsAndCacheSvg(QgsSvgCacheEntry * entry,bool blocking)285 void QgsSvgCache::replaceParamsAndCacheSvg( QgsSvgCacheEntry *entry, bool blocking )
286 {
287   if ( !entry )
288   {
289     return;
290   }
291 
292   const QByteArray content = getContent( entry->path, mMissingSvg, mFetchingSvg, blocking ) ;
293   entry->isMissingImage = content == mMissingSvg;
294   QDomDocument svgDoc;
295   if ( !svgDoc.setContent( content ) )
296   {
297     return;
298   }
299 
300   //replace fill color, stroke color, stroke with in all nodes
301   QDomElement docElem = svgDoc.documentElement();
302 
303   QSizeF viewboxSize;
304   double sizeScaleFactor = calcSizeScaleFactor( entry, docElem, viewboxSize );
305   entry->viewboxSize = viewboxSize;
306   replaceElemParams( docElem, entry->fill, entry->stroke, entry->strokeWidth * sizeScaleFactor );
307 
308   entry->svgContent = svgDoc.toByteArray( 0 );
309 
310 
311   // toByteArray screws up tspans inside text by adding new lines before and after each span... this should help, at the
312   // risk of potentially breaking some svgs where the newline is desired
313   entry->svgContent.replace( "\n<tspan", "<tspan" );
314   entry->svgContent.replace( "</tspan>\n", "</tspan>" );
315 
316   mTotalSize += entry->svgContent.size();
317 }
318 
calcSizeScaleFactor(QgsSvgCacheEntry * entry,const QDomElement & docElem,QSizeF & viewboxSize) const319 double QgsSvgCache::calcSizeScaleFactor( QgsSvgCacheEntry *entry, const QDomElement &docElem, QSizeF &viewboxSize ) const
320 {
321   QString viewBox;
322 
323   //bad size
324   if ( !entry || qgsDoubleNear( entry->size, 0.0 ) )
325     return 1.0;
326 
327   //find svg viewbox attribute
328   //first check if docElem is svg element
329   if ( docElem.tagName() == QLatin1String( "svg" ) && docElem.hasAttribute( QStringLiteral( "viewBox" ) ) )
330   {
331     viewBox = docElem.attribute( QStringLiteral( "viewBox" ), QString() );
332   }
333   else if ( docElem.tagName() == QLatin1String( "svg" ) && docElem.hasAttribute( QStringLiteral( "viewbox" ) ) )
334   {
335     viewBox = docElem.attribute( QStringLiteral( "viewbox" ), QString() );
336   }
337   else
338   {
339     QDomElement svgElem = docElem.firstChildElement( QStringLiteral( "svg" ) );
340     if ( !svgElem.isNull() )
341     {
342       if ( svgElem.hasAttribute( QStringLiteral( "viewBox" ) ) )
343         viewBox = svgElem.attribute( QStringLiteral( "viewBox" ), QString() );
344       else if ( svgElem.hasAttribute( QStringLiteral( "viewbox" ) ) )
345         viewBox = svgElem.attribute( QStringLiteral( "viewbox" ), QString() );
346     }
347   }
348 
349   //could not find valid viewbox attribute
350   if ( viewBox.isEmpty() )
351   {
352     // trying looking for width/height and use them as a fallback
353     if ( docElem.tagName() == QLatin1String( "svg" ) && docElem.hasAttribute( QStringLiteral( "width" ) ) )
354     {
355       const QString widthString = docElem.attribute( QStringLiteral( "width" ) );
356       const QRegularExpression measureRegEx( QStringLiteral( "([\\d\\.]+).*?$" ) );
357       const QRegularExpressionMatch widthMatch = measureRegEx.match( widthString );
358       if ( widthMatch.hasMatch() )
359       {
360         double width = widthMatch.captured( 1 ).toDouble();
361         const QString heightString = docElem.attribute( QStringLiteral( "height" ) );
362 
363         const QRegularExpressionMatch heightMatch = measureRegEx.match( heightString );
364         if ( heightMatch.hasMatch() )
365         {
366           double height = heightMatch.captured( 1 ).toDouble();
367           viewboxSize = QSizeF( width, height );
368           return width / entry->size;
369         }
370       }
371     }
372 
373     return 1.0;
374   }
375 
376 
377   //width should be 3rd element in a 4 part space delimited string
378   QStringList parts = viewBox.split( ' ' );
379   if ( parts.count() != 4 )
380     return 1.0;
381 
382   bool heightOk = false;
383   double height = parts.at( 3 ).toDouble( &heightOk );
384 
385   bool widthOk = false;
386   double width = parts.at( 2 ).toDouble( &widthOk );
387   if ( widthOk )
388   {
389     if ( heightOk )
390       viewboxSize = QSizeF( width, height );
391     return width / entry->size;
392   }
393 
394   return 1.0;
395 }
396 
397 
getImageData(const QString & path,bool blocking) const398 QByteArray QgsSvgCache::getImageData( const QString &path, bool blocking ) const
399 {
400   return getContent( path, mMissingSvg, mFetchingSvg, blocking );
401 };
402 
checkReply(QNetworkReply * reply,const QString & path) const403 bool QgsSvgCache::checkReply( QNetworkReply *reply, const QString &path ) const
404 {
405   // we accept both real SVG mime types AND plain text types - because some sites
406   // (notably github) serve up svgs as raw text
407   QString contentType = reply->header( QNetworkRequest::ContentTypeHeader ).toString();
408   if ( !contentType.startsWith( QLatin1String( "image/svg+xml" ), Qt::CaseInsensitive )
409        && !contentType.startsWith( QLatin1String( "text/plain" ), Qt::CaseInsensitive ) )
410   {
411     QgsMessageLog::logMessage( tr( "Unexpected MIME type %1 received for %2" ).arg( contentType, path ), tr( "SVG" ) );
412     return false;
413   }
414   return true;
415 }
416 
cacheImage(QgsSvgCacheEntry * entry)417 void QgsSvgCache::cacheImage( QgsSvgCacheEntry *entry )
418 {
419   if ( !entry )
420   {
421     return;
422   }
423 
424   entry->image.reset();
425 
426   QSizeF viewBoxSize;
427   QSizeF scaledSize;
428   QSize imageSize = sizeForImage( *entry, viewBoxSize, scaledSize );
429 
430   // cast double image sizes to int for QImage
431   std::unique_ptr< QImage > image = qgis::make_unique< QImage >( imageSize, QImage::Format_ARGB32_Premultiplied );
432   image->fill( 0 ); // transparent background
433 
434   const bool isFixedAR = entry->fixedAspectRatio > 0;
435 
436   QPainter p( image.get() );
437   QSvgRenderer r( entry->svgContent );
438   if ( qgsDoubleNear( viewBoxSize.width(), viewBoxSize.height() ) )
439   {
440     r.render( &p );
441   }
442   else
443   {
444     QSizeF s( viewBoxSize );
445     s.scale( scaledSize.width(), scaledSize.height(), isFixedAR ? Qt::IgnoreAspectRatio : Qt::KeepAspectRatio );
446     QRectF rect( ( imageSize.width() - s.width() ) / 2, ( imageSize.height() - s.height() ) / 2, s.width(), s.height() );
447     r.render( &p, rect );
448   }
449 
450   mTotalSize += ( image->width() * image->height() * 32 );
451   entry->image = std::move( image );
452 }
453 
cachePicture(QgsSvgCacheEntry * entry,bool forceVectorOutput)454 void QgsSvgCache::cachePicture( QgsSvgCacheEntry *entry, bool forceVectorOutput )
455 {
456   Q_UNUSED( forceVectorOutput )
457   if ( !entry )
458   {
459     return;
460   }
461 
462   entry->picture.reset();
463 
464   bool isFixedAR = entry->fixedAspectRatio > 0;
465 
466   //correct QPictures dpi correction
467   std::unique_ptr< QPicture > picture = qgis::make_unique< QPicture >();
468   QRectF rect;
469   QSvgRenderer r( entry->svgContent );
470   double hwRatio = 1.0;
471   if ( r.viewBoxF().width() > 0 )
472   {
473     if ( isFixedAR )
474     {
475       hwRatio = entry->fixedAspectRatio;
476     }
477     else
478     {
479       hwRatio = r.viewBoxF().height() / r.viewBoxF().width();
480     }
481   }
482 
483   double wSize = entry->size;
484   double hSize = wSize * hwRatio;
485 
486   QSizeF s( r.viewBoxF().size() );
487   s.scale( wSize, hSize, isFixedAR ? Qt::IgnoreAspectRatio : Qt::KeepAspectRatio );
488   rect = QRectF( -s.width() / 2.0, -s.height() / 2.0, s.width(), s.height() );
489 
490   QPainter p( picture.get() );
491   r.render( &p, rect );
492   entry->picture = std::move( picture );
493   mTotalSize += entry->picture->size();
494 }
495 
cacheEntry(const QString & path,double size,const QColor & fill,const QColor & stroke,double strokeWidth,double widthScaleFactor,double fixedAspectRatio,bool blocking,bool * isMissingImage)496 QgsSvgCacheEntry *QgsSvgCache::cacheEntry( const QString &path, double size, const QColor &fill, const QColor &stroke, double strokeWidth,
497     double widthScaleFactor, double fixedAspectRatio, bool blocking, bool *isMissingImage )
498 {
499   QgsSvgCacheEntry *currentEntry = findExistingEntry( new QgsSvgCacheEntry( path, size, strokeWidth, widthScaleFactor, fill, stroke, fixedAspectRatio ) );
500 
501   if ( currentEntry->svgContent.isEmpty() )
502   {
503     replaceParamsAndCacheSvg( currentEntry, blocking );
504   }
505 
506   if ( isMissingImage )
507     *isMissingImage = currentEntry->isMissingImage;
508 
509   return currentEntry;
510 }
511 
512 
replaceElemParams(QDomElement & elem,const QColor & fill,const QColor & stroke,double strokeWidth)513 void QgsSvgCache::replaceElemParams( QDomElement &elem, const QColor &fill, const QColor &stroke, double strokeWidth )
514 {
515   if ( elem.isNull() )
516   {
517     return;
518   }
519 
520   //go through attributes
521   QDomNamedNodeMap attributes = elem.attributes();
522   int nAttributes = attributes.count();
523   for ( int i = 0; i < nAttributes; ++i )
524   {
525     QDomAttr attribute = attributes.item( i ).toAttr();
526     //e.g. style="fill:param(fill);param(stroke)"
527     if ( attribute.name().compare( QLatin1String( "style" ), Qt::CaseInsensitive ) == 0 )
528     {
529       //entries separated by ';'
530       QString newAttributeString;
531 
532       QStringList entryList = attribute.value().split( ';' );
533       QStringList::const_iterator entryIt = entryList.constBegin();
534       for ( ; entryIt != entryList.constEnd(); ++entryIt )
535       {
536         QStringList keyValueSplit = entryIt->split( ':' );
537         if ( keyValueSplit.size() < 2 )
538         {
539           continue;
540         }
541         const QString key = keyValueSplit.at( 0 );
542         QString value = keyValueSplit.at( 1 );
543         QString newValue = value;
544         value = value.trimmed().toLower();
545 
546         if ( value.startsWith( QLatin1String( "param(fill)" ) ) )
547         {
548           newValue = fill.name();
549         }
550         else if ( value.startsWith( QLatin1String( "param(fill-opacity)" ) ) )
551         {
552           newValue = QString::number( fill.alphaF() );
553         }
554         else if ( value.startsWith( QLatin1String( "param(outline)" ) ) )
555         {
556           newValue = stroke.name();
557         }
558         else if ( value.startsWith( QLatin1String( "param(outline-opacity)" ) ) )
559         {
560           newValue = QString::number( stroke.alphaF() );
561         }
562         else if ( value.startsWith( QLatin1String( "param(outline-width)" ) ) )
563         {
564           newValue = QString::number( strokeWidth );
565         }
566 
567         if ( entryIt != entryList.constBegin() )
568         {
569           newAttributeString.append( ';' );
570         }
571         newAttributeString.append( key + ':' + newValue );
572       }
573       elem.setAttribute( attribute.name(), newAttributeString );
574     }
575     else
576     {
577       const QString value = attribute.value().trimmed().toLower();
578       if ( value.startsWith( QLatin1String( "param(fill)" ) ) )
579       {
580         elem.setAttribute( attribute.name(), fill.name() );
581       }
582       else if ( value.startsWith( QLatin1String( "param(fill-opacity)" ) ) )
583       {
584         elem.setAttribute( attribute.name(), fill.alphaF() );
585       }
586       else if ( value.startsWith( QLatin1String( "param(outline)" ) ) )
587       {
588         elem.setAttribute( attribute.name(), stroke.name() );
589       }
590       else if ( value.startsWith( QLatin1String( "param(outline-opacity)" ) ) )
591       {
592         elem.setAttribute( attribute.name(), stroke.alphaF() );
593       }
594       else if ( value.startsWith( QLatin1String( "param(outline-width)" ) ) )
595       {
596         elem.setAttribute( attribute.name(), QString::number( strokeWidth ) );
597       }
598     }
599   }
600 
601   QDomNodeList childList = elem.childNodes();
602   int nChildren = childList.count();
603   for ( int i = 0; i < nChildren; ++i )
604   {
605     QDomElement childElem = childList.at( i ).toElement();
606     replaceElemParams( childElem, fill, stroke, strokeWidth );
607   }
608 }
609 
containsElemParams(const QDomElement & elem,bool & hasFillParam,bool & hasDefaultFill,QColor & defaultFill,bool & hasFillOpacityParam,bool & hasDefaultFillOpacity,double & defaultFillOpacity,bool & hasStrokeParam,bool & hasDefaultStroke,QColor & defaultStroke,bool & hasStrokeWidthParam,bool & hasDefaultStrokeWidth,double & defaultStrokeWidth,bool & hasStrokeOpacityParam,bool & hasDefaultStrokeOpacity,double & defaultStrokeOpacity) const610 void QgsSvgCache::containsElemParams( const QDomElement &elem, bool &hasFillParam, bool &hasDefaultFill, QColor &defaultFill,
611                                       bool &hasFillOpacityParam, bool &hasDefaultFillOpacity, double &defaultFillOpacity,
612                                       bool &hasStrokeParam, bool &hasDefaultStroke, QColor &defaultStroke,
613                                       bool &hasStrokeWidthParam, bool &hasDefaultStrokeWidth, double &defaultStrokeWidth,
614                                       bool &hasStrokeOpacityParam, bool &hasDefaultStrokeOpacity, double &defaultStrokeOpacity ) const
615 {
616   if ( elem.isNull() )
617   {
618     return;
619   }
620 
621   //we already have all the information, no need to go deeper
622   if ( hasFillParam && hasStrokeParam && hasStrokeWidthParam && hasFillOpacityParam && hasStrokeOpacityParam )
623   {
624     return;
625   }
626 
627   //check this elements attribute
628   QDomNamedNodeMap attributes = elem.attributes();
629   int nAttributes = attributes.count();
630 
631   QStringList valueSplit;
632   for ( int i = 0; i < nAttributes; ++i )
633   {
634     QDomAttr attribute = attributes.item( i ).toAttr();
635     if ( attribute.name().compare( QLatin1String( "style" ), Qt::CaseInsensitive ) == 0 )
636     {
637       //entries separated by ';'
638       QStringList entryList = attribute.value().split( ';' );
639       QStringList::const_iterator entryIt = entryList.constBegin();
640       for ( ; entryIt != entryList.constEnd(); ++entryIt )
641       {
642         QStringList keyValueSplit = entryIt->split( ':' );
643         if ( keyValueSplit.size() < 2 )
644         {
645           continue;
646         }
647         QString value = keyValueSplit.at( 1 );
648         valueSplit = value.split( ' ' );
649         if ( !hasFillParam && value.startsWith( QLatin1String( "param(fill)" ) ) )
650         {
651           hasFillParam = true;
652           if ( valueSplit.size() > 1 )
653           {
654             defaultFill = QColor( valueSplit.at( 1 ) );
655             hasDefaultFill = true;
656           }
657         }
658         else if ( !hasFillOpacityParam && value.startsWith( QLatin1String( "param(fill-opacity)" ) ) )
659         {
660           hasFillOpacityParam = true;
661           if ( valueSplit.size() > 1 )
662           {
663             bool ok;
664             double opacity = valueSplit.at( 1 ).toDouble( &ok );
665             if ( ok )
666             {
667               defaultFillOpacity = opacity;
668               hasDefaultFillOpacity = true;
669             }
670           }
671         }
672         else if ( !hasStrokeParam && value.startsWith( QLatin1String( "param(outline)" ) ) )
673         {
674           hasStrokeParam = true;
675           if ( valueSplit.size() > 1 )
676           {
677             defaultStroke = QColor( valueSplit.at( 1 ) );
678             hasDefaultStroke = true;
679           }
680         }
681         else if ( !hasStrokeWidthParam && value.startsWith( QLatin1String( "param(outline-width)" ) ) )
682         {
683           hasStrokeWidthParam = true;
684           if ( valueSplit.size() > 1 )
685           {
686             defaultStrokeWidth = valueSplit.at( 1 ).toDouble();
687             hasDefaultStrokeWidth = true;
688           }
689         }
690         else if ( !hasStrokeOpacityParam && value.startsWith( QLatin1String( "param(outline-opacity)" ) ) )
691         {
692           hasStrokeOpacityParam = true;
693           if ( valueSplit.size() > 1 )
694           {
695             bool ok;
696             double opacity = valueSplit.at( 1 ).toDouble( &ok );
697             if ( ok )
698             {
699               defaultStrokeOpacity = opacity;
700               hasDefaultStrokeOpacity = true;
701             }
702           }
703         }
704       }
705     }
706     else
707     {
708       QString value = attribute.value();
709       valueSplit = value.split( ' ' );
710       if ( !hasFillParam && value.startsWith( QLatin1String( "param(fill)" ) ) )
711       {
712         hasFillParam = true;
713         if ( valueSplit.size() > 1 )
714         {
715           defaultFill = QColor( valueSplit.at( 1 ) );
716           hasDefaultFill = true;
717         }
718       }
719       else if ( !hasFillOpacityParam && value.startsWith( QLatin1String( "param(fill-opacity)" ) ) )
720       {
721         hasFillOpacityParam = true;
722         if ( valueSplit.size() > 1 )
723         {
724           bool ok;
725           double opacity = valueSplit.at( 1 ).toDouble( &ok );
726           if ( ok )
727           {
728             defaultFillOpacity = opacity;
729             hasDefaultFillOpacity = true;
730           }
731         }
732       }
733       else if ( !hasStrokeParam && value.startsWith( QLatin1String( "param(outline)" ) ) )
734       {
735         hasStrokeParam = true;
736         if ( valueSplit.size() > 1 )
737         {
738           defaultStroke = QColor( valueSplit.at( 1 ) );
739           hasDefaultStroke = true;
740         }
741       }
742       else if ( !hasStrokeWidthParam && value.startsWith( QLatin1String( "param(outline-width)" ) ) )
743       {
744         hasStrokeWidthParam = true;
745         if ( valueSplit.size() > 1 )
746         {
747           defaultStrokeWidth = valueSplit.at( 1 ).toDouble();
748           hasDefaultStrokeWidth = true;
749         }
750       }
751       else if ( !hasStrokeOpacityParam && value.startsWith( QLatin1String( "param(outline-opacity)" ) ) )
752       {
753         hasStrokeOpacityParam = true;
754         if ( valueSplit.size() > 1 )
755         {
756           bool ok;
757           double opacity = valueSplit.at( 1 ).toDouble( &ok );
758           if ( ok )
759           {
760             defaultStrokeOpacity = opacity;
761             hasDefaultStrokeOpacity = true;
762           }
763         }
764       }
765     }
766   }
767 
768   //pass it further to child items
769   QDomNodeList childList = elem.childNodes();
770   int nChildren = childList.count();
771   for ( int i = 0; i < nChildren; ++i )
772   {
773     QDomElement childElem = childList.at( i ).toElement();
774     containsElemParams( childElem, hasFillParam, hasDefaultFill, defaultFill,
775                         hasFillOpacityParam, hasDefaultFillOpacity, defaultFillOpacity,
776                         hasStrokeParam, hasDefaultStroke, defaultStroke,
777                         hasStrokeWidthParam, hasDefaultStrokeWidth, defaultStrokeWidth,
778                         hasStrokeOpacityParam, hasDefaultStrokeOpacity, defaultStrokeOpacity );
779   }
780 }
781 
sizeForImage(const QgsSvgCacheEntry & entry,QSizeF & viewBoxSize,QSizeF & scaledSize) const782 QSize QgsSvgCache::sizeForImage( const QgsSvgCacheEntry &entry, QSizeF &viewBoxSize, QSizeF &scaledSize ) const
783 {
784   bool isFixedAR = entry.fixedAspectRatio > 0;
785 
786   QSvgRenderer r( entry.svgContent );
787   double hwRatio = 1.0;
788   viewBoxSize = r.viewBoxF().size();
789   if ( viewBoxSize.width() > 0 )
790   {
791     if ( isFixedAR )
792     {
793       hwRatio = entry.fixedAspectRatio;
794     }
795     else
796     {
797       hwRatio = viewBoxSize.height() / viewBoxSize.width();
798     }
799   }
800 
801   // cast double image sizes to int for QImage
802   scaledSize.setWidth( entry.size );
803   int wImgSize = static_cast< int >( scaledSize.width() );
804   if ( wImgSize < 1 )
805   {
806     wImgSize = 1;
807   }
808   scaledSize.setHeight( scaledSize.width() * hwRatio );
809   int hImgSize = static_cast< int >( scaledSize.height() );
810   if ( hImgSize < 1 )
811   {
812     hImgSize = 1;
813   }
814   return QSize( wImgSize, hImgSize );
815 }
816 
imageFromCachedPicture(const QgsSvgCacheEntry & entry) const817 QImage QgsSvgCache::imageFromCachedPicture( const QgsSvgCacheEntry &entry ) const
818 {
819   QSizeF viewBoxSize;
820   QSizeF scaledSize;
821   QImage image( sizeForImage( entry, viewBoxSize, scaledSize ), QImage::Format_ARGB32_Premultiplied );
822   image.fill( 0 ); // transparent background
823 
824   QPainter p( &image );
825   p.drawPicture( QPoint( 0, 0 ), *entry.picture );
826   return image;
827 }
828 
829