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