29#include <QApplication> 
   30#include <QCoreApplication> 
   32#include <QDomDocument> 
   39#include <QNetworkReply> 
   40#include <QNetworkRequest> 
   42#include <QImageReader> 
   43#include <QSvgRenderer> 
   44#include <QTemporaryDir> 
   49QgsImageCacheEntry::QgsImageCacheEntry( 
const QString &path, QSize size, 
const bool keepAspectRatio, 
const double opacity, 
double dpi, 
int frameNumber )
 
   52  , keepAspectRatio( keepAspectRatio )
 
   55  , frameNumber( frameNumber )
 
   61  const QgsImageCacheEntry *otherImage = 
dynamic_cast< const QgsImageCacheEntry * 
>( other );
 
   64       || otherImage->keepAspectRatio != keepAspectRatio
 
   65       || otherImage->frameNumber != frameNumber
 
   66       || otherImage->size != size
 
   67       || ( !size.isValid() && otherImage->targetDpi != targetDpi )
 
   68       || otherImage->opacity != opacity
 
   69       || otherImage->path != path )
 
   75int QgsImageCacheEntry::dataSize()
 const 
   78  if ( !image.isNull() )
 
   80    size += image.sizeInBytes();
 
   85void QgsImageCacheEntry::dump()
 const 
   87  QgsDebugMsgLevel( QStringLiteral( 
"path: %1, size %2x%3" ).arg( path ).arg( size.width() ).arg( size.height() ), 3 );
 
   95  mTemporaryDir.reset( 
new QTemporaryDir() );
 
   97  const int bytes = 
QgsSettings().
value( QStringLiteral( 
"/qgis/maxImageCacheSize" ), 0 ).toInt();
 
  107      if ( sysMemory >= 32000 ) 
 
  109      else if ( sysMemory >= 16000 ) 
 
  116  mMissingSvg = QStringLiteral( 
"<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>?</text></svg>" ).toLatin1();
 
  119  if ( QFile::exists( downloadingSvgPath ) )
 
  121    QFile file( downloadingSvgPath );
 
  122    if ( file.open( QIODevice::ReadOnly ) )
 
  124      mFetchingSvg = file.readAll();
 
  128  if ( mFetchingSvg.isEmpty() )
 
  130    mFetchingSvg = QStringLiteral( 
"<svg width='10' height='10'><text x='5' y='10' font-size='10' text-anchor='middle'>?</text></svg>" ).toLatin1();
 
 
  138QImage 
QgsImageCache::pathAsImage( 
const QString &f, 
const QSize size, 
const bool keepAspectRatio, 
const double opacity, 
bool &fitsInCache, 
bool blocking, 
double targetDpi, 
int frameNumber, 
bool *isMissing )
 
  141  int nextFrameDelayMs = 0;
 
  142  return pathAsImagePrivate( f, size, keepAspectRatio, opacity, fitsInCache, blocking, targetDpi, frameNumber, isMissing, 
totalFrameCount, nextFrameDelayMs );
 
 
  145QImage 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 )
 
  147  QString file = f.trimmed();
 
  151  if ( file.isEmpty() )
 
  154  const QMutexLocker locker( &
mMutex );
 
  156  const auto extractedAnimationIt = mExtractedAnimationPaths.constFind( file );
 
  157  if ( extractedAnimationIt != mExtractedAnimationPaths.constEnd() )
 
  159    file = QDir( extractedAnimationIt.value() ).filePath( QStringLiteral( 
"frame_%1.png" ).arg( frameNumber ) );
 
  165  QgsImageCacheEntry *currentEntry = 
findExistingEntry( 
new QgsImageCacheEntry( file, size, keepAspectRatio, opacity, targetDpi, frameNumber ) );
 
  172  if ( currentEntry->image.isNull() )
 
  174    long cachedDataSize = 0;
 
  175    bool isBroken = 
false;
 
  176    result = renderImage( file, size, keepAspectRatio, opacity, targetDpi, frameNumber, isBroken, 
totalFrameCount, nextFrameDelayMs, blocking );
 
  177    cachedDataSize += result.sizeInBytes();
 
  181      currentEntry->image = QImage();
 
  186      currentEntry->image = result;
 
  188      currentEntry->nextFrameDelay = nextFrameDelayMs;
 
  192      *isMissing = isBroken;
 
  193    currentEntry->isMissingImage = isBroken;
 
  199    result = currentEntry->image;
 
  201    nextFrameDelayMs = currentEntry->nextFrameDelay;
 
  203      *isMissing = currentEntry->isMissingImage;
 
  211  if ( path.isEmpty() )
 
  215  if ( !path.startsWith( QLatin1String( 
"base64:" ) ) && QFile::exists( path ) )
 
  217    const QImageReader reader( path );
 
  218    if ( reader.size().isValid() )
 
  219      return reader.size();
 
  221      return QImage( path ).size();
 
  225    QByteArray ba = 
