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