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