File indexing completed on 2025-04-27 03:58:09

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2007-07-20
0007  * Description : Loader for thumbnails
0008  *
0009  * SPDX-FileCopyrightText: 2003-2005 by Renchi Raju <renchi dot raju at gmail dot com>
0010  * SPDX-FileCopyrightText: 2003-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0011  * SPDX-FileCopyrightText: 2006-2011 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
0012  *
0013  * SPDX-License-Identifier: GPL-2.0-or-later
0014  *
0015  * ============================================================ */
0016 
0017 #include "thumbnailcreator_p.h"
0018 
0019 namespace Digikam
0020 {
0021 
0022 ThumbnailIdentifier::ThumbnailIdentifier()
0023     : id(0)
0024 {
0025 }
0026 
0027 ThumbnailIdentifier::ThumbnailIdentifier(const QString& filePath)
0028     : filePath(filePath),
0029       id      (0)
0030 {
0031 }
0032 
0033 ThumbnailInfo::ThumbnailInfo()
0034     : fileSize       (0),
0035       isAccessible   (false),
0036       orientationHint(DMetadata::ORIENTATION_UNSPECIFIED)
0037 {
0038 }
0039 
0040 ThumbnailCreator::ThumbnailCreator(StorageMethod method)
0041     : d(new Private)
0042 {
0043     d->thumbnailStorage = method;
0044     initialize();
0045 }
0046 
0047 ThumbnailCreator::ThumbnailCreator(int thumbnailSize, StorageMethod method)
0048     : d(new Private)
0049 {
0050     setThumbnailSize(thumbnailSize);
0051     d->thumbnailStorage = method;
0052     initialize();
0053 }
0054 
0055 ThumbnailCreator::~ThumbnailCreator()
0056 {
0057     delete d;
0058 }
0059 
0060 void ThumbnailCreator::initialize()
0061 {
0062     QString alphaPath = QStandardPaths::locate(QStandardPaths::AppDataLocation,
0063                                                QLatin1String("thumbnail/background.png"));
0064 
0065     if (QFile::exists(alphaPath))
0066     {
0067         if (d->alphaImage.load(alphaPath, "PNG"))
0068         {
0069             int max = qMax(d->alphaImage.width(), d->alphaImage.height());
0070             int min = qMin(d->alphaImage.width(), d->alphaImage.height());
0071 
0072             if ((max > ThumbnailSize::MAX) || (min < 10))
0073             {
0074                 d->alphaImage = QImage();
0075             }
0076         }
0077     }
0078 
0079     if (d->alphaImage.isNull())
0080     {
0081         d->alphaImage = QImage(20, 20, QImage::Format_RGB32);
0082 
0083         // create checkerboard image
0084 
0085         QPainter p(&d->alphaImage);
0086         p.fillRect( 0,  0, 20, 20, Qt::white);
0087         p.fillRect( 0, 10 ,10, 10, Qt::lightGray);
0088         p.fillRect(10,  0, 10, 10, Qt::lightGray);
0089         p.end();
0090     }
0091 
0092     if (d->thumbnailStorage == FreeDesktopStandard)
0093     {
0094         initThumbnailDirs();
0095     }
0096 }
0097 
0098 int ThumbnailCreator::Private::storageSize() const
0099 {
0100     // on-disk thumbnail sizes according to freedesktop spec
0101     // for thumbnail db it's always max size
0102 
0103     double dpr = qApp->devicePixelRatio();
0104 
0105     if (onlyLargeThumbnails)
0106     {
0107         if ((dpr > 1.0) && (thumbnailStorage == ThumbnailDatabase))
0108         {
0109             return ThumbnailSize::getUseLargeThumbs() ? ThumbnailSize::MAX
0110                                                       : ThumbnailSize::HD;
0111         }
0112         else
0113         {
0114             return ThumbnailSize::maxThumbsSize();
0115         }
0116     }
0117     else
0118     {
0119         if ((dpr > 1.0) && (thumbnailStorage == ThumbnailDatabase))
0120         {
0121             return (thumbnailSize <= ThumbnailSize::Small) ? ThumbnailSize::Huge
0122                                                            : ThumbnailSize::HD;
0123         }
0124         else
0125         {
0126             return (thumbnailSize <= ThumbnailSize::Medium) ? ThumbnailSize::Medium
0127                                                             : ThumbnailSize::Huge;
0128         }
0129     }
0130 }
0131 
0132 void ThumbnailCreator::setThumbnailSize(int thumbnailSize)
0133 {
0134     d->thumbnailSize = thumbnailSize;
0135 }
0136 
0137 void ThumbnailCreator::setExifRotate(bool rotate)
0138 {
0139     d->exifRotate = rotate;
0140 }
0141 
0142 void ThumbnailCreator::setOnlyLargeThumbnails(bool onlyLarge)
0143 {
0144     d->onlyLargeThumbnails = onlyLarge;
0145 }
0146 
0147 void ThumbnailCreator::setRemoveAlphaChannel(bool removeAlpha)
0148 {
0149     d->removeAlphaChannel = removeAlpha;
0150 }
0151 
0152 void ThumbnailCreator::setLoadingProperties(DImgLoaderObserver* const observer, const DRawDecoding& settings)
0153 {
0154     d->observer    = observer;
0155     d->rawSettings = settings;
0156 }
0157 
0158 void ThumbnailCreator::setThumbnailInfoProvider(ThumbnailInfoProvider* const provider)
0159 {
0160     d->infoProvider = provider;
0161 }
0162 
0163 int ThumbnailCreator::thumbnailSize() const
0164 {
0165     return d->thumbnailSize;
0166 }
0167 
0168 int ThumbnailCreator::storedSize() const
0169 {
0170     return d->storageSize();
0171 }
0172 
0173 QString ThumbnailCreator::errorString() const
0174 {
0175     return d->error;
0176 }
0177 
0178 QImage ThumbnailCreator::load(const ThumbnailIdentifier& identifier, bool onlyStorage) const
0179 {
0180     return load(identifier, QRect(), false, onlyStorage);
0181 }
0182 
0183 QImage ThumbnailCreator::loadDetail(const ThumbnailIdentifier& identifier,
0184                                     const QRect& rect, bool onlyStorage) const
0185 {
0186     if (!rect.isValid())
0187     {
0188         qCWarning(DIGIKAM_GENERAL_LOG) << "Invalid rectangle" << rect;
0189 
0190         return QImage();
0191     }
0192 
0193     return load(identifier, rect, false, onlyStorage);
0194 }
0195 
0196 void ThumbnailCreator::pregenerate(const ThumbnailIdentifier& identifier) const
0197 {
0198     load(identifier, QRect(), true);
0199 }
0200 
0201 void ThumbnailCreator::pregenerateDetail(const ThumbnailIdentifier& identifier, const QRect& rect) const
0202 {
0203     if (!rect.isValid())
0204     {
0205         qCWarning(DIGIKAM_GENERAL_LOG) << "Invalid rectangle" << rect;
0206 
0207         return;
0208     }
0209 
0210     load(identifier, rect, true);
0211 }
0212 
0213 QImage ThumbnailCreator::load(const ThumbnailIdentifier& identifier,
0214                               const QRect& rect, bool pregenerate, bool onlyStorage) const
0215 {
0216     if (d->storageSize() <= 0)
0217     {
0218         d->error = i18n("No or invalid size specified");
0219         qCWarning(DIGIKAM_GENERAL_LOG) << "No or invalid size specified";
0220 
0221         return QImage();
0222     }
0223 
0224     if (d->thumbnailStorage == ThumbnailDatabase)
0225     {
0226         d->dbIdForReplacement = -1;    // Just to prevent bugs
0227     }
0228 
0229     ThumbnailInfo info;
0230     ThumbnailImage image;
0231 
0232     {
0233         FileReadLocker lock(identifier.filePath);
0234 
0235         // Get info about path
0236 
0237         info = makeThumbnailInfo(identifier, rect);
0238 
0239         // Load pregenerated thumbnail
0240 
0241         switch (d->thumbnailStorage)
0242         {
0243             case ThumbnailDatabase:
0244             {
0245                 if (pregenerate)
0246                 {
0247                     if (isInDatabase(info))
0248                     {
0249                         return QImage();
0250                     }
0251 
0252                     // Otherwise, fall through and generate
0253                 }
0254                 else
0255                 {
0256                     image = loadFromDatabase(info);
0257                 }
0258 
0259                 break;
0260             }
0261 
0262             case FreeDesktopStandard:
0263             {
0264                 image = loadFreedesktop(info);
0265                 break;
0266             }
0267         }
0268     }
0269 
0270     // For images in offline collections we can stop here, they are not available on disk
0271 
0272     if (image.isNull() && (onlyStorage || info.filePath.isEmpty()))
0273     {
0274         return QImage();
0275     }
0276 
0277     // If pre-generated thumbnail is not available, generate
0278 
0279     if (image.isNull())
0280     {
0281         FileWriteLocker lock(identifier.filePath);
0282 
0283         switch (d->thumbnailStorage)
0284         {
0285             case ThumbnailDatabase:
0286             {
0287                 if (isInDatabase(info))
0288                 {
0289                     image = loadFromDatabase(info);
0290                 }
0291                 else
0292                 {
0293                     image = createThumbnail(info, rect);
0294 
0295                     if (!image.isNull())
0296                     {
0297                         storeInDatabase(info, image);
0298                     }
0299                 }
0300 
0301                 break;
0302             }
0303 
0304             case FreeDesktopStandard:
0305             {
0306                 image = createThumbnail(info, rect);
0307 
0308                 if (!image.isNull())
0309                 {
0310                     // Image is stored rotated
0311 
0312                     if (d->exifRotate)
0313                     {
0314                         image.qimage = exifRotate(image.qimage, image.exifOrientation);
0315                     }
0316 
0317                     storeFreedesktop(info, image);
0318                 }
0319 
0320                 break;
0321             }
0322         }
0323     }
0324 
0325     if (image.isNull())
0326     {
0327         d->error = i18n("Thumbnail is null");
0328         qCWarning(DIGIKAM_GENERAL_LOG) << "Thumbnail is null for " << identifier.filePath;
0329 
0330         return image.qimage;
0331     }
0332 
0333     // If we only pregenerate, we have now created and stored in the database
0334 
0335     if (pregenerate)
0336     {
0337         return QImage();
0338     }
0339 
0340     // Prepare for usage in digikam
0341 
0342     image.qimage = image.qimage.scaled(d->thumbnailSize,
0343                                        d->thumbnailSize,
0344                                        Qt::KeepAspectRatio,
0345                                        Qt::SmoothTransformation);
0346 
0347     image.qimage = handleAlphaChannel(image.qimage);
0348 
0349     if (d->thumbnailStorage == ThumbnailDatabase)
0350     {
0351         // Image is stored, or created, unrotated, and is now rotated for display
0352         // detail thumbnails are stored readily rotated
0353 
0354         if ((d->exifRotate && rect.isNull()) || (info.mimeType == QLatin1String("video")))
0355         {
0356             image.qimage = exifRotate(image.qimage, image.exifOrientation);
0357         }
0358     }
0359 
0360     if (!info.customIdentifier.isNull())
0361     {
0362         image.qimage.setText(QLatin1String("customIdentifier"), info.customIdentifier);
0363     }
0364 
0365     return image.qimage;
0366 }
0367 
0368 QImage ThumbnailCreator::scaleForStorage(const QImage& qimage) const
0369 {
0370     if ((qimage.width() > d->storageSize()) || (qimage.height() > d->storageSize()))
0371     {
0372 /*
0373         Cheat scaling is disabled because of quality problems - see bug #224999
0374 
0375         // Perform cheat scaling (https://www.qtcentre.org/threads/28415-Creating-thumbnails-efficiently)
0376 
0377         int cheatSize = maxSize - (3*(maxSize - d->storageSize()) / 4);
0378         qimage        = qimage.scaled(cheatSize, cheatSize, Qt::KeepAspectRatio, Qt::FastTransformation);
0379 */
0380         QImage scaledThumb = qimage.scaled(d->storageSize(),
0381                                            d->storageSize(),
0382                                            Qt::KeepAspectRatio,
0383                                            Qt::SmoothTransformation);
0384 
0385         return scaledThumb;
0386     }
0387 
0388     return qimage;
0389 }
0390 
0391 QString ThumbnailCreator::identifierForDetail(const ThumbnailInfo& info, const QRect& rect)
0392 {
0393     QUrl url = QUrl::fromLocalFile(info.filePath);
0394     url.setScheme(QLatin1String("detail"));
0395 /*
0396     A scheme to support loading by database id, but this is a hack. Solve cleanly later (schema update)
0397 
0398     url.setPath(identifier.fileName);
0399 
0400     if (!identifier.uniqueHash.isNull())
0401     {
0402         url.addQueryItem("hash", identifier.uniqueHash);
0403         url.addQueryItem("filesize", QString::number(identifier.fileSize));
0404     }
0405     else
0406     {
0407         url.addQueryItem("path", identifier.filePath);
0408     }
0409 */
0410     QString r = QString::fromLatin1("%1,%2-%3x%4")
0411                 .arg(rect.x())
0412                 .arg(rect.y())
0413                 .arg(rect.width())
0414                 .arg(rect.height());
0415 
0416     QUrlQuery q(url);
0417     q.addQueryItem(QLatin1String("rect"), r);
0418     url.setQuery(q);
0419 
0420     return url.toString();
0421 }
0422 
0423 ThumbnailInfo ThumbnailCreator::makeThumbnailInfo(const ThumbnailIdentifier& identifier, const QRect& rect) const
0424 {
0425     ThumbnailInfo info;
0426 
0427     if (d->infoProvider)
0428     {
0429         info = d->infoProvider->thumbnailInfo(identifier);
0430     }
0431     else
0432     {
0433         info = fileThumbnailInfo(identifier.filePath);
0434     }
0435 
0436     if (!rect.isNull())
0437     {
0438         // Important: Pass the filled info, not the possibly half-filled identifier here because the hash is preferred for the customIdentifier!
0439 
0440         info.customIdentifier = identifierForDetail(info, rect);
0441     }
0442 
0443     return info;
0444 }
0445 
0446 void ThumbnailCreator::store(const QString& path, const QImage& i) const
0447 {
0448     store(path, i, QRect());
0449 }
0450 
0451 void ThumbnailCreator::storeDetailThumbnail(const QString& path, const QRect& detailRect, const QImage& i) const
0452 {
0453     store(path, i, detailRect);
0454 }
0455 
0456 void ThumbnailCreator::store(const QString& path, const QImage& i, const QRect& rect) const
0457 {
0458     if (i.isNull())
0459     {
0460         return;
0461     }
0462 
0463     QImage         qimage = scaleForStorage(i);
0464     ThumbnailInfo  info   = makeThumbnailInfo(ThumbnailIdentifier(path), rect);
0465     ThumbnailImage image;
0466     image.qimage          = qimage;
0467 
0468     switch (d->thumbnailStorage)
0469     {
0470         case ThumbnailDatabase:
0471         {
0472             // We must call isInDatabase or loadFromDatabase before storeInDatabase for d->dbIdForReplacement!
0473 
0474             if (!isInDatabase(info))
0475             {
0476                 storeInDatabase(info, image);
0477             }
0478 
0479             break;
0480         }
0481 
0482         case FreeDesktopStandard:
0483         {
0484             storeFreedesktop(info, image);
0485             break;
0486         }
0487     }
0488 }
0489 
0490 void ThumbnailCreator::deleteThumbnailsFromDisk(const QString& filePath) const
0491 {
0492     switch (d->thumbnailStorage)
0493     {
0494         case FreeDesktopStandard:
0495         {
0496             deleteFromDiskFreedesktop(filePath);
0497             break;
0498         }
0499 
0500         case ThumbnailDatabase:
0501         {
0502             ThumbnailInfo info;
0503 
0504             if (d->infoProvider)
0505             {
0506                 info = d->infoProvider->thumbnailInfo(ThumbnailIdentifier(filePath));
0507             }
0508             else
0509             {
0510                 info = fileThumbnailInfo(filePath);
0511             }
0512 
0513             deleteFromDatabase(info);
0514             break;
0515         }
0516     }
0517 }
0518 
0519 } // namespace Digikam