Skip to content
Permalink
Browse files

Check file modified time when retrieving svg images from cache

If file has been modified since the cache, regenerate a new cache
image.

We don't want to check the file modified time too often though,
(e.g., we don't want to check for every point render in a 100k
point file), so use a hardcoded 30 second minimum time between
consecutive file modified checks.

This means that file modifications occuring more often than
every 30 seconds won't be picked up till 30 seconds has elapsed
since the last modification. But at the same time it means that
if the render takes < 30 seconds we'll only check each svg
at most once (and if a render takes > 30 seconds, adding a few
more milliseconds won't hurt!).

Fixes #13565
  • Loading branch information
nyalldawson committed Oct 31, 2017
1 parent b07f675 commit 0b2de85fe74d992b2f38b71df152b9c20ede1c7a
@@ -40,19 +40,26 @@

QgsSvgCacheEntry::QgsSvgCacheEntry( const QString &p, double s, double ow, double wsf, const QColor &fi, const QColor &ou, double far )
: path( p )
, fileModified( QFileInfo( p ).lastModified() )
, size( s )
, strokeWidth( ow )
, widthScaleFactor( wsf )
, fixedAspectRatio( far )
, fill( fi )
, stroke( ou )
{
fileModifiedLastCheckTimer.start();
}

bool QgsSvgCacheEntry::operator==( const QgsSvgCacheEntry &other ) const
{
return other.path == path && qgsDoubleNear( other.size, size ) && qgsDoubleNear( other.strokeWidth, strokeWidth ) && qgsDoubleNear( other.widthScaleFactor, widthScaleFactor )
&& other.fill == fill && other.stroke == stroke;
bool equal = other.path == path && qgsDoubleNear( other.size, size ) && qgsDoubleNear( other.strokeWidth, strokeWidth ) && qgsDoubleNear( other.widthScaleFactor, widthScaleFactor )
&& other.fixedAspectRatio == fixedAspectRatio && other.fill == fill && other.stroke == stroke;

if ( equal && ( mFileModifiedCheckTimeout <= 0 || fileModifiedLastCheckTimer.hasExpired( mFileModifiedCheckTimeout ) ) )
equal = other.fileModified == fileModified;

return equal;
}

int QgsSvgCacheEntry::dataSize() const
@@ -186,6 +193,7 @@ QgsSvgCacheEntry *QgsSvgCache::insertSvg( const QString &path, double size, cons
double widthScaleFactor, double fixedAspectRatio )
{
QgsSvgCacheEntry *entry = new QgsSvgCacheEntry( path, size, strokeWidth, widthScaleFactor, fill, stroke, fixedAspectRatio );
entry->mFileModifiedCheckTimeout = mFileModifiedCheckTimeout;

replaceParamsAndCacheSvg( entry );

@@ -552,7 +560,7 @@ QgsSvgCacheEntry *QgsSvgCache::cacheEntry( const QString &path, double size, con
//search entries in mEntryLookup
QgsSvgCacheEntry *currentEntry = nullptr;
QList<QgsSvgCacheEntry *> entries = mEntryLookup.values( path );

QDateTime modified;
QList<QgsSvgCacheEntry *>::iterator entryIt = entries.begin();
for ( ; entryIt != entries.end(); ++entryIt )
{
@@ -561,6 +569,14 @@ QgsSvgCacheEntry *QgsSvgCache::cacheEntry( const QString &path, double size, con
qgsDoubleNear( cacheEntry->strokeWidth, strokeWidth ) && qgsDoubleNear( cacheEntry->widthScaleFactor, widthScaleFactor ) &&
qgsDoubleNear( cacheEntry->fixedAspectRatio, fixedAspectRatio ) )
{
if ( mFileModifiedCheckTimeout <= 0 || cacheEntry->fileModifiedLastCheckTimer.hasExpired( mFileModifiedCheckTimeout ) )
{
if ( !modified.isValid() )
modified = QFileInfo( path ).lastModified();

if ( cacheEntry->fileModified != modified )
continue;
}
currentEntry = cacheEntry;
break;
}
@@ -27,6 +27,8 @@
#include <QUrl>
#include <QObject>
#include <QSizeF>
#include <QDateTime>
#include <QElapsedTimer>

#include "qgis_core.h"

@@ -46,7 +48,7 @@ class CORE_EXPORT QgsSvgCacheEntry
{
public:

QgsSvgCacheEntry() = default;
QgsSvgCacheEntry() = delete;

/**
* Constructor.
@@ -68,6 +70,13 @@ class CORE_EXPORT QgsSvgCacheEntry

//! Absolute path to SVG file
QString path;

//! Timestamp when file was last modified
QDateTime fileModified;
//! Time since last check of file modified date
QElapsedTimer fileModifiedLastCheckTimer;
int mFileModifiedCheckTimeout = 30000;

double size = 0.0; //size in pixels (cast to int for QImage)
double strokeWidth = 0;
double widthScaleFactor = 1.0;
@@ -248,6 +257,9 @@ class CORE_EXPORT QgsSvgCache : public QObject
//Removes entry from the ordered list (but does not delete the entry itself)
void takeEntryFromList( QgsSvgCacheEntry *entry );

//! Minimum time (in ms) between consecutive svg file modified time checks
int mFileModifiedCheckTimeout = 30000;

//! Entry pointers accessible by file name
QMultiHash< QString, QgsSvgCacheEntry * > mEntryLookup;
//! Estimated total size of all images, pictures and svgContent
@@ -297,6 +309,7 @@ class CORE_EXPORT QgsSvgCache : public QObject
//! Mutex to prevent concurrent access to the class from multiple threads at once (may corrupt the entries otherwise).
QMutex mMutex;

friend class TestQgsSvgCache;
};

#endif // QGSSVGCACHE_H
@@ -23,7 +23,10 @@
#include <QPicture>
#include <QPainter>
#include <QtConcurrent>
#include <QElapsedTimer>
#include "qgssvgcache.h"
#include "qgsmultirenderchecker.h"
#include "qgsapplication.h"

/**
* \ingroup UnitTests
@@ -33,8 +36,11 @@ class TestQgsSvgCache : public QObject
{
Q_OBJECT

public:
TestQgsSvgCache() = default;
private:

QString mReport;

bool imageCheck( const QString &testName, QImage &image, int mismatchCount );

private slots:
void initTestCase();// will be called before the first testfunction is executed.
@@ -44,6 +50,7 @@ class TestQgsSvgCache : public QObject
void fillCache();
void threadSafePicture();
void threadSafeImage();
void changeImage(); //check that cache is updated if svg source file changes

};

@@ -52,10 +59,22 @@ void TestQgsSvgCache::initTestCase()
{
QgsApplication::init();
QgsApplication::initQgis();
mReport += "<h1>QgsSvgCache Tests</h1>\n";
}

void TestQgsSvgCache::cleanupTestCase()
{
QgsApplication::exitQgis();

QString myReportFile = QDir::tempPath() + "/qgistest.html";
QFile myFile( myReportFile );
if ( myFile.open( QIODevice::WriteOnly | QIODevice::Append ) )
{
QTextStream myQTextStream( &myFile );
myQTextStream << mReport;
myFile.close();
//QDesktopServices::openUrl( "file:///" + myReportFile );
}
}

void TestQgsSvgCache::fillCache()
@@ -155,5 +174,86 @@ void TestQgsSvgCache::threadSafeImage()
QtConcurrent::blockingMap( list, RenderImageWrapper( cache, svgPath ) );
}

void TestQgsSvgCache::changeImage()
{
bool inCache;
QgsSvgCache cache;
// no minimum time between checks
cache.mFileModifiedCheckTimeout = 0;

//copy an image to the temp folder
QString tempImagePath = QDir::tempPath() + "/svg_cache.svg";

QString originalImage = TEST_DATA_DIR + QStringLiteral( "/test_symbol_svg.svg" );
if ( QFileInfo::exists( tempImagePath ) )
QFile::remove( tempImagePath );
QFile::copy( originalImage, tempImagePath );

//render it through the cache
QImage img = cache.svgAsImage( tempImagePath, 200, QColor( 0, 0, 0 ), QColor( 0, 0, 0 ), 1.0,
1.0, inCache );
QVERIFY( imageCheck( "svgcache_changed_before", img, 30 ) );

// wait a second so that modified time is different
QElapsedTimer t;
t.start();
while ( !t.hasExpired( 1000 ) )
{}

//replace the image in the temp folder
QString newImage = TEST_DATA_DIR + QStringLiteral( "/test_symbol_svg2.svg" );
QFile::remove( tempImagePath );
QFile::copy( newImage, tempImagePath );

//re-render it
img = cache.svgAsImage( tempImagePath, 200, QColor( 0, 0, 0 ), QColor( 0, 0, 0 ), 1.0,
1.0, inCache );
QVERIFY( imageCheck( "svgcache_changed_after", img, 30 ) );

// repeat, with minimum time between checks
QgsSvgCache cache2;
QFile::remove( tempImagePath );
QFile::copy( originalImage, tempImagePath );
img = cache2.svgAsImage( tempImagePath, 200, QColor( 0, 0, 0 ), QColor( 0, 0, 0 ), 1.0,
1.0, inCache );
QVERIFY( imageCheck( "svgcache_changed_before", img, 30 ) );

// wait a second so that modified time is different
t.restart();
while ( !t.hasExpired( 1000 ) )
{}

//replace the image in the temp folder
QFile::remove( tempImagePath );
QFile::copy( newImage, tempImagePath );

//re-render it - not enough time has elapsed between checks, so file modification time will NOT be rechecked and
// existing cached image should be used
img = cache2.svgAsImage( tempImagePath, 200, QColor( 0, 0, 0 ), QColor( 0, 0, 0 ), 1.0,
1.0, inCache );
QVERIFY( imageCheck( "svgcache_changed_before", img, 30 ) );
}

bool TestQgsSvgCache::imageCheck( const QString &testName, QImage &image, int mismatchCount )
{
//draw background
QImage imageWithBackground( image.width(), image.height(), QImage::Format_RGB32 );
QgsRenderChecker::drawBackground( &imageWithBackground );
QPainter painter( &imageWithBackground );
painter.drawImage( 0, 0, image );
painter.end();

mReport += "<h2>" + testName + "</h2>\n";
QString tempDir = QDir::tempPath() + '/';
QString fileName = tempDir + testName + ".png";
imageWithBackground.save( fileName, "PNG" );
QgsRenderChecker checker;
checker.setControlName( "expected_" + testName );
checker.setRenderedImage( fileName );
checker.setColorTolerance( 2 );
bool resultFlag = checker.compareImages( testName, mismatchCount );
mReport += checker.report();
return resultFlag;
}
QGSTEST_MAIN( TestQgsSvgCache )
#include "testqgssvgcache.moc"
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->

<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="svg2"
inkscape:output_extension="org.inkscape.output.svg.inkscape"
sodipodi:docname="test_symbol_svg2.svg"
inkscape:version="0.91 r13725"
sodipodi:version="0.32"
x="0px"
y="0px"
width="306.33475"
height="484.79999"
viewBox="0 0 306.33475 484.79999"
enable-background="new 0 0 580 580"
xml:space="preserve"><metadata
id="metadata26"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><sodipodi:namedview
showgrid="false"
inkscape:cy="-115.04372"
inkscape:cx="-222.39645"
inkscape:zoom="0.46083856"
pagecolor="#ffffff"
bordercolor="#666666"
guidetolerance="10.0"
objecttolerance="10.0"
gridtolerance="10.0"
borderopacity="1.0"
id="base"
inkscape:current-layer="svg2"
inkscape:window-y="34"
inkscape:window-x="75"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:window-width="1014"
inkscape:window-height="711"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-maximized="0" /><defs
id="defs4" /><g
id="layer3"
transform="matrix(48.14969,0,0,48.14969,-673.10453,-80.905752)"
inkscape:label="Layout"
display="none"
style="display:none"><rect
id="rect4134"
x="1"
y="1"
display="inline"
width="10"
height="10"
style="display:inline;fill:none;stroke:#757575;stroke-width:0.1" /><rect
id="rect4136"
x="2"
y="2"
display="inline"
width="8"
height="8"
style="display:inline;fill:none;stroke:#757575;stroke-width:0.1" /></g><path
fill="param(fill)"
stroke="param(outline)"
stroke-width="param(outline-width)"
d="m 306.29574,465.246 c -0.076,10.965 -9.608,19.554 -21.7,19.554 l -0.179,-0.001 c -10.191,0 -18.79,-6.488 -20.891,-15.586 -14.36,0.915 -21.136,3.602 -28.298,6.441 -8.834,3.503 -17.969,7.125 -41.421,8.282 l -77.79,0.72 C 67.792742,484.118 28.808742,456.176 0.14574201,401.607 c -0.269,-0.512 -0.158,-1.14 0.27,-1.527 0.428,-0.389 1.06299999,-0.438 1.54599999,-0.123 l 2.563,1.678 c 0.188,0.123 0.34,0.293 0.44,0.494 9.638,19.316 45.785,36.26 77.355,36.26 20.794998,0 35.283998,-7.488 40.814998,-21.088 1.526,-7.885 9.78,-15.345 18.753,-17.111 l 0.232,-136.159 c -8.047,-0.854 -16.265,-9.484 -16.429,-17.656 l -4.264,-224.286 c -0.129,-6.844 1.612,-12.169 5.175,-15.83 5.975,-6.138 15.568,-6.214 21.3,-6.259 0.004,0 0.008,0 0.012,0 l 41.061,0.062 c 6.554,0.03 15.794,2.78 21.66,8.855 4.101,4.246 6.084,9.527 5.896,15.693 l -6.164,221.815 c -0.216,8.035 -8.456,16.616 -16.454,17.507 l -0.179,136.605 c 12.057,0.934 20.348,6.056 26.027,16.053 4.234,7.45 10.999,10.773 21.935,10.773 4.717,0 9.758,-0.598 14.633,-1.176 2.118,-0.251 4.297,-0.51 6.422,-0.715 l -0.037,-5.658 c 0.075,-10.813 9.862,-19.609 21.854,-19.609 12.018,0.019 21.782,8.835 21.767,19.652 l -0.039,45.389 z"
id="path24"
inkscape:connector-curvature="0" /></svg>

0 comments on commit 0b2de85

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