QGIS API Documentation  3.23.0-Master (dd0cd13a00)
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 
45 
46 QgsImageCacheEntry::QgsImageCacheEntry( const QString &path, QSize size, const bool keepAspectRatio, const double opacity, double dpi )
48  , size( size )
49  , keepAspectRatio( keepAspectRatio )
50  , opacity( opacity )
51  , targetDpi( dpi )
52 {
53 }
54 
55 bool QgsImageCacheEntry::isEqual( const QgsAbstractContentCacheEntry *other ) const
56 {
57  const QgsImageCacheEntry *otherImage = dynamic_cast< const QgsImageCacheEntry * >( other );
58  // cheapest checks first!
59  if ( !otherImage || otherImage->keepAspectRatio != keepAspectRatio || otherImage->size != size || ( !size.isValid() && otherImage->targetDpi != targetDpi ) || otherImage->opacity != opacity || otherImage->path != path )
60  return false;
61 
62  return true;
63 }
64 
65 int QgsImageCacheEntry::dataSize() const
66 {
67  int size = 0;
68  if ( !image.isNull() )
69  {
70  size += image.sizeInBytes();
71  }
72  return size;
73 }
74 
75 void QgsImageCacheEntry::dump() const
76 {
77  QgsDebugMsgLevel( QStringLiteral( "path: %1, size %2x%3" ).arg( path ).arg( size.width() ).arg( size.height() ), 3 );
78 }
79 
81 
82 static const int DEFAULT_IMAGE_CACHE_MAX_BYTES = 104857600;
83 
84 QgsImageCache::QgsImageCache( QObject *parent )
85  : QgsAbstractContentCache< QgsImageCacheEntry >( parent, QObject::tr( "Image" ), DEFAULT_IMAGE_CACHE_MAX_BYTES )
86 {
87  int bytes = QgsSettings().value( QStringLiteral( "/qgis/maxImageCacheSize" ), 0 ).toInt();
88  if ( bytes > 0 )
89  {
90  mMaxCacheSize = bytes;
91  }
92 
93  mMissingSvg = QStringLiteral( "<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>?</text></svg>" ).toLatin1();
94 
95  const QString downloadingSvgPath = QgsApplication::defaultThemePath() + QStringLiteral( "downloading_svg.svg" );
96  if ( QFile::exists( downloadingSvgPath ) )
97  {
98  QFile file( downloadingSvgPath );
99  if ( file.open( QIODevice::ReadOnly ) )
100  {
101  mFetchingSvg = file.readAll();
102  }
103  }
104 
105  if ( mFetchingSvg.isEmpty() )
106  {
107  mFetchingSvg = QStringLiteral( "<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>?</text></svg>" ).toLatin1();
108  }
109 
111 }
112 
113 QImage QgsImageCache::pathAsImage( const QString &f, const QSize size, const bool keepAspectRatio, const double opacity, bool &fitsInCache, bool blocking, double targetDpi, bool *isMissing )
114 {
115  const QString file = f.trimmed();
116  if ( isMissing )
117  *isMissing = true;
118 
119  if ( file.isEmpty() )
120  return QImage();
121 
122  const QMutexLocker locker( &mMutex );
123 
124  fitsInCache = true;
125 
126  QgsImageCacheEntry *currentEntry = findExistingEntry( new QgsImageCacheEntry( file, size, keepAspectRatio, opacity, targetDpi ) );
127 
128  QImage result;
129 
130  //if current entry image is null: create the image
131  // checks to see if image will fit into cache
132  //update stats for memory usage
133  if ( currentEntry->image.isNull() )
134  {
135  long cachedDataSize = 0;
136  bool isBroken = false;
137  result = renderImage( file, size, keepAspectRatio, opacity, targetDpi, isBroken, blocking );
138  cachedDataSize += result.sizeInBytes();
139  if ( cachedDataSize > mMaxCacheSize / 2 )
140  {
141  fitsInCache = false;
142  currentEntry->image = QImage();
143  }
144  else
145  {
146  mTotalSize += result.sizeInBytes();
147  currentEntry->image = result;
148  }
149 
150  if ( isMissing )
151  *isMissing = isBroken;
152  currentEntry->isMissingImage = isBroken;
153 
155  }
156  else
157  {
158  result = currentEntry->image;
159  if ( isMissing )
160  *isMissing = currentEntry->isMissingImage;
161  }
162 
163  return result;
164 }
165 
166 QSize QgsImageCache::originalSize( const QString &path, bool blocking ) const
167 {
168  if ( path.isEmpty() )
169  return QSize();
170 
171  // direct read if path is a file -- maybe more efficient than going the bytearray route? (untested!)
172  if ( !path.startsWith( QLatin1String( "base64:" ) ) && QFile::exists( path ) )
173  {
174  const QImageReader reader( path );
175  if ( reader.size().isValid() )
176  return reader.size();
177  else
178  return QImage( path ).size();
179  }
180  else
181  {
182  QByteArray ba = getContent( path, QByteArray( "broken" ), QByteArray( "fetching" ), blocking );
183 
184  if ( ba != "broken" && ba != "fetching" )
185  {
186  QBuffer buffer( &ba );
187  buffer.open( QIODevice::ReadOnly );
188 
189  QImageReader reader( &buffer );
190  // if QImageReader::size works, then it's more efficient as it doesn't
191  // read the whole image (see Qt docs)
192  const QSize s = reader.size();
193  if ( s.isValid() )
194  return s;
195  const QImage im = reader.read();
196  return im.isNull() ? QSize() : im.size();
197  }
198  }
199  return QSize();
200 }
201 
202 QImage QgsImageCache::renderImage( const QString &path, QSize size, const bool keepAspectRatio, const double opacity, double targetDpi, bool &isBroken, bool blocking ) const
203 {
204  QImage im;
205  isBroken = false;
206 
207  // direct read if path is a file -- maybe more efficient than going the bytearray route? (untested!)
208  if ( !path.startsWith( QLatin1String( "base64:" ) ) && QFile::exists( path ) )
209  {
210  QImageReader reader( path );
211  reader.setAutoTransform( true );
212 
213  if ( reader.format() == "pdf" )
214  {
215  if ( !size.isEmpty() )
216  {
217  // special handling for this format -- we need to pass the desired target size onto the image reader
218  // so that it can correctly render the (vector) pdf content at the desired dpi. Otherwise it returns
219  // a very low resolution image (the driver assumes points == pixels!)
220  // For other image formats, we read the original image size only and defer resampling to later in this
221  // function. That gives us more control over the resampling method used.
222  reader.setScaledSize( size );
223  }
224  else
225  {
226  // driver assumes points == pixels, so driver image size is reported assuming 72 dpi.
227  const QSize sizeAt72Dpi = reader.size();
228  const QSize sizeAtTargetDpi = sizeAt72Dpi * targetDpi / 72;
229  reader.setScaledSize( sizeAtTargetDpi );
230  }
231  }
232 
233  im = reader.read();
234  }
235  else
236  {
237  QByteArray ba = getContent( path, QByteArray( "broken" ), QByteArray( "fetching" ), blocking );
238 
239  if ( ba == "broken" )
240  {
241  isBroken = true;
242 
243  // if the size parameter is not valid, skip drawing of missing image symbol
244  if ( !size.isValid() )
245  return im;
246 
247  // if image size is set to respect aspect ratio, correct for broken image aspect ratio
248  if ( size.width() == 0 )
249  size.setWidth( size.height() );
250  if ( size.height() == 0 )
251  size.setHeight( size.width() );
252  // render "broken" svg
253  im = QImage( size, QImage::Format_ARGB32_Premultiplied );
254  im.fill( 0 ); // transparent background
255 
256  QPainter p( &im );
257  QSvgRenderer r( mMissingSvg );
258 
259  QSizeF s( r.viewBox().size() );
260  s.scale( size.width(), size.height(), Qt::KeepAspectRatio );
261  const QRectF rect( ( size.width() - s.width() ) / 2, ( size.height() - s.height() ) / 2, s.width(), s.height() );
262  r.render( &p, rect );
263  }
264  else if ( ba == "fetching" )
265  {
266  // if image size is set to respect aspect ratio, correct for broken image aspect ratio
267  if ( size.width() == 0 )
268  size.setWidth( size.height() );
269  if ( size.height() == 0 )
270  size.setHeight( size.width() );
271 
272  // render "fetching" svg
273  im = QImage( size, QImage::Format_ARGB32_Premultiplied );
274  im.fill( 0 ); // transparent background
275 
276  QPainter p( &im );
277  QSvgRenderer r( mFetchingSvg );
278 
279  QSizeF s( r.viewBox().size() );
280  s.scale( size.width(), size.height(), Qt::KeepAspectRatio );
281  const QRectF rect( ( size.width() - s.width() ) / 2, ( size.height() - s.height() ) / 2, s.width(), s.height() );
282  r.render( &p, rect );
283  }
284  else
285  {
286  QBuffer buffer( &ba );
287  buffer.open( QIODevice::ReadOnly );
288 
289  QImageReader reader( &buffer );
290  reader.setAutoTransform( true );
291 
292  if ( reader.format() == "pdf" )
293  {
294  if ( !size.isEmpty() )
295  {
296  // special handling for this format -- we need to pass the desired target size onto the image reader
297  // so that it can correctly render the (vector) pdf content at the desired dpi. Otherwise it returns
298  // a very low resolution image (the driver assumes points == pixels!)
299  // For other image formats, we read the original image size only and defer resampling to later in this
300  // function. That gives us more control over the resampling method used.
301  reader.setScaledSize( size );
302  }
303  else
304  {
305  // driver assumes points == pixels, so driver image size is reported assuming 72 dpi.
306  const QSize sizeAt72Dpi = reader.size();
307  const QSize sizeAtTargetDpi = sizeAt72Dpi * targetDpi / 72;
308  reader.setScaledSize( sizeAtTargetDpi );
309  }
310  }
311 
312  im = reader.read();
313  }
314  }
315 
316  if ( !im.hasAlphaChannel() )
317  im = im.convertToFormat( QImage::Format_ARGB32 );
318 
319  if ( opacity < 1.0 )
320  QgsImageOperation::multiplyOpacity( im, opacity );
321 
322  // render image at desired size -- null size means original size
323  if ( !size.isValid() || size.isNull() || im.size() == size )
324  return im;
325  // when original aspect ratio is respected and provided height value is 0, automatically compute height
326  else if ( keepAspectRatio && size.height() == 0 )
327  return im.scaledToWidth( size.width(), Qt::SmoothTransformation );
328  // when original aspect ratio is respected and provided width value is 0, automatically compute width
329  else if ( keepAspectRatio && size.width() == 0 )
330  return im.scaledToHeight( size.height(), Qt::SmoothTransformation );
331  else
332  return im.scaled( size, keepAspectRatio ? Qt::KeepAspectRatio : Qt::IgnoreAspectRatio, Qt::SmoothTransformation );
333 }
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.
QSize originalSize(const QString &path, bool blocking=false) const
Returns the original size (in pixels) of the image at the specified path.
QgsImageCache(QObject *parent=nullptr)
Constructor for QgsImageCache, with the specified parent object.
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, bool *isMissing=nullptr)
Returns the specified path rendered as an image.
static void multiplyOpacity(QImage &image, double factor, QgsFeedback *feedback=nullptr)
Multiplies opacity of image pixel values by a factor.
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39