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 - thumbnail generation and image handling
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 ThumbnailImage ThumbnailCreator::createThumbnail(const ThumbnailInfo& info, const QRect& detailRect) const
0023 {
0024     const QString path = info.filePath;
0025     QFileInfo fileInfo(path);
0026 
0027     if (!info.isAccessible || !fileInfo.exists() || !fileInfo.isFile())
0028     {
0029         d->error = i18n("File does not exist or is not a file");
0030         return ThumbnailImage();
0031     }
0032 
0033     QImage qimage;
0034     QScopedPointer<DMetadata> metadata(new DMetadata(path));
0035     bool fromEmbeddedPreview = false;
0036     bool fromDetail          = false;
0037     bool failedAtDImg        = false;
0038     bool failedAtJPEGScaled  = false;
0039     bool failedAtPGFScaled   = false;
0040 
0041     // -- Get the image preview --------------------------------
0042 
0043     IccProfile profile;
0044     bool colorManage = IccSettings::instance()->useManagedPreviews();
0045 
0046     if (!detailRect.isNull())
0047     {
0048         qCDebug(DIGIKAM_GENERAL_LOG) << "Trying to get thumbnail with details for" << path;
0049 
0050         // When taking a detail, we have to load the image full size
0051 
0052         qimage     = loadImageDetail(info, *metadata, detailRect, &profile);
0053         fromDetail = !qimage.isNull();
0054     }
0055     else
0056     {
0057         qCDebug(DIGIKAM_GENERAL_LOG) << "Trying to get thumbnail from" << path << "(" << info.mimeType << ")";
0058         QString ext = fileInfo.suffix().toUpper();
0059 
0060         if (
0061             (info.mimeType == QLatin1String("image")) ||
0062             (ext           == QLatin1String("AVIF"))  ||      // See bug #109060
0063             (ext           == QLatin1String("JPX"))
0064            )
0065         {
0066             if (qimage.isNull())
0067             {
0068                 // Try to extract Exif/IPTC preview first.
0069 
0070                 qimage = loadImagePreview(*metadata);
0071             }
0072 
0073             if (d->observer && !d->observer->continueQuery())
0074             {
0075                 return ThumbnailImage();
0076             }
0077 
0078             // To speed-up thumb extraction, we now try to load the images by the file extension.
0079 
0080             if (qimage.isNull() && !ext.isEmpty())
0081             {
0082                 if      ((ext == QLatin1String("JPEG")) ||
0083                          (ext == QLatin1String("JPG"))  ||
0084                          (ext == QLatin1String("JPE")))
0085                 {
0086                     if (colorManage)
0087                     {
0088                         qimage = loadWithDImgScaled(path, &profile);
0089                     }
0090                     else
0091                     {
0092                         // Use jpegutils
0093 
0094                         JPEGUtils::loadJPEGScaled(qimage, path, d->storageSize());
0095                     }
0096 
0097                     failedAtJPEGScaled = qimage.isNull();
0098                 }
0099                 else if ((ext == QLatin1String("PNG"))  ||
0100                          (ext == QLatin1String("TIFF")) ||
0101                          (ext == QLatin1String("TIF")))
0102                 {
0103                     // Use DImg load scaled mode
0104 
0105                     qimage       = loadWithDImgScaled(path, &profile);
0106                     failedAtDImg = qimage.isNull();
0107                 }
0108                 else if (ext == QLatin1String("PGF"))
0109                 {
0110                     // Use pgf library to extract reduced version
0111 
0112                     PGFUtils::loadPGFScaled(qimage, path, d->storageSize());
0113                     failedAtPGFScaled = qimage.isNull();
0114                 }
0115 
0116                 if (d->observer && !d->observer->continueQuery())
0117                 {
0118                     return ThumbnailImage();
0119                 }
0120             }
0121 
0122             // Trying to load with libraw: RAW files.
0123 
0124             if (qimage.isNull())
0125             {
0126                 qCDebug(DIGIKAM_GENERAL_LOG) << "Trying to get thumbnail from Embedded preview with libraw for" << path;
0127 
0128                 if (DRawDecoder::loadEmbeddedPreview(qimage, path))
0129                 {
0130                     fromEmbeddedPreview = true;
0131                     profile             = metadata->getIccProfile();
0132                 }
0133             }
0134 
0135             if (qimage.isNull())
0136             {
0137                 qCDebug(DIGIKAM_GENERAL_LOG) << "Trying to get thumbnail from half preview with libraw for" << path;
0138 
0139                 // TODO: Use DImg based loader instead?
0140                 // We store thumbnails unrotated, loading
0141                 // half preview from libraw unrotated here.
0142 
0143                 DRawDecoder::loadHalfPreview(qimage, path, false);
0144             }
0145 
0146             // Special case with DNG file. See bug #338081
0147 
0148             if (qimage.isNull())
0149             {
0150                 qCDebug(DIGIKAM_GENERAL_LOG) << "Trying to get thumbnail from Embedded preview with Exiv2 for" << path;
0151 
0152                 MetaEnginePreviews preview(path);
0153                 qimage = preview.image();
0154             }
0155 
0156             // DImg-dependent loading methods: TIFF, PNG, everything supported by QImage
0157 
0158             if (qimage.isNull() && !failedAtDImg)
0159             {
0160                 qimage = loadWithDImgScaled(path, &profile);
0161 
0162                 if (d->observer && !d->observer->continueQuery())
0163                 {
0164                     return ThumbnailImage();
0165                 }
0166             }
0167 
0168             // Try JPEG anyway
0169 
0170             if (qimage.isNull() && !failedAtJPEGScaled)
0171             {
0172                 // Use jpegutils
0173 
0174                 JPEGUtils::loadJPEGScaled(qimage, path, d->storageSize());
0175             }
0176 
0177             // Try PGF anyway
0178 
0179             if (qimage.isNull() && !failedAtPGFScaled)
0180             {
0181                 // Use pgfutils
0182 
0183                 PGFUtils::loadPGFScaled(qimage, path, d->storageSize());
0184             }
0185         }
0186         else
0187         {
0188             // Try video thumbnail anyway
0189 
0190 #ifdef HAVE_MEDIAPLAYER
0191 
0192             qCDebug(DIGIKAM_GENERAL_LOG) << "Trying to load video preview with FFmpeg";
0193 
0194             VideoThumbnailer thumbnailer;
0195             VideoStripFilter videoStrip;
0196 
0197             thumbnailer.addFilter(&videoStrip);
0198             thumbnailer.setThumbnailSize(d->storageSize());
0199             thumbnailer.generateThumbnail(path, qimage);
0200 
0201 #else
0202 
0203             qDebug(DIGIKAM_GENERAL_LOG) << "Cannot load video preview for" << path;
0204             qDebug(DIGIKAM_GENERAL_LOG) << "Video support is not available";
0205 
0206 #endif
0207 
0208         }
0209     }
0210 
0211     if (qimage.isNull())
0212     {
0213         d->error = i18n("Cannot create thumbnail for %1", path);
0214         qCWarning(DIGIKAM_GENERAL_LOG) << "Cannot create thumbnail for" << path;
0215 
0216         return ThumbnailImage();
0217     }
0218 
0219     qimage = scaleForStorage(qimage);
0220 
0221     if (colorManage && !profile.isNull())
0222     {
0223         IccManager::transformToSRGB(qimage, profile);
0224     }
0225 
0226     ThumbnailImage image;
0227     image.qimage          = qimage;
0228     image.exifOrientation = exifOrientation(info, *metadata, fromEmbeddedPreview, fromDetail);
0229 
0230     return image;
0231 }
0232 
0233 QImage ThumbnailCreator::loadWithDImgScaled(const QString& path, IccProfile* const profile) const
0234 {
0235     DImg img;
0236     img.setAttribute(QLatin1String("scaledLoadingSize"), d->storageSize());
0237 
0238     if (!img.load(path, false, profile ? true : false, false, false, d->observer, d->rawSettings))
0239     {
0240         return QImage();
0241     }
0242 
0243     if (profile)
0244     {
0245         *profile = img.getIccProfile();
0246     }
0247 
0248     return img.copyQImage();
0249 }
0250 
0251 QImage ThumbnailCreator::loadImageDetail(const ThumbnailInfo& info,
0252                                          const DMetadata& metadata,
0253                                          const QRect& detailRect,
0254                                          IccProfile* const profile) const
0255 {
0256     const QString& path = info.filePath;
0257 
0258     qDebug(DIGIKAM_GENERAL_LOG) << "Try get thumbnail from Metadata preview for" << path;
0259 
0260     // Check the first and largest preview (Raw files)
0261 
0262     MetaEnginePreviews previews(path);
0263 
0264     if (!previews.isEmpty())
0265     {
0266         QSize orgSize;
0267 
0268         if (metadata.getExifTagString("Exif.Image.Make").toUpper() == QLatin1String("FUJIFILM"))
0269         {
0270             QString sHeight = metadata.getExifTagString("Exif.Fujifilm.RawImageFullHeight");
0271             QString sWidth  = metadata.getExifTagString("Exif.Fujifilm.RawImageFullWidth");
0272 
0273             if (!sWidth.isEmpty() && !sHeight.isEmpty())
0274             {
0275                 orgSize = QSize(sWidth.toInt(),
0276                                 sHeight.toInt());
0277             }
0278         }
0279 
0280         if (!orgSize.isValid())
0281         {
0282             orgSize = previews.originalSize();
0283         }
0284 
0285         // Discard if smaller than half preview
0286 
0287         QImage qimage        = previews.image();
0288         int acceptableWidth  = lround(orgSize.width()  * 0.5);
0289         int acceptableHeight = lround(orgSize.height() * 0.5);
0290 
0291         if (!qimage.isNull() && (previews.width() >= acceptableWidth) && (previews.height() >= acceptableHeight))
0292         {
0293             qimage = exifRotate(qimage, exifOrientation(info, metadata, true, false));
0294 
0295             if (((qimage.width() < qimage.height()) && (orgSize.width() > orgSize.height())) ||
0296                 ((qimage.width() > qimage.height()) && (orgSize.width() < orgSize.height())))
0297             {
0298                 orgSize.transpose();
0299             }
0300 
0301             QRect reducedSizeDetail = TagRegion::mapFromOriginalSize(orgSize, qimage.size(), detailRect);
0302 
0303             return qimage.copy(reducedSizeDetail.intersected(qimage.rect()));
0304         }
0305     }
0306 
0307     // load DImg
0308 
0309     DImg img;
0310 
0311     DImgLoader::LoadFlags loadFlags = DImgLoader::LoadItemInfo |
0312                                       DImgLoader::LoadMetadata |
0313                                       DImgLoader::LoadICCData  |
0314                                       DImgLoader::LoadPreview;
0315 
0316     qDebug(DIGIKAM_GENERAL_LOG) << "Try to get thumbnail from DImg preview for" << path;
0317 
0318     if (img.load(path, loadFlags, d->observer, d->fastRawSettings))
0319     {
0320         // Discard if smaller than half preview
0321 
0322         unsigned int acceptableWidth  = lround(img.originalRatioSize().width()  * 0.5);
0323         unsigned int acceptableHeight = lround(img.originalRatioSize().height() * 0.5);
0324 
0325         if ((img.width() < acceptableWidth) && (img.height() < acceptableHeight))
0326         {
0327             qDebug(DIGIKAM_GENERAL_LOG) << "Preview image is smaller than the accepted size:"
0328                                         << acceptableWidth << "x" << acceptableHeight;
0329             img.reset();
0330         }
0331     }
0332 
0333     if (img.isNull())
0334     {
0335         qDebug(DIGIKAM_GENERAL_LOG) << "Try to get thumbnail from DImg scaled for" << path;
0336 
0337         // TODO: scaledLoading if detailRect is large
0338         // TODO: use code from PreviewTask, including cache storage
0339 
0340         if (!img.load(path, false, profile ? true : false, false, false, d->observer, d->fastRawSettings))
0341         {
0342             return QImage();
0343         }
0344     }
0345 
0346     if (profile)
0347     {
0348         *profile = img.getIccProfile();
0349     }
0350 
0351     // We must rotate before clipping because the rect refers to the oriented image.
0352     // I do not know currently how to back-rotate the rect for clipping before rotation.
0353     // If someone has the mathematics, have a go.
0354 
0355     img.rotateAndFlip(exifOrientation(info, metadata, false, false));
0356 
0357     QRect mappedDetail = TagRegion::mapFromOriginalSize(img, detailRect);
0358     img.crop(mappedDetail.intersected(QRect(0, 0, img.width(), img.height())));
0359 
0360     return img.copyQImage();
0361 }
0362 
0363 QImage ThumbnailCreator::loadImagePreview(const DMetadata& metadata) const
0364 {
0365     QImage image;
0366 
0367     qCDebug(DIGIKAM_GENERAL_LOG) << "Trying to get thumbnail with Exiv2 for" << metadata.getFilePath();
0368 
0369     if (metadata.getItemPreview(image))
0370     {
0371         qCDebug(DIGIKAM_GENERAL_LOG) << "Get thumbnail from Exif/IPTC preview. Size of image: "
0372                                      << image.width() << "x" << image.height();
0373     }
0374     else
0375     {
0376         // load DImg preview
0377 
0378         qCDebug(DIGIKAM_GENERAL_LOG) << "Trying to get thumbnail with DImg preview for" << metadata.getFilePath();
0379 
0380         DImg img;
0381         DImgLoader::LoadFlags loadFlags = DImgLoader::LoadItemInfo |
0382                                           DImgLoader::LoadMetadata |
0383                                           DImgLoader::LoadICCData  |
0384                                           DImgLoader::LoadPreview;
0385 
0386         if (img.load(metadata.getFilePath(), loadFlags, d->observer, d->fastRawSettings))
0387         {
0388             image = img.copyQImage();
0389         }
0390     }
0391 
0392     return image;
0393 }
0394 
0395 QImage ThumbnailCreator::handleAlphaChannel(const QImage& qimage) const
0396 {
0397     switch (qimage.format())
0398     {
0399         case QImage::Format_RGB32:
0400         {
0401             break;
0402         }
0403 
0404         case QImage::Format_ARGB32:
0405         case QImage::Format_ARGB32_Premultiplied:
0406         {
0407             QImage newImage(qimage.size(), QImage::Format_RGB32);
0408             newImage.fill(Qt::transparent);
0409             QPainter p(&newImage);
0410 
0411             if (d->removeAlphaChannel)
0412             {
0413                 QBrush brush(d->alphaImage);
0414                 p.fillRect(newImage.rect(), brush);
0415             }
0416 
0417             p.drawImage(0, 0, qimage);
0418             p.end();
0419 
0420             return newImage;
0421         }
0422 
0423         default: // indexed and monochrome formats
0424         {
0425             return qimage.convertToFormat(QImage::Format_RGB32);
0426         }
0427     }
0428 
0429     return qimage;
0430 }
0431 
0432 int ThumbnailCreator::exifOrientation(const ThumbnailInfo& info,
0433                                       const DMetadata& metadata,
0434                                       bool fromEmbeddedPreview,
0435                                       bool fromDetail) const
0436 {
0437     if (fromDetail)
0438     {
0439         return DMetadata::ORIENTATION_NORMAL;
0440     }
0441 
0442     return LoadSaveThread::exifOrientation(info.filePath, metadata,
0443                                            DImg::fileFormat(info.filePath) == DImg::RAW,
0444                                            fromEmbeddedPreview);
0445 }
0446 
0447 QImage ThumbnailCreator::exifRotate(const QImage& thumb, int orientation) const
0448 {
0449     if ((orientation == DMetadata::ORIENTATION_NORMAL) ||
0450         (orientation == DMetadata::ORIENTATION_UNSPECIFIED))
0451     {
0452         return thumb;
0453     }
0454 
0455     QTransform matrix = MetaEngineRotation::toTransform((MetaEngine::ImageOrientation)orientation);
0456 
0457     // Transform accordingly
0458 
0459     return thumb.transformed(matrix);
0460 }
0461 
0462 } // namespace Digikam