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