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 )
67 const QgsImageCacheEntry *otherImage =
dynamic_cast< const QgsImageCacheEntry *
>( other );
70 || otherImage->keepAspectRatio != keepAspectRatio
71 || otherImage->frameNumber != frameNumber
72 || otherImage->size != size
73 || ( !size.isValid() && otherImage->targetDpi != targetDpi )
74 || otherImage->opacity != opacity
75 || otherImage->path != path )
81int QgsImageCacheEntry::dataSize()
const
84 if ( !image.isNull() )
86 size += image.sizeInBytes();
91void QgsImageCacheEntry::dump()
const
93 QgsDebugMsgLevel( u
"path: %1, size %2x%3"_s.arg( path ).arg( size.width() ).arg( size.height() ), 3 );
101 mTemporaryDir = std::make_unique<QTemporaryDir>();
103 const int bytes =
QgsSettings().
value( u
"/qgis/maxImageCacheSize"_s, 0 ).toInt();
113 if ( sysMemory >= 32000 )
115 else if ( sysMemory >= 16000 )
122 mMissingSvg = u
"<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>?</text></svg>"_s.toLatin1();
125 if ( QFile::exists( downloadingSvgPath ) )
127 QFile file( downloadingSvgPath );
128 if ( file.open( QIODevice::ReadOnly ) )
130 mFetchingSvg = file.readAll();
134 if ( mFetchingSvg.isEmpty() )
136 mFetchingSvg = u
"<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>?</text></svg>"_s.toLatin1();
144QImage
QgsImageCache::pathAsImage(
const QString &f,
const QSize size,
const bool keepAspectRatio,
const double opacity,
bool &fitsInCache,
bool blocking,
double targetDpi,
int frameNumber,
bool *isMissing )
147 int nextFrameDelayMs = 0;
148 return pathAsImagePrivate( f, size, keepAspectRatio, opacity, fitsInCache, blocking, targetDpi, frameNumber, isMissing,
totalFrameCount, nextFrameDelayMs );
151QImage QgsImageCache::pathAsImagePrivate(
152 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
155 QString file = f.trimmed();
159 if ( file.isEmpty() )
162 const QMutexLocker locker( &
mMutex );
164 const auto extractedAnimationIt = mExtractedAnimationPaths.constFind( file );
165 if ( extractedAnimationIt != mExtractedAnimationPaths.constEnd() )
167 file = QDir( extractedAnimationIt.value() ).filePath( u
"frame_%1.png"_s.arg( frameNumber ) );
173 QString base64String;
175 if (
parseBase64DataUrl( file, &mimeType, &base64String ) && mimeType.startsWith(
"image/"_L1 ) )
177 file = u
"base64:%1"_s.arg( base64String );
180 QgsImageCacheEntry *currentEntry =
findExistingEntry(
new QgsImageCacheEntry( file, size, keepAspectRatio, opacity, targetDpi, frameNumber ) );
187 if ( currentEntry->image.isNull() )
189 long cachedDataSize = 0;
190 bool isBroken =
false;
191 result = renderImage( file, size, keepAspectRatio, opacity, targetDpi, frameNumber, isBroken,
totalFrameCount, nextFrameDelayMs, blocking );
192 cachedDataSize += result.sizeInBytes();
196 currentEntry->image = QImage();
201 currentEntry->image = result;
203 currentEntry->nextFrameDelay = nextFrameDelayMs;
207 *isMissing = isBroken;
208 currentEntry->isMissingImage = isBroken;
214 result = currentEntry->image;
216 nextFrameDelayMs = currentEntry->nextFrameDelay;
218 *isMissing = currentEntry->isMissingImage;
226 return mImageSizeCache.originalSize( path, blocking );
229QSize QgsImageCache::originalSizePrivate(
const QString &path,
bool blocking )
const
231 if ( path.isEmpty() )
237 const QImageReader reader( path );
238 if ( reader.size().isValid() )
239 return reader.size();
241 return QImage( path ).size();
245 QByteArray ba =
getContent( path, QByteArray(
"broken" ), QByteArray(
"fetching" ), blocking );
247 if ( ba !=
"broken" && ba !=
"fetching" )
249 QBuffer buffer( &ba );
250 buffer.open( QIODevice::ReadOnly );
252 QImageReader reader( &buffer );
255 const QSize s = reader.size();
258 const QImage im = reader.read();
259 return im.isNull() ? QSize() : im.size();
267 const QString file = path.trimmed();
269 if ( file.isEmpty() )
272 const QMutexLocker locker( &
mMutex );
274 auto it = mTotalFrameCounts.find( path );
275 if ( it != mTotalFrameCounts.end() )
279 int nextFrameDelayMs = 0;
280 bool fitsInCache =
false;
281 bool isMissing =
false;
282 ( void ) pathAsImagePrivate( file, QSize(),
true, 1.0, fitsInCache, blocking, 96, 0, &isMissing, res, nextFrameDelayMs );
289 const QString file = path.trimmed();
291 if ( file.isEmpty() )
294 const QMutexLocker locker( &
mMutex );
296 auto it = mImageDelays.find( path );
297 if ( it != mImageDelays.end() )
298 return it.value().value( currentFrame );
301 int nextFrameDelayMs = 0;
302 bool fitsInCache =
false;
303 bool isMissing =
false;
304 const QImage res = pathAsImagePrivate( file, QSize(),
true, 1.0, fitsInCache, blocking, 96, currentFrame, &isMissing, frameCount, nextFrameDelayMs );
306 return nextFrameDelayMs <= 0 || res.isNull() ? -1 : nextFrameDelayMs;
311 const QMutexLocker locker( &
mMutex );
313 auto it = mExtractedAnimationPaths.find( path );
314 if ( it != mExtractedAnimationPaths.end() )
318 std::unique_ptr< QImageReader > reader;
319 std::unique_ptr< QBuffer > buffer;
323 const QString basePart = QFileInfo( path ).baseName();
325 filePath = mTemporaryDir->filePath( u
"%1_%2"_s.arg( basePart ).arg(
id ) );
326 while ( QFile::exists( filePath ) )
327 filePath = mTemporaryDir->filePath( u
"%1_%2"_s.arg( basePart ).arg( ++
id ) );
329 reader = std::make_unique< QImageReader >( path );
333 QByteArray ba =
getContent( path, QByteArray(
"broken" ), QByteArray(
"fetching" ),
false );
334 if ( ba ==
"broken" || ba ==
"fetching" )
340 const QString path = QUuid::createUuid().toString( QUuid::WithoutBraces );
341 filePath = mTemporaryDir->filePath( path );
343 buffer = std::make_unique< QBuffer >( &ba );
344 buffer->open( QIODevice::ReadOnly );
345 reader = std::make_unique< QImageReader>( buffer.get() );
349 QDir().mkpath( filePath );
350 mExtractedAnimationPaths.insert( path, filePath );
352 const QDir frameDirectory( filePath );
355 reader->setAutoTransform(
true );
359 const QImage frame = reader->read();
360 if ( frame.isNull() )
363 mImageDelays[path].append( reader->nextImageDelay() );
365 const QString framePath = frameDirectory.filePath( u
"frame_%1.png"_s.arg( frameNumber++ ) );
366 frame.save( framePath,
"PNG" );
369 mTotalFrameCounts.insert( path, frameNumber );
372QImage QgsImageCache::renderImage(
373 const QString &path, QSize size,
const bool keepAspectRatio,
const double opacity,
double targetDpi,
int frameNumber,
bool &isBroken,
int &totalFrameCount,
int &nextFrameDelayMs,
bool blocking
382 QImageReader reader( path );
383 reader.setAutoTransform(
true );
385 if ( reader.format() ==
"pdf" )
387 if ( !size.isEmpty() )
394 reader.setScaledSize( size );
399 const QSize sizeAt72Dpi = reader.size();
400 const QSize sizeAtTargetDpi = sizeAt72Dpi * targetDpi / 72;
401 reader.setScaledSize( sizeAtTargetDpi );
407 if ( frameNumber == -1 )
413 im = getFrameFromReader( reader, frameNumber );
415 nextFrameDelayMs = reader.nextImageDelay();
419 QByteArray ba =
getContent( path, QByteArray(
"broken" ), QByteArray(
"fetching" ), blocking );
421 if ( ba ==
"broken" )
426 if ( !size.isValid() || size.isNull() )
430 if ( size.width() == 0 )
431 size.setWidth( size.height() );
432 if ( size.height() == 0 )
433 size.setHeight( size.width() );
435 im = QImage( size, QImage::Format_ARGB32_Premultiplied );
439 QSvgRenderer r( mMissingSvg );
441 QSizeF s( r.viewBox().size() );
442 s.scale( size.width(), size.height(), Qt::KeepAspectRatio );
443 const QRectF rect( ( size.width() - s.width() ) / 2, ( size.height() - s.height() ) / 2, s.width(), s.height() );
444 r.render( &p, rect );
446 else if ( ba ==
"fetching" )
449 if ( size.width() == 0 )
450 size.setWidth( size.height() );
451 if ( size.height() == 0 )
452 size.setHeight( size.width() );
455 im = QImage( size, QImage::Format_ARGB32_Premultiplied );
459 QSvgRenderer r( mFetchingSvg );
461 QSizeF s( r.viewBox().size() );
462 s.scale( size.width(), size.height(), Qt::KeepAspectRatio );
463 const QRectF rect( ( size.width() - s.width() ) / 2, ( size.height() - s.height() ) / 2, s.width(), s.height() );
464 r.render( &p, rect );
468 QBuffer buffer( &ba );
469 buffer.open( QIODevice::ReadOnly );
471 QImageReader reader( &buffer );
472 reader.setAutoTransform(
true );
474 if ( reader.format() ==
"pdf" )
476 if ( !size.isEmpty() )
483 reader.setScaledSize( size );
488 const QSize sizeAt72Dpi = reader.size();
489 const QSize sizeAtTargetDpi = sizeAt72Dpi * targetDpi / 72;
490 reader.setScaledSize( sizeAtTargetDpi );
495 if ( frameNumber == -1 )
501 im = getFrameFromReader( reader, frameNumber );
503 nextFrameDelayMs = reader.nextImageDelay();
508 !im.hasAlphaChannel()
509#
if QT_VERSION >= QT_VERSION_CHECK( 6, 8, 0 )
510 && im.format() != QImage::Format_CMYK8888
513 im = im.convertToFormat( QImage::Format_ARGB32 );
519 if ( !size.isValid() || size.isNull() || im.size() == size )
522 else if ( keepAspectRatio && size.height() == 0 )
523 return im.scaledToWidth( size.width(), Qt::SmoothTransformation );
525 else if ( keepAspectRatio && size.width() == 0 )
526 return im.scaledToHeight( size.height(), Qt::SmoothTransformation );
528 return im.scaled( size, keepAspectRatio ? Qt::KeepAspectRatio : Qt::IgnoreAspectRatio, Qt::SmoothTransformation );
531QImage QgsImageCache::getFrameFromReader( QImageReader &reader,
int frameNumber )
533 if ( reader.jumpToImage( frameNumber ) )
534 return reader.read();
537 for (
int frame = 0; frame < frameNumber; ++frame )
539 if ( reader.read().isNull() )
542 return reader.read();
548QgsImageSizeCacheEntry::QgsImageSizeCacheEntry(
const QString &path )
552int QgsImageSizeCacheEntry::dataSize()
const
554 return sizeof( QSize );
557void QgsImageSizeCacheEntry::dump()
const
564 const QgsImageSizeCacheEntry *otherImage =
dynamic_cast< const QgsImageSizeCacheEntry *
>( other );
565 if ( !otherImage || otherImage->path != path )
578QgsImageSizeCache::QgsImageSizeCache( QObject *parent )
584QgsImageSizeCache::~QgsImageSizeCache() =
default;
586QSize QgsImageSizeCache::originalSize(
const QString &f,
bool blocking )
588 QString file = f.trimmed();
590 if ( file.isEmpty() )
593 const QMutexLocker locker( &mMutex );
595 QString base64String;
597 if ( parseBase64DataUrl( file, &mimeType, &base64String ) && mimeType.startsWith(
"image/"_L1 ) )
599 file = u
"base64:%1"_s.arg( base64String );
602 QgsImageSizeCacheEntry *currentEntry = findExistingEntry(
new QgsImageSizeCacheEntry( file ) );
606 if ( !currentEntry->size.isValid() )
609 mTotalSize += currentEntry->dataSize();
610 currentEntry->size = result;
615 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)