Skip to content
Permalink
Browse files

Fix crash when rendering with SVG based symbols and SVG cache is filled

When the svg cache was full, any attempt to render an svg from the
cache would crash QGIS.

Fixes #16643
  • Loading branch information
nyalldawson committed Oct 30, 2017
1 parent b3ef4da commit 942f431f0f6c9d3a38f6a9b3b8984bb5b29e12bb
@@ -101,6 +101,8 @@ QImage QgsSvgCache::svgAsImage( const QString &file, double size, const QColor &
fitsInCache = true;
QgsSvgCacheEntry *currentEntry = cacheEntry( file, size, fill, stroke, strokeWidth, widthScaleFactor, fixedAspectRatio );

QImage result;

//if current entry image is 0: cache image for entry
// checks to see if image will fit into cache
//update stats for memory usage
@@ -134,15 +136,23 @@ QImage QgsSvgCache::svgAsImage( const QString &file, double size, const QColor &
{
cachePicture( currentEntry, false );
}

// ...and render cached picture to result image
result = imageFromCachedPicture( *currentEntry );
}
else
{
cacheImage( currentEntry );
result = *( currentEntry->image );
}
trimToMaximumSize();
}
else
{
result = *( currentEntry->image );
}

return *( currentEntry->image );
return result;
}

QPicture QgsSvgCache::svgAsPicture( const QString &path, double size, const QColor &fill, const QColor &stroke, double strokeWidth,
@@ -479,47 +489,25 @@ void QgsSvgCache::cacheImage( QgsSvgCacheEntry *entry )
delete entry->image;
entry->image = nullptr;

bool isFixedAR = entry->fixedAspectRatio > 0;
QSizeF viewBoxSize;
QSizeF scaledSize;
QSize imageSize = sizeForImage( *entry, viewBoxSize, scaledSize );

QSvgRenderer r( entry->svgContent );
double hwRatio = 1.0;
if ( r.viewBoxF().width() > 0 )
{
if ( isFixedAR )
{
hwRatio = entry->fixedAspectRatio;
}
else
{
hwRatio = r.viewBoxF().height() / r.viewBoxF().width();
}
}
double wSize = entry->size;
int wImgSize = static_cast< int >( wSize );
if ( wImgSize < 1 )
{
wImgSize = 1;
}
double hSize = wSize * hwRatio;
int hImgSize = static_cast< int >( hSize );
if ( hImgSize < 1 )
{
hImgSize = 1;
}
// cast double image sizes to int for QImage
QImage *image = new QImage( wImgSize, hImgSize, QImage::Format_ARGB32_Premultiplied );
QImage *image = new QImage( imageSize, QImage::Format_ARGB32_Premultiplied );
image->fill( 0 ); // transparent background

QPainter p( image );
if ( qgsDoubleNear( r.viewBoxF().width(), r.viewBoxF().height() ) )
QSvgRenderer r( entry->svgContent );
if ( qgsDoubleNear( viewBoxSize.width(), viewBoxSize.height() ) )
{
r.render( &p );
}
else
{
QSizeF s( r.viewBoxF().size() );
s.scale( wSize, hSize, Qt::KeepAspectRatio );
QRectF rect( ( wImgSize - s.width() ) / 2, ( hImgSize - s.height() ) / 2, s.width(), s.height() );
QSizeF s( viewBoxSize );
s.scale( scaledSize.width(), scaledSize.height(), Qt::KeepAspectRatio );
QRectF rect( ( imageSize.width() - s.width() ) / 2, ( imageSize.height() - s.height() ) / 2, s.width(), s.height() );
r.render( &p, rect );
}

@@ -906,6 +894,53 @@ void QgsSvgCache::printEntryList()
}
}

