File indexing completed on 2024-05-12 05:09:50

0001 /***************************************************************************
0002     Copyright (C) 2003-2009 Robby Stephenson <robby@periapsis.org>
0003  ***************************************************************************/
0004 
0005 /***************************************************************************
0006  *                                                                         *
0007  *   This program is free software; you can redistribute it and/or         *
0008  *   modify it under the terms of the GNU General Public License as        *
0009  *   published by the Free Software Foundation; either version 2 of        *
0010  *   the License or (at your option) version 3 or any later version        *
0011  *   accepted by the membership of KDE e.V. (or its successor approved     *
0012  *   by the membership of KDE e.V.), which shall act as a proxy            *
0013  *   defined in Section 14 of version 3 of the license.                    *
0014  *                                                                         *
0015  *   This program is distributed in the hope that it will be useful,       *
0016  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0017  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0018  *   GNU General Public License for more details.                          *
0019  *                                                                         *
0020  *   You should have received a copy of the GNU General Public License     *
0021  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
0022  *                                                                         *
0023  ***************************************************************************/
0024 
0025 #include "imagefactory.h"
0026 #include "image.h"
0027 #include "imageinfo.h"
0028 #include "imagedirectory.h"
0029 #include "imagejob.h"
0030 #include "../config/tellico_config.h"
0031 #include "../utils/tellico_utils.h"
0032 #include "../utils/gradient.h"
0033 #include "../tellico_debug.h"
0034 
0035 #include <KColorUtils>
0036 #include <KZip>
0037 
0038 #include <QCache>
0039 #include <QFileInfo>
0040 #include <QDir>
0041 
0042 #define RELEASE_IMAGES
0043 
0044 using namespace Tellico;
0045 using Tellico::ImageFactory;
0046 
0047 // this image info map is primarily for big images that don't fit
0048 // in the cache, so that don't have to be continually reloaded to get info
0049 QHash<QString, Tellico::Data::ImageInfo> ImageFactory::s_imageInfoMap;
0050 Tellico::StringSet ImageFactory::s_imagesToRelease;
0051 
0052 Tellico::ImageFactory* ImageFactory::factory = nullptr;
0053 
0054 class ImageFactory::Private {
0055 public:
0056   Private() = default;
0057 
0058   QHash<QString, Data::Image*> imageDict;
0059   QCache<QString, Data::Image> imageCache;
0060   QCache<QString, QPixmap> pixmapCache;
0061   ImageDirectory dataImageDir; // kept in $HOME/.local/share/tellico/data/
0062   ImageDirectory localImageDir; // kept local to data file
0063   TemporaryImageDirectory tempImageDir; // kept in tmp directory
0064   ImageZipArchive imageZipArchive;
0065   StringSet nullImages;
0066 };
0067 
0068 ImageFactory::ImageFactory() : QObject(), d(new Private()) {
0069 }
0070 
0071 ImageFactory::~ImageFactory() {
0072   delete d;
0073 }
0074 
0075 void ImageFactory::init() {
0076   if(factory) {
0077     return;
0078   }
0079   factory = new ImageFactory();
0080   factory->d->imageCache.setMaxCost(Config::imageCacheSize());
0081   factory->d->pixmapCache.setMaxCost(Config::imageCacheSize());
0082   factory->d->dataImageDir.setPath(Tellico::saveLocation(QStringLiteral("data/")));
0083 }
0084 
0085 Tellico::ImageFactory* ImageFactory::self() {
0086   Q_ASSERT(factory && "ImageFactory is not initialized!");
0087   return factory;
0088 }
0089 
0090 QString ImageFactory::tempDir() {
0091   return factory->d->tempImageDir.path();
0092 }
0093 
0094 QString ImageFactory::dataDir() {
0095   return factory->d->dataImageDir.path();
0096 }
0097 
0098 QString ImageFactory::localDir() {
0099   const QString dir = factory->d->localImageDir.path();
0100   return dir.isEmpty() ? dataDir() : dir;
0101 }
0102 
0103 QString ImageFactory::imageDir() {
0104   Q_ASSERT(factory);
0105   switch(cacheDir()) {
0106     case LocalDir: return localDir();
0107     case DataDir: return dataDir();
0108     case ZipArchive: return tempDir();
0109     case TempDir: return tempDir();
0110   }
0111   return tempDir();
0112 }
0113 
0114 Tellico::ImageFactory::CacheDir ImageFactory::cacheDir() {
0115   CacheDir dir = TempDir;
0116   switch(Config::imageLocation()) {
0117     case Config::ImagesInLocalDir: dir = LocalDir; break;
0118     case Config::ImagesInAppDir:   dir = DataDir;  break;
0119     case Config::ImagesInFile:     dir = TempDir;  break;
0120   }
0121   // special case when configured to use local dir but for a new collection when no local dir exists
0122   if(dir == LocalDir && factory->d->localImageDir.path().isEmpty()) {
0123     dir = TempDir;
0124   }
0125   return dir;
0126 }
0127 
0128 QString ImageFactory::addImage(const QUrl& url_, bool quiet_, const QUrl& refer_, bool link_) {
0129   Q_ASSERT(factory && "ImageFactory is not initialized!");
0130   return factory->addImageImpl(url_, quiet_, refer_, link_).id();
0131 }
0132 
0133 const Tellico::Data::Image& ImageFactory::addImageImpl(const QUrl& url_, bool quiet_, const QUrl& refer_, bool link_) {
0134   if(url_.isEmpty() || !url_.isValid() || d->nullImages.contains(url_.url())) {
0135     myDebug() << "Returning null image";
0136     return Data::Image::null;
0137   }
0138   ImageJob* job = new ImageJob(url_, QString(), quiet_);
0139   job->setLinkOnly(link_);
0140   job->setReferrer(refer_);
0141 
0142   if(!job->exec()) {
0143     myDebug() << "ImageJob failed to exec:" << job->errorString();
0144     // ERR_UNKNOWN is used when the returned image is truly null
0145     // rather than network error or some such
0146     if(job->error() == KIO::ERR_UNKNOWN) {
0147       d->nullImages.add(url_.url());
0148     }
0149     return Data::Image::null;
0150   }
0151 
0152   const Data::Image& img = job->image();
0153   if(img.isNull()) {
0154     myDebug() << "Null image for" << url_.toDisplayString();
0155     return Data::Image::null;
0156   }
0157 
0158   // hold the image in memory since it probably isn't written locally to disk yet
0159   if(!d->imageDict.contains(img.id())) {
0160     d->imageDict.insert(img.id(), new Data::Image(img));
0161     s_imageInfoMap.insert(img.id(), Data::ImageInfo(img));
0162   }
0163   return img;
0164 }
0165 
0166 QString ImageFactory::addImage(const QImage& image_, const QString& format_) {
0167   Q_ASSERT(factory && "ImageFactory is not initialized!");
0168   return factory->addImageImpl(image_, format_).id();
0169 }
0170 
0171 QString ImageFactory::addImage(const QPixmap& pix_, const QString& format_) {
0172   Q_ASSERT(factory && "ImageFactory is not initialized!");
0173   return factory->addImageImpl(pix_.toImage(), format_).id();
0174 }
0175 
0176 const Tellico::Data::Image& ImageFactory::addImageImpl(const QImage& image_, const QString& format_) {
0177   Data::Image* img = new Data::Image(image_, format_);
0178   if(hasImageInMemory(img->id())) {
0179     const Data::Image& img2 = imageById(img->id());
0180     if(!img2.isNull()) {
0181       delete img;
0182       return img2;
0183     }
0184   }
0185   if(img->isNull()) {
0186     delete img;
0187     return Data::Image::null;
0188   }
0189   d->imageDict.insert(img->id(), img);
0190   s_imageInfoMap.insert(img->id(), Data::ImageInfo(*img));
0191   return *img;
0192 }
0193 
0194 QString ImageFactory::addImage(const QByteArray& data_, const QString& format_, const QString& id_) {
0195   Q_ASSERT(factory && "ImageFactory is not initialized!");
0196   return factory->addImageImpl(data_, format_, id_).id();
0197 }
0198 
0199 const Tellico::Data::Image& ImageFactory::addImageImpl(const QByteArray& data_, const QString& format_,
0200                                                        const QString& id_) {
0201   Data::Image* img;
0202   if(!id_.isEmpty()) {
0203     // do not call imageById(), it causes infinite looping with Document::loadImage()
0204     img = d->imageCache.object(id_);
0205     if(img) {
0206       myLog() << "already exists in cache: " << id_;
0207       return *img;
0208     }
0209     img = d->imageDict.value(id_);
0210     if(img) {
0211       myLog() << "already exists in dict: " << id_;
0212       return *img;
0213     }
0214   }
0215   img = new Data::Image(data_, format_, id_);
0216   if(img->isNull()) {
0217     myDebug() << "NULL IMAGE!!!!!";
0218     delete img;
0219     return Data::Image::null;
0220   }
0221 
0222 //  myLog() << "format = " << format_ << ", id = "<< img->id();
0223 
0224   d->imageDict.insert(img->id(), img);
0225   s_imageInfoMap.insert(img->id(), Data::ImageInfo(*img));
0226   return *img;
0227 }
0228 
0229 const Tellico::Data::Image& ImageFactory::addCachedImageImpl(const QString& id_, CacheDir dir_) {
0230 //  myLog() << "dir =" << (dir_ == DataDir ? "DataDir" : "TmpDir" ) << "; id =" << id_;
0231   Data::Image* img = nullptr;
0232   switch(dir_) {
0233     case DataDir:
0234       img = d->dataImageDir.imageById(id_);
0235       break;
0236     case LocalDir:
0237       img = d->localImageDir.imageById(id_);
0238       break;
0239     case TempDir:
0240       img = d->tempImageDir.imageById(id_);
0241       break;
0242     case ZipArchive:
0243       img = d->imageZipArchive.imageById(id_);
0244       break;
0245   }
0246   if(!img) {
0247     myWarning() << "image not found:" << id_;
0248     return Data::Image::null;
0249   }
0250 
0251   s_imageInfoMap.insert(img->id(), Data::ImageInfo(*img));
0252 
0253   // if byteCount() is greater than maxCost, then trying and failing to insert it would
0254   // mean the image gets deleted
0255   if(img->sizeInBytes() > d->imageCache.maxCost()) {
0256     // can't hold it in the cache
0257     myWarning() << "Image cache is unable to hold the image, it's too big!";
0258     myWarning() << "Image name is " << img->id();
0259     myWarning() << "Image size is " << img->sizeInBytes();
0260     myWarning() << "Max cache size is " << d->imageCache.maxCost();
0261 
0262     // add it back to the dict, but add the image to the list of
0263     // images to release later. Necessary to avoid a memory leak since new Image()
0264     // was called, we need to keep the pointer
0265     d->imageDict.insert(img->id(), img);
0266     s_imagesToRelease.add(img->id());
0267   } else if(!d->imageCache.insert(img->id(), img, img->sizeInBytes())) {
0268     // at this point, img has been deleted!
0269     myWarning() << "Unable to insert into image cache";
0270     return Data::Image::null;
0271   }
0272   return *img;
0273 }
0274 
0275 bool ImageFactory::writeCachedImage(const QString& id_, CacheDir dir_, bool force_ /*=false*/) {
0276   if(id_.isEmpty()) {
0277     return false;
0278   }
0279 //  myLog() << "dir =" << (dir_ == DataDir ? "DataDir" : "TmpDir" ) << "; id =" << id_;
0280   ImageDirectory* imgDir = nullptr;
0281   switch(dir_) {
0282     case DataDir:
0283       imgDir = &factory->d->dataImageDir;
0284       break;
0285     case TempDir:
0286       imgDir = &factory->d->tempImageDir;
0287       break;
0288     case LocalDir:
0289       // special case when configured to use local dir but for a new collection when no local dir exists
0290       imgDir = factory->d->localImageDir.path().isEmpty() ?
0291                  &factory->d->tempImageDir :
0292                  &factory->d->localImageDir;
0293       break;
0294     case ZipArchive:
0295       myDebug() << "writeCachedImage() - ZipArchive - should never be called";
0296       imgDir = &factory->d->tempImageDir;
0297       break;
0298   }
0299 
0300   Q_ASSERT(imgDir);
0301   bool success = writeCachedImage(id_, imgDir, force_);
0302 
0303   if(success) {
0304     // remove from dict and add to cache
0305     // it might not be in dict though
0306     if(factory->d->imageDict.contains(id_)) {
0307       Data::Image* img = factory->d->imageDict.take(id_);
0308       Q_ASSERT(img);
0309       // imageCache.insert will delete the image by itself if the cost exceeds the cache size
0310       if(factory->d->imageCache.insert(img->id(), img, img->sizeInBytes())) {
0311         s_imageInfoMap.remove(id_);
0312       }
0313     }
0314   }
0315   return success;
0316 }
0317 
0318 bool ImageFactory::writeCachedImage(const QString& id_, ImageDirectory* imgDir_, bool force_ /*=false*/) {
0319   if(id_.isEmpty() || !imgDir_) {
0320     return false;
0321   }
0322 //  myLog() << "ImageFactory::writeCachedImage() - dir =" << imgDir_->path() << "; id =" << id_;
0323   const bool exists = imgDir_->hasImage(id_);
0324   // only write if it doesn't exist
0325   bool success = (!force_ && exists);
0326   if(!success) {
0327     const Data::Image& img = imageById(id_);
0328     if(!img.isNull()) {
0329       success = imgDir_->writeImage(img);
0330     }
0331   }
0332   return success;
0333 }
0334 
0335 const Tellico::Data::Image& ImageFactory::imageById(const QString& id_) {
0336   Q_ASSERT(factory && "ImageFactory is not initialized!");
0337   if(id_.isEmpty() || !factory || factory->d->nullImages.contains(id_)) {
0338     return Data::Image::null;
0339   }
0340 //  myLog() << "imageById" << id_;
0341 
0342   // can't think of a better place to regularly check for images to release
0343   // but don't release image that just got asked for
0344   s_imagesToRelease.remove(id_);
0345   factory->releaseImages();
0346 
0347  // first check the cache, used for images that are in the data file, or are only temporary
0348  // then the dict, used for images downloaded, but not yet saved anywhere
0349   Data::Image* img = factory->d->imageCache.object(id_);
0350   if(img) {
0351 //    myLog() << "found in cache";
0352     return *img;
0353   }
0354 
0355   img = factory->d->imageDict.value(id_);
0356   if(img) {
0357 //    myLog() << "found in dict";
0358     return *img;
0359   }
0360 
0361   // if the image is link only, we need to load it
0362   // but can't call imageInfo() since that might recurse into imageById()
0363   // also, the image info cache might not have it so check if the
0364   // id is a valid absolute url
0365   // yeah, it's probably slow
0366   if((s_imageInfoMap.contains(id_) && s_imageInfoMap[id_].linkOnly) || !QUrl(id_).isRelative()) {
0367     QUrl u(id_);
0368     if(u.isValid()) {
0369       return factory->addImageImpl(u, true, QUrl(), true);
0370     }
0371   }
0372 
0373   // the document does a delayed loading of the images, sometimes
0374   // so an image could be in the tmp dir and not be in the cache
0375   // or it could be too big for the cache
0376   if(factory->d->tempImageDir.hasImage(id_)) {
0377     const Data::Image& img2 = factory->addCachedImageImpl(id_, TempDir);
0378     if(!img2.isNull()) {
0379 //      myLog() << "found in tmp dir";
0380       return img2;
0381     }
0382   }
0383 
0384   // try to do a delayed loading of the image
0385   if(factory->d->imageZipArchive.hasImage(id_)) {
0386     const Data::Image& img2 = factory->addCachedImageImpl(id_, ZipArchive);
0387     if(!img2.isNull()) {
0388 //      myLog() << "found in zip archive";
0389       // go ahead and write image to disk so we don't have to keep it in memory
0390       // calling pixmap() could be loading all the covers, and we don't want one
0391       // to get pushed out of the cache yet
0392       writeCachedImage(id_, TempDir);
0393       return img2;
0394     }
0395   }
0396 
0397   // This section uses the config image setting plus the fallback location
0398   // to provide confidence that the user's image can be found
0399   CacheDir configLoc = TempDir;
0400   CacheDir fallbackLoc = TempDir;
0401   ImageDirectory* configImgDir = nullptr;
0402   ImageDirectory* fallbackImgDir = nullptr;
0403   if(Config::imageLocation() == Config::ImagesInLocalDir) {
0404     configLoc = LocalDir;
0405     fallbackLoc = DataDir;
0406     configImgDir = &factory->d->localImageDir;
0407     fallbackImgDir = &factory->d->dataImageDir;
0408   } else if(Config::imageLocation() == Config::ImagesInAppDir) {
0409     configLoc = DataDir;
0410     fallbackLoc = LocalDir;
0411     configImgDir = &factory->d->dataImageDir;
0412     fallbackImgDir = &factory->d->localImageDir;
0413   }
0414 
0415   // check the configured location first
0416   if(configImgDir && configImgDir->hasImage(id_)) {
0417     const Data::Image& img2 = factory->addCachedImageImpl(id_, configLoc);
0418     if(!img2.isNull()) {
0419 //      myLog() << "found image in configured location" << configImgDir->path();
0420       return img2;
0421     } else {
0422       myDebug() << "tried to add" << id_ << "from" << configImgDir->path() << "but failed";
0423     }
0424   } else if(fallbackImgDir && fallbackImgDir->hasImage(id_)) {
0425     const Data::Image& img2 = factory->addCachedImageImpl(id_, fallbackLoc);
0426     if(!img2.isNull()) {
0427 //      myLog() << "found image in fallback location" << fallbackImgDir->path() << id_;
0428       // the img is in the other location
0429       factory->emitImageMismatch();
0430       return img2;
0431     } else {
0432       myDebug() << "tried to add" << id_ << "from" << fallbackImgDir->path() << "but failed";
0433     }
0434   }
0435   // at this point, there's a possibility that the user has changed settings so that the images
0436   // are currently in a local or data directory, but Config::imageLocation() doesn't match that location
0437   if(factory->d->dataImageDir.hasImage(id_)) {
0438     const Data::Image& img2 = factory->addCachedImageImpl(id_, DataDir);
0439     if(!img2.isNull()) {
0440 //      myLog() << "found image in data dir location";
0441       return img2;
0442     }
0443   }
0444   if(factory->d->localImageDir.hasImage(id_)) {
0445     const Data::Image& img2 = factory->addCachedImageImpl(id_, LocalDir);
0446     if(!img2.isNull()) {
0447 //      myLog() << "found image in local dir location";
0448       return img2;
0449     }
0450   }
0451   // now, it appears the image doesn't exist. The only remaining possibility
0452   // is that the file name has multiple periods and as a result of the fix for bug 348088,
0453   // the image is lurking in a local image directory without the multiple periods
0454   if(Config::imageLocation() == Config::ImagesInLocalDir && QDir(localDir()).dirName().contains(QLatin1Char('.'))) {
0455     QString realImageDir = localDir();
0456     QDir d(realImageDir);
0457     // try to cd up and into the other old directory
0458     QString cdString = QLatin1String("../") + d.dirName().section(QLatin1Char('.'), 0, 0) + QLatin1String("_files/");
0459     if(d.cd(cdString)) {
0460       factory->d->localImageDir.setPath(d.path() + QDir::separator());
0461       if(factory->d->localImageDir.hasImage(id_)) {
0462 //        myDebug() << "Reading image from old local directory" << (cdString+id_);
0463         const Data::Image& img2 = factory->addCachedImageImpl(id_, LocalDir);
0464         // Be sure to reset the image directory location!!
0465         factory->d->localImageDir.setPath(realImageDir);
0466         factory->d->localImageDir.writeImage(img2);
0467         return img2;
0468       }
0469       factory->d->localImageDir.setPath(realImageDir);
0470     }
0471   }
0472 
0473   myDebug() << "***ImageFactory::imageById() - not found:" << id_;
0474   return Data::Image::null;
0475 }
0476 
0477 bool ImageFactory::hasLocalImage(const QString& id_) {
0478   Q_ASSERT(factory && "ImageFactory is not initialized!");
0479   if(id_.isEmpty() || !factory) {
0480     return false;
0481   }
0482   const QUrl u(id_);
0483   return factory->d->imageCache.contains(id_) ||
0484          factory->d->imageDict.contains(id_) ||
0485          factory->d->tempImageDir.hasImage(id_) ||
0486          factory->d->imageZipArchive.hasImage(id_) ||
0487          (u.isValid() && !u.isRelative() && u.isLocalFile()) ||
0488          (Config::imageLocation() == Config::ImagesInLocalDir &&
0489                                      factory->d->localImageDir.hasImage(id_)) ||
0490          (Config::imageLocation() == Config::ImagesInAppDir &&
0491                                      factory->d->dataImageDir.hasImage(id_));
0492 }
0493 
0494 void ImageFactory::requestImageById(const QString& id_) {
0495   Q_ASSERT(factory && "ImageFactory is not initialized!");
0496   if(hasLocalImage(id_)) {
0497     emit factory->imageAvailable(id_);
0498     return;
0499   }
0500   if(factory->d->nullImages.contains(id_)) {
0501     // don't emit anything
0502     return;
0503   }
0504   const QUrl u(id_);
0505   // what does it mean when the Id is an absolute Url and yet the image is not link only?
0506   // Probably a heritage image id before the bugs were fixed.
0507   const bool linkOnly = (s_imageInfoMap.contains(id_) && s_imageInfoMap[id_].linkOnly);
0508   if(linkOnly || !u.isRelative()) {
0509     if(u.isValid()) {
0510       factory->requestImageByUrlImpl(u, true /* quiet */, QUrl() /* referrer */, linkOnly);
0511       if(!linkOnly) {
0512         myDebug() << "Loading an image url that is not link only. The image id will get updated.";
0513       }
0514     }
0515   }
0516 }
0517 
0518 void ImageFactory::requestImageByUrlImpl(const QUrl& url_, bool quiet_, const QUrl& refer_, bool link_) {
0519   ImageJob* job = new ImageJob(url_, QString() /* id, use calculated one */, quiet_);
0520   job->setLinkOnly(link_);
0521   job->setReferrer(refer_);
0522   connect(job, &ImageJob::result,
0523           this, &ImageFactory::slotImageJobResult);
0524 }
0525 
0526 Tellico::Data::ImageInfo ImageFactory::imageInfo(const QString& id_) {
0527   if(s_imageInfoMap.contains(id_)) {
0528     return s_imageInfoMap[id_];
0529   }
0530 
0531   const Data::Image& img = imageById(id_);
0532   if(img.isNull()) {
0533     return Data::ImageInfo();
0534   }
0535   return Data::ImageInfo(img);
0536 }
0537 
0538 void ImageFactory::cacheImageInfo(const Tellico::Data::ImageInfo& info) {
0539   s_imageInfoMap.insert(info.id, info);
0540 }
0541 
0542 bool ImageFactory::hasImageInfo(const QString& id_) {
0543   return s_imageInfoMap.contains(id_);
0544 }
0545 
0546 bool ImageFactory::validImage(const QString& id_) {
0547   // don't try s_imageInfoMap[id_] cause it inserts an empty image info
0548   return s_imageInfoMap.contains(id_) || factory->hasImageInMemory(id_) || !imageById(id_).isNull();
0549 }
0550 
0551 QPixmap ImageFactory::pixmap(const QString& id_, int width_, int height_) {
0552   if(id_.isEmpty()) {
0553     return QPixmap();
0554   }
0555 
0556   const QString key = id_ + QLatin1Char('|') + QString::number(width_) + QLatin1Char('|') + QString::number(height_);
0557   QPixmap* pix = factory->d->pixmapCache.object(key);
0558   if(pix) {
0559     return *pix;
0560   }
0561 
0562   const Data::Image& img = imageById(id_);
0563   if(img.isNull()) {
0564     return QPixmap();
0565   }
0566 
0567   if(width_ > 0 && height_ > 0) {
0568     pix = new QPixmap(img.convertToPixmap(width_, height_));
0569   } else {
0570     pix = new QPixmap(img.convertToPixmap());
0571   }
0572 
0573   QPixmap pix2(*pix); // retain a copy of pix in case it doesn't go into the cache
0574   // pixmap size is w x h x d, divided by 8 bits
0575   const int size = (pix->width()*pix->height()*pix->depth()/8);
0576   if(!factory->d->pixmapCache.insert(key, pix, pix->width()*pix->height()*pix->depth()/8)) {
0577     // at this point, pix might be deleted
0578     myWarning() << "can't save in cache: " << id_;
0579     myWarning() << "### Current pixmap size is " << size;
0580     myWarning() << "### Max pixmap cache size is " << factory->d->pixmapCache.maxCost();
0581     return pix2;
0582   }
0583   return *pix;
0584 }
0585 
0586 void ImageFactory::clean(bool purgeTempDirectory_) {
0587   // the caches all auto-delete
0588   s_imagesToRelease.clear();
0589   qDeleteAll(factory->d->imageDict);
0590   factory->d->imageDict.clear();
0591   s_imageInfoMap.clear();
0592   factory->d->imageCache.clear();
0593   factory->d->pixmapCache.clear();
0594   if(purgeTempDirectory_) {
0595     factory->d->tempImageDir.purge();
0596     // just to make sure all the image locations clean themselves up
0597     // delete the factory (which deletes the storage objects) and
0598     // then recreate the factory, in case anything else needs it
0599     // be sure to save local image directory if it's not a temp dir!
0600     const QString localDirName = localDir();
0601     delete factory;
0602     factory = nullptr;
0603     ImageFactory::init();
0604     if(QDir(localDirName).exists()) {
0605       setLocalDirectory(QUrl::fromLocalFile(localDirName));
0606     }
0607   }
0608 }
0609 
0610 void ImageFactory::createStyleImages(int collectionType_, const Tellico::StyleOptions& opt_) {
0611   const QColor& baseColor = opt_.baseColor.isValid()
0612                           ? opt_.baseColor
0613                           : Config::templateBaseColor(collectionType_);
0614   const QColor& highColor = opt_.highlightedBaseColor.isValid()
0615                           ? opt_.highlightedBaseColor
0616                           : Config::templateHighlightedBaseColor(collectionType_);
0617 
0618   const QString bgname(QStringLiteral("gradient_bg.png"));
0619   const QColor& bgc1 = KColorUtils::mix(baseColor, highColor, 0.3);
0620   QImage bgImage = Tellico::gradient(QSize(600, 1), bgc1, baseColor,
0621                                      Tellico::PipeCrossGradient);
0622   bgImage = bgImage.transformed(QTransform().rotate(90));
0623 
0624   const QString hdrname(QStringLiteral("gradient_header.png"));
0625   const QColor& bgc2 = KColorUtils::mix(baseColor, highColor, 0.5);
0626   QImage hdrImage = Tellico::unbalancedGradient(QSize(1, 10), highColor, bgc2,
0627                                                 Tellico::VerticalGradient, 100, -100);
0628 
0629   if(opt_.imgDir.isEmpty()) {
0630     ImageFactory::removeImage(bgname, true /*delete */);
0631     factory->addImageImpl(Data::Image::byteArray(bgImage, "PNG"), QStringLiteral("PNG"), bgname);
0632     ImageFactory::writeCachedImage(bgname, cacheDir(), true /*force*/);
0633 
0634     ImageFactory::removeImage(hdrname, true /*delete */);
0635     factory->addImageImpl(Data::Image::byteArray(hdrImage, "PNG"), QStringLiteral("PNG"), hdrname);
0636     ImageFactory::writeCachedImage(hdrname, cacheDir(), true /*force*/);
0637   } else {
0638     bgImage.save(opt_.imgDir + bgname, "PNG");
0639     hdrImage.save(opt_.imgDir + hdrname, "PNG");
0640   }
0641 }
0642 
0643 void ImageFactory::removeImage(const QString& id_, bool deleteImage_) {
0644   // be careful using this
0645   delete factory->d->imageDict.take(id_);
0646   factory->d->imageCache.remove(id_);
0647 
0648   if(deleteImage_) {
0649     // remove from everywhere
0650     factory->d->dataImageDir.removeImage(id_);
0651     factory->d->localImageDir.removeImage(id_);
0652     factory->d->tempImageDir.removeImage(id_);
0653   }
0654 }
0655 
0656 Tellico::StringSet ImageFactory::imagesNotInCache() {
0657   StringSet set;
0658   QHash<QString, Tellico::Data::Image*>::const_iterator end = factory->d->imageDict.constEnd();
0659   for(QHash<QString, Tellico::Data::Image*>::const_iterator it = factory->d->imageDict.constBegin(); it != end; ++it) {
0660     if(!factory->d->imageCache.contains(it.key())) {
0661       set.add(it.key());
0662     }
0663   }
0664   return set;
0665 }
0666 
0667 bool ImageFactory::hasImageInMemory(const QString& id_) const {
0668   return d->imageCache.contains(id_) || d->imageDict.contains(id_);
0669 }
0670 
0671 bool ImageFactory::hasNullImage(const QString& id_) const {
0672   return d->nullImages.contains(id_);
0673 }
0674 
0675 // the purpose here is to remove images from the dict if they're is on the disk somewhere,
0676 // either in tempDir() or in dataDir(). The use for this is for calling pixmap() on an
0677 // image too big to stay in the cache. Then it stays in the dict forever.
0678 void ImageFactory::releaseImages() {
0679 #ifdef RELEASE_IMAGES
0680   if(s_imagesToRelease.isEmpty()) {
0681     return;
0682   }
0683 
0684   foreach(const QString& id, s_imagesToRelease) {
0685     if(!d->imageDict.contains(id)) {
0686       continue;
0687     }
0688     if(d->dataImageDir.hasImage(id) ||
0689        d->localImageDir.hasImage(id) ||
0690        d->tempImageDir.hasImage(id)) {
0691       delete d->imageDict.take(id);
0692     }
0693   }
0694   s_imagesToRelease.clear();
0695 #endif
0696 }
0697 
0698 void ImageFactory::emitImageMismatch() {
0699   emit imageLocationMismatch();
0700 }
0701 
0702 QString ImageFactory::localDirectory(const QUrl& url_) {
0703   if(url_.isEmpty()) {
0704     return QString();
0705   }
0706   if(!url_.isLocalFile()) {
0707     myWarning() << "Tellico can only save images to local disk";
0708     myWarning() << "unable to save to " << url_;
0709     return QString();
0710   }
0711   QString dir = url_.adjusted(QUrl::RemoveFilename).toLocalFile();
0712   // could have already been set once
0713   if(!dir.contains(QLatin1String("_files"))) {
0714     QFileInfo fi(url_.fileName());
0715     dir += fi.completeBaseName() + QLatin1String("_files/");
0716   }
0717   return dir;
0718 }
0719 
0720 void ImageFactory::setLocalDirectory(const QUrl& url_) {
0721   Q_ASSERT(factory && "ImageFactory is not initialized!");
0722   const QString localDirName = localDirectory(url_);
0723   if(!localDirName.isEmpty()) {
0724     factory->d->localImageDir.setPath(localDirName);
0725   }
0726 }
0727 
0728 void ImageFactory::setZipArchive(std::unique_ptr<KZip> zip_) {
0729   Q_ASSERT(factory && "ImageFactory is not initialized!");
0730   if(!zip_) {
0731     return;
0732   }
0733   factory->d->imageZipArchive.setZip(std::move(zip_));
0734 }
0735 
0736 void ImageFactory::slotImageJobResult(KJob* job_) {
0737   ImageJob* imageJob = qobject_cast<ImageJob*>(job_);
0738   Q_ASSERT(imageJob);
0739   if(!imageJob) {
0740     myWarning() << "No image job";
0741     return;
0742   }
0743   const Data::Image& img = imageJob->image();
0744   if(img.isNull()) {
0745      myDebug() << "null image for" << imageJob->url();
0746      d->nullImages.add(imageJob->url().url());
0747      // don't emit anything
0748      return;
0749   }
0750 
0751   // hold the image in memory since it probably isn't written locally to disk yet
0752   if(!d->imageDict.contains(img.id())) {
0753     d->imageDict.insert(img.id(), new Data::Image(img));
0754     s_imageInfoMap.insert(img.id(), Data::ImageInfo(img));
0755   }
0756   emit factory->imageAvailable(img.id());
0757 }
0758 
0759 #undef RELEASE_IMAGES