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.
 
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)