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