QGIS API Documentation 3.43.0-Master (ebb4087afc0)
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 return mImageSizeCache.originalSize( path, blocking );
220}
221
222QSize QgsImageCache::originalSizePrivate( const QString &path, bool blocking ) const
223{
224 if ( path.isEmpty() )
225 return QSize();
226
227 // direct read if path is a file -- maybe more efficient than going the bytearray route? (untested!)
228 if ( !isBase64Data( path ) && QFile::exists( path ) )
229 {
230 const QImageReader reader( path );
231 if ( reader.size().isValid() )
232 return reader.size();
233 else
234 return QImage( path ).size();
235 }
236 else
237 {
238 QByteArray ba = getContent( path, QByteArray( "broken" ), QByteArray( "fetching" ), blocking );
239
240 if ( ba != "broken" && ba != "fetching" )
241 {
242 QBuffer buffer( &ba );
243 buffer.open( QIODevice::ReadOnly );
244
245 QImageReader reader( &buffer );
246 // if QImageReader::size works, then it's more efficient as it doesn't
247 // read the whole image (see Qt docs)
248 const QSize s = reader.size();
249 if ( s.isValid() )
250 return s;
251 const QImage im = reader.read();
252 return im.isNull() ? QSize() : im.size();
253 }
254 }
255 return QSize();
256}
257
258int QgsImageCache::totalFrameCount( const QString &path, bool blocking )
259{
260 const QString file = path.trimmed();
261
262 if ( file.isEmpty() )
263 return -1;
264
265 const QMutexLocker locker( &mMutex );
266
267 auto it = mTotalFrameCounts.find( path );
268 if ( it != mTotalFrameCounts.end() )
269 return it.value(); // already prepared
270
271 int res = -1;
272 int nextFrameDelayMs = 0;
273 bool fitsInCache = false;
274 bool isMissing = false;
275 ( void )pathAsImagePrivate( file, QSize(), true, 1.0, fitsInCache, blocking, 96, 0, &isMissing, res, nextFrameDelayMs );
276
277 return res;
278}
279
280int QgsImageCache::nextFrameDelay( const QString &path, int currentFrame, bool blocking )
281{
282 const QString file = path.trimmed();
283
284 if ( file.isEmpty() )
285 return -1;
286
287 const QMutexLocker locker( &mMutex );
288
289 auto it = mImageDelays.find( path );
290 if ( it != mImageDelays.end() )
291 return it.value().value( currentFrame ); // already prepared
292
293 int frameCount = -1;
294 int nextFrameDelayMs = 0;
295 bool fitsInCache = false;
296 bool isMissing = false;
297 const QImage res = pathAsImagePrivate( file, QSize(), true, 1.0, fitsInCache, blocking, 96, currentFrame, &isMissing, frameCount, nextFrameDelayMs );
298
299 return nextFrameDelayMs <= 0 || res.isNull() ? -1 : nextFrameDelayMs;
300}
301
302void QgsImageCache::prepareAnimation( const QString &path )
303{
304 const QMutexLocker locker( &mMutex );
305
306 auto it = mExtractedAnimationPaths.find( path );
307 if ( it != mExtractedAnimationPaths.end() )
308 return; // already prepared
309
310 QString filePath;
311 std::unique_ptr< QImageReader > reader;
312 std::unique_ptr< QBuffer > buffer;
313
314 if ( !isBase64Data( path ) && QFile::exists( path ) )
315 {
316 const QString basePart = QFileInfo( path ).baseName();
317 int id = 1;
318 filePath = mTemporaryDir->filePath( QStringLiteral( "%1_%2" ).arg( basePart ).arg( id ) );
319 while ( QFile::exists( filePath ) )
320 filePath = mTemporaryDir->filePath( QStringLiteral( "%1_%2" ).arg( basePart ).arg( ++id ) );
321
322 reader = std::make_unique< QImageReader >( path );
323 }
324 else
325 {
326 QByteArray ba = getContent( path, QByteArray( "broken" ), QByteArray( "fetching" ), false );
327 if ( ba == "broken" || ba == "fetching" )
328 {
329 return;
330 }
331 else
332 {
333 const QString path = QUuid::createUuid().toString( QUuid::WithoutBraces );
334 filePath = mTemporaryDir->filePath( path );
335
336 buffer = std::make_unique< QBuffer >( &ba );
337 buffer->open( QIODevice::ReadOnly );
338 reader = std::make_unique< QImageReader> ( buffer.get() );
339 }
340 }
341
342 QDir().mkpath( filePath );
343 mExtractedAnimationPaths.insert( path, filePath );
344
345 const QDir frameDirectory( filePath );
346 // extract all the frames to separate images
347
348 reader->setAutoTransform( true );
349 int frameNumber = 0;
350 while ( true )
351 {
352 const QImage frame = reader->read();
353 if ( frame.isNull() )
354 break;
355
356 mImageDelays[ path ].append( reader->nextImageDelay() );
357
358 const QString framePath = frameDirectory.filePath( QStringLiteral( "frame_%1.png" ).arg( frameNumber++ ) );
359 frame.save( framePath, "PNG" );
360 }
361
362 mTotalFrameCounts.insert( path, frameNumber );
363}
364
365QImage 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
366{
367 QImage im;
368 isBroken = false;
369
370 // direct read if path is a file -- maybe more efficient than going the bytearray route? (untested!)
371 if ( !isBase64Data( path ) && QFile::exists( path ) )
372 {
373 QImageReader reader( path );
374 reader.setAutoTransform( true );
375
376 if ( reader.format() == "pdf" )
377 {
378 if ( !size.isEmpty() )
379 {
380 // special handling for this format -- we need to pass the desired target size onto the image reader
381 // so that it can correctly render the (vector) pdf content at the desired dpi. Otherwise it returns
382 // a very low resolution image (the driver assumes points == pixels!)
383 // For other image formats, we read the original image size only and defer resampling to later in this
384 // function. That gives us more control over the resampling method used.
385 reader.setScaledSize( size );
386 }
387 else
388 {
389 // driver assumes points == pixels, so driver image size is reported assuming 72 dpi.
390 const QSize sizeAt72Dpi = reader.size();
391 const QSize sizeAtTargetDpi = sizeAt72Dpi * targetDpi / 72;
392 reader.setScaledSize( sizeAtTargetDpi );
393 }
394 }
395
396 totalFrameCount = reader.imageCount();
397
398 if ( frameNumber == -1 )
399 {
400 im = reader.read();
401 }
402 else
403 {
404 im = getFrameFromReader( reader, frameNumber );
405 }
406 nextFrameDelayMs = reader.nextImageDelay();
407 }
408 else
409 {
410 QByteArray ba = getContent( path, QByteArray( "broken" ), QByteArray( "fetching" ), blocking );
411
412 if ( ba == "broken" )
413 {
414 isBroken = true;
415
416 // if the size parameter is not valid, skip drawing of missing image symbol
417 if ( !size.isValid() || size.isNull() )
418 return im;
419
420 // if image size is set to respect aspect ratio, correct for broken image aspect ratio
421 if ( size.width() == 0 )
422 size.setWidth( size.height() );
423 if ( size.height() == 0 )
424 size.setHeight( size.width() );
425 // render "broken" svg
426 im = QImage( size, QImage::Format_ARGB32_Premultiplied );
427 im.fill( 0 ); // transparent background
428
429 QPainter p( &im );
430 QSvgRenderer r( mMissingSvg );
431
432 QSizeF s( r.viewBox().size() );
433 s.scale( size.width(), size.height(), Qt::KeepAspectRatio );
434 const QRectF rect( ( size.width() - s.width() ) / 2, ( size.height() - s.height() ) / 2, s.width(), s.height() );
435 r.render( &p, rect );
436 }
437 else if ( ba == "fetching" )
438 {
439 // if image size is set to respect aspect ratio, correct for broken image aspect ratio
440 if ( size.width() == 0 )
441 size.setWidth( size.height() );
442 if ( size.height() == 0 )
443 size.setHeight( size.width() );
444
445 // render "fetching" svg
446 im = QImage( size, QImage::Format_ARGB32_Premultiplied );
447 im.fill( 0 ); // transparent background
448
449 QPainter p( &im );
450 QSvgRenderer r( mFetchingSvg );
451
452 QSizeF s( r.viewBox().size() );
453 s.scale( size.width(), size.height(), Qt::KeepAspectRatio );
454 const QRectF rect( ( size.width() - s.width() ) / 2, ( size.height() - s.height() ) / 2, s.width(), s.height() );
455 r.render( &p, rect );
456 }
457 else
458 {
459 QBuffer buffer( &ba );
460 buffer.open( QIODevice::ReadOnly );
461
462 QImageReader reader( &buffer );
463 reader.setAutoTransform( true );
464
465 if ( reader.format() == "pdf" )
466 {
467 if ( !size.isEmpty() )
468 {
469 // special handling for this format -- we need to pass the desired target size onto the image reader
470 // so that it can correctly render the (vector) pdf content at the desired dpi. Otherwise it returns
471 // a very low resolution image (the driver assumes points == pixels!)
472 // For other image formats, we read the original image size only and defer resampling to later in this
473 // function. That gives us more control over the resampling method used.
474 reader.setScaledSize( size );
475 }
476 else
477 {
478 // driver assumes points == pixels, so driver image size is reported assuming 72 dpi.
479 const QSize sizeAt72Dpi = reader.size();
480 const QSize sizeAtTargetDpi = sizeAt72Dpi * targetDpi / 72;
481 reader.setScaledSize( sizeAtTargetDpi );
482 }
483 }
484
485 totalFrameCount = reader.imageCount();
486 if ( frameNumber == -1 )
487 {
488 im = reader.read();
489 }
490 else
491 {
492 im = getFrameFromReader( reader, frameNumber );
493 }
494 nextFrameDelayMs = reader.nextImageDelay();
495 }
496 }
497
498 if ( !im.hasAlphaChannel()
499#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
500 && im.format() != QImage::Format_CMYK8888
501#endif
502 )
503 im = im.convertToFormat( QImage::Format_ARGB32 );
504
505 if ( opacity < 1.0 )
507
508 // render image at desired size -- null size means original size
509 if ( !size.isValid() || size.isNull() || im.size() == size )
510 return im;
511 // when original aspect ratio is respected and provided height value is 0, automatically compute height
512 else if ( keepAspectRatio && size.height() == 0 )
513 return im.scaledToWidth( size.width(), Qt::SmoothTransformation );
514 // when original aspect ratio is respected and provided width value is 0, automatically compute width
515 else if ( keepAspectRatio && size.width() == 0 )
516 return im.scaledToHeight( size.height(), Qt::SmoothTransformation );
517 else
518 return im.scaled( size, keepAspectRatio ? Qt::KeepAspectRatio : Qt::IgnoreAspectRatio, Qt::SmoothTransformation );
519}
520
521QImage QgsImageCache::getFrameFromReader( QImageReader &reader, int frameNumber )
522{
523 if ( reader.jumpToImage( frameNumber ) )
524 return reader.read();
525
526 // couldn't seek directly, may require iteration through previous frames
527 for ( int frame = 0; frame < frameNumber; ++frame )
528 {
529 if ( reader.read().isNull() )
530 return QImage();
531 }
532 return reader.read();
533}
534
536template class QgsAbstractContentCache<QgsImageCacheEntry>; // clazy:exclude=missing-qobject-macro
537
538QgsImageSizeCacheEntry::QgsImageSizeCacheEntry( const QString &path )
540{
541
542}
543
544int QgsImageSizeCacheEntry::dataSize() const
545{
546 return sizeof( QSize );
547}
548
549void QgsImageSizeCacheEntry::dump() const
550{
551 QgsDebugMsgLevel( QStringLiteral( "path: %1" ).arg( path ), 3 );
552}
553
554bool QgsImageSizeCacheEntry::isEqual( const QgsAbstractContentCacheEntry *other ) const
555{
556 const QgsImageSizeCacheEntry *otherImage = dynamic_cast< const QgsImageSizeCacheEntry * >( other );
557 if ( !otherImage
558 || otherImage->path != path )
559 return false;
560
561 return true;
562}
563
564template class QgsAbstractContentCache<QgsImageSizeCacheEntry>; // clazy:exclude=missing-qobject-macro
565
566
567//
568// QgsImageSizeCache
569//
570
571QgsImageSizeCache::QgsImageSizeCache( QObject *parent )
572 : QgsAbstractContentCache< QgsImageSizeCacheEntry >( parent, QObject::tr( "Image" ) )
573{
574 mMaxCacheSize = 524288; // 500kb max cache size, we are only storing QSize objects here, so that should be heaps
575}
576
577QgsImageSizeCache::~QgsImageSizeCache() = default;
578
579QSize QgsImageSizeCache::originalSize( const QString &f, bool blocking )
580{
581 QString file = f.trimmed();
582
583 if ( file.isEmpty() )
584 return QSize();
585
586 const QMutexLocker locker( &mMutex );
587
588 QString base64String;
589 QString mimeType;
590 if ( parseBase64DataUrl( file, &mimeType, &base64String ) && mimeType.startsWith( QLatin1String( "image/" ) ) )
591 {
592 file = QStringLiteral( "base64:%1" ).arg( base64String );
593 }
594
595 QgsImageSizeCacheEntry *currentEntry = findExistingEntry( new QgsImageSizeCacheEntry( file ) );
596
597 QSize result;
598
599 if ( !currentEntry->size.isValid() )
600 {
601 result = QgsApplication::imageCache()->originalSizePrivate( file, blocking );
602 mTotalSize += currentEntry->dataSize();
603 currentEntry->size = result;
604 trimToMaximumSize();
605 }
606 else
607 {
608 result = currentEntry->size;
609 }
610
611 return result;
612}
613
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.
static QgsImageCache * imageCache()
Returns the application's image cache, used for caching resampled versions of raster images.
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:41