getContent( path, QByteArray( 
"broken" ), QByteArray( 
"fetching" ), blocking );
 
  227    if ( ba != 
"broken" && ba != 
"fetching" )
 
  229      QBuffer buffer( &ba );
 
  230      buffer.open( QIODevice::ReadOnly );
 
  232      QImageReader reader( &buffer );
 
  235      const QSize s = reader.size();
 
  238      const QImage im = reader.read();
 
  239      return im.isNull() ? QSize() : im.size();
 
 
  247  const QString file = path.trimmed();
 
  249  if ( file.isEmpty() )
 
  252  const QMutexLocker locker( &
mMutex );
 
  254  auto it = mTotalFrameCounts.find( path );
 
  255  if ( it != mTotalFrameCounts.end() )
 
  259  int nextFrameDelayMs = 0;
 
  260  bool fitsInCache = 
false;
 
  261  bool isMissing = 
false;
 
  262  ( void )pathAsImagePrivate( file, QSize(), 
true, 1.0, fitsInCache, blocking, 96, 0, &isMissing, res, nextFrameDelayMs );
 
 
  269  const QString file = path.trimmed();
 
  271  if ( file.isEmpty() )
 
  274  const QMutexLocker locker( &
mMutex );
 
  276  auto it = mImageDelays.find( path );
 
  277  if ( it != mImageDelays.end() )
 
  278    return it.value().value( currentFrame ); 
 
  281  int nextFrameDelayMs = 0;
 
  282  bool fitsInCache = 
false;
 
  283  bool isMissing = 
false;
 
  284  const QImage res = pathAsImagePrivate( file, QSize(), 
true, 1.0, fitsInCache, blocking, 96, currentFrame, &isMissing, frameCount, nextFrameDelayMs );
 
  286  return nextFrameDelayMs <= 0 || res.isNull() ? -1 : nextFrameDelayMs;
 
 
  291  const QMutexLocker locker( &
mMutex );
 
  293  auto it = mExtractedAnimationPaths.find( path );
 
  294  if ( it != mExtractedAnimationPaths.end() )
 
  298  std::unique_ptr< QImageReader > reader;
 
  299  std::unique_ptr< QBuffer > buffer;
 
  301  if ( !path.startsWith( QLatin1String( 
"base64:" ) ) && QFile::exists( path ) )
 
  303    const QString basePart = QFileInfo( path ).baseName();
 
  305    filePath = mTemporaryDir->filePath( QStringLiteral( 
"%1_%2" ).arg( basePart ).arg( 
id ) );
 
  306    while ( QFile::exists( filePath ) )
 
  307      filePath = mTemporaryDir->filePath( QStringLiteral( 
"%1_%2" ).arg( basePart ).arg( ++
id ) );
 
  309    reader = std::make_unique< QImageReader >( path );
 
  313    QByteArray ba = 
getContent( path, QByteArray( 
"broken" ), QByteArray( 
"fetching" ), 
false );
 
  314    if ( ba == 
"broken" || ba == 
"fetching" )
 
  320      const QString path = QUuid::createUuid().toString( QUuid::WithoutBraces );
 
  321      filePath = mTemporaryDir->filePath( path );
 
  323      buffer = std::make_unique< QBuffer >( &ba );
 
  324      buffer->open( QIODevice::ReadOnly );
 
  325      reader = std::make_unique< QImageReader> ( buffer.get() );
 
  329  QDir().mkpath( filePath );
 
  330  mExtractedAnimationPaths.insert( path, filePath );
 
  332  const QDir frameDirectory( filePath );
 
  335  reader->setAutoTransform( 
true );
 
  339    const QImage frame = reader->read();
 
  340    if ( frame.isNull() )
 
  343    mImageDelays[ path ].append( reader->nextImageDelay() );
 
  345    const QString framePath = frameDirectory.filePath( QStringLiteral( 
"frame_%1.png" ).arg( frameNumber++ ) );
 
  346    frame.save( framePath, 
"PNG" );
 
  349  mTotalFrameCounts.insert( path, frameNumber );
 
 
  352QImage 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 
  358  if ( !path.startsWith( QLatin1String( 
"base64:" ) ) && QFile::exists( path ) )
 
  360    QImageReader reader( path );
 
  361    reader.setAutoTransform( 
true );
 
  363    if ( reader.format() == 
"pdf" )
 
  365      if ( !size.isEmpty() )
 
  372        reader.setScaledSize( size );
 
  377        const QSize sizeAt72Dpi = reader.size();
 
  378        const QSize sizeAtTargetDpi = sizeAt72Dpi * targetDpi / 72;
 
  379        reader.setScaledSize( sizeAtTargetDpi );
 
  385    if ( frameNumber == -1 )
 
  391      im = getFrameFromReader( reader, frameNumber );
 
  393    nextFrameDelayMs = reader.nextImageDelay();
 
  397    QByteArray ba = 
