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