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