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