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