31#include <QApplication>
33#include <QCoreApplication>
35#include <QDomDocument>
40#include <QImageReader>
41#include <QNetworkReply>
42#include <QNetworkRequest>
46#include <QSvgRenderer>
47#include <QTemporaryDir>
50#include "moc_qgsimagecache.cpp"
52using namespace Qt::StringLiterals;
56QgsImageCacheEntry::QgsImageCacheEntry(
const QString &path, QSize size,
const bool keepAspectRatio,
const double opacity,
double dpi,
int frameNumber )
59 , keepAspectRatio( keepAspectRatio )
62 , frameNumber( frameNumber )
68 const QgsImageCacheEntry *otherImage =
dynamic_cast< const QgsImageCacheEntry *
>( other );
71 || otherImage->keepAspectRatio != keepAspectRatio
72 || otherImage->frameNumber != frameNumber
73 || otherImage->size != size
74 || ( !size.isValid() && otherImage->targetDpi != targetDpi )
75 || otherImage->opacity != opacity
76 || otherImage->path != path )
82int QgsImageCacheEntry::dataSize()
const
85 if ( !image.isNull() )
87 size += image.sizeInBytes();
92void QgsImageCacheEntry::dump()
const
94 QgsDebugMsgLevel( u
"path: %1, size %2x%3"_s.arg( path ).arg( size.width() ).arg( size.height() ), 3 );
102 mTemporaryDir = std::make_unique<QTemporaryDir>( );
104 const int bytes =
QgsSettings().
value( u
"/qgis/maxImageCacheSize"_s, 0 ).toInt();
114 if ( sysMemory >= 32000 )
116 else if ( sysMemory >= 16000 )
123 mMissingSvg = u
"<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>?</text></svg>"_s.toLatin1();
126 if ( QFile::exists( downloadingSvgPath ) )
128 QFile file( downloadingSvgPath );
129 if ( file.open( QIODevice::ReadOnly ) )
131 mFetchingSvg = file.readAll();
135 if ( mFetchingSvg.isEmpty() )
137 mFetchingSvg = u
"<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>?</text></svg>"_s.toLatin1();
145QImage
QgsImageCache::pathAsImage(
const QString &f,
const QSize size,
const bool keepAspectRatio,
const double opacity,
bool &fitsInCache,
bool blocking,
double targetDpi,
int frameNumber,
bool *isMissing )
148 int nextFrameDelayMs = 0;
149 return pathAsImagePrivate( f, size, keepAspectRatio, opacity, fitsInCache, blocking, targetDpi, frameNumber, isMissing,
totalFrameCount, nextFrameDelayMs );
152QImage 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 )
154 QString file = f.trimmed();
158 if ( file.isEmpty() )
161 const QMutexLocker locker( &
mMutex );
163 const auto extractedAnimationIt = mExtractedAnimationPaths.constFind( file );
164 if ( extractedAnimationIt != mExtractedAnimationPaths.constEnd() )
166 file = QDir( extractedAnimationIt.value() ).filePath( u
"frame_%1.png"_s.arg( frameNumber ) );
172 QString base64String;
174 if (
parseBase64DataUrl( file, &mimeType, &base64String ) && mimeType.startsWith(
"image/"_L1 ) )
176 file = u
"base64:%1"_s.arg( base64String );
179 QgsImageCacheEntry *currentEntry =
findExistingEntry(
new QgsImageCacheEntry( file, size, keepAspectRatio, opacity, targetDpi, frameNumber ) );
186 if ( currentEntry->image.isNull() )
188 long cachedDataSize = 0;
189 bool isBroken =
false;
190 result = renderImage( file, size, keepAspectRatio, opacity, targetDpi, frameNumber, isBroken,
totalFrameCount, nextFrameDelayMs, blocking );
191 cachedDataSize += result.sizeInBytes();
195 currentEntry->image = QImage();
200 currentEntry->image = result;
202 currentEntry->nextFrameDelay = nextFrameDelayMs;
206 *isMissing = isBroken;
207 currentEntry->isMissingImage = isBroken;
213 result = currentEntry->image;
215 nextFrameDelayMs = currentEntry->nextFrameDelay;
217 *isMissing = currentEntry->isMissingImage;
225 return mImageSizeCache.originalSize( path, blocking );
228QSize QgsImageCache::originalSizePrivate(
const QString &path,
bool blocking )
const
230 if ( path.isEmpty() )
236 const QImageReader reader( path );
237 if ( reader.size().isValid() )
238 return reader.size();
240 return QImage( path ).size();
244 QByteArray ba =
getContent( path, QByteArray(
"broken" ), QByteArray(
"fetching" ), blocking );
246 if ( ba !=
"broken" && ba !=
"fetching" )
248 QBuffer buffer( &ba );
249 buffer.open( QIODevice::ReadOnly );
251 QImageReader reader( &buffer );
254 const QSize s = reader.size();
257 const QImage im = reader.read();
258 return im.isNull() ? QSize() : im.size();
266 const QString file = path.trimmed();
268 if ( file.isEmpty() )
271 const QMutexLocker locker( &
mMutex );
273 auto it = mTotalFrameCounts.find( path );
274 if ( it != mTotalFrameCounts.end() )
278 int nextFrameDelayMs = 0;
279 bool fitsInCache =
false;
280 bool isMissing =
false;
281 ( void )pathAsImagePrivate( file, QSize(),
true, 1.0, fitsInCache, blocking, 96, 0, &isMissing, res, nextFrameDelayMs );
288 const QString file = path.trimmed();
290 if ( file.isEmpty() )
293 const QMutexLocker locker( &
mMutex );
295 auto it = mImageDelays.find( path );
296 if ( it != mImageDelays.end() )
297 return it.value().value( currentFrame );
300 int nextFrameDelayMs = 0;
301 bool fitsInCache =
false;
302 bool isMissing =
false;
303 const QImage res = pathAsImagePrivate( file, QSize(),
true, 1.0, fitsInCache, blocking, 96, currentFrame, &isMissing, frameCount, nextFrameDelayMs );
305 return nextFrameDelayMs <= 0 || res.isNull() ? -1 : nextFrameDelayMs;
310 const QMutexLocker locker( &
mMutex );
312 auto it = mExtractedAnimationPaths.find( path );
313 if ( it != mExtractedAnimationPaths.end() )
317 std::unique_ptr< QImageReader > reader;
318 std::unique_ptr< QBuffer > buffer;
322 const QString basePart = QFileInfo( path ).baseName();
324 filePath = mTemporaryDir->filePath( u
"%1_%2"_s.arg( basePart ).arg(
id ) );
325 while ( QFile::exists( filePath ) )
326 filePath = mTemporaryDir->filePath( u
"%1_%2"_s.arg( basePart ).arg( ++
id ) );
328 reader = std::make_unique< QImageReader >( path );
332 QByteArray ba =
getContent( path, QByteArray(
"broken" ), QByteArray(
"fetching" ),
false );
333 if ( ba ==
"broken" || ba ==
"fetching" )
339 const QString path = QUuid::createUuid().toString( QUuid::WithoutBraces );
340 filePath = mTemporaryDir->filePath( path );
342 buffer = std::make_unique< QBuffer >( &ba );
343 buffer->open( QIODevice::ReadOnly );
344 reader = std::make_unique< QImageReader> ( buffer.get() );
348 QDir().mkpath( filePath );
349 mExtractedAnimationPaths.insert( path, filePath );
351 const QDir frameDirectory( filePath );
354 reader->setAutoTransform(
true );
358 const QImage frame = reader->read();
359 if ( frame.isNull() )
362 mImageDelays[ path ].append( reader->nextImageDelay() );
364 const QString framePath = frameDirectory.filePath( u
"frame_%1.png"_s.arg( frameNumber++ ) );
365 frame.save( framePath,
"PNG" );
368 mTotalFrameCounts.insert( path, frameNumber );
371QImage 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
379 QImageReader reader( path );
380 reader.setAutoTransform(
true );
382 if ( reader.format() ==
"pdf" )
384 if ( !size.isEmpty() )
391 reader.setScaledSize( size );
396 const QSize sizeAt72Dpi = reader.size();
397 const QSize sizeAtTargetDpi = sizeAt72Dpi * targetDpi / 72;
398 reader.setScaledSize( sizeAtTargetDpi );
404 if ( frameNumber == -1 )
410 im = getFrameFromReader( reader, frameNumber );
412 nextFrameDelayMs = reader.nextImageDelay();
416 QByteArray ba =
getContent( path, QByteArray(
"broken" ), QByteArray(
"fetching" ), blocking );
418 if ( ba ==
"broken" )
423 if ( !size.isValid() || size.isNull() )
427 if ( size.width() == 0 )
428 size.setWidth( size.height() );
429 if ( size.height() == 0 )
430 size.setHeight( size.width() );
432 im = QImage( size, QImage::Format_ARGB32_Premultiplied );
436 QSvgRenderer r( mMissingSvg );
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 );
443 else if ( ba ==
"fetching" )
446 if ( size.width() == 0 )
447 size.setWidth( size.height() );
448 if ( size.height() == 0 )
449 size.setHeight( size.width() );
452 im = QImage( size, QImage::Format_ARGB32_Premultiplied );
456 QSvgRenderer r( mFetchingSvg );
458 QSizeF s( r.viewBox().size() );
459 s.scale( size.width(), size.height(), Qt::KeepAspectRatio );
460 const QRectF rect( ( size.width() - s.width() ) / 2, ( size.height() - s.height() ) / 2, s.width(), s.height() );
461 r.render( &p, rect );
465 QBuffer buffer( &ba );
466 buffer.open( QIODevice::ReadOnly );
468 QImageReader reader( &buffer );
469 reader.setAutoTransform(
true );
471 if ( reader.format() ==
"pdf" )
473 if ( !size.isEmpty() )
480 reader.setScaledSize( size );
485 const QSize sizeAt72Dpi = reader.size();
486 const QSize sizeAtTargetDpi = sizeAt72Dpi * targetDpi / 72;
487 reader.setScaledSize( sizeAtTargetDpi );
492 if ( frameNumber == -1 )
498 im = getFrameFromReader( reader, frameNumber );
500 nextFrameDelayMs = reader.nextImageDelay();
504 if ( !im.hasAlphaChannel()
505#
if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
506 && im.format() != QImage::Format_CMYK8888
509 im = im.convertToFormat( QImage::Format_ARGB32 );
515 if ( !size.isValid() || size.isNull() || im.size() == size )
518 else if ( keepAspectRatio && size.height() == 0 )
519 return im.scaledToWidth( size.width(), Qt::SmoothTransformation );
521 else if ( keepAspectRatio && size.width() == 0 )
522 return im.scaledToHeight( size.height(), Qt::SmoothTransformation );
524 return im.scaled( size, keepAspectRatio ? Qt::KeepAspectRatio : Qt::IgnoreAspectRatio, Qt::SmoothTransformation );
527QImage QgsImageCache::getFrameFromReader( QImageReader &reader,
int frameNumber )
529 if ( reader.jumpToImage( frameNumber ) )
530 return reader.read();
533 for (
int frame = 0; frame < frameNumber; ++frame )
535 if ( reader.read().isNull() )
538 return reader.read();
544QgsImageSizeCacheEntry::QgsImageSizeCacheEntry(
const QString &path )
550int QgsImageSizeCacheEntry::dataSize()
const
552 return sizeof( QSize );
555void QgsImageSizeCacheEntry::dump()
const
562 const QgsImageSizeCacheEntry *otherImage =
dynamic_cast< const QgsImageSizeCacheEntry *
>( other );
564 || otherImage->path != path )
577QgsImageSizeCache::QgsImageSizeCache( QObject *parent )
583QgsImageSizeCache::~QgsImageSizeCache() =
default;
585QSize QgsImageSizeCache::originalSize(
const QString &f,
bool blocking )
587 QString file = f.trimmed();
589 if ( file.isEmpty() )
592 const QMutexLocker locker( &mMutex );
594 QString base64String;
596 if ( parseBase64DataUrl( file, &mimeType, &base64String ) && mimeType.startsWith(
"image/"_L1 ) )
598 file = u
"base64:%1"_s.arg( base64String );
601 QgsImageSizeCacheEntry *currentEntry = findExistingEntry(
new QgsImageSizeCacheEntry( file ) );
605 if ( !currentEntry->size.isValid() )
608 mTotalSize += currentEntry->dataSize();
609 currentEntry->size = result;
614 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.
QByteArray getContent(const QString &path, const QByteArray &missingContent, const QByteArray &fetchingContent, bool blocking=false) const
QgsImageCacheEntry * findExistingEntry(QgsImageCacheEntry *entryTemplate)
QgsAbstractContentCache(QObject *parent=nullptr, const QString &typeString=QString(), long maxCacheSize=20000000, int fileModifiedCheckTimeout=30000)
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.
Stores settings for use within QGIS.
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
#define QgsDebugMsgLevel(str, level)