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

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2006-12-26
0007  * Description : Multithreaded loader for previews
0008  *
0009  * SPDX-FileCopyrightText: 2006-2011 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
0010  * SPDX-FileCopyrightText: 2006-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0011  *
0012  * SPDX-License-Identifier: GPL-2.0-or-later
0013  *
0014  * ============================================================ */
0015 
0016 #include "previewtask.h"
0017 
0018 // Qt includes
0019 
0020 #include <QVariant>
0021 #include <QScopedPointer>
0022 
0023 // Local includes
0024 
0025 #include "dimgloader.h"
0026 #include "drawdecoder.h"
0027 #include "digikam_debug.h"
0028 #include "dmetadata.h"
0029 #include "jpegutils.h"
0030 #include "metaenginesettings.h"
0031 #include "previewloadthread.h"
0032 
0033 namespace Digikam
0034 {
0035 
0036 PreviewLoadingTask::PreviewLoadingTask(LoadSaveThread* const thread, const LoadingDescription& description)
0037     : SharedLoadingTask       (thread, description, LoadSaveThread::AccessModeRead, LoadingTaskStatusLoading),
0038       m_fromRawEmbeddedPreview(false)
0039 {
0040 }
0041 
0042 PreviewLoadingTask::~PreviewLoadingTask()
0043 {
0044 }
0045 
0046 void PreviewLoadingTask::execute()
0047 {
0048     if (m_loadingTaskStatus == LoadingTaskStatusStopping)
0049     {
0050         if (m_thread)
0051         {
0052             m_thread->taskHasFinished();
0053         }
0054 
0055         return;
0056     }
0057 
0058     // Check if preview is in cache first.
0059 
0060     LoadingCache* const cache = LoadingCache::cache();
0061     {
0062         LoadingCache::CacheLock lock(cache);
0063 
0064         // find possible cached images
0065 
0066         DImg* cachedImg        = nullptr;
0067         QStringList lookupKeys = m_loadingDescription.lookupCacheKeys();
0068 
0069         // lookupCacheKeys returns "best first". Prepend the cache key to make the list "fastest first":
0070         // Scaling a full version takes longer!
0071 
0072         lookupKeys.prepend(m_loadingDescription.cacheKey());
0073 
0074         Q_FOREACH (const QString& key, lookupKeys)
0075         {
0076             if ((cachedImg = cache->retrieveImage(key)))
0077             {
0078                 if (m_loadingDescription.needCheckRawDecoding())
0079                 {
0080                     if (cachedImg->rawDecodingSettings() == m_loadingDescription.rawDecodingSettings)
0081                     {
0082                         break;
0083                     }
0084                     else
0085                     {
0086                         cachedImg = nullptr;
0087                     }
0088                 }
0089                 else
0090                 {
0091                     break;
0092                 }
0093             }
0094         }
0095 
0096         if (cachedImg)
0097         {
0098             // image is found in image cache, loading is successful
0099 
0100             m_img = *cachedImg;
0101         }
0102         else
0103         {
0104             // find possible running loading process
0105 
0106             LoadingProcess* usedProcess = nullptr;
0107 
0108             for (QStringList::const_iterator it = lookupKeys.constBegin() ; it != lookupKeys.constEnd() ; ++it)
0109             {
0110                 if ((usedProcess = cache->retrieveLoadingProcess(*it)))
0111                 {
0112                     break;
0113                 }
0114             }
0115 
0116             if (usedProcess)
0117             {
0118                 // Other process is right now loading this image.
0119                 // Add this task to the list of listeners and
0120                 // attach this thread to the other thread, wait until loading
0121                 // has finished.
0122 
0123                 usedProcess->addListener(this);
0124 
0125                 // break loop when either the loading has completed, or this task is being stopped
0126 
0127                 // cppcheck-suppress knownConditionTrueFalse
0128                 while ((m_loadingTaskStatus != LoadingTaskStatusStopping) && !usedProcess->completed())
0129                 {
0130                     lock.timedWait();
0131                 }
0132 
0133                 // remove listener from process
0134 
0135                 usedProcess->removeListener(this);
0136 
0137                 // wake up the process which is waiting until all listeners have removed themselves
0138 
0139                 lock.wakeAll();
0140 
0141                 // m_img is now set to the result
0142             }
0143         }
0144     }
0145 
0146     if (continueQuery() && m_img.isNull())
0147     {
0148         {
0149             LoadingCache::CacheLock lock(cache);
0150 
0151             // Neither in cache, nor currently loading in different thread.
0152             // Load it here and now, add this LoadingProcess to cache list.
0153 
0154             cache->addLoadingProcess(this);
0155 
0156             // Notify other processes that we are now loading this image.
0157             // They might be interested - see notifyNewLoadingProcess below
0158 
0159             cache->notifyNewLoadingProcess(this, m_loadingDescription);
0160         }
0161 
0162         // Preview is not in cache, we will load image from file.
0163 
0164         DImg::FORMAT format      = DImg::fileFormat(m_loadingDescription.filePath);
0165         m_fromRawEmbeddedPreview = false;
0166 
0167         if (format == DImg::RAW)
0168         {
0169             MetaEnginePreviews previews(m_loadingDescription.filePath);
0170 
0171             // Check original image size using Exiv2.
0172 
0173             QSize originalSize  = previews.originalSize();
0174 
0175             // If not valid, get original size from LibRaw
0176 
0177             if (!originalSize.isValid())
0178             {
0179                 DRawInfo container;
0180 
0181                 if (DRawDecoder::rawFileIdentify(container, m_loadingDescription.filePath))
0182                 {
0183                     originalSize = container.imageSize;
0184                 }
0185             }
0186 
0187             switch (m_loadingDescription.previewParameters.previewSettings.quality)
0188             {
0189                 case PreviewSettings::FastPreview:
0190                 case PreviewSettings::FastButLargePreview:
0191                 {
0192                     // Size calculations
0193 
0194                     int sizeLimit = -1;
0195                     int bestSize  = qMax(originalSize.width(), originalSize.height());
0196 
0197                     // for RAWs, the alternative is the half preview, so best size is already originalSize / 2
0198 
0199                     bestSize     /= 2;
0200 
0201                     if (m_loadingDescription.previewParameters.previewSettings.quality == PreviewSettings::FastButLargePreview)
0202                     {
0203                         sizeLimit = qMin(m_loadingDescription.previewParameters.size, bestSize);
0204                     }
0205 
0206                     if (loadExiv2Preview(previews, sizeLimit))
0207                     {
0208                         break;
0209                     }
0210 
0211                     if (loadLibRawPreview(sizeLimit))
0212                     {
0213                         break;
0214                     }
0215 
0216                     loadHalfSizeRaw();
0217                     break;
0218                 }
0219 
0220                 case PreviewSettings::HighQualityPreview:
0221                 {
0222                     switch (m_loadingDescription.previewParameters.previewSettings.rawLoading)
0223                     {
0224                         case PreviewSettings::RawPreviewAutomatic:
0225                         {
0226                             // If we find a preview that is larger than half size (which is what we get from half-size original data), we take it
0227 
0228                             int acceptableSize = qMax(lround(originalSize.width()  * 0.48), lround(originalSize.height() * 0.48));
0229 
0230                             if (loadExiv2Preview(previews, acceptableSize))
0231                             {
0232                                 break;
0233                             }
0234 
0235                             if (loadLibRawPreview(acceptableSize))
0236                             {
0237                                 break;
0238                             }
0239 
0240                             loadHalfSizeRaw();
0241                             break;
0242                         }
0243 
0244                         case PreviewSettings::RawPreviewFromEmbeddedPreview:
0245                         {
0246                             if (loadExiv2Preview(previews))
0247                             {
0248                                 break;
0249                             }
0250 
0251                             if (loadLibRawPreview())
0252                             {
0253                                 break;
0254                             }
0255 
0256                             loadHalfSizeRaw();
0257                             break;
0258                         }
0259 
0260                         case PreviewSettings::RawPreviewFromRawHalfSize:
0261                         {
0262                             loadHalfSizeRaw();
0263                             break;
0264                         }
0265 
0266                         case PreviewSettings::RawPreviewFromRawFullSize:
0267                         {
0268                             loadFullSizeRaw();
0269                             break;
0270                         }
0271                     }
0272                 }
0273             }
0274 
0275             // So far, everything loaded QImage. Convert to DImg.
0276 
0277             convertQImageToDImg();
0278         }
0279         else // Non-RAW images
0280         {
0281             qCDebug(DIGIKAM_GENERAL_LOG) << "Try to get preview from" << m_loadingDescription.filePath;
0282             qCDebug(DIGIKAM_GENERAL_LOG) << "Preview quality: " << m_loadingDescription.previewParameters.previewSettings.quality;
0283 
0284             bool isFast = (m_loadingDescription.previewParameters.previewSettings.quality == PreviewSettings::FastPreview);
0285 
0286             switch (m_loadingDescription.previewParameters.previewSettings.quality)
0287             {
0288                 case PreviewSettings::FastPreview:
0289                 case PreviewSettings::FastButLargePreview:
0290                 {
0291                     if (isFast && loadImagePreview(m_loadingDescription.previewParameters.size))
0292                     {
0293                         convertQImageToDImg();
0294                         break;
0295                     }
0296 
0297                     // Set a hint to try to load a JPEG or PGF with the fast scale-before-decoding method
0298 
0299                     if (isFast)
0300                     {
0301                         m_img.setAttribute(QLatin1String("scaledLoadingSize"), m_loadingDescription.previewParameters.size);
0302                     }
0303 
0304                     m_img.load(m_loadingDescription.filePath, this, m_loadingDescription.rawDecodingSettings);
0305                     break;
0306                 }
0307 
0308                 case PreviewSettings::HighQualityPreview:
0309                 {
0310                     m_img.load(m_loadingDescription.filePath, this, m_loadingDescription.rawDecodingSettings);
0311                     break;
0312                 }
0313             }
0314         }
0315 
0316         if (continueQuery() && !m_img.isNull())
0317         {
0318             if (needToScale())
0319             {
0320                 QSize scaledSize = m_img.size();
0321                 scaledSize.scale(m_loadingDescription.previewParameters.size,
0322                                  m_loadingDescription.previewParameters.size,
0323                                  Qt::KeepAspectRatio);
0324                 m_img = m_img.smoothScale(scaledSize.width(), scaledSize.height());
0325             }
0326 
0327             if (MetaEngineSettings::instance()->settings().exifRotate)
0328             {
0329                 m_img.exifRotate(m_loadingDescription.filePath);
0330             }
0331 
0332             if (needsPostProcessing())
0333             {
0334                 postProcess();
0335             }
0336         }
0337 
0338         {
0339             LoadingCache::CacheLock lock(cache);
0340 
0341             // remove this from the list of loading processes in cache
0342 
0343             cache->removeLoadingProcess(this);
0344 
0345             // put valid image into cache of loaded images
0346 
0347             if (continueQuery() && !m_img.isNull())
0348             {
0349                 cache->putImage(m_loadingDescription.cacheKey(), m_img,
0350                                 m_loadingDescription.filePath);
0351 
0352                 // dispatch image to all listeners
0353 
0354                 for (int i = 0 ; i < m_listeners.count() ; ++i)
0355                 {
0356                     LoadingProcessListener* const l = m_listeners.at(i);
0357 
0358                     if (l->accessMode() == LoadSaveThread::AccessModeReadWrite)
0359                     {
0360                         // If a listener requested ReadWrite access, it gets a deep copy.
0361                         // DImg is explicitly shared.
0362 
0363                         l->setResult(m_loadingDescription, m_img.copy());
0364                     }
0365                     else
0366                     {
0367                         l->setResult(m_loadingDescription, m_img);
0368                     }
0369                 }
0370             }
0371 
0372             // indicate that loading has finished so that listeners can stop waiting
0373 
0374             m_completed = true;
0375 
0376             // wake all listeners waiting on cache condVar, so that they remove themselves
0377 
0378             lock.wakeAll();
0379 
0380             // wait until all listeners have removed themselves
0381 
0382             while (m_listeners.count() != 0)
0383             {
0384                 lock.timedWait();
0385             }
0386         }
0387     }
0388 
0389     // following the golden rule to avoid deadlocks, do this when CacheLock is not held
0390 
0391     if (continueQuery() && !m_img.isNull())
0392     {
0393         // We check before to find out if we need to provide a deep copy
0394 
0395         const bool needConvertToEightBit = m_loadingDescription.previewParameters.previewSettings.convertToEightBit;
0396 
0397         if ((accessMode() == LoadSaveThread::AccessModeReadWrite) || needConvertToEightBit)
0398         {
0399             m_img.detach();
0400         }
0401 
0402         if (needConvertToEightBit)
0403         {
0404             m_img.convertToEightBit();
0405         }
0406     }
0407     else if (continueQuery())
0408     {
0409         qCDebug(DIGIKAM_GENERAL_LOG) << "Cannot extract preview for" << m_loadingDescription.filePath;
0410     }
0411     else
0412     {
0413         m_img = DImg();
0414     }
0415 
0416     if (m_thread)
0417     {
0418         m_thread->taskHasFinished();
0419         m_thread->imageLoaded(m_loadingDescription, m_img);
0420     }
0421 }
0422 
0423 bool PreviewLoadingTask::needToScale()
0424 {
0425     switch (m_loadingDescription.previewParameters.previewSettings.quality)
0426     {
0427         case PreviewSettings::FastPreview:
0428         {
0429             if (m_loadingDescription.previewParameters.size > 0)
0430             {
0431                 int maxSize             = qMax(m_img.width(), m_img.height());
0432                 int acceptableUpperSize = lround(1.25 * (double)m_loadingDescription.previewParameters.size);
0433                 return (maxSize >= acceptableUpperSize);
0434             }
0435 
0436             break;
0437         }
0438 
0439         case PreviewSettings::FastButLargePreview:
0440         case PreviewSettings::HighQualityPreview:
0441         {
0442             break;
0443         }
0444     }
0445 
0446     return false;
0447 }
0448 
0449 // -- Exif/IPTC preview extraction using Exiv2 --------------------------------------------------------
0450 
0451 bool PreviewLoadingTask::loadExiv2Preview(MetaEnginePreviews& previews, int sizeLimit)
0452 {
0453     if (previews.isEmpty() || !continueQuery())
0454     {
0455         return false;
0456     }
0457 
0458     if ((sizeLimit == -1) || (qMax(previews.width(), previews.height()) >= sizeLimit))
0459     {
0460         m_qimage = previews.image();
0461 
0462         if (!m_qimage.isNull())
0463         {
0464             m_fromRawEmbeddedPreview = true;
0465             return true;
0466         }
0467     }
0468 
0469     return false;
0470 }
0471 
0472 bool PreviewLoadingTask::loadLibRawPreview(int sizeLimit)
0473 {
0474     if (!continueQuery())
0475     {
0476         return false;
0477     }
0478 
0479     QImage rawPreview;
0480     DRawDecoder::loadEmbeddedPreview(rawPreview, m_loadingDescription.filePath);
0481 
0482     if (!rawPreview.isNull() &&
0483         ((sizeLimit == -1) || (qMax(rawPreview.width(), rawPreview.height()) >= sizeLimit)))
0484     {
0485         m_qimage                 = rawPreview;
0486         m_fromRawEmbeddedPreview = true;
0487         return true;
0488     }
0489 
0490     return false;
0491 }
0492 
0493 bool PreviewLoadingTask::loadHalfSizeRaw()
0494 {
0495     if (!continueQuery())
0496     {
0497         return false;
0498     }
0499 
0500     DRawDecoder::loadHalfPreview(m_qimage, m_loadingDescription.filePath);
0501 
0502     return (!m_qimage.isNull());
0503 }
0504 
0505 bool PreviewLoadingTask::loadFullSizeRaw()
0506 {
0507     if (!continueQuery())
0508     {
0509         return false;
0510     }
0511 
0512     DRawDecoder::loadFullImage(m_qimage, m_loadingDescription.filePath);
0513 
0514     return (!m_qimage.isNull());
0515 }
0516 
0517 void PreviewLoadingTask::convertQImageToDImg()
0518 {
0519     if (!continueQuery())
0520     {
0521         return;
0522     }
0523 
0524     // convert from QImage
0525 
0526     m_img               = DImg(m_qimage);
0527     DImg::FORMAT format = DImg::fileFormat(m_loadingDescription.filePath);
0528 
0529     m_img.setAttribute(QLatin1String("detectedFileFormat"), format);
0530     m_img.setAttribute(QLatin1String("originalFilePath"),   m_loadingDescription.filePath);
0531 
0532     QScopedPointer<DMetadata> metadata(new DMetadata(m_loadingDescription.filePath));
0533     QSize orgSize = metadata->getPixelSize();
0534 
0535     if ((format == DImg::RAW) && LoadSaveThread::infoProvider())
0536     {
0537         orgSize = LoadSaveThread::infoProvider()->dimensionsHint(m_loadingDescription.filePath);
0538     }
0539 
0540     // In case we don't get the original size from the metadata.
0541 
0542     if (orgSize.isNull())
0543     {
0544         orgSize = QSize(m_img.width(), m_img.height());
0545     }
0546 
0547     // Set the ratio of width and height of the
0548     // original size to the same ratio of the loaded image.
0549     // Because a half RAW preview was probably already rotated.
0550 
0551     if ((format == DImg::RAW) && !m_fromRawEmbeddedPreview)
0552     {
0553         if (((m_img.width() < m_img.height()) && (orgSize.width() > orgSize.height())) ||
0554             ((m_img.width() > m_img.height()) && (orgSize.width() < orgSize.height())))
0555         {
0556             orgSize.transpose();
0557         }
0558     }
0559 
0560     m_img.setAttribute(QLatin1String("originalSize"),   orgSize);
0561 
0562     m_img.setMetadata(metadata->data());
0563 
0564     // mark as embedded preview (for Exif rotation)
0565 
0566     if (m_fromRawEmbeddedPreview)
0567     {
0568         m_img.setAttribute(QLatin1String("fromRawEmbeddedPreview"), true);
0569 
0570         // If we loaded the embedded preview, the Exif of the RAW indicates
0571         // the color space of the preview (see bug 195950 for NEF files)
0572 
0573         m_img.setIccProfile(metadata->getIccProfile());
0574     }
0575 
0576     // free memory
0577 
0578     m_qimage = QImage();
0579 }
0580 
0581 bool PreviewLoadingTask::loadImagePreview(int sizeLimit)
0582 {
0583     QScopedPointer<DMetadata> metadata(new DMetadata(m_loadingDescription.filePath));
0584 
0585     QImage previewImage;
0586 
0587     if (metadata->getItemPreview(previewImage))
0588     {
0589         if ((sizeLimit == -1) || (qMax(previewImage.width(), previewImage.height()) > sizeLimit))
0590         {
0591             m_qimage = previewImage;
0592             return true;
0593         }
0594     }
0595 
0596     qDebug(DIGIKAM_GENERAL_LOG) << "Try to load DImg preview from:" << m_loadingDescription.filePath;
0597 
0598     DImg img;
0599     DImgLoader::LoadFlags loadFlags = DImgLoader::LoadItemInfo |
0600                                       DImgLoader::LoadMetadata |
0601                                       DImgLoader::LoadICCData  |
0602                                       DImgLoader::LoadPreview;
0603 
0604     if (img.load(m_loadingDescription.filePath, loadFlags, this))
0605     {
0606         if ((sizeLimit == -1) || (qMax(img.width(), img.height()) > (uint)sizeLimit))
0607         {
0608             m_qimage = img.copyQImage();
0609             return true;
0610         }
0611     }
0612 
0613     return false;
0614 }
0615 
0616 } // namespace Digikam