28#include <QApplication>
29#include <QCoreApplication>
31#include <QDomDocument>
38#include <QNetworkReply>
39#include <QNetworkRequest>
41#include <QImageReader>
42#include <QSvgRenderer>
43#include <QTemporaryDir>
48QgsImageCacheEntry::QgsImageCacheEntry(
const QString &path, QSize size,
const bool keepAspectRatio,
const double opacity,
double dpi,
int frameNumber )
51 , keepAspectRatio( keepAspectRatio )
54 , frameNumber( frameNumber )
60 const QgsImageCacheEntry *otherImage =
dynamic_cast< const QgsImageCacheEntry *
>( other );
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 )
74int QgsImageCacheEntry::dataSize()
const
77 if ( !image.isNull() )
79 size += image.sizeInBytes();
84void QgsImageCacheEntry::dump()
const
86 QgsDebugMsgLevel( QStringLiteral(
"path: %1, size %2x%3" ).arg( path ).arg( size.width() ).arg( size.height() ), 3 );
94 mTemporaryDir.reset(
new QTemporaryDir() );
96 const int bytes =
QgsSettings().
value( QStringLiteral(
"/qgis/maxImageCacheSize" ), 0 ).toInt();
106 if ( sysMemory >= 32000 )
108 else if ( sysMemory >= 16000 )
115 mMissingSvg = QStringLiteral(
"<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>?</text></svg>" ).toLatin1();
118 if ( QFile::exists( downloadingSvgPath ) )
120 QFile file( downloadingSvgPath );
121 if ( file.open( QIODevice::ReadOnly ) )
123 mFetchingSvg = file.readAll();
127 if ( mFetchingSvg.isEmpty() )
129 mFetchingSvg = QStringLiteral(
"<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>?</text></svg>" ).toLatin1();
137QImage
QgsImageCache::pathAsImage(
const QString &f,
const QSize size,
const bool keepAspectRatio,
const double opacity,
bool &fitsInCache,
bool blocking,
double targetDpi,
int frameNumber,
bool *isMissing )
140 int nextFrameDelayMs = 0;
141 return pathAsImagePrivate( f, size, keepAspectRatio, opacity, fitsInCache, blocking, targetDpi, frameNumber, isMissing,
totalFrameCount, nextFrameDelayMs );
144QImage 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 )
146 QString file = f.trimmed();
150 if ( file.isEmpty() )
153 const QMutexLocker locker( &
mMutex );
155 const auto extractedAnimationIt = mExtractedAnimationPaths.constFind( file );
156 if ( extractedAnimationIt != mExtractedAnimationPaths.constEnd() )
158 file = QDir( extractedAnimationIt.value() ).filePath( QStringLiteral(
"frame_%1.png" ).arg( frameNumber ) );
164 QgsImageCacheEntry *currentEntry =
findExistingEntry(
new QgsImageCacheEntry( file, size, keepAspectRatio, opacity, targetDpi, frameNumber ) );
171 if ( currentEntry->image.isNull() )
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();
180 currentEntry->image = QImage();
185 currentEntry->image = result;
187 currentEntry->nextFrameDelay = nextFrameDelayMs;
191 *isMissing = isBroken;
192 currentEntry->isMissingImage = isBroken;
198 result = currentEntry->image;
200 nextFrameDelayMs = currentEntry->nextFrameDelay;
202 *isMissing = currentEntry->isMissingImage;
210 if ( path.isEmpty() )
214 if ( !path.startsWith( QLatin1String(
"base64:" ) ) && QFile::exists( path ) )
216 const QImageReader reader( path );
217 if ( reader.size().isValid() )
218 return reader.size();
220 return QImage( path ).size();
224 QByteArray ba =
getContent( path, QByteArray(
"broken" ), QByteArray(
"fetching" ), blocking );
226 if ( ba !=
"broken" && ba !=
"fetching" )
228 QBuffer buffer( &ba );
229 buffer.open( QIODevice::ReadOnly );
231 QImageReader reader( &buffer );
234 const QSize s = reader.size();
237 const QImage im = reader.read();
238 return im.isNull() ? QSize() : im.size();
246 const QString file = path.trimmed();
248 if ( file.isEmpty() )
251 const QMutexLocker locker( &
mMutex );
253 auto it = mTotalFrameCounts.find( path );
254 if ( it != mTotalFrameCounts.end() )
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 );
268 const QString file = path.trimmed();
270 if ( file.isEmpty() )
273 const QMutexLocker locker( &
mMutex );
275 auto it = mImageDelays.find( path );
276 if ( it != mImageDelays.end() )
277 return it.value().value( currentFrame );
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 );
285 return nextFrameDelayMs <= 0 || res.isNull() ? -1 : nextFrameDelayMs;
290 const QMutexLocker locker( &
mMutex );
292 auto it = mExtractedAnimationPaths.find( path );
293 if ( it != mExtractedAnimationPaths.end() )
297 std::unique_ptr< QImageReader > reader;
298 std::unique_ptr< QBuffer > buffer;
300 if ( !path.startsWith( QLatin1String(
"base64:" ) ) && QFile::exists( path ) )
302 const QString basePart = QFileInfo( path ).baseName();
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 ) );
308 reader = std::make_unique< QImageReader >( path );
312 QByteArray ba =
getContent( path, QByteArray(
"broken" ), QByteArray(
"fetching" ),
false );
313 if ( ba ==
"broken" || ba ==
"fetching" )
319 const QString path = QUuid::createUuid().toString( QUuid::WithoutBraces );
320 filePath = mTemporaryDir->filePath( path );
322 buffer = std::make_unique< QBuffer >( &ba );
323 buffer->open( QIODevice::ReadOnly );
324 reader = std::make_unique< QImageReader> ( buffer.get() );
328 QDir().mkpath( filePath );
329 mExtractedAnimationPaths.insert( path, filePath );
331 const QDir frameDirectory( filePath );
334 reader->setAutoTransform(
true );
338 const QImage frame = reader->read();
339 if ( frame.isNull() )
342 mImageDelays[ path ].append( reader->nextImageDelay() );
344 const QString framePath = frameDirectory.filePath( QStringLiteral(
"frame_%1.png" ).arg( frameNumber++ ) );
345 frame.save( framePath,
"PNG" );
348 mTotalFrameCounts.insert( path, frameNumber );
351QImage 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
357 if ( !path.startsWith( QLatin1String(
"base64:" ) ) && QFile::exists( path ) )
359 QImageReader reader( path );
360 reader.setAutoTransform(
true );
362 if ( reader.format() ==
"pdf" )
364 if ( !size.isEmpty() )
371 reader.setScaledSize( size );
376 const QSize sizeAt72Dpi = reader.size();
377 const QSize sizeAtTargetDpi = sizeAt72Dpi * targetDpi / 72;
378 reader.setScaledSize( sizeAtTargetDpi );
384 if ( frameNumber == -1 )
390 im = getFrameFromReader( reader, frameNumber );
392 nextFrameDelayMs = reader.nextImageDelay();
396 QByteArray ba =
getContent( path, QByteArray(
"broken" ), QByteArray(
"fetching" ), blocking );
398 if ( ba ==
"broken" )
403 if ( !size.isValid() || size.isNull() )
407 if ( size.width() == 0 )
408 size.setWidth( size.height() );
409 if ( size.height() == 0 )
410 size.setHeight( size.width() );
412 im = QImage( size, QImage::Format_ARGB32_Premultiplied );
416 QSvgRenderer r( mMissingSvg );
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 );
423 else if ( ba ==
"fetching" )
426 if ( size.width() == 0 )
427 size.setWidth( size.height() );
428 if ( size.height() == 0 )
429 size.setHeight( size.width() );
432 im = QImage( size, QImage::Format_ARGB32_Premultiplied );
436 QSvgRenderer r( mFetchingSvg );
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 );
445 QBuffer buffer( &ba );
446 buffer.open( QIODevice::ReadOnly );
448 QImageReader reader( &buffer );
449 reader.setAutoTransform(
true );
451 if ( reader.format() ==
"pdf" )
453 if ( !size.isEmpty() )
460 reader.setScaledSize( size );
465 const QSize sizeAt72Dpi = reader.size();
466 const QSize sizeAtTargetDpi = sizeAt72Dpi * targetDpi / 72;
467 reader.setScaledSize( sizeAtTargetDpi );
472 if ( frameNumber == -1 )
478 im = getFrameFromReader( reader, frameNumber );
480 nextFrameDelayMs = reader.nextImageDelay();
484 if ( !im.hasAlphaChannel() )
485 im = im.convertToFormat( QImage::Format_ARGB32 );
491 if ( !size.isValid() || size.isNull() || im.size() == size )
494 else if ( keepAspectRatio && size.height() == 0 )
495 return im.scaledToWidth( size.width(), Qt::SmoothTransformation );
497 else if ( keepAspectRatio && size.width() == 0 )
498 return im.scaledToHeight( size.height(), Qt::SmoothTransformation );
500 return im.scaled( size, keepAspectRatio ? Qt::KeepAspectRatio : Qt::IgnoreAspectRatio, Qt::SmoothTransformation );
503QImage QgsImageCache::getFrameFromReader( QImageReader &reader,
int frameNumber )
505 if ( reader.jumpToImage( frameNumber ) )
506 return reader.read();
509 for (
int frame = 0; frame < frameNumber; ++frame )
511 if ( reader.read().isNull() )
514 return reader.read();
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.
long mMaxCacheSize
Maximum cache size.
QByteArray getContent(const QString &path, const QByteArray &missingContent, const QByteArray &fetchingContent, bool blocking=false) const
Gets the file content corresponding to the given path.
QgsImageCacheEntry * findExistingEntry(QgsImageCacheEntry *entryTemplate)
Returns the existing entry from the cache which matches entryTemplate (deleting entryTemplate when do...
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:
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
#define QgsDebugMsgLevel(str, level)