QGIS API Documentation  3.25.0-Master (10b47c2603)
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 }
void remoteContentFetched(const QString &url)
Emitted when the cache has finished retrieving content from a remote url.
Base class for entries in a QgsAbstractContentCache.
Abstract base class for file content caches, such as SVG or raster image caches.
QgsImageCacheEntry * findExistingEntry(QgsImageCacheEntry *entryTemplate)
Returns the existing entry from the cache which matches entryTemplate (deleting entryTemplate when do...
QByteArray getContent(const QString &path, const QByteArray &missingContent, const QByteArray &fetchingContent, bool blocking=false) const
Gets the file content corresponding to the given path.
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:62
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