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