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