QSize QgsSvgCache::sizeForImage( const QgsSvgCacheEntry &entry, QSizeF &viewBoxSize, QSizeF &scaledSize ) const
{
bool isFixedAR = entry.fixedAspectRatio > 0;

QSvgRenderer r( entry.svgContent );
double hwRatio = 1.0;
viewBoxSize = r.viewBoxF().size();
if ( viewBoxSize.width() > 0 )
{
if ( isFixedAR )
{
hwRatio = entry.fixedAspectRatio;
}
else
{
hwRatio = viewBoxSize.height() / viewBoxSize.width();
}
}

// cast double image sizes to int for QImage
scaledSize.setWidth( entry.size );
int wImgSize = static_cast< int >( scaledSize.width() );
if ( wImgSize < 1 )
{
wImgSize = 1;
}
scaledSize.setHeight( scaledSize.width() * hwRatio );
int hImgSize = static_cast< int >( scaledSize.height() );
if ( hImgSize < 1 )
{
hImgSize = 1;
}
return QSize( wImgSize, hImgSize );
}

QImage QgsSvgCache::imageFromCachedPicture( const QgsSvgCacheEntry &entry ) const
{
QSizeF viewBoxSize;
QSizeF scaledSize;
QImage image( sizeForImage( entry, viewBoxSize, scaledSize ), QImage::Format_ARGB32_Premultiplied );
image.fill( 0 ); // transparent background

QPainter p( &image );
p.drawPicture( QPoint( 0, 0 ), *entry.picture );
return image;
}

void QgsSvgCache::trimToMaximumSize()
{
//only one entry in cache
@@ -275,6 +275,17 @@ class CORE_EXPORT QgsSvgCache : public QObject
//! For debugging
void printEntryList();

/**
* Returns the target size (in pixels) and calculates the \a viewBoxSize
* for a cache \a entry.
*/
QSize sizeForImage( const QgsSvgCacheEntry &entry, QSizeF &viewBoxSize, QSizeF &scaledSize ) const;

/**
* Returns a rendered image for a cached picture \a entry.
*/
QImage imageFromCachedPicture( const QgsSvgCacheEntry &entry ) const;

//! SVG content to be rendered if SVG file was not found.
QByteArray mMissingSvg;

@@ -180,6 +180,7 @@ SET(TESTS
testqgsstatisticalsummary.cpp
testqgsstringutils.cpp
testqgsstyle.cpp
testqgssvgcache.cpp
testqgssvgmarker.cpp
testqgssymbol.cpp
testqgstaskmanager.cpp
@@ -0,0 +1,82 @@
/***************************************************************************
testqgssvgcache.cpp
--------------------
Date : October 2017
Copyright : (C) 2017 by Nyall Dawson
Email : nyall dot dawson at gmail dot com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include "qgstest.h"
#include <QObject>
#include <QString>
#include <QStringList>
#include <QApplication>
#include <QFileInfo>
#include <QDir>
#include <QDesktopServices>

#include "qgssvgcache.h"

/**
* \ingroup UnitTests
* This is a unit test for QgsSvgCache.
*/
class TestQgsSvgCache : public QObject
{
Q_OBJECT

public:
TestQgsSvgCache() = default;

private slots:
void initTestCase();// will be called before the first testfunction is executed.
void cleanupTestCase();// will be called after the last testfunction was executed.
void init() {} // will be called before each testfunction is executed.
void cleanup() {} // will be called after every testfunction.
void fillCache();

};


void TestQgsSvgCache::initTestCase()
{
QgsApplication::init();
QgsApplication::initQgis();
}

void TestQgsSvgCache::cleanupTestCase()
{
}

void TestQgsSvgCache::fillCache()
{
QgsSvgCache cache;
// flood cache to fill it
QString svgPath = TEST_DATA_DIR + QStringLiteral( "/sample_svg.svg" );
bool fitInCache = false;

// we loop forever, continually increasing the size of the requested
// svg render. The continually changing image size should quickly fill
// the svg cache size, forcing use of non-cached images.
// We break after hitting a certain threshold of non-cached images,
// (after testing that the result is non-null, i.e. rendered on demand,
// not from cache).
int uncached = 0;
for ( double size = 1000; uncached < 10; size += 100 )
{
QImage image = cache.svgAsImage( svgPath, size, QColor( 255, 0, 0 ), QColor( 0, 255, 0 ), 1, 1, fitInCache );
QVERIFY( !image.isNull() );
if ( !fitInCache )
uncached++;
}
}


QGSTEST_MAIN( TestQgsSvgCache )
#include "testqgssvgcache.moc"

0 comments on commit 942f431

Please sign in to comment.
You can’t perform that action at this time.