32#include <QApplication>
34#include <QCoreApplication>
36#include <QDomDocument>
41#include <QImageReader>
42#include <QNetworkReply>
43#include <QNetworkRequest>
47#include <QSvgRenderer>
48#include <QTemporaryDir>
51#include "moc_qgsimagecache.cpp"
53using namespace Qt::StringLiterals;
57QgsImageCacheEntry::QgsImageCacheEntry(
const QString &path, QSize size,
const bool keepAspectRatio,
const double opacity,
double dpi,
int frameNumber )
60 , keepAspectRatio( keepAspectRatio )
63 , 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 );
105 mTemporaryDir = std::make_unique<QTemporaryDir>();
117 if ( sysMemory >= 32000 )
119 else if ( sysMemory >= 16000 )
126 mMissingSvg = u
"<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>?</text></svg>"_s.toLatin1();
129 if ( QFile::exists( downloadingSvgPath ) )
131 QFile file( downloadingSvgPath );
132 if ( file.open( QIODevice::ReadOnly ) )
134 mFetchingSvg = file.readAll();
138 if ( mFetchingSvg.isEmpty() )
140 mFetchingSvg = u
"<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>?</text></svg>"_s.toLatin1();
148QImage
QgsImageCache::pathAsImage(
const QString &f,
const QSize size,
const bool keepAspectRatio,
const double opacity,
bool &fitsInCache,
bool blocking,
double targetDpi,
int frameNumber,
bool *isMissing )
151 int nextFrameDelayMs = 0;
152 return pathAsImagePrivate( f, size, keepAspectRatio, opacity, fitsInCache, blocking, targetDpi, frameNumber, isMissing,
totalFrameCount, nextFrameDelayMs );
155QImage QgsImageCache::pathAsImagePrivate(
156 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
159 QString file = f.trimmed();
163 if ( file.isEmpty() )
166 const QMutexLocker locker( &
mMutex );
168 const auto extractedAnimationIt = mExtractedAnimationPaths.constFind( file );
169 if ( extractedAnimationIt != mExtractedAnimationPaths.constEnd() )
171 file = QDir( extractedAnimationIt.value() ).filePath( u
"frame_%1.png"_s.arg( frameNumber ) );
177 QString base64String;
179 if (
parseBase64DataUrl( file, &mimeType, &base64String ) && mimeType.startsWith(
"image/"_L1 ) )
181 file = u
"base64:%1"_s.arg( base64String );
184 QgsImageCacheEntry *currentEntry =
findExistingEntry(
new QgsImageCacheEntry( file, size, keepAspectRatio, opacity, targetDpi, frameNumber ) );
191 if ( currentEntry->image.isNull() )
193 long cachedDataSize = 0;
194 bool isBroken =
false;
195 result = renderImage( file, size, keepAspectRatio, opacity, targetDpi, frameNumber, isBroken,
totalFrameCount, nextFrameDelayMs, blocking );
196 cachedDataSize += result.sizeInBytes();
200 currentEntry->image = QImage();
205 currentEntry->image = result;
207 currentEntry->nextFrameDelay = nextFrameDelayMs;
211 *isMissing = isBroken;
212 currentEntry->isMissingImage = isBroken;
218 result = currentEntry->image;
220 nextFrameDelayMs = currentEntry->nextFrameDelay;
222 *isMissing = currentEntry->isMissingImage;
230 return mImageSizeCache.originalSize( path, blocking );
233QSize QgsImageCache::originalSizePrivate(
const QString &path,
bool blocking )
const
235 if ( path.isEmpty() )
241 const QImageReader reader( path );
242 if ( reader.size().isValid() )
243 return reader.size();
245 return QImage( path ).size();
249 QByteArray ba =
getContent( path, QByteArray(
"broken" ), QByteArray(
"fetching" ), blocking );
251 if ( ba !=
"broken" && ba !=
"fetching" )
253 QBuffer buffer( &ba );
254 buffer.open( QIODevice::ReadOnly );
256 QImageReader reader( &buffer );
259 const QSize s = reader.size();
262 const QImage im = reader.read();
263 return im.isNull() ? QSize() : im.size();
271 const QString file = path.trimmed();
273 if ( file.isEmpty() )
276 const QMutexLocker locker( &
mMutex );
278 auto it = mTotalFrameCounts.find( path );
279 if ( it != mTotalFrameCounts.end() )
283 int nextFrameDelayMs = 0;
284 bool fitsInCache =
false;
285 bool isMissing =
false;
286 ( void ) pathAsImagePrivate( file, QSize(),
true, 1.0, fitsInCache, blocking, 96, 0, &isMissing, res, nextFrameDelayMs );
293 const QString file = path.trimmed();
295 if ( file.isEmpty() )
298 const QMutexLocker locker( &
mMutex );
300 auto it = mImageDelays.find( path );
301 if ( it != mImageDelays.end() )
302 return it.value().value( currentFrame );
305 int nextFrameDelayMs = 0;
306 bool fitsInCache =
false;
307 bool isMissing =
false;
308 const QImage res = pathAsImagePrivate( file, QSize(),
true, 1.0, fitsInCache, blocking, 96, currentFrame, &isMissing, frameCount, nextFrameDelayMs );
310 return nextFrameDelayMs <= 0 || res.isNull() ? -1 : nextFrameDelayMs;
315 const QMutexLocker locker( &
mMutex );
317 auto it = mExtractedAnimationPaths.find( path );
318 if ( it != mExtractedAnimationPaths.end() )
322 std::unique_ptr< QImageReader > reader;
323 std::unique_ptr< QBuffer > buffer;
327 const QString basePart = QFileInfo( path ).baseName();
329 filePath = mTemporaryDir->filePath( u
"%1_%2"_s.arg( basePart ).arg(
id ) );
330 while ( QFile::exists( filePath ) )
331 filePath = mTemporaryDir->filePath( u
"%1_%2"_s.arg( basePart ).arg( ++
id ) );
333 reader = std::make_unique< QImageReader >( path );
337 QByteArray ba =
getContent( path, QByteArray(
"broken" ), QByteArray(
"fetching" ),
false );
338 if ( ba ==
"broken" || ba ==
"fetching" )
344 const QString path = QUuid::createUuid().toString( QUuid::WithoutBraces );
345 filePath = mTemporaryDir->filePath( path );
347 buffer = std::make_unique< QBuffer >( &ba );
348 buffer->open( QIODevice::ReadOnly );
349 reader = std::make_unique< QImageReader>( buffer.get() );
353 QDir().mkpath( filePath );
354 mExtractedAnimationPaths.insert( path, filePath );
356 const QDir frameDirectory( filePath );
359 reader->setAutoTransform(
true );
363 const QImage frame = reader->read();
364 if ( frame.isNull() )
367 mImageDelays[path].append( reader->nextImageDelay() );
369 const QString framePath = frameDirectory.filePath( u
"frame_%1.png"_s.arg( frameNumber++ ) );
370 frame.save( framePath,
"PNG" );
373 mTotalFrameCounts.insert( path, frameNumber );
376QImage QgsImageCache::renderImage(
377 const QString &path, QSize size,
const bool keepAspectRatio,
const double opacity,
double targetDpi,
int frameNumber,
bool &isBroken,
int &totalFrameCount,
int &nextFrameDelayMs,
bool blocking
386 QImageReader reader( path );
387 reader.setAutoTransform(
true );
389 if ( reader.format() ==
"pdf" )
391 if ( !size.isEmpty() )
398 reader.setScaledSize( size );
403 const QSize sizeAt72Dpi = reader.size();
404 const QSize sizeAtTargetDpi = sizeAt72Dpi * targetDpi / 72;
405 reader.setScaledSize( sizeAtTargetDpi );
411 if ( frameNumber == -1 )
417 im = getFrameFromReader( reader, frameNumber );
419 nextFrameDelayMs = reader.nextImageDelay();
423 QByteArray ba =
getContent( path, QByteArray(
"broken" ), QByteArray(
"fetching" ), blocking );
425 if ( ba ==
"broken" )
430 if ( !size.isValid() || size.isNull() )
434 if ( size.width() == 0 )
435 size.setWidth( size.height() );
436 if ( size.height() == 0 )
437 size.setHeight( size.width() );
439 im = QImage( size, QImage::Format_ARGB32_Premultiplied );
443 QSvgRenderer r( mMissingSvg );
445 QSizeF s( r.viewBox().size() );
446 s.scale( size.width(), size.height(), Qt::KeepAspectRatio );
447 const QRectF rect( ( size.width() - s.width() ) / 2, ( size.height() - s.height() ) / 2, s.width(), s.height() );
448 r.render( &p, rect );
450 else if ( ba ==
"fetching" )
453 if ( size.width() == 0 )
454 size.setWidth( size.height() );
455 if ( size.height() == 0 )
456 size.setHeight( size.width() );
459 im = QImage( size, QImage::Format_ARGB32_Premultiplied );
463 QSvgRenderer r( mFetchingSvg );
465 QSizeF s( r.viewBox().size() );
466 s.scale( size.width(), size.height(), Qt::KeepAspectRatio );
467 const QRectF rect( ( size.width() - s.width() ) / 2, ( size.height() - s.height() ) / 2, s.width(), s.height() );
468 r.render( &p, rect );
472 QBuffer buffer( &ba );
473 buffer.open( QIODevice::ReadOnly );
475 QImageReader reader( &buffer );
476 reader.setAutoTransform(
true );
478 if ( reader.format() ==
"pdf" )
480 if ( !size.isEmpty() )
487 reader.setScaledSize( size );
492 const QSize sizeAt72Dpi = reader.size();
493 const QSize sizeAtTargetDpi = sizeAt72Dpi * targetDpi / 72;
494 reader.setScaledSize( sizeAtTargetDpi );
499 if ( frameNumber == -1 )
505 im = getFrameFromReader( reader, frameNumber );
507 nextFrameDelayMs = reader.nextImageDelay();
512 !im.hasAlphaChannel()
513#
if QT_VERSION >= QT_VERSION_CHECK( 6, 8, 0 )
514 && im.format() != QImage::Format_CMYK8888
517 im = im.convertToFormat( QImage::Format_ARGB32 );
523 if ( !size.isValid() || size.isNull() || im.size() == size )
526 else if ( keepAspectRatio && size.height() == 0 )
527 return im.scaledToWidth( size.width(), Qt::SmoothTransformation );
529 else if ( keepAspectRatio && size.width() == 0 )
530 return im.scaledToHeight( size.height(), Qt::SmoothTransformation );
532 return im.scaled( size, keepAspectRatio ? Qt::KeepAspectRatio : Qt::IgnoreAspectRatio, Qt::SmoothTransformation );
535QImage QgsImageCache::getFrameFromReader( QImageReader &reader,
int frameNumber )
537 if ( reader.jumpToImage( frameNumber ) )
538 return reader.read();
541 for (
int frame = 0; frame < frameNumber; ++frame )
543 if ( reader.read().isNull() )
546 return reader.read();
552QgsImageSizeCacheEntry::QgsImageSizeCacheEntry(
const QString &path )
556int QgsImageSizeCacheEntry::dataSize()
const
558 return sizeof( QSize );
561void QgsImageSizeCacheEntry::dump()
const
568 const QgsImageSizeCacheEntry *otherImage =
dynamic_cast< const QgsImageSizeCacheEntry *
>( other );
569 if ( !otherImage || otherImage->path != path )
582QgsImageSizeCache::QgsImageSizeCache( QObject *parent )
588QgsImageSizeCache::~QgsImageSizeCache() =
default;
590QSize QgsImageSizeCache::originalSize(
const QString &f,
bool blocking )
592 QString file = f.trimmed();
594 if ( file.isEmpty() )
597 const QMutexLocker locker( &mMutex );
599 QString base64String;
601 if ( parseBase64DataUrl( file, &mimeType, &base64String ) && mimeType.startsWith(
"image/"_L1 ) )
603 file = u
"base64:%1"_s.arg( base64String );
606 QgsImageSizeCacheEntry *currentEntry = findExistingEntry(
new QgsImageSizeCacheEntry( file ) );
610 if ( !currentEntry->size.isValid() )
613 mTotalSize += currentEntry->dataSize();
614 currentEntry->size = result;
619 result = currentEntry->size;
QFlags< SettingsOption > SettingsOptions
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...
static const QgsSettingsEntryInteger * settingsMaxImageCacheSize
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.
An integer settings entry.
static QgsSettingsTreeNode * sTreeNetworkCache
#define QgsDebugMsgLevel(str, level)