19#include "moc_qgsimagecache.cpp"
30#include <QApplication>
31#include <QCoreApplication>
33#include <QDomDocument>
40#include <QNetworkReply>
41#include <QNetworkRequest>
43#include <QImageReader>
44#include <QSvgRenderer>
45#include <QTemporaryDir>
50QgsImageCacheEntry::QgsImageCacheEntry(
const QString &path, QSize size,
const bool keepAspectRatio,
const double opacity,
double dpi,
int frameNumber )
53 , keepAspectRatio( keepAspectRatio )
56 , frameNumber( frameNumber )
62 const QgsImageCacheEntry *otherImage =
dynamic_cast< const QgsImageCacheEntry *
>( other );
65 || otherImage->keepAspectRatio != keepAspectRatio
66 || otherImage->frameNumber != frameNumber
67 || otherImage->size != size
68 || ( !size.isValid() && otherImage->targetDpi != targetDpi )
69 || otherImage->opacity != opacity
70 || otherImage->path != path )
76int QgsImageCacheEntry::dataSize()
const
79 if ( !image.isNull() )
81 size += image.sizeInBytes();
86void QgsImageCacheEntry::dump()
const
88 QgsDebugMsgLevel( QStringLiteral(
"path: %1, size %2x%3" ).arg( path ).arg( size.width() ).arg( size.height() ), 3 );
96 mTemporaryDir.reset(
new QTemporaryDir() );
98 const int bytes =
QgsSettings().
value( QStringLiteral(
"/qgis/maxImageCacheSize" ), 0 ).toInt();
108 if ( sysMemory >= 32000 )
110 else if ( sysMemory >= 16000 )
117 mMissingSvg = QStringLiteral(
"<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>?</text></svg>" ).toLatin1();
120 if ( QFile::exists( downloadingSvgPath ) )
122 QFile file( downloadingSvgPath );
123 if ( file.open( QIODevice::ReadOnly ) )
125 mFetchingSvg = file.readAll();
129 if ( mFetchingSvg.isEmpty() )
131 mFetchingSvg = QStringLiteral(
"<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>?</text></svg>" ).toLatin1();
139QImage
QgsImageCache::pathAsImage(
const QString &f,
const QSize size,
const bool keepAspectRatio,
const double opacity,
bool &fitsInCache,
bool blocking,
double targetDpi,
int frameNumber,
bool *isMissing )
142 int nextFrameDelayMs = 0;
143 return pathAsImagePrivate( f, size, keepAspectRatio, opacity, fitsInCache, blocking, targetDpi, frameNumber, isMissing,
totalFrameCount, nextFrameDelayMs );
146QImage 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 )
148 QString file = f.trimmed();
152 if ( file.isEmpty() )
155 const QMutexLocker locker( &
mMutex );
157 const auto extractedAnimationIt = mExtractedAnimationPaths.constFind( file );
158 if ( extractedAnimationIt != mExtractedAnimationPaths.constEnd() )
160 file = QDir( extractedAnimationIt.value() ).filePath( QStringLiteral(
"frame_%1.png" ).arg( frameNumber ) );
166 QString base64String;
168 if (
parseBase64DataUrl( file, &mimeType, &base64String ) && mimeType.startsWith( QLatin1String(
"image/" ) ) )
170 file = QStringLiteral(
"base64:%1" ).arg( base64String );
173 QgsImageCacheEntry *currentEntry =
findExistingEntry(
new QgsImageCacheEntry( file, size, keepAspectRatio, opacity, targetDpi, frameNumber ) );
180 if ( currentEntry->image.isNull() )
182 long cachedDataSize = 0;
183 bool isBroken =
false;
184 result = renderImage( file, size, keepAspectRatio, opacity, targetDpi, frameNumber, isBroken,
totalFrameCount, nextFrameDelayMs, blocking );
185 cachedDataSize += result.sizeInBytes();
189 currentEntry->image = QImage();
194 currentEntry->image = result;
196 currentEntry->nextFrameDelay = nextFrameDelayMs;
200 *isMissing = isBroken;
201 currentEntry->isMissingImage = isBroken;
207 result = currentEntry->image;
209 nextFrameDelayMs = currentEntry->nextFrameDelay;
211 *isMissing = currentEntry->isMissingImage;
219 return mImageSizeCache.originalSize( path, blocking );
222QSize QgsImageCache::originalSizePrivate(
const QString &path,
bool blocking )
const
224 if ( path.isEmpty() )
230 const QImageReader reader( path );
231 if ( reader.size().isValid() )
232 return reader.size();
234 return QImage( path ).size();
238 QByteArray ba =
getContent( path, QByteArray(
"broken" ), QByteArray(
"fetching" ), blocking );
240 if ( ba !=
"broken" && ba !=
"fetching" )
242 QBuffer buffer( &ba );
243 buffer.open( QIODevice::ReadOnly );
245 QImageReader reader( &buffer );
248 const QSize s = reader.size();
251 const QImage im = reader.read();
252 return im.isNull() ? QSize() : im.size();
260 const QString file = path.trimmed();
262 if ( file.isEmpty() )
265 const QMutexLocker locker( &
mMutex );
267 auto it = mTotalFrameCounts.find( path );
268 if ( it != mTotalFrameCounts.end() )
272 int nextFrameDelayMs = 0;
273 bool fitsInCache =
false;
274 bool isMissing =
false;
275 ( void )pathAsImagePrivate( file, QSize(),
true, 1.0, fitsInCache, blocking, 96, 0, &isMissing, res, nextFrameDelayMs );
282 const QString file = path.trimmed();
284 if ( file.isEmpty() )
287 const QMutexLocker locker( &
mMutex );
289 auto it = mImageDelays.find( path );
290 if ( it != mImageDelays.end() )
291 return it.value().value( currentFrame );
294 int nextFrameDelayMs = 0;
295 bool fitsInCache =
false;
296 bool isMissing =
false;
297 const QImage res = pathAsImagePrivate( file, QSize(),
true, 1.0, fitsInCache, blocking, 96, currentFrame, &isMissing, frameCount, nextFrameDelayMs );
299 return nextFrameDelayMs <= 0 || res.isNull() ? -1 : nextFrameDelayMs;
304 const QMutexLocker locker( &
mMutex );
306 auto it = mExtractedAnimationPaths.find( path );
307 if ( it != mExtractedAnimationPaths.end() )
311 std::unique_ptr< QImageReader > reader;
312 std::unique_ptr< QBuffer > buffer;
316 const QString basePart = QFileInfo( path ).baseName();
318 filePath = mTemporaryDir->filePath( QStringLiteral(
"%1_%2" ).arg( basePart ).arg(
id ) );
319 while ( QFile::exists( filePath ) )
320 filePath = mTemporaryDir->filePath( QStringLiteral(
"%1_%2" ).arg( basePart ).arg( ++
id ) );
322 reader = std::make_unique< QImageReader >( path );
326 QByteArray ba =
getContent( path, QByteArray(
"broken" ), QByteArray(
"fetching" ),
false );
327 if ( ba ==
"broken" || ba ==
"fetching" )
333 const QString path = QUuid::createUuid().toString( QUuid::WithoutBraces );
334 filePath = mTemporaryDir->filePath( path );
336 buffer = std::make_unique< QBuffer >( &ba );
337 buffer->open( QIODevice::ReadOnly );
338 reader = std::make_unique< QImageReader> ( buffer.get() );
342 QDir().mkpath( filePath );
343 mExtractedAnimationPaths.insert( path, filePath );
345 const QDir frameDirectory( filePath );
348 reader->setAutoTransform(
true );
352 const QImage frame = reader->read();
353 if ( frame.isNull() )
356 mImageDelays[ path ].append( reader->nextImageDelay() );
358 const QString framePath = frameDirectory.filePath( QStringLiteral(
"frame_%1.png" ).arg( frameNumber++ ) );
359 frame.save( framePath,
"PNG" );
362 mTotalFrameCounts.insert( path, frameNumber );
365QImage 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
373 QImageReader reader( path );
374 reader.setAutoTransform(
true );
376 if ( reader.format() ==
"pdf" )
378 if ( !size.isEmpty() )
385 reader.setScaledSize( size );
390 const QSize sizeAt72Dpi = reader.size();
391 const QSize sizeAtTargetDpi = sizeAt72Dpi * targetDpi / 72;
392 reader.setScaledSize( sizeAtTargetDpi );
398 if ( frameNumber == -1 )
404 im = getFrameFromReader( reader, frameNumber );
406 nextFrameDelayMs = reader.nextImageDelay();
410 QByteArray ba =
getContent( path, QByteArray(
"broken" ), QByteArray(
"fetching" ), blocking );
412 if ( ba ==
"broken" )
417 if ( !size.isValid() || size.isNull() )
421 if ( size.width() == 0 )
422 size.setWidth( size.height() );
423 if ( size.height() == 0 )
424 size.setHeight( size.width() );
426 im = QImage( size, QImage::Format_ARGB32_Premultiplied );
430 QSvgRenderer r( mMissingSvg );
432 QSizeF s( r.viewBox().size() );
433 s.scale( size.width(), size.height(), Qt::KeepAspectRatio );
434 const QRectF rect( ( size.width() - s.width() ) / 2, ( size.height() - s.height() ) / 2, s.width(), s.height() );
435 r.render( &p, rect );
437 else if ( ba ==
"fetching" )
440 if ( size.width() == 0 )
441 size.setWidth( size.height() );
442 if ( size.height() == 0 )
443 size.setHeight( size.width() );
446 im = QImage( size, QImage::Format_ARGB32_Premultiplied );
450 QSvgRenderer r( mFetchingSvg );
452 QSizeF s( r.viewBox().size() );
453 s.scale( size.width(), size.height(), Qt::KeepAspectRatio );
454 const QRectF rect( ( size.width() - s.width() ) / 2, ( size.height() - s.height() ) / 2, s.width(), s.height() );
455 r.render( &p, rect );
459 QBuffer buffer( &ba );
460 buffer.open( QIODevice::ReadOnly );
462 QImageReader reader( &buffer );
463 reader.setAutoTransform(
true );
465 if ( reader.format() ==
"pdf" )
467 if ( !size.isEmpty() )
474 reader.setScaledSize( size );
479 const QSize sizeAt72Dpi = reader.size();
480 const QSize sizeAtTargetDpi = sizeAt72Dpi * targetDpi / 72;
481 reader.setScaledSize( sizeAtTargetDpi );
486 if ( frameNumber == -1 )
492 im = getFrameFromReader( reader, frameNumber );
494 nextFrameDelayMs = reader.nextImageDelay();
498 if ( !im.hasAlphaChannel()
499#
if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
500 && im.format() != QImage::Format_CMYK8888
503 im = im.convertToFormat( QImage::Format_ARGB32 );
509 if ( !size.isValid() || size.isNull() || im.size() == size )
512 else if ( keepAspectRatio && size.height() == 0 )
513 return im.scaledToWidth( size.width(), Qt::SmoothTransformation );
515 else if ( keepAspectRatio && size.width() == 0 )
516 return im.scaledToHeight( size.height(), Qt::SmoothTransformation );
518 return im.scaled( size, keepAspectRatio ? Qt::KeepAspectRatio : Qt::IgnoreAspectRatio, Qt::SmoothTransformation );
521QImage QgsImageCache::getFrameFromReader( QImageReader &reader,
int frameNumber )
523 if ( reader.jumpToImage( frameNumber ) )
524 return reader.read();
527 for (
int frame = 0; frame < frameNumber; ++frame )
529 if ( reader.read().isNull() )
532 return reader.read();
538QgsImageSizeCacheEntry::QgsImageSizeCacheEntry(
const QString &path )
544int QgsImageSizeCacheEntry::dataSize()
const
546 return sizeof( QSize );
549void QgsImageSizeCacheEntry::dump()
const
556 const QgsImageSizeCacheEntry *otherImage =
dynamic_cast< const QgsImageSizeCacheEntry *
>( other );
558 || otherImage->path != path )
571QgsImageSizeCache::QgsImageSizeCache( QObject *parent )
577QgsImageSizeCache::~QgsImageSizeCache() =
default;
579QSize QgsImageSizeCache::originalSize(
const QString &f,
bool blocking )
581 QString file = f.trimmed();
583 if ( file.isEmpty() )
586 const QMutexLocker locker( &mMutex );
588 QString base64String;
590 if ( parseBase64DataUrl( file, &mimeType, &base64String ) && mimeType.startsWith( QLatin1String(
"image/" ) ) )
592 file = QStringLiteral(
"base64:%1" ).arg( base64String );
595 QgsImageSizeCacheEntry *currentEntry = findExistingEntry(
new QgsImageSizeCacheEntry( file ) );
599 if ( !currentEntry->size.isValid() )
602 mTotalSize += currentEntry->dataSize();
603 currentEntry->size = result;
608 result = currentEntry->size;
void remoteContentFetched(const QString &url)
Emitted when the cache has finished retrieving content from a remote url.
static bool parseBase64DataUrl(const QString &path, QString *mimeType=nullptr, QString *data=nullptr)
Parses a path to determine if it represents a base 64 encoded HTML data URL, and if so,...
static bool isBase64Data(const QString &path)
Returns true if path represents base64 encoded data.
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.
static QgsImageCache * imageCache()
Returns the application's image cache, used for caching resampled versions of raster images.
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)