QGIS API Documentation 3.29.0-Master (006c3c0232)
qgsimagecache.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsimagecache.cpp
3 -----------------
4 begin : December 2018
5 copyright : (C) 2018 by Nyall Dawson
6 email : nyall dot dawson at gmail dot com
7 ***************************************************************************/
8
9/***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
18#include "qgsimagecache.h"
19
20#include "qgis.h"
21#include "qgsimageoperation.h"
22#include "qgslogger.h"
24#include "qgsmessagelog.h"
26#include "qgssettings.h"
27
28#include <QApplication>
29#include <QCoreApplication>
30#include <QCursor>
31#include <QDomDocument>
32#include <QDomElement>
33#include <QFile>
34#include <QImage>
35#include <QPainter>
36#include <QPicture>
37#include <QFileInfo>
38#include <QNetworkReply>
39#include <QNetworkRequest>
40#include <QBuffer>
41#include <QImageReader>
42#include <QSvgRenderer>
43#include <QTemporaryDir>
44#include <QUuid>
45
47
48QgsImageCacheEntry::QgsImageCacheEntry( const QString &path, QSize size, const bool keepAspectRatio, const double opacity, double dpi, int frameNumber )
50 , size( size )
51 , keepAspectRatio( keepAspectRatio )
52 , opacity( opacity )
53 , targetDpi( dpi )
54 , frameNumber( frameNumber )
55{
56}
57
58bool QgsImageCacheEntry::isEqual( const QgsAbstractContentCacheEntry *other ) const
59{
60 const QgsImageCacheEntry *otherImage = dynamic_cast< const QgsImageCacheEntry * >( other );
61 // cheapest checks first!
62 if ( !otherImage
63 || otherImage->keepAspectRatio != keepAspectRatio
64 || otherImage->frameNumber != frameNumber
65 || otherImage->size != size
66 || ( !size.isValid() && otherImage->targetDpi != targetDpi )
67 || otherImage->opacity != opacity
68 || otherImage->path != path )
69 return false;
70
71 return true;
72}
73
74int QgsImageCacheEntry::dataSize() const
75{
76 int size = 0;
77 if ( !image.isNull() )
78 {
79 size += image.sizeInBytes();
80 }
81 return size;
82}
83
84void QgsImageCacheEntry::dump() const
85{
86 QgsDebugMsgLevel( QStringLiteral( "path: %1, size %2x%3" ).arg( path ).arg( size.width() ).arg( size.height() ), 3 );
87}
88
90
92 : QgsAbstractContentCache< QgsImageCacheEntry >( parent, QObject::tr( "Image" ) )
93{
94 mTemporaryDir.reset( new QTemporaryDir() );
95
96 const int bytes = QgsSettings().value( QStringLiteral( "/qgis/maxImageCacheSize" ), 0 ).toInt();
97 if ( bytes > 0 )
98 {
99 mMaxCacheSize = bytes;
100 }
101 else
102 {
103 const int sysMemory = QgsApplication::systemMemorySizeMb();
104 if ( sysMemory > 0 )
105 {
106 if ( sysMemory >= 32000 ) // 32 gb RAM (or more) = 500mb cache size
107 mMaxCacheSize = 500000000;
108 else if ( sysMemory >= 16000 ) // 16 gb RAM = 250mb cache size
109 mMaxCacheSize = 250000000;
110 else
111 mMaxCacheSize = 104857600; // otherwise default to 100mb cache size
112 }
113 }
114
115 mMissingSvg = QStringLiteral( "<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>?</text></svg>" ).toLatin1();
116
117 const QString downloadingSvgPath = QgsApplication::defaultThemePath() + QStringLiteral( "downloading_svg.svg" );
118 if ( QFile::exists( downloadingSvgPath ) )
119 {
120 QFile file( downloadingSvgPath );
121 if ( file.open( QIODevice::ReadOnly ) )
122 {
123 mFetchingSvg = file.readAll();
124 }
125 }
126
127 if ( mFetchingSvg.isEmpty() )
128 {
129 mFetchingSvg = QStringLiteral( "<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>?</text></svg>" ).toLatin1();
130 }
131
133}
134
136
137QImage QgsImageCache::pathAsImage( const QString &f, const QSize size, const bool keepAspectRatio, const double opacity, bool &fitsInCache, bool blocking, double targetDpi, int frameNumber, bool *isMissing )
138{
139 int totalFrameCount = -1;
140 int nextFrameDelayMs = 0;
141 return pathAsImagePrivate( f, size, keepAspectRatio, opacity, fitsInCache, blocking, targetDpi, frameNumber, isMissing, totalFrameCount, nextFrameDelayMs );
142}
143
144QImage QgsImageCache::pathAsImagePrivate( const QString &f, const QSize size, const bool keepAspectRatio, const double opacity, bool &fitsInCache, bool blocking, double targetDpi, int frameNumber, bool *isMissing, int &totalFrameCount, int &nextFrameDelayMs )
145{
146 QString file = f.trimmed();
147 if ( isMissing )
148 *isMissing = true;
149
150 if ( file.isEmpty() )
151 return QImage();
152
153 const QMutexLocker locker( &mMutex );
154
155 const auto extractedAnimationIt = mExtractedAnimationPaths.constFind( file );
156 if ( extractedAnimationIt != mExtractedAnimationPaths.constEnd() )
157 {
158 file = QDir( extractedAnimationIt.value() ).filePath( QStringLiteral( "frame_%1.png" ).arg( frameNumber ) );
159 frameNumber = -1;
160 }
161
162 fitsInCache = true;
163
164 QgsImageCacheEntry *currentEntry = findExistingEntry( new QgsImageCacheEntry( file, size, keepAspectRatio, opacity, targetDpi, frameNumber ) );
165
166 QImage result;
167
168 //if current entry image is null: create the image
169 // checks to see if image will fit into cache
170 //update stats for memory usage
171 if ( currentEntry->image.isNull() )
172 {
173 long cachedDataSize = 0;
174 bool isBroken = false;
175 result = renderImage( file, size, keepAspectRatio, opacity, targetDpi, frameNumber, isBroken, totalFrameCount, nextFrameDelayMs, blocking );
176 cachedDataSize += result.sizeInBytes();
177 if ( cachedDataSize > mMaxCacheSize / 2 )
178 {
179 fitsInCache = false;
180 currentEntry->image = QImage();
181 }
182 else
183 {
184 mTotalSize += result.sizeInBytes();
185 currentEntry->image = result;
186 currentEntry->totalFrameCount = totalFrameCount;
187 currentEntry->nextFrameDelay = nextFrameDelayMs;
188 }
189
190 if ( isMissing )
191 *isMissing = isBroken;
192 currentEntry->isMissingImage = isBroken;
193
195 }
196 else
197 {
198 result = currentEntry->image;
199 totalFrameCount = currentEntry->totalFrameCount;
200 nextFrameDelayMs = currentEntry->nextFrameDelay;
201 if ( isMissing )
202 *isMissing = currentEntry->isMissingImage;
203 }
204
205 return result;
206}
207
208QSize QgsImageCache::originalSize( const QString &path, bool blocking ) const
209{
210 if ( path.isEmpty() )
211 return QSize();
212
213 // direct read if path is a file -- maybe more efficient than going the bytearray route? (untested!)
214 if ( !path.startsWith( QLatin1String( "base64:" ) ) && QFile::exists( path ) )
215 {
216 const QImageReader reader( path );
217 if ( reader.size().isValid() )
218 return reader.size();
219 else
220 return QImage( path ).size();
221 }
222 else
223 {
224 QByteArray ba = getContent( path, QByteArray( "broken" ), QByteArray( "fetching" ), blocking );
225
226 if ( ba != "broken" && ba != "fetching" )
227 {
228 QBuffer buffer( &ba );
229 buffer.open( QIODevice::ReadOnly );
230
231 QImageReader reader( &buffer );
232 // if QImageReader::size works, then it's more efficient as it doesn't
233 // read the whole image (see Qt docs)
234 const QSize s = reader.size();
235 if ( s.isValid() )
236 return s;
237 const QImage im = reader.read();
238 return im.isNull() ? QSize() : im.size();
239 }
240 }
241 return QSize();
242}
243
244int QgsImageCache::totalFrameCount( const QString &path, bool blocking )
245{
246 const QString file = path.trimmed();
247
248 if ( file.isEmpty() )
249 return -1;
250
251 const QMutexLocker locker( &mMutex );
252
253 auto it = mTotalFrameCounts.find( path );
254 if ( it != mTotalFrameCounts.end() )
255 return it.value(); // already prepared
256
257 int res = -1;
258 int nextFrameDelayMs = 0;
259 bool fitsInCache = false;
260 bool isMissing = false;
261 ( void )pathAsImagePrivate( file, QSize(), true, 1.0, fitsInCache, blocking, 96, 0, &isMissing, res, nextFrameDelayMs );
262
263 return res;
264}
265
266int QgsImageCache::nextFrameDelay( const QString &path, int currentFrame, bool blocking )
267{
268 const QString file = path.trimmed();
269
270 if ( file.isEmpty() )
271 return -1;
272
273 const QMutexLocker locker( &mMutex );
274
275 auto it = mImageDelays.find( path );
276 if ( it != mImageDelays.end() )
277 return it.value().value( currentFrame ); // already prepared
278
279 int frameCount = -1;
280 int nextFrameDelayMs = 0;
281 bool fitsInCache = false;
282 bool isMissing = false;
283 const QImage res = pathAsImagePrivate( file, QSize(), true, 1.0, fitsInCache, blocking, 96, currentFrame, &isMissing, frameCount, nextFrameDelayMs );
284
285 return nextFrameDelayMs <= 0 || res.isNull() ? -1 : nextFrameDelayMs;
286}
287
288void QgsImageCache::prepareAnimation( const QString &path )
289{
290 const QMutexLocker locker( &mMutex );
291
292 auto it = mExtractedAnimationPaths.find( path );
293 if ( it != mExtractedAnimationPaths.end() )
294 return; // already prepared
295
296 QString filePath;
297 std::unique_ptr< QImageReader > reader;
298 std::unique_ptr< QBuffer > buffer;
299
300 if ( !path.startsWith( QLatin1String( "base64:" ) ) && QFile::exists( path ) )
301 {
302 const QString basePart = QFileInfo( path ).baseName();
303 int id = 1;
304 filePath = mTemporaryDir->filePath( QStringLiteral( "%1_%2" ).arg( basePart ).arg( id ) );
305 while ( QFile::exists( filePath ) )
306 filePath = mTemporaryDir->filePath( QStringLiteral( "%1_%2" ).arg( basePart ).arg( ++id ) );
307
308 reader = std::make_unique< QImageReader >( path );
309 }
310 else
311 {
312 QByteArray ba = getContent( path, QByteArray( "broken" ), QByteArray( "fetching" ), false );
313 if ( ba == "broken" || ba == "fetching" )
314 {
315 return;
316 }
317 else
318 {
319 const QString path = QUuid::createUuid().toString( QUuid::WithoutBraces );
320 filePath = mTemporaryDir->filePath( path );
321
322 buffer = std::make_unique< QBuffer >( &ba );
323 buffer->open( QIODevice::ReadOnly );
324 reader = std::make_unique< QImageReader> ( buffer.get() );
325 }
326 }
327
328 QDir().mkpath( filePath );
329 mExtractedAnimationPaths.insert( path, filePath );
330
331 const QDir frameDirectory( filePath );
332 // extract all the frames to separate images
333
334 reader->setAutoTransform( true );
335 int frameNumber = 0;
336 while ( true )
337 {
338 const QImage frame = reader->read();
339 if ( frame.isNull() )
340 break;
341
342 mImageDelays[ path ].append( reader->nextImageDelay() );
343
344 const QString framePath = frameDirectory.filePath( QStringLiteral( "frame_%1.png" ).arg( frameNumber++ ) );
345 frame.save( framePath, "PNG" );
346 }
347
348 mTotalFrameCounts.insert( path, frameNumber );
349}
350
351QImage QgsImageCache::renderImage( const QString &path, QSize size, const bool keepAspectRatio, const double opacity, double targetDpi, int frameNumber, bool &isBroken, int &totalFrameCount, int &nextFrameDelayMs, bool blocking ) const
352{
353 QImage im;
354 isBroken = false;
355
356 // direct read if path is a file -- maybe more efficient than going the bytearray route? (untested!)
357 if ( !path.startsWith( QLatin1String( "base64:" ) ) && QFile::exists( path ) )
358 {
359 QImageReader reader( path );
360 reader.setAutoTransform( true );
361
362 if ( reader.format() == "pdf" )
363 {
364 if ( !size.isEmpty() )
365 {
366 // special handling for this format -- we need to pass the desired target size onto the image reader
367 // so that it can correctly render the (vector) pdf content at the desired dpi. Otherwise it returns
368 // a very low resolution image (the driver assumes points == pixels!)
369 // For other image formats, we read the original image size only and defer resampling to later in this
370 // function. That gives us more control over the resampling method used.
371 reader.setScaledSize( size );
372 }
373 else
374 {
375 // driver assumes points == pixels, so driver image size is reported assuming 72 dpi.
376 const QSize sizeAt72Dpi = reader.size();
377 const QSize sizeAtTargetDpi = sizeAt72Dpi * targetDpi / 72;
378 reader.setScaledSize( sizeAtTargetDpi );
379 }
380 }
381
382 totalFrameCount = reader.imageCount();
383
384 if ( frameNumber == -1 )
385 {
386 im = reader.read();
387 }
388 else
389 {
390 im = getFrameFromReader( reader, frameNumber );
391 }
392 nextFrameDelayMs = reader.nextImageDelay();
393 }
394 else
395 {
396 QByteArray ba = getContent( path, QByteArray( "broken" ), QByteArray( "fetching" ), blocking );
397
398 if ( ba == "broken" )
399 {
400 isBroken = true;
401
402 // if the size parameter is not valid, skip drawing of missing image symbol
403 if ( !size.isValid() )
404 return im;
405
406 // if image size is set to respect aspect ratio, correct for broken image aspect ratio
407 if ( size.width() == 0 )
408 size.setWidth( size.height() );
409 if ( size.height() == 0 )
410 size.setHeight( size.width() );
411 // render "broken" svg
412 im = QImage( size, QImage::Format_ARGB32_Premultiplied );
413 im.fill( 0 ); // transparent background
414
415 QPainter p( &im );
416 QSvgRenderer r( mMissingSvg );
417
418 QSizeF s( r.viewBox().size() );
419 s.scale( size.width(), size.height(), Qt::KeepAspectRatio );
420 const QRectF rect( ( size.width() - s.width() ) / 2, ( size.height() - s.height() ) / 2, s.width(), s.height() );
421 r.render( &p, rect );
422 }
423 else if ( ba == "fetching" )
424 {
425 // if image size is set to respect aspect ratio, correct for broken image aspect ratio
426 if ( size.width() == 0 )
427 size.setWidth( size.height() );
428 if ( size.height() == 0 )
429 size.setHeight( size.width() );
430
431 // render "fetching" svg
432 im = QImage( size, QImage::Format_ARGB32_Premultiplied );
433 im.fill( 0 ); // transparent background
434
435 QPainter p( &im );
436 QSvgRenderer r( mFetchingSvg );
437
438 QSizeF s( r.viewBox().size() );
439 s.scale( size.width(), size.height(), Qt::KeepAspectRatio );
440 const QRectF rect( ( size.width() - s.width() ) / 2, ( size.height() - s.height() ) / 2, s.width(), s.height() );
441 r.render( &p, rect );
442 }
443 else
444 {
445 QBuffer buffer( &ba );
446 buffer.open( QIODevice::ReadOnly );
447
448 QImageReader reader( &buffer );
449 reader.setAutoTransform( true );
450
451 if ( reader.format() == "pdf" )
452 {
453 if ( !size.isEmpty() )
454 {
455 // special handling for this format -- we need to pass the desired target size onto the image reader
456 // so that it can correctly render the (vector) pdf content at the desired dpi. Otherwise it returns
457 // a very low resolution image (the driver assumes points == pixels!)
458 // For other image formats, we read the original image size only and defer resampling to later in this
459 // function. That gives us more control over the resampling method used.
460 reader.setScaledSize( size );
461 }
462 else
463 {
464 // driver assumes points == pixels, so driver image size is reported assuming 72 dpi.
465 const QSize sizeAt72Dpi = reader.size();
466 const QSize sizeAtTargetDpi = sizeAt72Dpi * targetDpi / 72;
467 reader.setScaledSize( sizeAtTargetDpi );
468 }
469 }
470
471 totalFrameCount = reader.imageCount();
472 if ( frameNumber == -1 )
473 {
474 im = reader.read();
475 }
476 else
477 {
478 im = getFrameFromReader( reader, frameNumber );
479 }
480 nextFrameDelayMs = reader.nextImageDelay();
481 }
482 }
483
484 if ( !im.hasAlphaChannel() )
485 im = im.convertToFormat( QImage::Format_ARGB32 );
486
487 if ( opacity < 1.0 )
489
490 // render image at desired size -- null size means original size
491 if ( !size.isValid() || size.isNull() || im.size() == size )
492 return im;
493 // when original aspect ratio is respected and provided height value is 0, automatically compute height
494 else if ( keepAspectRatio && size.height() == 0 )
495 return im.scaledToWidth( size.width(), Qt::SmoothTransformation );
496 // when original aspect ratio is respected and provided width value is 0, automatically compute width
497 else if ( keepAspectRatio && size.width() == 0 )
498 return im.scaledToHeight( size.height(), Qt::SmoothTransformation );
499 else
500 return im.scaled( size, keepAspectRatio ? Qt::KeepAspectRatio : Qt::IgnoreAspectRatio, Qt::SmoothTransformation );
501}
502
503QImage QgsImageCache::getFrameFromReader( QImageReader &reader, int frameNumber )
504{
505 if ( reader.jumpToImage( frameNumber ) )
506 return reader.read();
507
508 // couldn't seek directly, may require iteration through previous frames
509 for ( int frame = 0; frame < frameNumber; ++frame )
510 {
511 if ( reader.read().isNull() )
512 return QImage();
513 }
514 return reader.read();
515}
void remoteContentFetched(const QString &url)
Emitted when the cache has finished retrieving content from a remote url.
Base class for entries in a QgsAbstractContentCache.
Abstract base class for file content caches, such as SVG or raster image caches.
QByteArray getContent(const QString &path, const QByteArray &missingContent, const QByteArray &fetchingContent, bool blocking=false) const
Gets the file content corresponding to the given path.
QgsImageCacheEntry * findExistingEntry(QgsImageCacheEntry *entryTemplate)
Returns the existing entry from the cache which matches entryTemplate (deleting entryTemplate when do...
long mTotalSize
Estimated total size of all cached content.
void trimToMaximumSize()
Removes the least used cache entries until the maximum cache size is under the predefined size limit.
static QString defaultThemePath()
Returns the path to the default theme directory.
static int systemMemorySizeMb()
Returns the size of the system memory (RAM) in megabytes.
QSize originalSize(const QString &path, bool blocking=false) const
Returns the original size (in pixels) of the image at the specified path.
int nextFrameDelay(const QString &path, int currentFrame=0, bool blocking=false)
For image formats that support animation, this function returns the number of milliseconds to wait un...
QgsImageCache(QObject *parent=nullptr)
Constructor for QgsImageCache, with the specified parent object.
int totalFrameCount(const QString &path, bool blocking=false)
Returns the total frame count of the image at the specified path.
~QgsImageCache() override
void remoteImageFetched(const QString &url)
Emitted when the cache has finished retrieving an image file from a remote url.
QImage pathAsImage(const QString &path, const QSize size, const bool keepAspectRatio, const double opacity, bool &fitsInCache, bool blocking=false, double targetDpi=96, int frameNumber=-1, bool *isMissing=nullptr)
Returns the specified path rendered as an image.
void prepareAnimation(const QString &path)
Prepares for optimized retrieval of frames for the animation at the given path.
static void multiplyOpacity(QImage &image, double factor, QgsFeedback *feedback=nullptr)
Multiplies opacity of image pixel values by a factor.
This class is a composition of two QSettings instances:
Definition: qgssettings.h:62
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39