Skip to content

Commit

Permalink
Fix crash when rendering with SVG based symbols and SVG cache is filled
Browse files Browse the repository at this point in the history
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 942f431
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 32 deletions.
99 changes: 67 additions & 32 deletions src/core/symbology/qgssvgcache.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 );
}

Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions src/core/symbology/qgssvgcache.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
1 change: 1 addition & 0 deletions tests/src/core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ SET(TESTS
testqgsstatisticalsummary.cpp
testqgsstringutils.cpp
testqgsstyle.cpp
testqgssvgcache.cpp
testqgssvgmarker.cpp
testqgssymbol.cpp
testqgstaskmanager.cpp
Expand Down
82 changes: 82 additions & 0 deletions tests/src/core/testqgssvgcache.cpp
Original file line number Diff line number Diff line change
@@ -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.