26 #include <QApplication>
27 #include <QCoreApplication>
29 #include <QDomDocument>
30 #include <QDomElement>
35 #include <QSvgRenderer>
37 #include <QNetworkReply>
38 #include <QNetworkRequest>
46 QgsSvgCacheEntry::QgsSvgCacheEntry(
const QString &path,
double size,
double strokeWidth,
double widthScaleFactor,
const QColor &fill,
const QColor &stroke,
double fixedAspectRatio )
49 , strokeWidth( strokeWidth )
50 , widthScaleFactor( widthScaleFactor )
51 , fixedAspectRatio( fixedAspectRatio )
59 const QgsSvgCacheEntry *otherSvg =
dynamic_cast< const QgsSvgCacheEntry *
>( other );
62 || !
qgsDoubleNear( otherSvg->fixedAspectRatio, fixedAspectRatio )
65 || !
qgsDoubleNear( otherSvg->widthScaleFactor, widthScaleFactor )
66 || otherSvg->fill != fill
67 || otherSvg->stroke != stroke
68 || otherSvg->path != path )
74 int QgsSvgCacheEntry::dataSize()
const
76 int size = svgContent.size();
79 size += picture->size();
83 size += ( image->width() * image->height() * 32 );
88 void QgsSvgCacheEntry::dump()
const
90 QgsDebugMsgLevel( QStringLiteral(
"path: %1, size %2, width scale factor %3" ).arg( path ).arg( size ).arg( widthScaleFactor ), 4 );
102 mMissingSvg = QStringLiteral(
"<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>?</text></svg>" ).toLatin1();
105 if ( QFile::exists( downloadingSvgPath ) )
107 QFile file( downloadingSvgPath );
108 if ( file.open( QIODevice::ReadOnly ) )
110 mFetchingSvg = file.readAll();
114 if ( mFetchingSvg.isEmpty() )
116 mFetchingSvg = QStringLiteral(
"<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>?</text></svg>" ).toLatin1();
122 QImage
QgsSvgCache::svgAsImage(
const QString &file,
double size,
const QColor &fill,
const QColor &stroke,
double strokeWidth,
123 double widthScaleFactor,
bool &fitsInCache,
double fixedAspectRatio,
bool blocking )
125 QMutexLocker locker( &
mMutex );
128 QgsSvgCacheEntry *currentEntry = cacheEntry( file, size, fill, stroke, strokeWidth, widthScaleFactor, fixedAspectRatio, blocking );
135 if ( !currentEntry->image )
137 QSvgRenderer r( currentEntry->svgContent );
138 double hwRatio = 1.0;
139 if ( r.viewBoxF().width() > 0 )
141 if ( currentEntry->fixedAspectRatio > 0 )
143 hwRatio = currentEntry->fixedAspectRatio;
147 hwRatio = r.viewBoxF().height() / r.viewBoxF().width();
150 long cachedDataSize = 0;
151 cachedDataSize += currentEntry->svgContent.size();
152 cachedDataSize +=
static_cast< int >( currentEntry->size * currentEntry->size * hwRatio * 32 );
156 currentEntry->image.reset();
159 if ( !currentEntry->picture )
161 cachePicture( currentEntry,
false );
165 result = imageFromCachedPicture( *currentEntry );
169 cacheImage( currentEntry );
170 result = *( currentEntry->image );
176 result = *( currentEntry->image );
183 double widthScaleFactor,
bool forceVectorOutput,
double fixedAspectRatio,
bool blocking )
185 QMutexLocker locker( &
mMutex );
187 QgsSvgCacheEntry *currentEntry = cacheEntry( path, size, fill, stroke, strokeWidth, widthScaleFactor, fixedAspectRatio, blocking );
191 if ( !currentEntry->picture )
193 cachePicture( currentEntry, forceVectorOutput );
202 p.setData( currentEntry->picture->data(), currentEntry->picture->size() );
206 QByteArray
QgsSvgCache::svgContent(
const QString &path,
double size,
const QColor &fill,
const QColor &stroke,
double strokeWidth,
207 double widthScaleFactor,
double fixedAspectRatio,
bool blocking,
bool *isMissingImage )
209 QMutexLocker locker( &
mMutex );
211 QgsSvgCacheEntry *currentEntry = cacheEntry( path, size, fill, stroke, strokeWidth, widthScaleFactor, fixedAspectRatio, blocking, isMissingImage );
213 return currentEntry->svgContent;
217 double widthScaleFactor,
double fixedAspectRatio,
bool blocking )
219 QMutexLocker locker( &
mMutex );
221 QgsSvgCacheEntry *currentEntry = cacheEntry( path, size, fill, stroke, strokeWidth, widthScaleFactor, fixedAspectRatio, blocking );
222 return currentEntry->viewboxSize;
225 void QgsSvgCache::containsParams(
const QString &path,
bool &hasFillParam, QColor &defaultFillColor,
bool &hasStrokeParam, QColor &defaultStrokeColor,
226 bool &hasStrokeWidthParam,
double &defaultStrokeWidth,
bool blocking )
const
228 bool hasDefaultFillColor =
false;
229 bool hasFillOpacityParam =
false;
230 bool hasDefaultFillOpacity =
false;
231 double defaultFillOpacity = 1.0;
232 bool hasDefaultStrokeColor =
false;
233 bool hasDefaultStrokeWidth =
false;
234 bool hasStrokeOpacityParam =
false;
235 bool hasDefaultStrokeOpacity =
false;
236 double defaultStrokeOpacity = 1.0;
238 containsParams( path, hasFillParam, hasDefaultFillColor, defaultFillColor,
239 hasFillOpacityParam, hasDefaultFillOpacity, defaultFillOpacity,
240 hasStrokeParam, hasDefaultStrokeColor, defaultStrokeColor,
241 hasStrokeWidthParam, hasDefaultStrokeWidth, defaultStrokeWidth,
242 hasStrokeOpacityParam, hasDefaultStrokeOpacity, defaultStrokeOpacity,
247 bool &hasFillParam,
bool &hasDefaultFillParam, QColor &defaultFillColor,
248 bool &hasFillOpacityParam,
bool &hasDefaultFillOpacity,
double &defaultFillOpacity,
249 bool &hasStrokeParam,
bool &hasDefaultStrokeColor, QColor &defaultStrokeColor,
250 bool &hasStrokeWidthParam,
bool &hasDefaultStrokeWidth,
double &defaultStrokeWidth,
251 bool &hasStrokeOpacityParam,
bool &hasDefaultStrokeOpacity,
double &defaultStrokeOpacity,
252 bool blocking )
const
254 hasFillParam =
false;
255 hasFillOpacityParam =
false;
256 hasStrokeParam =
false;
257 hasStrokeWidthParam =
false;
258 hasStrokeOpacityParam =
false;
259 defaultFillColor = QColor( Qt::white );
260 defaultFillOpacity = 1.0;
261 defaultStrokeColor = QColor( Qt::black );
262 defaultStrokeWidth = 0.2;
263 defaultStrokeOpacity = 1.0;
265 hasDefaultFillParam =
false;
266 hasDefaultFillOpacity =
false;
267 hasDefaultStrokeColor =
false;
268 hasDefaultStrokeWidth =
false;
269 hasDefaultStrokeOpacity =
false;
272 if ( !svgDoc.setContent(
getContent( path, mMissingSvg, mFetchingSvg, blocking ) ) )
277 QDomElement docElem = svgDoc.documentElement();
278 containsElemParams( docElem, hasFillParam, hasDefaultFillParam, defaultFillColor,
279 hasFillOpacityParam, hasDefaultFillOpacity, defaultFillOpacity,
280 hasStrokeParam, hasDefaultStrokeColor, defaultStrokeColor,
281 hasStrokeWidthParam, hasDefaultStrokeWidth, defaultStrokeWidth,
282 hasStrokeOpacityParam, hasDefaultStrokeOpacity, defaultStrokeOpacity );
285 void QgsSvgCache::replaceParamsAndCacheSvg( QgsSvgCacheEntry *entry,
bool blocking )
292 const QByteArray content =
getContent( entry->path, mMissingSvg, mFetchingSvg, blocking ) ;
293 entry->isMissingImage = content == mMissingSvg;
295 if ( !svgDoc.setContent( content ) )
301 QDomElement docElem = svgDoc.documentElement();
304 double sizeScaleFactor = calcSizeScaleFactor( entry, docElem, viewboxSize );
305 entry->viewboxSize = viewboxSize;
306 replaceElemParams( docElem, entry->fill, entry->stroke, entry->strokeWidth * sizeScaleFactor );
308 entry->svgContent = svgDoc.toByteArray( 0 );
313 entry->svgContent.replace(
"\n<tspan",
"<tspan" );
314 entry->svgContent.replace(
"</tspan>\n",
"</tspan>" );
319 double QgsSvgCache::calcSizeScaleFactor( QgsSvgCacheEntry *entry,
const QDomElement &docElem, QSizeF &viewboxSize )
const
329 if ( docElem.tagName() == QLatin1String(
"svg" ) && docElem.hasAttribute( QStringLiteral(
"viewBox" ) ) )
331 viewBox = docElem.attribute( QStringLiteral(
"viewBox" ), QString() );
333 else if ( docElem.tagName() == QLatin1String(
"svg" ) && docElem.hasAttribute( QStringLiteral(
"viewbox" ) ) )
335 viewBox = docElem.attribute( QStringLiteral(
"viewbox" ), QString() );
339 QDomElement svgElem = docElem.firstChildElement( QStringLiteral(
"svg" ) );
340 if ( !svgElem.isNull() )
342 if ( svgElem.hasAttribute( QStringLiteral(
"viewBox" ) ) )
343 viewBox = svgElem.attribute( QStringLiteral(
"viewBox" ), QString() );
344 else if ( svgElem.hasAttribute( QStringLiteral(
"viewbox" ) ) )
345 viewBox = svgElem.attribute( QStringLiteral(
"viewbox" ), QString() );
350 if ( viewBox.isEmpty() )
353 if ( docElem.tagName() == QLatin1String(
"svg" ) && docElem.hasAttribute( QStringLiteral(
"width" ) ) )
355 const QString widthString = docElem.attribute( QStringLiteral(
"width" ) );
356 const QRegularExpression measureRegEx( QStringLiteral(
"([\\d\\.]+).*?$" ) );
357 const QRegularExpressionMatch widthMatch = measureRegEx.match( widthString );
358 if ( widthMatch.hasMatch() )
360 double width = widthMatch.captured( 1 ).toDouble();
361 const QString heightString = docElem.attribute( QStringLiteral(
"height" ) );
363 const QRegularExpressionMatch heightMatch = measureRegEx.match( heightString );
364 if ( heightMatch.hasMatch() )
366 double height = heightMatch.captured( 1 ).toDouble();
367 viewboxSize = QSizeF( width, height );
368 return width / entry->size;
378 QStringList parts = viewBox.split(
' ' );
379 if ( parts.count() != 4 )
382 bool heightOk =
false;
383 double height = parts.at( 3 ).toDouble( &heightOk );
385 bool widthOk =
false;
386 double width = parts.at( 2 ).toDouble( &widthOk );
390 viewboxSize = QSizeF( width, height );
391 return width / entry->size;
400 return getContent( path, mMissingSvg, mFetchingSvg, blocking );
407 QString contentType = reply->header( QNetworkRequest::ContentTypeHeader ).toString();
408 if ( !contentType.startsWith( QLatin1String(
"image/svg+xml" ), Qt::CaseInsensitive )
409 && !contentType.startsWith( QLatin1String(
"text/plain" ), Qt::CaseInsensitive ) )
417 void QgsSvgCache::cacheImage( QgsSvgCacheEntry *entry )
424 entry->image.reset();
428 QSize imageSize = sizeForImage( *entry, viewBoxSize, scaledSize );
431 std::unique_ptr< QImage > image = qgis::make_unique< QImage >( imageSize, QImage::Format_ARGB32_Premultiplied );
434 const bool isFixedAR = entry->fixedAspectRatio > 0;
436 QPainter p( image.get() );
437 QSvgRenderer r( entry->svgContent );
438 if (
qgsDoubleNear( viewBoxSize.width(), viewBoxSize.height() ) )
444 QSizeF s( viewBoxSize );
445 s.scale( scaledSize.width(), scaledSize.height(), isFixedAR ? Qt::IgnoreAspectRatio : Qt::KeepAspectRatio );
446 QRectF rect( ( imageSize.width() - s.width() ) / 2, ( imageSize.height() - s.height() ) / 2, s.width(), s.height() );
447 r.render( &p, rect );
450 mTotalSize += ( image->width() * image->height() * 32 );
451 entry->image = std::move( image );
454 void QgsSvgCache::cachePicture( QgsSvgCacheEntry *entry,
bool forceVectorOutput )
456 Q_UNUSED( forceVectorOutput )
462 entry->picture.reset();
464 bool isFixedAR = entry->fixedAspectRatio > 0;
467 std::unique_ptr< QPicture > picture = qgis::make_unique< QPicture >();
469 QSvgRenderer r( entry->svgContent );
470 double hwRatio = 1.0;
471 if ( r.viewBoxF().width() > 0 )
475 hwRatio = entry->fixedAspectRatio;
479 hwRatio = r.viewBoxF().height() / r.viewBoxF().width();
483 double wSize = entry->size;
484 double hSize = wSize * hwRatio;
486 QSizeF s( r.viewBoxF().size() );
487 s.scale( wSize, hSize, isFixedAR ? Qt::IgnoreAspectRatio : Qt::KeepAspectRatio );
488 rect = QRectF( -s.width() / 2.0, -s.height() / 2.0, s.width(), s.height() );
490 QPainter p( picture.get() );
491 r.render( &p, rect );
492 entry->picture = std::move( picture );
496 QgsSvgCacheEntry *QgsSvgCache::cacheEntry(
const QString &path,
double size,
const QColor &fill,
const QColor &stroke,
double strokeWidth,
497 double widthScaleFactor,
double fixedAspectRatio,
bool blocking,
bool *isMissingImage )
499 QgsSvgCacheEntry *currentEntry =
findExistingEntry(
new QgsSvgCacheEntry( path, size, strokeWidth, widthScaleFactor, fill, stroke, fixedAspectRatio ) );
501 if ( currentEntry->svgContent.isEmpty() )
503 replaceParamsAndCacheSvg( currentEntry, blocking );
506 if ( isMissingImage )
507 *isMissingImage = currentEntry->isMissingImage;
513 void QgsSvgCache::replaceElemParams( QDomElement &elem,
const QColor &fill,
const QColor &stroke,
double strokeWidth )
521 QDomNamedNodeMap attributes = elem.attributes();
522 int nAttributes = attributes.count();
523 for (
int i = 0; i < nAttributes; ++i )
525 QDomAttr attribute = attributes.item( i ).toAttr();
527 if ( attribute.name().compare( QLatin1String(
"style" ), Qt::CaseInsensitive ) == 0 )
530 QString newAttributeString;
532 QStringList entryList = attribute.value().split(
';' );
533 QStringList::const_iterator entryIt = entryList.constBegin();
534 for ( ; entryIt != entryList.constEnd(); ++entryIt )
536 QStringList keyValueSplit = entryIt->split(
':' );
537 if ( keyValueSplit.size() < 2 )
541 const QString key = keyValueSplit.at( 0 );
542 QString value = keyValueSplit.at( 1 );
543 QString newValue = value;
544 value = value.trimmed().toLower();
546 if ( value.startsWith( QLatin1String(
"param(fill)" ) ) )
548 newValue = fill.name();
550 else if ( value.startsWith( QLatin1String(
"param(fill-opacity)" ) ) )
552 newValue = QString::number( fill.alphaF() );
554 else if ( value.startsWith( QLatin1String(
"param(outline)" ) ) )
556 newValue = stroke.name();
558 else if ( value.startsWith( QLatin1String(
"param(outline-opacity)" ) ) )
560 newValue = QString::number( stroke.alphaF() );
562 else if ( value.startsWith( QLatin1String(
"param(outline-width)" ) ) )
564 newValue = QString::number( strokeWidth );
567 if ( entryIt != entryList.constBegin() )
569 newAttributeString.append(
';' );
571 newAttributeString.append( key +
':' + newValue );
573 elem.setAttribute( attribute.name(), newAttributeString );
577 const QString value = attribute.value().trimmed().toLower();
578 if ( value.startsWith( QLatin1String(
"param(fill)" ) ) )
580 elem.setAttribute( attribute.name(), fill.name() );
582 else if ( value.startsWith( QLatin1String(
"param(fill-opacity)" ) ) )
584 elem.setAttribute( attribute.name(), fill.alphaF() );
586 else if ( value.startsWith( QLatin1String(
"param(outline)" ) ) )
588 elem.setAttribute( attribute.name(), stroke.name() );
590 else if ( value.startsWith( QLatin1String(
"param(outline-opacity)" ) ) )
592 elem.setAttribute( attribute.name(), stroke.alphaF() );
594 else if ( value.startsWith( QLatin1String(
"param(outline-width)" ) ) )
596 elem.setAttribute( attribute.name(), QString::number( strokeWidth ) );
601 QDomNodeList childList = elem.childNodes();
602 int nChildren = childList.count();
603 for (
int i = 0; i < nChildren; ++i )
605 QDomElement childElem = childList.at( i ).toElement();
606 replaceElemParams( childElem, fill, stroke, strokeWidth );
610 void QgsSvgCache::containsElemParams(
const QDomElement &elem,
bool &hasFillParam,
bool &hasDefaultFill, QColor &defaultFill,
611 bool &hasFillOpacityParam,
bool &hasDefaultFillOpacity,
double &defaultFillOpacity,
612 bool &hasStrokeParam,
bool &hasDefaultStroke, QColor &defaultStroke,
613 bool &hasStrokeWidthParam,
bool &hasDefaultStrokeWidth,
double &defaultStrokeWidth,
614 bool &hasStrokeOpacityParam,
bool &hasDefaultStrokeOpacity,
double &defaultStrokeOpacity )
const
622 if ( hasFillParam && hasStrokeParam && hasStrokeWidthParam && hasFillOpacityParam && hasStrokeOpacityParam )
628 QDomNamedNodeMap attributes = elem.attributes();
629 int nAttributes = attributes.count();
631 QStringList valueSplit;
632 for (
int i = 0; i < nAttributes; ++i )
634 QDomAttr attribute = attributes.item( i ).toAttr();
635 if ( attribute.name().compare( QLatin1String(
"style" ), Qt::CaseInsensitive ) == 0 )
638 QStringList entryList = attribute.value().split(
';' );
639 QStringList::const_iterator entryIt = entryList.constBegin();
640 for ( ; entryIt != entryList.constEnd(); ++entryIt )
642 QStringList keyValueSplit = entryIt->split(
':' );
643 if ( keyValueSplit.size() < 2 )
647 QString value = keyValueSplit.at( 1 );
648 valueSplit = value.split(
' ' );
649 if ( !hasFillParam && value.startsWith( QLatin1String(
"param(fill)" ) ) )
652 if ( valueSplit.size() > 1 )
654 defaultFill = QColor( valueSplit.at( 1 ) );
655 hasDefaultFill =
true;
658 else if ( !hasFillOpacityParam && value.startsWith( QLatin1String(
"param(fill-opacity)" ) ) )
660 hasFillOpacityParam =
true;
661 if ( valueSplit.size() > 1 )
664 double opacity = valueSplit.at( 1 ).toDouble( &ok );
667 defaultFillOpacity = opacity;
668 hasDefaultFillOpacity =
true;
672 else if ( !hasStrokeParam && value.startsWith( QLatin1String(
"param(outline)" ) ) )
674 hasStrokeParam =
true;
675 if ( valueSplit.size() > 1 )
677 defaultStroke = QColor( valueSplit.at( 1 ) );
678 hasDefaultStroke =
true;
681 else if ( !hasStrokeWidthParam && value.startsWith( QLatin1String(
"param(outline-width)" ) ) )
683 hasStrokeWidthParam =
true;
684 if ( valueSplit.size() > 1 )
686 defaultStrokeWidth = valueSplit.at( 1 ).toDouble();
687 hasDefaultStrokeWidth =
true;
690 else if ( !hasStrokeOpacityParam && value.startsWith( QLatin1String(
"param(outline-opacity)" ) ) )
692 hasStrokeOpacityParam =
true;
693 if ( valueSplit.size() > 1 )
696 double opacity = valueSplit.at( 1 ).toDouble( &ok );
699 defaultStrokeOpacity = opacity;
700 hasDefaultStrokeOpacity =
true;
708 QString value = attribute.value();
709 valueSplit = value.split(
' ' );
710 if ( !hasFillParam && value.startsWith( QLatin1String(
"param(fill)" ) ) )
713 if ( valueSplit.size() > 1 )
715 defaultFill = QColor( valueSplit.at( 1 ) );
716 hasDefaultFill =
true;
719 else if ( !hasFillOpacityParam && value.startsWith( QLatin1String(
"param(fill-opacity)" ) ) )
721 hasFillOpacityParam =
true;
722 if ( valueSplit.size() > 1 )
725 double opacity = valueSplit.at( 1 ).toDouble( &ok );
728 defaultFillOpacity = opacity;
729 hasDefaultFillOpacity =
true;
733 else if ( !hasStrokeParam && value.startsWith( QLatin1String(
"param(outline)" ) ) )
735 hasStrokeParam =
true;
736 if ( valueSplit.size() > 1 )
738 defaultStroke = QColor( valueSplit.at( 1 ) );
739 hasDefaultStroke =
true;
742 else if ( !hasStrokeWidthParam && value.startsWith( QLatin1String(
"param(outline-width)" ) ) )
744 hasStrokeWidthParam =
true;
745 if ( valueSplit.size() > 1 )
747 defaultStrokeWidth = valueSplit.at( 1 ).toDouble();
748 hasDefaultStrokeWidth =
true;
751 else if ( !hasStrokeOpacityParam && value.startsWith( QLatin1String(
"param(outline-opacity)" ) ) )
753 hasStrokeOpacityParam =
true;
754 if ( valueSplit.size() > 1 )
757 double opacity = valueSplit.at( 1 ).toDouble( &ok );
760 defaultStrokeOpacity = opacity;
761 hasDefaultStrokeOpacity =
true;
769 QDomNodeList childList = elem.childNodes();
770 int nChildren = childList.count();
771 for (
int i = 0; i < nChildren; ++i )
773 QDomElement childElem = childList.at( i ).toElement();
774 containsElemParams( childElem, hasFillParam, hasDefaultFill, defaultFill,
775 hasFillOpacityParam, hasDefaultFillOpacity, defaultFillOpacity,
776 hasStrokeParam, hasDefaultStroke, defaultStroke,
777 hasStrokeWidthParam, hasDefaultStrokeWidth, defaultStrokeWidth,
778 hasStrokeOpacityParam, hasDefaultStrokeOpacity, defaultStrokeOpacity );
782 QSize QgsSvgCache::sizeForImage(
const QgsSvgCacheEntry &entry, QSizeF &viewBoxSize, QSizeF &scaledSize )
const
784 bool isFixedAR = entry.fixedAspectRatio > 0;
786 QSvgRenderer r( entry.svgContent );
787 double hwRatio = 1.0;
788 viewBoxSize = r.viewBoxF().size();
789 if ( viewBoxSize.width() > 0 )
793 hwRatio = entry.fixedAspectRatio;
797 hwRatio = viewBoxSize.height() / viewBoxSize.width();
802 scaledSize.setWidth( entry.size );
803 int wImgSize =
static_cast< int >( scaledSize.width() );
808 scaledSize.setHeight( scaledSize.width() * hwRatio );
809 int hImgSize =
static_cast< int >( scaledSize.height() );
814 return QSize( wImgSize, hImgSize );
817 QImage QgsSvgCache::imageFromCachedPicture(
const QgsSvgCacheEntry &entry )
const
821 QImage image( sizeForImage( entry, viewBoxSize, scaledSize ), QImage::Format_ARGB32_Premultiplied );
824 QPainter p( &image );
825 p.drawPicture( QPoint( 0, 0 ), *entry.picture );