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