Skip to content

Commit da0de64

Browse files
committed
QgsSvgCache fetches remote SVG files in a background task
Previously QgsSvgCache would often try to fetch remote images using a network request on the main thread, by calling processEvents repeatedly until the request was complete. This caused lots of bugs, since the main thread processEvents would proceed with all kinds of stuff assuming that the svg fetch operation was complete, leading to frequent crashes and deadlocks and making remote svg use impossible (it's likely that the SVG cache remote fetching code was written in the pre-multi-threaded rendering era). There's no way to fix this with async svg fetching - we HAVE to remove the processEvents call, and a QEventLoop won't help either (since the method may be called on the main thread). Accordingly the only solution is to fetch the requested svg in the background, and return a temporary "downloading" svg for use in the meantime. We use a QgsNetworkContentFetcherTask to do this, so it's nicely integrated with task manager. A request task is fired up when a remote svg is requested for the first time, with the temporary downloading svg returned for use by the caller asynchronously. QgsSvgCache then emits the remoteSvgFetched signal when a previously requested remote SVG has been successfully fetched, triggering a map canvas redraw with the correct SVG graphic. Fixes #18504 (cherry-picked from 45c400c)
1 parent 5de0bdc commit da0de64

File tree

6 files changed

+113
-67
lines changed

6 files changed

+113
-67
lines changed

python/core/symbology/qgssvgcache.sip.in

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,13 @@ Get SVG content
138138
void statusChanged( const QString &statusQString );
139139
%Docstring
140140
Emit a signal to be caught by qgisapp and display a msg on status bar
141+
%End
142+
143+
void remoteSvgFetched( const QString &url );
144+
%Docstring
145+
Emitted when the cache has finished retrieving an SVG file from a remote ``url``.
146+
147+
.. versionadded:: 3.2
141148
%End
142149

143150
};

python/gui/qgsmapcanvas.sip.in

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -112,13 +112,6 @@ Check whether images of rendered layers are curerently being cached
112112
Make sure to remove any rendered images from cache (does nothing if cache is not enabled)
113113

114114
.. versionadded:: 2.4
115-
%End
116-
117-
void refreshAllLayers();
118-
%Docstring
119-
Reload all layers, clear the cache and refresh the canvas
120-
121-
.. versionadded:: 2.9
122115
%End
123116

124117
void waitWhileRendering();
@@ -721,6 +714,13 @@ for the canvas.
721714
void refresh();
722715
%Docstring
723716
Repaints the canvas map
717+
%End
718+
719+
void refreshAllLayers();
720+
%Docstring
721+
Reload all layers, clear the cache and refresh the canvas
722+
723+
.. versionadded:: 2.9
724724
%End
725725

726726
void selectionChangedSlot();

src/core/symbology/qgssvgcache.cpp

Lines changed: 71 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include "qgsnetworkaccessmanager.h"
2222
#include "qgsmessagelog.h"
2323
#include "qgssymbollayerutils.h"
24+
#include "qgsnetworkcontentfetchertask.h"
2425

2526
#include <QApplication>
2627
#include <QCoreApplication>
@@ -80,8 +81,10 @@ int QgsSvgCacheEntry::dataSize() const
8081

8182
QgsSvgCache::QgsSvgCache( QObject *parent )
8283
: QObject( parent )
84+
, mMutex( QMutex::Recursive )
8385
{
8486
mMissingSvg = QStringLiteral( "<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>?</text></svg>" ).toLatin1();
87+
mFetchingSvg = QStringLiteral( "<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>x</text></svg>" ).toLatin1();
8588
}
8689

8790
QgsSvgCache::~QgsSvgCache()
@@ -406,77 +409,70 @@ QByteArray QgsSvgCache::getImageData( const QString &path ) const
406409
return mMissingSvg;
407410
}
408411

409-
// the url points to a remote resource, download it!
410-
QNetworkReply *reply = nullptr;
412+
QMutexLocker locker( &mMutex );
413+
414+
// already a request in progress for this url
415+
if ( mPendingRemoteUrls.contains( path ) )
416+
return mFetchingSvg;
411417

412-
// The following code blocks until the file is downloaded...
413-
// TODO: use signals to get reply finished notification, in this moment
414-
// it's executed while rendering.
415-
while ( true )
418+
if ( mRemoteContentCache.contains( path ) )
416419
{
417-
QgsDebugMsg( QString( "get svg: %1" ).arg( svgUrl.toString() ) );
418-
QNetworkRequest request( svgUrl );
419-
request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
420-
request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
420+
// already fetched this content - phew. Just return what we already got.
421+
return *mRemoteContentCache[ path ];
422+
}
421423

422-
reply = QgsNetworkAccessManager::instance()->get( request );
423-
connect( reply, &QNetworkReply::downloadProgress, this, &QgsSvgCache::downloadProgress );
424+
mPendingRemoteUrls.insert( path );
425+
//fire up task to fetch image in background
426+
QNetworkRequest request( svgUrl );
427+
request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache );
428+
request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true );
424429

