QGIS API Documentation  3.20.0-Odense (decaadbb31)
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 
27 #include <QApplication>
28 #include <QCoreApplication>
29 #include <QCursor>
30 #include <QDomDocument>
31 #include <QDomElement>
32 #include <QFile>
33 #include <QImage>
34 #include <QPainter>
35 #include <QPicture>
36 #include <QFileInfo>
37 #include <QNetworkReply>
38 #include <QNetworkRequest>
39 #include <QBuffer>
40 #include <QImageReader>
41 #include <QSvgRenderer>
42 
44 
45 QgsImageCacheEntry::QgsImageCacheEntry( const QString &path, QSize size, const bool keepAspectRatio, const double opacity )
47  , size( size )
48  , keepAspectRatio( keepAspectRatio )
49  , opacity( opacity )
50 {
51 }
52 
53 bool QgsImageCacheEntry::isEqual( const QgsAbstractContentCacheEntry *other ) const
54 {
55  const QgsImageCacheEntry *otherImage = dynamic_cast< const QgsImageCacheEntry * >( other );
56  // cheapest checks first!
57  if ( !otherImage || otherImage->keepAspectRatio != keepAspectRatio || otherImage->size != size || otherImage->path != path || otherImage->opacity != opacity )
58  return false;
59 
60  return true;
61 }
62 
63 int QgsImageCacheEntry::dataSize() const
64 {
65  int size = 0;
66  if ( !image.isNull() )
67  {
68  size += ( image.width() * image.height() * 32 );
69  }
70  return size;
71 }
72 
73 void QgsImageCacheEntry::dump() const
74 {
75  QgsDebugMsgLevel( QStringLiteral( "path: %1, size %2x%3" ).arg( path ).arg( size.width() ).arg( size.height() ), 3 );
76 }
77 
79 
80 
81 QgsImageCache::QgsImageCache( QObject *parent )
82  : QgsAbstractContentCache< QgsImageCacheEntry >( parent, QObject::tr( "Image" ) )
83 {
84  mMissingSvg = QStringLiteral( "<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>?</text></svg>" ).toLatin1();
85 
86  const QString downloadingSvgPath = QgsApplication::defaultThemePath() + QStringLiteral( "downloading_svg.svg" );
87  if ( QFile::exists( downloadingSvgPath ) )
88  {
89  QFile file( downloadingSvgPath );
90  if ( file.open( QIODevice::ReadOnly ) )
91  {
92  mFetchingSvg = file.readAll();
93  }
94  }
95 
96  if ( mFetchingSvg.isEmpty() )
97  {
98  mFetchingSvg = QStringLiteral( "<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>?</text></svg>" ).toLatin1();
99  }
100 
102 }
103 
104 QImage QgsImageCache::pathAsImage( const QString &f, const QSize size, const bool keepAspectRatio, const double opacity, bool &fitsInCache, bool blocking, bool *isMissing )
105 {
106  const QString file = f.trimmed();
107  if ( isMissing )
108  *isMissing = true;
109 
110  if ( file.isEmpty() )
111  return QImage();
112 
113  QMutexLocker locker( &mMutex );
114 
115  fitsInCache = true;
116 
117  QgsImageCacheEntry *currentEntry = findExistingEntry( new QgsImageCacheEntry( file, size, keepAspectRatio, opacity ) );
118 
119  QImage result;
120 
121  //if current entry image is null: create the image
122  // checks to see if image will fit into cache
123  //update stats for memory usage
124  if ( currentEntry->image.isNull() )
125  {
126  long cachedDataSize = 0;
127  bool isBroken = false;
128  result = renderImage( file, size, keepAspectRatio, opacity, isBroken, blocking );
129  cachedDataSize += result.width() * result.height() * 32;
130  if ( cachedDataSize > mMaxCacheSize / 2 )
131  {
132  fitsInCache = false;
133  currentEntry->image = QImage();
134  }
135  else
136  {
137  mTotalSize += ( result.width() * result.height() * 32 );
138  currentEntry->image = result;
139  }
140 
141  if ( isMissing )
142  *isMissing = isBroken;
143  currentEntry->isMissingImage = isBroken;
144 
146  }
147  else
148  {
149  result = currentEntry->image;
150  if ( isMissing )
151  *isMissing = currentEntry->isMissingImage;
152  }
153 
154  return result;
155 }
156 
157 QSize QgsImageCache::originalSize( const QString &path, bool blocking ) const
158 {
159  if ( path.isEmpty() )
160  return QSize();
161 
162  // direct read if path is a file -- maybe more efficient than going the bytearray route? (untested!)
163  if ( !path.startsWith( QLatin1String( "base64:" ) ) && QFile::exists( path ) )
164  {
165  QImageReader reader( path );
166  if ( reader.size().isValid() )
167  return reader.size();
168  else
169  return QImage( path ).size();
170  }
171  else
172  {
173  QByteArray ba = getContent( path, QByteArray( "broken" ), QByteArray( "fetching" ), blocking );
174 
175  if ( ba != "broken" && ba != "fetching" )
176  {
177  QBuffer buffer( &ba );
178  buffer.open( QIODevice::ReadOnly );
179 
180  QImageReader reader( &buffer );
181  // if QImageReader::size works, then it's more efficient as it doesn't
182  // read the whole image (see Qt docs)
183  const QSize s = reader.size();
184  if ( s.isValid() )
185  return s;
186  QImage im = reader.read();
187  return im.isNull() ? QSize() : im.size();
188  }
189  }
190  return QSize();
191 }
192 
193 QImage QgsImageCache::renderImage( const QString &path, QSize size, const bool keepAspectRatio, const double opacity, bool &isBroken, bool blocking ) const
194 {
195  QImage im;
196  isBroken = false;
197 
198  // direct read if path is a file -- maybe more efficient than going the bytearray route? (untested!)
199  if ( !path.startsWith( QLatin1String( "base64:" ) ) && QFile::exists( path ) )
200  {
201  im = QImage( path );
202  }
203  else
204  {
205  QByteArray ba = getContent( path, QByteArray( "broken" ), QByteArray( "fetching" ), blocking );
206 
207  if ( ba == "broken" )
208  {
209  isBroken = true;
210 
211  // if the size parameter is not valid, skip drawing of missing image symbol
212  if ( !size.isValid() )
213  return im;
214 
215  // if image size is set to respect aspect ratio, correct for broken image aspect ratio
216  if ( size.width() == 0 )
217  size.setWidth( size.height() );
218  if ( size.height() == 0 )
219  size.setHeight( size.width() );
220  // render "broken" svg
221  im = QImage( size, QImage::Format_ARGB32_Premultiplied );
222  im.fill( 0 ); // transparent background
223 
224  QPainter p( &im );
225  QSvgRenderer r( mMissingSvg );
226 
227  QSizeF s( r.viewBox().size() );
228  s.scale( size.width(), size.height(), Qt::KeepAspectRatio );
229  QRectF rect( ( size.width() - s.width() ) / 2, ( size.height() - s.height() ) / 2, s.width(), s.height() );
230  r.render( &p, rect );
231  }
232  else if ( ba == "fetching" )
233  {
234  // if image size is set to respect aspect ratio, correct for broken image aspect ratio
235  if ( size.width() == 0 )
236  size.setWidth( size.height() );
237  if ( size.height() == 0 )
238  size.setHeight( size.width() );
239 
240  // render "fetching" svg
241  im = QImage( size, QImage::Format_ARGB32_Premultiplied );
242  im.fill( 0 ); // transparent background
243 
244  QPainter p( &im );
245  QSvgRenderer r( mFetchingSvg );
246 
247  QSizeF s( r.viewBox().size() );
248  s.scale( size.width(), size.height(), Qt::KeepAspectRatio );
249  QRectF rect( ( size.width() - s.width() ) / 2, ( size.height() - s.height() ) / 2, s.width(), s.height() );
250  r.render( &p, rect );
251  }
252  else
253  {
254  QBuffer buffer( &ba );
255  buffer.open( QIODevice::ReadOnly );
256 
257  QImageReader reader( &buffer );
258  im = reader.read();
259  }
260  }
261 
262  if ( !im.hasAlphaChannel() )
263  im = im.convertToFormat( QImage::Format_ARGB32 );
264 
265  if ( opacity < 1.0 )
266  QgsImageOperation::multiplyOpacity( im, opacity );
267 
268  // render image at desired size -- null size means original size
269  if ( !size.isValid() || size.isNull() || im.size() == size )
270  return im;
271  // when original aspect ratio is respected and provided height value is 0, automatically compute height
272  else if ( keepAspectRatio && size.height() == 0 )
273  return im.scaledToWidth( size.width(), Qt::SmoothTransformation );
274  // when original aspect ratio is respected and provided width value is 0, automatically compute width
275  else if ( keepAspectRatio && size.width() == 0 )
276  return im.scaledToHeight( size.height(), Qt::SmoothTransformation );
277  else
278  return im.scaled( size, keepAspectRatio ? Qt::KeepAspectRatio : Qt::IgnoreAspectRatio, Qt::SmoothTransformation );
279 }
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.
QImage pathAsImage(const QString &path, const QSize size, const bool keepAspectRatio, const double opacity, bool &fitsInCache, bool blocking=false, bool *isMissing=nullptr)
Returns the specified path rendered as an image.
void remoteImageFetched(const QString &url)
Emitted when the cache has finished retrieving an image file from a remote url.
static void multiplyOpacity(QImage &image, double factor)
Multiplies opacity of image pixel values by a factor.
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39