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