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