31#include <QApplication>
33#include <QCoreApplication>
35#include <QDomDocument>
40#include <QImageReader>
41#include <QNetworkReply>
42#include <QNetworkRequest>
45#include <QSvgRenderer>
46#include <QTemporaryDir>
49#include "moc_qgsimagecache.cpp"
53QgsImageCacheEntry::QgsImageCacheEntry(
const QString &path, QSize size,
const bool keepAspectRatio,
const double opacity,
double dpi,
int frameNumber )
56 , keepAspectRatio( keepAspectRatio )
59 , frameNumber( frameNumber )
65 const QgsImageCacheEntry *otherImage =
dynamic_cast< const QgsImageCacheEntry *
>( other );
68 || otherImage->keepAspectRatio != keepAspectRatio
69 || otherImage->frameNumber != frameNumber
70 || otherImage->size != size
71 || ( !size.isValid() && otherImage->targetDpi != targetDpi )
72 || otherImage->opacity != opacity
73 || otherImage->path != path )
79int QgsImageCacheEntry::dataSize()
const
82 if ( !image.isNull() )
84 size += image.sizeInBytes();
89void QgsImageCacheEntry::dump()
const
91 QgsDebugMsgLevel( QStringLiteral(
"path: %1, size %2x%3" ).arg( path ).arg( size.width() ).arg( size.height() ), 3 );
99 mTemporaryDir = std::make_unique<QTemporaryDir>( );
101 const int bytes =
QgsSettings().
value( QStringLiteral(
"/qgis/maxImageCacheSize" ), 0 ).toInt();
111 if ( sysMemory >= 32000 )
113 else if ( sysMemory >= 16000 )
120 mMissingSvg = QStringLiteral(
"<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>?</text></svg>" ).toLatin1();
123 if ( QFile::exists( downloadingSvgPath ) )
125 QFile file( downloadingSvgPath );
126 if ( file.open( QIODevice::ReadOnly ) )
128 mFetchingSvg = file.readAll();
132 if ( mFetchingSvg.isEmpty() )
134 mFetchingSvg = QStringLiteral(
"<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>?</text></svg>" ).toLatin1();
142QImage
QgsImageCache::pathAsImage(
const QString &f,
const QSize size,
const bool keepAspectRatio,
const double opacity,
bool &fitsInCache,
bool blocking,
double targetDpi,
int frameNumber,
bool *isMissing )
145 int nextFrameDelayMs = 0;
146 return pathAsImagePrivate( f, size, keepAspectRatio, opacity, fitsInCache, blocking, targetDpi, frameNumber, isMissing,
totalFrameCount, nextFrameDelayMs );
149QImage 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 )
151 QString file = f.trimmed();
155 if ( file.isEmpty() )
158 const QMutexLocker locker( &
mMutex );
160 const auto extractedAnimationIt = mExtractedAnimationPaths.constFind( file );
161 if ( extractedAnimationIt != mExtractedAnimationPaths.constEnd() )
163 file = QDir( extractedAnimationIt.value() ).filePath( QStringLiteral(
"frame_%1.png" ).arg( frameNumber ) );
169 QString base64String;
171 if (
parseBase64DataUrl( file, &mimeType, &base64String ) && mimeType.startsWith( QLatin1String(
"image/" ) ) )
173 file = QStringLiteral(
"base64:%1" ).arg( base64String );
176 QgsImageCacheEntry *currentEntry =
findExistingEntry(
new QgsImageCacheEntry( file, size, keepAspectRatio, opacity, targetDpi, frameNumber ) );
183 if ( currentEntry->image.isNull() )
185 long cachedDataSize = 0;
186 bool isBroken =
false;
187 result = renderImage( file, size, keepAspectRatio, opacity, targetDpi, frameNumber, isBroken,
totalFrameCount, nextFrameDelayMs, blocking );
188 cachedDataSize += result.sizeInBytes();
192 currentEntry->image = QImage();
197 currentEntry->image = result;
199 currentEntry->nextFrameDelay = nextFrameDelayMs;
203 *isMissing = isBroken;
204 currentEntry->isMissingImage = isBroken;
210 result = currentEntry->image;
212 nextFrameDelayMs = currentEntry->nextFrameDelay;
214 *isMissing = currentEntry->isMissingImage;
222 return mImageSizeCache.originalSize( path, blocking );
225QSize QgsImageCache::originalSizePrivate(
const QString &path,
bool blocking )
const
227 if ( path.isEmpty() )
233 const QImageReader reader( path );
234 if ( reader.size().isValid() )
235 return reader.size();
237 return QImage( path ).size();
241 QByteArray ba =
getContent( path, QByteArray(
"broken" ), QByteArray(
"fetching" ), blocking );
243 if ( ba !=
"broken" && ba !=
"fetching" )
245 QBuffer buffer( &ba );
246 buffer.open( QIODevice::ReadOnly );
248 QImageReader reader( &buffer );
251 const QSize s = reader.size();
254 const QImage im = reader.read();
255 return im.isNull() ? QSize() : im.size();
263 const QString file = path.trimmed();
265 if ( file.isEmpty() )
268 const QMutexLocker locker( &
mMutex );
270 auto it = mTotalFrameCounts.find( path );
271 if ( it != mTotalFrameCounts.end() )
275 int nextFrameDelayMs = 0;
276 bool fitsInCache =
false;
277 bool isMissing =
false;
278 ( void )pathAsImagePrivate( file, QSize(),
true, 1.0, fitsInCache, blocking, 96, 0, &isMissing, res, nextFrameDelayMs );
285 const QString file = path.trimmed();
287 if ( file.isEmpty() )
290 const QMutexLocker locker( &
mMutex );
292 auto it = mImageDelays.find( path );
293 if ( it != mImageDelays.end() )
294 return it.value().value( currentFrame );
297 int nextFrameDelayMs = 0;
298 bool fitsInCache =
false;
299 bool isMissing =
false;
300 const QImage res = pathAsImagePrivate( file, QSize(),
true, 1.0, fitsInCache, blocking, 96, currentFrame, &isMissing, frameCount, nextFrameDelayMs );
302 return nextFrameDelayMs <= 0 || res.isNull() ? -1 : nextFrameDelayMs;
307 const QMutexLocker locker( &
mMutex );
309 auto it = mExtractedAnimationPaths.find( path );
310 if ( it != mExtractedAnimationPaths.end() )
314 std::unique_ptr< QImageReader > reader;
315 std::unique_ptr< QBuffer > buffer;
319 const QString basePart = QFileInfo( path ).baseName();
321 filePath = mTemporaryDir->filePath( QStringLiteral(
"%1_%2" ).arg( basePart ).arg(
id ) );
322 while ( QFile::exists( filePath ) )
323 filePath = mTemporaryDir->filePath( QStringLiteral(
"%1_%2" ).arg( basePart ).arg( ++
id ) );
325 reader = std::make_unique< QImageReader >( path );
329 QByteArray ba =
getContent( path, QByteArray(
"broken" ), QByteArray(
"fetching" ),
false );
330 if ( ba ==
"broken" || ba ==
"fetching" )
336 const QString path = QUuid::createUuid().toString( QUuid::WithoutBraces );
337 filePath = mTemporaryDir->filePath( path );
339 buffer = std::make_unique< QBuffer >( &ba );
340 buffer->open( QIODevice::ReadOnly );
341 reader = std::make_unique< QImageReader> ( buffer.get() );
345 QDir().mkpath( filePath );
346 mExtractedAnimationPaths.insert( path, filePath );
348 const QDir frameDirectory( filePath );
351 reader->setAutoTransform(
true );
355 const QImage frame = reader->read();
356 if ( frame.isNull() )
359 mImageDelays[ path ].append( reader->nextImageDelay() );
361 const QString framePath = frameDirectory.filePath( QStringLiteral(
"frame_%1.png" ).arg( frameNumber++ ) );
362 frame.save( framePath,
"PNG" );
365 mTotalFrameCounts.insert( path, frameNumber );
368QImage 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
376 QImageReader reader( path );
377 reader.setAutoTransform(
true );
379 if ( reader.format() ==
"pdf" )
381 if ( !size.isEmpty() )
388 reader.setScaledSize( size );
393 const QSize sizeAt72Dpi = reader.size();
394 const QSize sizeAtTargetDpi = sizeAt72Dpi * targetDpi / 72;
395 reader.setScaledSize( sizeAtTargetDpi );
401 if ( frameNumber == -1 )
407 im = getFrameFromReader( reader, frameNumber );
409 nextFrameDelayMs = reader.nextImageDelay();
413 QByteArray ba =
getContent( path, QByteArray(
"broken" ), QByteArray(
"fetching" ), blocking );
415 if ( ba ==
"broken" )
420 if ( !size.isValid() || size.isNull() )
424 if ( size.width() == 0 )
425 size.setWidth( size.height() );
426 if ( size.height() == 0 )
427 size.setHeight( size.width() );
429 im = QImage( size, QImage::Format_ARGB32_Premultiplied );
433 QSvgRenderer r( mMissingSvg );
435 QSizeF s( r.viewBox().size() );
436 s.scale( size.width(), size.height(), Qt::KeepAspectRatio );
437 const QRectF rect( ( size.width() - s.width() ) / 2, ( size.height() - s.height() ) / 2, s.width(), s.height() );
438 r.render( &p, rect );
440 else if ( ba ==
"fetching" )
443 if ( size.width() == 0 )
444 size.setWidth( size.height() );
445 if ( size.height() == 0 )
446 size.setHeight( size.width() );
449 im = QImage( size, QImage::Format_ARGB32_Premultiplied );
453 QSvgRenderer r( mFetchingSvg );
455 QSizeF s( r.viewBox().size() );
456 s.scale( size.width(), size.height(), Qt::KeepAspectRatio );
457 const QRectF rect( ( size.width() - s.width() ) / 2, ( size.height() - s.height() ) / 2, s.width(), s.height() );
458 r.render( &p, rect );
462 QBuffer buffer( &ba );
463 buffer.open( QIODevice::ReadOnly );
465 QImageReader reader( &buffer );
466 reader.setAutoTransform(
true );
468 if ( reader.format() ==
"pdf" )
470 if ( !size.isEmpty() )
477 reader.setScaledSize( size );
482 const QSize sizeAt72Dpi = reader.size();
483 const QSize sizeAtTargetDpi = sizeAt72Dpi * targetDpi / 72;
484 reader.setScaledSize( sizeAtTargetDpi );
489 if ( frameNumber == -1 )
495 im = getFrameFromReader( reader, frameNumber );
497 nextFrameDelayMs = reader.nextImageDelay();
501 if ( !im.hasAlphaChannel()
502#
if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
503 && im.format() != QImage::Format_CMYK8888
506 im = im.convertToFormat( QImage::Format_ARGB32 );
512 if ( !size.isValid() || size.isNull() || im.size() == size )
515 else if ( keepAspectRatio && size.height() == 0 )
516 return im.scaledToWidth( size.width(), Qt::SmoothTransformation );
518 else if ( keepAspectRatio && size.width() == 0 )
519 return im.scaledToHeight( size.height(), Qt::SmoothTransformation );
521 return im.scaled( size, keepAspectRatio ? Qt::KeepAspectRatio : Qt::IgnoreAspectRatio, Qt::SmoothTransformation );
524QImage QgsImageCache::getFrameFromReader( QImageReader &reader,
int frameNumber )
526 if ( reader.jumpToImage( frameNumber ) )
527 return reader.read();
530 for (
int frame = 0; frame < frameNumber; ++frame )
532 if ( reader.read().isNull() )
535 return reader.read();
541QgsImageSizeCacheEntry::QgsImageSizeCacheEntry(
const QString &path )
547int QgsImageSizeCacheEntry::dataSize()
const
549 return sizeof( QSize );
552void QgsImageSizeCacheEntry::dump()
const
559 const QgsImageSizeCacheEntry *otherImage =
dynamic_cast< const QgsImageSizeCacheEntry *
>( other );
561 || otherImage->path != path )
574QgsImageSizeCache::QgsImageSizeCache( QObject *parent )
580QgsImageSizeCache::~QgsImageSizeCache() =
default;
582QSize QgsImageSizeCache::originalSize(
const QString &f,
bool blocking )
584 QString file = f.trimmed();
586 if ( file.isEmpty() )
589 const QMutexLocker locker( &mMutex );
591 QString base64String;
593 if ( parseBase64DataUrl( file, &mimeType, &base64String ) && mimeType.startsWith( QLatin1String(
"image/" ) ) )
595 file = QStringLiteral(
"base64:%1" ).arg( base64String );
598 QgsImageSizeCacheEntry *currentEntry = findExistingEntry(
new QgsImageSizeCacheEntry( file ) );
602 if ( !currentEntry->size.isValid() )
605 mTotalSize += currentEntry->dataSize();
606 currentEntry->size = result;
611 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)