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