425-
//emit statusChanged( tr( "Downloading svg." ) );
430+
QgsNetworkContentFetcherTask *task = new QgsNetworkContentFetcherTask( request );
431+
connect( task, &QgsNetworkContentFetcherTask::fetched, this, [this, task, path]
432+
{
433+
QMutexLocker locker( &mMutex );
426434

427-
// wait until the image download finished
428-
// TODO: connect to the reply->finished() signal
429-
while ( !reply->isFinished() )
435+
QNetworkReply *reply = task->reply();
436+
if ( !reply )
430437
{
431-
QCoreApplication::processEvents( QEventLoop::ExcludeUserInputEvents, 500 );
438+
// cancelled
439+
QMetaObject::invokeMethod( const_cast< QgsSvgCache * >( this ), "onRemoteSvgFetched", Qt::QueuedConnection, Q_ARG( QString, path ), Q_ARG( bool, false ) );
440+
return;
432441
}
433442

434443
if ( reply->error() != QNetworkReply::NoError )
435444
{
436445
QgsMessageLog::logMessage( tr( "SVG request failed [error: %1 - url: %2]" ).arg( reply->errorString(), path ), tr( "SVG" ) );
437-
reply->deleteLater();
438-
return mMissingSvg;
446+
return;
439447
}
440448

441-
QVariant redirect = reply->attribute( QNetworkRequest::RedirectionTargetAttribute );
442-
if ( redirect.isNull() )
449+
QVariant status = reply->attribute( QNetworkRequest::HttpStatusCodeAttribute );
450+
if ( !status.isNull() && status.toInt() >= 400 )
443451
{
444-
// neither network error nor redirection
445-
// TODO: cache the image
446-
break;
452+
QVariant phrase = reply->attribute( QNetworkRequest::HttpReasonPhraseAttribute );
453+
QgsMessageLog::logMessage( tr( "SVG request error [status: %1 - reason phrase: %2] for %3" ).arg( status.toInt() ).arg( phrase.toString(), path ), tr( "SVG" ) );
454+
mRemoteContentCache.insert( path, new QByteArray( mMissingSvg ) );
455+
return;
447456
}
448457

449-
// do a new request to the redirect url
450-
svgUrl = redirect.toUrl();
451-
reply->deleteLater();
452-
}
453-
454-
QVariant status = reply->attribute( QNetworkRequest::HttpStatusCodeAttribute );
455-
if ( !status.isNull() && status.toInt() >= 400 )
456-
{
457-
QVariant phrase = reply->attribute( QNetworkRequest::HttpReasonPhraseAttribute );
458-
QgsMessageLog::logMessage( tr( "SVG request error [status: %1 - reason phrase: %2] for %3" ).arg( status.toInt() ).arg( phrase.toString(), path ), tr( "SVG" ) );
459-
460-
reply->deleteLater();
461-
return mMissingSvg;
462-
}
463-
464-
// we accept both real SVG mime types AND plain text types - because some sites
465-
// (notably github) serve up svgs as raw text
466-
QString contentType = reply->header( QNetworkRequest::ContentTypeHeader ).toString();
467-
if ( !contentType.startsWith( QLatin1String( "image/svg+xml" ), Qt::CaseInsensitive )
468-
&& !contentType.startsWith( QLatin1String( "text/plain" ), Qt::CaseInsensitive ) )
469-
{
470-
QgsMessageLog::logMessage( tr( "Unexpected MIME type %1 received for %2" ).arg( contentType, path ), tr( "SVG" ) );
471-
reply->deleteLater();
472-
return mMissingSvg;
473-
}
458+
// we accept both real SVG mime types AND plain text types - because some sites
459+
// (notably github) serve up svgs as raw text
460+
QString contentType = reply->header( QNetworkRequest::ContentTypeHeader ).toString();
461+
if ( !contentType.startsWith( QLatin1String( "image/svg+xml" ), Qt::CaseInsensitive )
462+
&& !contentType.startsWith( QLatin1String( "text/plain" ), Qt::CaseInsensitive ) )
463+
{
464+
QgsMessageLog::logMessage( tr( "Unexpected MIME type %1 received for %2" ).arg( contentType, path ), tr( "SVG" ) );
465+
mRemoteContentCache.insert( path, new QByteArray( mMissingSvg ) );
466+
return;
467+
}
474468

475-
// read the image data
476-
QByteArray ba = reply->readAll();
477-
reply->deleteLater();
469+
// read the image data
470+
mRemoteContentCache.insert( path, new QByteArray( reply->readAll() ) );
471+
QMetaObject::invokeMethod( const_cast< QgsSvgCache * >( this ), "onRemoteSvgFetched", Qt::QueuedConnection, Q_ARG( QString, path ), Q_ARG( bool, true ) );
472+
} );
478473

