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