getContent( path, QByteArray( 
"broken" ), QByteArray( 
"fetching" ), blocking );
 
  399    if ( ba == 
"broken" )
 
  404      if ( !size.isValid() || size.isNull() )
 
  408      if ( size.width() == 0 )
 
  409        size.setWidth( size.height() );
 
  410      if ( size.height() == 0 )
 
  411        size.setHeight( size.width() );
 
  413      im = QImage( size, QImage::Format_ARGB32_Premultiplied );
 
  417      QSvgRenderer r( mMissingSvg );
 
  419      QSizeF s( r.viewBox().size() );
 
  420      s.scale( size.width(), size.height(), Qt::KeepAspectRatio );
 
  421      const QRectF rect( ( size.width() - s.width() ) / 2, ( size.height() - s.height() ) / 2, s.width(), s.height() );
 
  422      r.render( &p, rect );
 
  424    else if ( ba == 
"fetching" )
 
  427      if ( size.width() == 0 )
 
  428        size.setWidth( size.height() );
 
  429      if ( size.height() == 0 )
 
  430        size.setHeight( size.width() );
 
  433      im = QImage( size, QImage::Format_ARGB32_Premultiplied );
 
  437      QSvgRenderer r( mFetchingSvg );
 
  439      QSizeF s( r.viewBox().size() );
 
  440      s.scale( size.width(), size.height(), Qt::KeepAspectRatio );
 
  441      const QRectF rect( ( size.width() - s.width() ) / 2, ( size.height() - s.height() ) / 2, s.width(), s.height() );
 
  442      r.render( &p, rect );
 
  446      QBuffer buffer( &ba );
 
  447      buffer.open( QIODevice::ReadOnly );
 
  449      QImageReader reader( &buffer );
 
  450      reader.setAutoTransform( 
true );
 
  452      if ( reader.format() == 
"pdf" )
 
  454        if ( !size.isEmpty() )
 
  461          reader.setScaledSize( size );
 
  466          const QSize sizeAt72Dpi = reader.size();
 
  467          const QSize sizeAtTargetDpi = sizeAt72Dpi * targetDpi / 72;
 
  468          reader.setScaledSize( sizeAtTargetDpi );
 
  473      if ( frameNumber == -1 )
 
  479        im = getFrameFromReader( reader, frameNumber );
 
  481      nextFrameDelayMs = reader.nextImageDelay();
 
  485  if ( !im.hasAlphaChannel()
 
  486#
if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
 
  487       && im.format() != QImage::Format_CMYK8888
 
  490    im = im.convertToFormat( QImage::Format_ARGB32 );
 
  496  if ( !size.isValid() || size.isNull() || im.size() == size )
 
  499  else if ( keepAspectRatio && size.height() == 0 )
 
  500    return im.scaledToWidth( size.width(), Qt::SmoothTransformation );
 
  502  else if ( keepAspectRatio && size.width() == 0 )
 
  503    return im.scaledToHeight( size.height(), Qt::SmoothTransformation );
 
  505    return im.scaled( size, keepAspectRatio ? Qt::KeepAspectRatio : Qt::IgnoreAspectRatio, Qt::SmoothTransformation );
 
  508QImage QgsImageCache::getFrameFromReader( QImageReader &reader, 
int frameNumber )
 
  510  if ( reader.jumpToImage( frameNumber ) )
 
  511    return reader.read();
 
  514  for ( 
int frame = 0; frame < frameNumber; ++frame )
 
  516    if ( reader.read().isNull() )
 
  519  return reader.read();
 
void remoteContentFetched(const QString &url)
Emitted when the cache has finished retrieving content from a remote url.
 
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.
 
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.
 
This class is a composition of two QSettings instances:
 
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
 
#define QgsDebugMsgLevel(str, level)