479-
return ba;
474+
QgsApplication::taskManager()->addTask( task );
475+
return mFetchingSvg;
480476
}
481477

482478
void QgsSvgCache::cacheImage( QgsSvgCacheEntry *entry )
@@ -998,3 +994,25 @@ void QgsSvgCache::downloadProgress( qint64 bytesReceived, qint64 bytesTotal )
998994
QgsDebugMsg( msg );
999995
emit statusChanged( msg );
1000996
}
997+
998+
void QgsSvgCache::onRemoteSvgFetched( const QString &url, bool success )
999+
{
1000+
QMutexLocker locker( &mMutex );
1001+
mPendingRemoteUrls.remove( url );
1002+
1003+
QgsSvgCacheEntry *nextEntry = mLeastRecentEntry;
1004+
while ( QgsSvgCacheEntry *entry = nextEntry )
1005+
{
1006+
nextEntry = entry->nextEntry;
1007+
if ( entry->path == url )
1008+
{
1009+
takeEntryFromList( entry );
1010+
mEntryLookup.remove( entry->path, entry );
1011+
mTotalSize -= entry->dataSize();
1012+
delete entry;
1013+
}
1014+
}
1015+
1016+
if ( success )
1017+
emit remoteSvgFetched( url );
1018+
}

src/core/symbology/qgssvgcache.h

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
#include <QElapsedTimer>
3232
#include <QPicture>
3333
#include <QImage>
34+
#include <QCache>
35+
#include <QSet>
3436

3537
#include "qgis_core.h"
3638

@@ -226,9 +228,17 @@ class CORE_EXPORT QgsSvgCache : public QObject
226228
//! Emit a signal to be caught by qgisapp and display a msg on status bar
227229
void statusChanged( const QString &statusQString );
228230

231+
/**
232+
* Emitted when the cache has finished retrieving an SVG file from a remote \a url.
233+
* \since QGIS 3.2
234+
*/
235+
void remoteSvgFetched( const QString &url );
236+
229237
private slots:
230238
void downloadProgress( qint64, qint64 );
231239

240+
void onRemoteSvgFetched( const QString &url, bool success );
241+
232242
private:
233243

234244
/**
@@ -303,11 +313,18 @@ class CORE_EXPORT QgsSvgCache : public QObject
303313
*/
304314
QImage imageFromCachedPicture( const QgsSvgCacheEntry &entry ) const;
305315

316+
QByteArray fetchImageData( const QString &path, bool &ok ) const;
317+
306318
//! SVG content to be rendered if SVG file was not found.
307319
QByteArray mMissingSvg;
308320

321+
QByteArray mFetchingSvg;
322+
309323
//! Mutex to prevent concurrent access to the class from multiple threads at once (may corrupt the entries otherwise).
310-
QMutex mMutex;
324+
mutable QMutex mMutex;
325+
326+
mutable QCache< QString, QByteArray > mRemoteContentCache;
327+
mutable QSet< QString > mPendingRemoteUrls;
311328

312329
friend class TestQgsSvgCache;
313330
};

src/gui/qgsmapcanvas.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ email : sherman at mrcc.com
6565
#include "qgsvectorlayer.h"
6666
#include "qgsmapthemecollection.h"
6767
#include "qgscoordinatetransformcontext.h"
68+
#include "qgssvgcache.h"
6869
#include <cmath>
6970

7071
/**
@@ -148,6 +149,9 @@ QgsMapCanvas::QgsMapCanvas( QWidget *parent )
148149
refresh();
149150
} );
150151

152+
// refresh canvas when a remote svg has finished downloading
153+
connect( QgsApplication::svgCache(), &QgsSvgCache::remoteSvgFetched, this, &QgsMapCanvas::refreshAllLayers );
154+
151155
//segmentation parameters
152156
QgsSettings settings;
153157
double segmentationTolerance = settings.value( QStringLiteral( "qgis/segmentationTolerance" ), "0.01745" ).toDouble();

src/gui/qgsmapcanvas.h

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -157,12 +157,6 @@ class GUI_EXPORT QgsMapCanvas : public QGraphicsView
157157
*/
158158
void clearCache();
159159

160-
/**
161-
* Reload all layers, clear the cache and refresh the canvas
162-
* \since QGIS 2.9
163-
*/
164-
void refreshAllLayers();
165-
166160
/**
167161
* Blocks until the rendering job has finished.
168162
*
@@ -643,6 +637,12 @@ class GUI_EXPORT QgsMapCanvas : public QGraphicsView
643637
//! Repaints the canvas map
644638
void refresh();
645639

640+
/**
641+
* Reload all layers, clear the cache and refresh the canvas
642+
* \since QGIS 2.9
643+
*/
644+
void refreshAllLayers();
645+
646646
//! Receives signal about selection change, and pass it on with layer info
647647
void selectionChangedSlot();
648648

0 commit comments

Comments
 (0)