QGIS API Documentation  3.8.0-Zanzibar (11aff65)
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  QgsDebugMsg( QStringLiteral( "path: %1, size %2x%3" ).arg( path ).arg( size.width() ).arg( size.height() ) );
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 )
105 {
106  const QString file = f.trimmed();
107 
108  if ( file.isEmpty() )
109  return QImage();
110 
111  QMutexLocker locker( &mMutex );
112 
113  fitsInCache = true;
114 
115  QgsImageCacheEntry *currentEntry = findExistingEntry( new QgsImageCacheEntry( file, size, keepAspectRatio, opacity ) );
116 
117  QImage result;
118 
119  //if current entry image is null: create the image
120  // checks to see if image will fit into cache
121  //update stats for memory usage
122  if ( currentEntry->image.isNull() )
123  {
124  long cachedDataSize = 0;
125  result = renderImage( file, size, keepAspectRatio, opacity );
126  cachedDataSize += result.width() * result.height() * 32;
127  if ( cachedDataSize > mMaxCacheSize / 2 )
128  {
129  fitsInCache = false;
130  currentEntry->image = QImage();
131  }
132  else
133  {
134  mTotalSize += ( result.width() * result.height() * 32 );
135  currentEntry->image = result;
136  }
138  }
139  else
140  {
141  result = currentEntry->image;
142  }
143 
144  return result;
145 }
146 
147 QSize QgsImageCache::originalSize( const QString &path ) const
148 {
149  if ( path.isEmpty() )
150  return QSize();
151 
152  // direct read if path is a file -- maybe more efficient than going the bytearray route? (untested!)
153  if ( QFile::exists( path ) )
154  {
155  QImageReader reader( path );
156  if ( reader.size().isValid() )
157  return reader.size();
158  else
159  return QImage( path ).size();
160  }
161  else
162  {
163  QByteArray ba = getContent( path, QByteArray( "broken" ), QByteArray( "fetching" ) );
164 
165  if ( ba != "broken" && ba != "fetching" )
166  {
167  QBuffer buffer( &ba );
168  buffer.open( QIODevice::ReadOnly );
169 
170  QImageReader reader( &buffer );
171  // if QImageReader::size works, then it's more efficient as it doesn't
172  // read the whole image (see Qt docs)
173  const QSize s = reader.size();
174  if ( s.isValid() )
175  return s;
176  QImage im = reader.read();
177  return im.isNull() ? QSize() : im.size();
178  }
179  }
180  return QSize();
181 }
182 
183 QImage QgsImageCache::renderImage( const QString &path, QSize size, const bool keepAspectRatio, const double opacity ) const
184 {
185  QImage im;
186  // direct read if path is a file -- maybe more efficient than going the bytearray route? (untested!)
187  if ( QFile::exists( path ) )
188  {
189  im = QImage( path );
190  }
191  else
192  {
193  QByteArray ba = getContent( path, QByteArray( "broken" ), QByteArray( "fetching" ) );
194 
195  if ( ba == "broken" )
196  {
197  // if image size is set to respect aspect ratio, correct for broken image aspect ratio
198  if ( size.width() == 0 )
199  size.setWidth( size.height() );
200  if ( size.height() == 0 )
201  size.setHeight( size.width() );
202  // render "broken" svg
203  im = QImage( size, QImage::Format_ARGB32_Premultiplied );
204  im.fill( 0 ); // transparent background
205 
206  QPainter p( &im );
207  QSvgRenderer r( mMissingSvg );
208 
209  QSizeF s( r.viewBox().size() );
210  s.scale( size.width(), size.height(), Qt::KeepAspectRatio );
211  QRectF rect( ( size.width() - s.width() ) / 2, ( size.height() - s.height() ) / 2, s.width(), s.height() );
212  r.render( &p, rect );
213  }
214  else if ( ba == "fetching" )
215  {
216  // if image size is set to respect aspect ratio, correct for broken image aspect ratio
217  if ( size.width() == 0 )
218  size.setWidth( size.height() );
219  if ( size.height() == 0 )
220  size.setHeight( size.width() );
221 
222  // render "fetching" svg
223  im = QImage( size, QImage::Format_ARGB32_Premultiplied );
224  im.fill( 0 ); // transparent background
225 
226  QPainter p( &im );
227  QSvgRenderer r( mFetchingSvg );
228 
229  QSizeF s( r.viewBox().size() );
230  s.scale( size.width(), size.height(), Qt::KeepAspectRatio );
231  QRectF rect( ( size.width() - s.width() ) / 2, ( size.height() - s.height() ) / 2, s.width(), s.height() );
232  r.render( &p, rect );
233  }
234  else
235  {
236  QBuffer buffer( &ba );
237  buffer.open( QIODevice::ReadOnly );
238 
239  QImageReader reader( &buffer );
240  im = reader.read();
241  }
242  }
243 
244  if ( !im.hasAlphaChannel() )
245  im = im.convertToFormat( QImage::Format_ARGB32 );
246 
247  if ( opacity < 1.0 )
248  QgsImageOperation::multiplyOpacity( im, opacity );
249 
250  // render image at desired size -- null size means original size
251  if ( !size.isValid() || size.isNull() || im.size() == size )
252  return im;
253  // when original aspect ratio is respected and provided height value is 0, automatically compute height
254  else if ( keepAspectRatio && size.height() == 0 )
255  return im.scaledToWidth( size.width(), Qt::SmoothTransformation );
256  // when original aspect ratio is respected and provided width value is 0, automatically compute width
257  else if ( keepAspectRatio && size.width() == 0 )
258  return im.scaledToHeight( size.height(), Qt::SmoothTransformation );
259  else
260  return im.scaled( size, keepAspectRatio ? Qt::KeepAspectRatio : Qt::IgnoreAspectRatio, Qt::SmoothTransformation );
261 }
static void multiplyOpacity(QImage &image, double factor)
Multiplies opacity of image pixel values by a factor.
Abstract base class for file content caches, such as SVG or raster image caches.
QgsImageCache(QObject *parent=nullptr)
Constructor for QgsImageCache, with the specified parent object.
static QString defaultThemePath()
Returns the path to the default theme directory.
#define QgsDebugMsg(str)
Definition: qgslogger.h:38
Base class for entries in a QgsAbstractContentCache.
void remoteContentFetched(const QString &url)
Emitted when the cache has finished retrieving content from a remote url.
QSize originalSize(const QString &path) const
Returns the original size (in pixels) of the image at the specified path.
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)
Returns the specified path rendered as an image.
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) 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...