File indexing completed on 2025-03-09 03:50:51
0001 /* ============================================================ 0002 * 0003 * This file is a part of digiKam project 0004 * https://www.digikam.org 0005 * 0006 * Date : 2009-11-13 0007 * Description : a tool to blend bracketed images. 0008 * 0009 * SPDX-FileCopyrightText: 2009-2024 by Gilles Caulier <caulier dot gilles at gmail dot com> 0010 * SPDX-FileCopyrightText: 2009-2011 by Johannes Wienke <languitar at semipol dot de> 0011 * SPDX-FileCopyrightText: 2012-2015 by Benjamin Girault <benjamin dot girault at gmail dot com> 0012 * 0013 * SPDX-License-Identifier: GPL-2.0-or-later 0014 * 0015 * ============================================================ */ 0016 0017 #include "expoblendingthread.h" 0018 0019 // C++ includes 0020 0021 #include <cmath> 0022 0023 // Under Win32, log2f is not defined... 0024 #ifdef Q_OS_WIN32 0025 # define log2f(x) (logf(x)*1.4426950408889634f) 0026 #endif 0027 0028 #ifdef Q_OS_FREEBSD 0029 #include <osreldate.h> 0030 # if __FreeBSD_version < 802502 0031 # define log2f(x) (logf(x)*1.4426950408889634f) 0032 # endif 0033 #endif 0034 0035 // Qt includes 0036 0037 #include <QPair> 0038 #include <QMutex> 0039 #include <QMutexLocker> 0040 #include <QWaitCondition> 0041 #include <QDateTime> 0042 #include <QFileInfo> 0043 #include <QPointer> 0044 #include <QFuture> 0045 #include <QtConcurrent> // krazy:exclude=includes 0046 #include <QTemporaryDir> 0047 #include <QProcess> 0048 0049 // KDE includes 0050 0051 #include <klocalizedstring.h> 0052 #include <ksharedconfig.h> 0053 #include <kconfiggroup.h> 0054 0055 // Local includes 0056 0057 #include "digikam_version.h" 0058 #include "digikam_debug.h" 0059 #include "digikam_globals.h" 0060 #include "drawdecoder.h" 0061 #include "dimg.h" 0062 #include "dimgloaderobserver.h" 0063 #include "drawdecoderwidget.h" 0064 #include "drawdecoding.h" 0065 0066 namespace DigikamGenericExpoBlendingPlugin 0067 { 0068 0069 class RawObserver; 0070 0071 class Q_DECL_HIDDEN ExpoBlendingThread::Private 0072 { 0073 public: 0074 0075 explicit Private() 0076 : cancel (false), 0077 align (false), 0078 enfuseVersion4x (true), 0079 rawObserver (nullptr) 0080 { 0081 } 0082 0083 struct Task 0084 { 0085 bool align; 0086 QList<QUrl> urls; 0087 QUrl outputUrl; 0088 QString binaryPath; 0089 ExpoBlendingAction action; 0090 EnfuseSettings enfuseSettings; 0091 }; 0092 0093 volatile bool cancel; 0094 bool align; 0095 bool enfuseVersion4x; 0096 0097 QMutex mutex; 0098 QMutex lock; 0099 0100 QWaitCondition condVar; 0101 0102 QList<Task*> todo; 0103 0104 QSharedPointer<QTemporaryDir> preprocessingTmpDir; 0105 QSharedPointer<QProcess> enfuseProcess; 0106 QSharedPointer<QProcess> alignProcess; 0107 0108 RawObserver* rawObserver; 0109 0110 /** 0111 * List of results files produced by enfuse that may need cleaning. 0112 * Only access this through the provided mutex. 0113 */ 0114 QList<QUrl> enfuseTmpUrls; 0115 QMutex enfuseTmpUrlsMutex; 0116 0117 /// Preprocessing 0118 QList<QUrl> mixedUrls; ///< Original non-RAW + Raw converted urls to align. 0119 ExpoBlendingItemUrlsMap preProcessedUrlsMap; 0120 0121 MetaEngine meta; 0122 }; 0123 0124 class Q_DECL_HIDDEN RawObserver : public DImgLoaderObserver 0125 { 0126 public: 0127 0128 explicit RawObserver(ExpoBlendingThread::Private* const priv) 0129 : DImgLoaderObserver(), 0130 d (priv) 0131 { 0132 } 0133 0134 ~RawObserver() override 0135 { 0136 } 0137 0138 bool continueQuery() override 0139 { 0140 return (!d->cancel); 0141 } 0142 0143 private: 0144 0145 ExpoBlendingThread::Private* const d; 0146 }; 0147 0148 ExpoBlendingThread::ExpoBlendingThread(QObject* const parent) 0149 : QThread(parent), 0150 d (new Private) 0151 { 0152 d->rawObserver = new RawObserver(d); 0153 qRegisterMetaType<ExpoBlendingActionData>("ExpoBlendingActionData"); 0154 } 0155 0156 ExpoBlendingThread::~ExpoBlendingThread() 0157 { 0158 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "ExpoBlendingThread shutting down." 0159 << "Canceling all actions and waiting for them"; 0160 0161 // cancel the thread 0162 0163 cancel(); 0164 0165 // wait for the thread to finish 0166 0167 wait(); 0168 0169 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Thread finished"; 0170 0171 cleanUpResultFiles(); 0172 0173 delete d; 0174 } 0175 0176 void ExpoBlendingThread::setEnfuseVersion(const double version) 0177 { 0178 d->enfuseVersion4x = (version >= 4.0); 0179 } 0180 0181 void ExpoBlendingThread::cleanUpResultFiles() 0182 { 0183 // Cleanup all tmp files created by Enfuse process. 0184 0185 QMutexLocker locker(&d->enfuseTmpUrlsMutex); 0186 0187 Q_FOREACH (const QUrl& url, d->enfuseTmpUrls) 0188 { 0189 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Removing temp file" << url.toLocalFile(); 0190 QFile(url.toLocalFile()).remove(); 0191 } 0192 0193 d->enfuseTmpUrls.clear(); 0194 } 0195 0196 void ExpoBlendingThread::setPreProcessingSettings(bool align) 0197 { 0198 d->align = align; 0199 } 0200 0201 void ExpoBlendingThread::identifyFiles(const QList<QUrl>& urlList) 0202 { 0203 Q_FOREACH (const QUrl& url, urlList) 0204 { 0205 Private::Task* const t = new Private::Task; 0206 t->action = EXPOBLENDING_IDENTIFY; 0207 t->urls.append(url); 0208 0209 QMutexLocker lock(&d->mutex); 0210 d->todo << t; 0211 d->condVar.wakeAll(); 0212 } 0213 } 0214 0215 void ExpoBlendingThread::loadProcessed(const QUrl& url) 0216 { 0217 Private::Task* const t = new Private::Task; 0218 t->action = EXPOBLENDING_LOAD; 0219 t->urls.append(url); 0220 0221 QMutexLocker lock(&d->mutex); 0222 d->todo << t; 0223 d->condVar.wakeAll(); 0224 } 0225 0226 void ExpoBlendingThread::preProcessFiles(const QList<QUrl>& urlList, const QString& alignPath) 0227 { 0228 Private::Task* const t = new Private::Task; 0229 t->action = EXPOBLENDING_PREPROCESSING; 0230 t->urls = urlList; 0231 t->align = d->align; 0232 t->binaryPath = alignPath; 0233 0234 QMutexLocker lock(&d->mutex); 0235 d->todo << t; 0236 d->condVar.wakeAll(); 0237 } 0238 0239 void ExpoBlendingThread::enfusePreview(const QList<QUrl>& alignedUrls, const QUrl& outputUrl, 0240 const EnfuseSettings& settings, const QString& enfusePath) 0241 { 0242 Private::Task* const t = new Private::Task; 0243 t->action = EXPOBLENDING_ENFUSEPREVIEW; 0244 t->urls = alignedUrls; 0245 t->outputUrl = outputUrl; 0246 t->enfuseSettings = settings; 0247 t->binaryPath = enfusePath; 0248 0249 QMutexLocker lock(&d->mutex); 0250 d->todo << t; 0251 d->condVar.wakeAll(); 0252 } 0253 0254 void ExpoBlendingThread::enfuseFinal(const QList<QUrl>& alignedUrls, const QUrl& outputUrl, 0255 const EnfuseSettings& settings, const QString& enfusePath) 0256 { 0257 Private::Task* const t = new Private::Task; 0258 t->action = EXPOBLENDING_ENFUSEFINAL; 0259 t->urls = alignedUrls; 0260 t->outputUrl = outputUrl; 0261 t->enfuseSettings = settings; 0262 t->binaryPath = enfusePath; 0263 0264 QMutexLocker lock(&d->mutex); 0265 d->todo << t; 0266 d->condVar.wakeAll(); 0267 } 0268 0269 void ExpoBlendingThread::cancel() 0270 { 0271 QMutexLocker lock(&d->mutex); 0272 d->todo.clear(); 0273 d->cancel = true; 0274 0275 if (d->enfuseProcess) 0276 { 0277 d->enfuseProcess->kill(); 0278 } 0279 0280 if (d->alignProcess) 0281 { 0282 d->alignProcess->kill(); 0283 } 0284 0285 d->condVar.wakeAll(); 0286 } 0287 0288 void ExpoBlendingThread::run() 0289 { 0290 d->cancel = false; 0291 0292 while (!d->cancel) 0293 { 0294 Private::Task* t = nullptr; 0295 { 0296 QMutexLocker lock(&d->mutex); 0297 0298 if (!d->todo.isEmpty()) 0299 t = d->todo.takeFirst(); 0300 else 0301 d->condVar.wait(&d->mutex); 0302 } 0303 0304 if (t) 0305 { 0306 switch (t->action) 0307 { 0308 case EXPOBLENDING_IDENTIFY: 0309 { 0310 // Identify Exposure. 0311 0312 QString avLum; 0313 0314 if (!t->urls.isEmpty()) 0315 { 0316 float val = getAverageSceneLuminance(t->urls[0]); 0317 0318 if (val != -1) 0319 { 0320 avLum.setNum(log2f(val), 'g', 2); 0321 } 0322 } 0323 0324 ExpoBlendingActionData ad; 0325 ad.action = t->action; 0326 ad.inUrls = t->urls; 0327 ad.message = avLum.isEmpty() ? i18nc("average scene luminance value unknown", "unknown") : avLum; 0328 ad.success = avLum.isEmpty(); 0329 Q_EMIT finished(ad); 0330 break; 0331 } 0332 0333 case EXPOBLENDING_PREPROCESSING: 0334 { 0335 ExpoBlendingActionData ad1; 0336 ad1.action = EXPOBLENDING_PREPROCESSING; 0337 ad1.inUrls = t->urls; 0338 ad1.starting = true; 0339 Q_EMIT starting(ad1); 0340 0341 QString errors; 0342 0343 bool result = startPreProcessing(t->urls, t->align, t->binaryPath, errors); 0344 0345 ExpoBlendingActionData ad2; 0346 ad2.action = EXPOBLENDING_PREPROCESSING; 0347 ad2.inUrls = t->urls; 0348 ad2.preProcessedUrlsMap = d->preProcessedUrlsMap; 0349 ad2.success = result; 0350 ad2.message = errors; 0351 Q_EMIT finished(ad2); 0352 break; 0353 } 0354 0355 case EXPOBLENDING_LOAD: 0356 { 0357 ExpoBlendingActionData ad1; 0358 ad1.action = EXPOBLENDING_LOAD; 0359 ad1.inUrls = t->urls; 0360 ad1.starting = true; 0361 Q_EMIT starting(ad1); 0362 0363 QImage image; 0364 bool result = image.load(t->urls[0].toLocalFile()); 0365 0366 // rotate image 0367 0368 if (result) 0369 { 0370 if (d->meta.load(t->urls[0].toLocalFile())) 0371 d->meta.rotateExifQImage(image, d->meta.getItemOrientation()); 0372 } 0373 0374 ExpoBlendingActionData ad2; 0375 ad2.action = EXPOBLENDING_LOAD; 0376 ad2.inUrls = t->urls; 0377 ad2.success = result; 0378 ad2.image = image; 0379 Q_EMIT finished(ad2); 0380 break; 0381 } 0382 0383 case EXPOBLENDING_ENFUSEPREVIEW: 0384 { 0385 ExpoBlendingActionData ad1; 0386 ad1.action = EXPOBLENDING_ENFUSEPREVIEW; 0387 ad1.inUrls = t->urls; 0388 ad1.starting = true; 0389 ad1.enfuseSettings = t->enfuseSettings; 0390 Q_EMIT starting(ad1); 0391 0392 QString errors; 0393 QUrl destUrl = t->outputUrl; 0394 EnfuseSettings settings = t->enfuseSettings; 0395 settings.outputFormat = DSaveSettingsWidget::OUTPUT_JPEG; // JPEG for preview: fast and small. 0396 bool result = startEnfuse(t->urls, destUrl, settings, t->binaryPath, errors); 0397 0398 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Preview result was:" << result; 0399 0400 // preserve exif information for auto rotation 0401 0402 if (result) 0403 { 0404 if (d->meta.load(t->urls[0].toLocalFile())) 0405 { 0406 MetaEngine::ImageOrientation orientation = d->meta.getItemOrientation(); 0407 0408 if (d->meta.load(destUrl.toLocalFile())) 0409 { 0410 d->meta.setItemOrientation(orientation); 0411 d->meta.applyChanges(true); 0412 } 0413 } 0414 } 0415 0416 // To be cleaned in destructor. 0417 0418 QMutexLocker locker(&d->enfuseTmpUrlsMutex); 0419 d->enfuseTmpUrls << destUrl; 0420 0421 ExpoBlendingActionData ad2; 0422 ad2.action = EXPOBLENDING_ENFUSEPREVIEW; 0423 ad2.inUrls = t->urls; 0424 ad2.outUrls = QList<QUrl>() << destUrl; 0425 ad2.success = result; 0426 ad2.message = errors; 0427 ad2.enfuseSettings = t->enfuseSettings; 0428 Q_EMIT finished(ad2); 0429 break; 0430 } 0431 0432 case EXPOBLENDING_ENFUSEFINAL: 0433 { 0434 ExpoBlendingActionData ad1; 0435 ad1.action = EXPOBLENDING_ENFUSEFINAL; 0436 ad1.inUrls = t->urls; 0437 ad1.starting = true; 0438 ad1.enfuseSettings = t->enfuseSettings; 0439 Q_EMIT starting(ad1); 0440 0441 QString errors; 0442 QUrl destUrl = t->outputUrl; 0443 bool result = startEnfuse(t->urls, destUrl, t->enfuseSettings, t->binaryPath, errors); 0444 0445 // We will take first image metadata from stack to restore Exif, Iptc, and Xmp. 0446 0447 if (d->meta.load(t->urls[0].toLocalFile())) 0448 { 0449 result = result & d->meta.setXmpTagString("Xmp.digiKam.EnfuseInputFiles", 0450 t->enfuseSettings.inputImagesList()); 0451 0452 result = result & d->meta.setXmpTagString("Xmp.digiKam.EnfuseSettings", 0453 t->enfuseSettings.asCommentString(). 0454 replace(QLatin1Char('\n'), 0455 QLatin1String(" ; "))); 0456 0457 d->meta.setImageDateTime(QDateTime::currentDateTime()); 0458 0459 if (t->enfuseSettings.outputFormat != DSaveSettingsWidget::OUTPUT_JPEG) 0460 { 0461 QImage img; 0462 0463 if (img.load(destUrl.toLocalFile())) 0464 { 0465 d->meta.setItemPreview(img.scaled(1280, 1024, Qt::KeepAspectRatio)); 0466 } 0467 } 0468 0469 d->meta.save(destUrl.toLocalFile(), true); 0470 } 0471 0472 // To be cleaned in destructor. 0473 0474 QMutexLocker locker(&d->enfuseTmpUrlsMutex); 0475 d->enfuseTmpUrls << destUrl; 0476 0477 ExpoBlendingActionData ad2; 0478 ad2.action = EXPOBLENDING_ENFUSEFINAL; 0479 ad2.inUrls = t->urls; 0480 ad2.outUrls = QList<QUrl>() << destUrl; 0481 ad2.success = result; 0482 ad2.message = errors; 0483 ad2.enfuseSettings = t->enfuseSettings; 0484 Q_EMIT finished(ad2); 0485 break; 0486 } 0487 0488 default: 0489 { 0490 qCritical(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Unknown action specified" << QT_ENDL; 0491 break; 0492 } 0493 } 0494 } 0495 0496 delete t; 0497 } 0498 } 0499 0500 bool ExpoBlendingThread::preProcessingMultithreaded(const QUrl& url) 0501 { 0502 bool error = false; 0503 0504 // check if we have RAW or HEIF file -> use preview image then 0505 0506 QString ext = QFileInfo(url.toLocalFile()).suffix().toUpper(); 0507 0508 if ( 0509 DRawDecoder::isRawFile(url) || 0510 (ext == QLatin1String("HIF")) || 0511 (ext == QLatin1String("HEIC")) || 0512 (ext == QLatin1String("HEIF")) 0513 ) 0514 { 0515 QUrl preprocessedUrl, previewUrl; 0516 0517 if (!convertRaw(url, preprocessedUrl)) 0518 { 0519 error = true; 0520 return error; 0521 } 0522 0523 if (!computePreview(preprocessedUrl, previewUrl)) 0524 { 0525 error = true; 0526 return error; 0527 } 0528 0529 d->lock.lock(); 0530 d->mixedUrls.append(preprocessedUrl); 0531 0532 // In case of alignment is not performed. 0533 0534 d->preProcessedUrlsMap.insert(url, ExpoBlendingItemPreprocessedUrls(preprocessedUrl, previewUrl)); 0535 d->lock.unlock(); 0536 } 0537 else 0538 { 0539 // NOTE: in this case, preprocessed Url is original file Url. 0540 0541 QUrl previewUrl; 0542 0543 if (!computePreview(url, previewUrl)) 0544 { 0545 error = true; 0546 return error; 0547 } 0548 0549 d->lock.lock(); 0550 d->mixedUrls.append(url); 0551 0552 // In case of alignment is not performed. 0553 0554 d->preProcessedUrlsMap.insert(url, ExpoBlendingItemPreprocessedUrls(url, previewUrl)); 0555 d->lock.unlock(); 0556 } 0557 0558 return error; 0559 } 0560 0561 bool ExpoBlendingThread::startPreProcessing(const QList<QUrl>& inUrls, 0562 bool align, 0563 const QString& alignPath, QString& errors) 0564 { 0565 QString prefix = QDir::tempPath() + QLatin1Char('/') + 0566 QLatin1String("digiKam-expoblending-tmp-XXXXXX"); 0567 0568 d->preprocessingTmpDir = QSharedPointer<QTemporaryDir>(new QTemporaryDir(prefix)); 0569 0570 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Expoblending temp dir:" 0571 << d->preprocessingTmpDir->path(); 0572 0573 // Parallelized pre-process RAW files if necessary. 0574 0575 d->mixedUrls.clear(); 0576 d->preProcessedUrlsMap.clear(); 0577 0578 bool error = false; 0579 0580 QList <QFuture<bool> > tasks; 0581 0582 for (int i = 0 ; i < inUrls.size() ; ++i) 0583 { 0584 QUrl url = inUrls.at(i); 0585 0586 tasks.append(QtConcurrent::run( 0587 0588 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) 0589 0590 &ExpoBlendingThread::preProcessingMultithreaded, this, 0591 0592 #else 0593 0594 this, &ExpoBlendingThread::preProcessingMultithreaded, 0595 0596 #endif 0597 0598 url 0599 ) 0600 ); 0601 } 0602 0603 for (QFuture<bool>& t: tasks) 0604 { 0605 t.waitForFinished(); 0606 0607 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) 0608 0609 error |= t.takeResult(); 0610 0611 #else 0612 0613 error |= t.result(); 0614 0615 #endif 0616 0617 } 0618 0619 if (error) 0620 { 0621 return false; 0622 } 0623 0624 if (align) 0625 { 0626 // Re-align images 0627 0628 d->alignProcess.reset(new QProcess()); 0629 d->alignProcess->setProcessChannelMode(QProcess::MergedChannels); 0630 d->alignProcess->setWorkingDirectory(d->preprocessingTmpDir->path()); 0631 0632 QProcessEnvironment env = adjustedEnvironmentForAppImage(); 0633 env.insert(QLatin1String("OMP_NUM_THREADS"), 0634 QString::number(QThread::idealThreadCount())); 0635 d->alignProcess->setProcessEnvironment(env); 0636 0637 QStringList args; 0638 args << QLatin1String("-v"); 0639 args << QLatin1String("-c"); 0640 args << QLatin1String("16"); 0641 args << QLatin1String("-a"); 0642 args << QLatin1String("aligned"); 0643 0644 Q_FOREACH (const QUrl& url, d->mixedUrls) 0645 { 0646 args << url.toLocalFile(); 0647 } 0648 0649 d->alignProcess->setProgram(alignPath); 0650 d->alignProcess->setArguments(args); 0651 0652 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Align command line:" << d->alignProcess->program(); 0653 0654 d->alignProcess->start(); 0655 0656 if (!d->alignProcess->waitForFinished(-1)) 0657 { 0658 errors = getProcessError(*(d->alignProcess)); 0659 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "align_image_stack error:" << errors; 0660 return false; 0661 } 0662 0663 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Align exit status: " << d->alignProcess->exitStatus(); 0664 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Align exit code: " << d->alignProcess->exitCode(); 0665 0666 if (d->alignProcess->exitStatus() != QProcess::NormalExit) 0667 { 0668 return false; 0669 } 0670 0671 if (d->alignProcess->exitCode() != 0) 0672 { 0673 errors = getProcessError(*(d->alignProcess)); 0674 return false; 0675 } 0676 0677 uint i = 0; 0678 QString temp; 0679 d->preProcessedUrlsMap.clear(); 0680 0681 Q_FOREACH (const QUrl& url, inUrls) 0682 { 0683 QUrl previewUrl; 0684 QUrl alignedUrl = QUrl::fromLocalFile(d->preprocessingTmpDir->path() + 0685 QLatin1Char('/') + 0686 QLatin1String("aligned") + 0687 QString::number(i).rightJustified(4, QLatin1Char('0')) + 0688 QLatin1String(".tif")); 0689 0690 if (!computePreview(alignedUrl, previewUrl)) 0691 { 0692 return false; 0693 } 0694 0695 d->preProcessedUrlsMap.insert(url, ExpoBlendingItemPreprocessedUrls(alignedUrl, previewUrl)); 0696 ++i; 0697 } 0698 0699 Q_FOREACH (const QUrl& inputUrl, d->preProcessedUrlsMap.keys()) 0700 { 0701 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Pre-processed output urls map:" 0702 << inputUrl << "=>" 0703 << d->preProcessedUrlsMap[inputUrl].preprocessedUrl << "," 0704 << d->preProcessedUrlsMap[inputUrl].previewUrl; 0705 } 0706 0707 return true; 0708 } 0709 else 0710 { 0711 Q_FOREACH (const QUrl& inputUrl, d->preProcessedUrlsMap.keys()) 0712 { 0713 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Pre-processed output urls map:" 0714 << inputUrl << "=>" 0715 << d->preProcessedUrlsMap[inputUrl].preprocessedUrl << "," 0716 << d->preProcessedUrlsMap[inputUrl].previewUrl; 0717 } 0718 0719 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Alignment not performed."; 0720 return true; 0721 } 0722 } 0723 0724 bool ExpoBlendingThread::computePreview(const QUrl& inUrl, QUrl& outUrl) 0725 { 0726 outUrl = QUrl::fromLocalFile(d->preprocessingTmpDir->path() + 0727 QLatin1Char('/') + 0728 QLatin1Char('.') + 0729 inUrl.fileName().replace(QLatin1Char('.'), QLatin1Char('_')) + 0730 QLatin1String("-preview.jpg")); 0731 0732 DImg img; 0733 0734 if (img.load(inUrl.toLocalFile())) 0735 { 0736 DImg preview = img.smoothScale(1280, 1024, Qt::KeepAspectRatio); 0737 bool saved = preview.save(outUrl.toLocalFile(), QLatin1String("JPG")); 0738 0739 // save exif information also to preview image for auto rotation 0740 0741 if (saved) 0742 { 0743 if (d->meta.load(inUrl.toLocalFile())) 0744 { 0745 MetaEngine::ImageOrientation orientation = d->meta.getItemOrientation(); 0746 0747 if (d->meta.load(outUrl.toLocalFile())) 0748 { 0749 d->meta.setItemOrientation(orientation); 0750 d->meta.applyChanges(true); 0751 } 0752 } 0753 } 0754 0755 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Preview Image url:" << outUrl << ", saved:" << saved; 0756 return saved; 0757 } 0758 0759 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Input image not loaded:" << inUrl; 0760 0761 return false; 0762 } 0763 0764 bool ExpoBlendingThread::convertRaw(const QUrl& inUrl, QUrl& outUrl) 0765 { 0766 DImg img; 0767 0768 DRawDecoding settings; 0769 KSharedConfig::Ptr config = KSharedConfig::openConfig(); 0770 KConfigGroup group = config->group(QLatin1String("ImageViewer Settings")); 0771 DRawDecoderWidget::readSettings(settings.rawPrm, group); 0772 0773 if (img.load(inUrl.toLocalFile(), d->rawObserver, settings)) 0774 { 0775 QFileInfo fi(inUrl.toLocalFile()); 0776 outUrl = QUrl::fromLocalFile(d->preprocessingTmpDir->path() + 0777 QLatin1Char('/') + 0778 QLatin1Char('.') + 0779 fi.completeBaseName().replace(QLatin1Char('.'), QLatin1Char('_')) + 0780 QLatin1String(".tif")); 0781 0782 if (!img.save(outUrl.toLocalFile(), QLatin1String("TIF"))) 0783 { 0784 return false; 0785 } 0786 0787 if (d->meta.load(outUrl.toLocalFile())) 0788 { 0789 d->meta.setItemDimensions(img.size()); 0790 d->meta.setExifTagString("Exif.Image.DocumentName", inUrl.fileName()); 0791 d->meta.setXmpTagString("Xmp.tiff.Make", d->meta.getExifTagString("Exif.Image.Make")); 0792 d->meta.setXmpTagString("Xmp.tiff.Model", d->meta.getExifTagString("Exif.Image.Model")); 0793 d->meta.setItemOrientation(MetaEngine::ORIENTATION_NORMAL); 0794 d->meta.applyChanges(true); 0795 } 0796 } 0797 else 0798 { 0799 return false; 0800 } 0801 0802 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Convert RAW output url:" << outUrl; 0803 0804 return true; 0805 } 0806 0807 bool ExpoBlendingThread::startEnfuse(const QList<QUrl>& inUrls, QUrl& outUrl, 0808 const EnfuseSettings& settings, 0809 const QString& enfusePath, QString& errors) 0810 { 0811 QString comp; 0812 QString ext = DSaveSettingsWidget::extensionForFormat(settings.outputFormat); 0813 0814 if (ext == QLatin1String(".tif")) 0815 { 0816 comp = QLatin1String("--compression=DEFLATE"); 0817 } 0818 0819 outUrl.setPath(outUrl.adjusted(QUrl::RemoveFilename).path() + 0820 QLatin1String(".digiKam-expoblending-tmp-") + 0821 QString::number(QDateTime::currentDateTime().toSecsSinceEpoch()) + ext); 0822 0823 d->enfuseProcess.reset(new QProcess()); 0824 d->enfuseProcess->setProcessChannelMode(QProcess::MergedChannels); 0825 d->enfuseProcess->setWorkingDirectory(d->preprocessingTmpDir->path()); 0826 0827 QProcessEnvironment env = adjustedEnvironmentForAppImage(); 0828 env.insert(QLatin1String("OMP_NUM_THREADS"), 0829 QString::number(QThread::idealThreadCount())); 0830 d->enfuseProcess->setProcessEnvironment(env); 0831 0832 QStringList args; 0833 0834 if (!settings.autoLevels) 0835 { 0836 args << QLatin1String("-l"); 0837 args << QString::number(settings.levels); 0838 } 0839 0840 if (settings.ciecam02) 0841 { 0842 args << QLatin1String("-c"); 0843 } 0844 0845 if (!comp.isEmpty()) 0846 { 0847 args << comp; 0848 } 0849 0850 if (settings.hardMask) 0851 { 0852 if (d->enfuseVersion4x) 0853 { 0854 args << QLatin1String("--hard-mask"); 0855 } 0856 else 0857 { 0858 args << QLatin1String("--HardMask"); 0859 } 0860 } 0861 0862 if (d->enfuseVersion4x) 0863 { 0864 args << QString::fromUtf8("--exposure-weight=%1").arg(settings.exposure); 0865 args << QString::fromUtf8("--saturation-weight=%1").arg(settings.saturation); 0866 args << QString::fromUtf8("--contrast-weight=%1").arg(settings.contrast); 0867 } 0868 else 0869 { 0870 args << QString::fromUtf8("--wExposure=%1").arg(settings.exposure); 0871 args << QString::fromUtf8("--wSaturation=%1").arg(settings.saturation); 0872 args << QString::fromUtf8("--wContrast=%1").arg(settings.contrast); 0873 } 0874 0875 args << QLatin1String("-v"); 0876 args << QLatin1String("-o"); 0877 args << outUrl.toLocalFile(); 0878 0879 Q_FOREACH (const QUrl& url, inUrls) 0880 { 0881 args << url.toLocalFile(); 0882 } 0883 0884 d->enfuseProcess->setProgram(enfusePath); 0885 d->enfuseProcess->setArguments(args); 0886 0887 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Enfuse command line:" << d->enfuseProcess->program(); 0888 0889 d->enfuseProcess->start(); 0890 0891 if (!d->enfuseProcess->waitForFinished(-1)) 0892 { 0893 errors = getProcessError(*(d->enfuseProcess)); 0894 return false; 0895 } 0896 0897 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Enfuse output url: " << outUrl; 0898 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Enfuse exit status:" << d->enfuseProcess->exitStatus(); 0899 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Enfuse exit code: " << d->enfuseProcess->exitCode(); 0900 0901 if (d->enfuseProcess->exitStatus() != QProcess::NormalExit) 0902 { 0903 return false; 0904 } 0905 0906 if (d->enfuseProcess->exitCode() == 0) 0907 { 0908 // Process finished successfully ! 0909 0910 return true; 0911 } 0912 0913 errors = getProcessError(*(d->enfuseProcess)); 0914 0915 return false; 0916 } 0917 0918 QString ExpoBlendingThread::getProcessError(QProcess& proc) const 0919 { 0920 QString std = QString::fromLocal8Bit(proc.readAll()); 0921 0922 return (i18n("Cannot run %1:\n\n %2", proc.program(), std)); 0923 } 0924 0925 /** 0926 * This function obtains the "average scene luminance" (cd/m^2) from an image file. 0927 * "average scene luminance" is the L (aka B) value mentioned in [1] 0928 * You have to take a log2f of the returned value to get an EV value. 0929 * 0930 * We are using K=12.07488f and the exif-implied value of N=1/3.125 (see [1]). 0931 * K=12.07488f is the 1.0592f * 11.4f value in pfscalibration's pfshdrcalibrate.cpp file. 0932 * Based on [3] we can say that the value can also be 12.5 or even 14. 0933 * Another reference for APEX is [4] where N is 0.3, closer to the APEX specification of 2^(-7/4)=0.2973. 0934 * 0935 * [1] en.wikipedia.org/wiki/APEX_system 0936 * [2] en.wikipedia.org/wiki/Exposure_value 0937 * [3] en.wikipedia.org/wiki/Light_meter 0938 * [4] doug.kerr.home.att.net/pumpkin/#APEX 0939 * 0940 * This function tries first to obtain the shutter speed from either of 0941 * two exif tags (there is no standard between camera manufacturers): 0942 * ExposureTime or ShutterSpeedValue. 0943 * Same thing for f-number: it can be found in FNumber or in ApertureValue. 0944 * 0945 * F-number and shutter speed are mandatory in exif data for EV calculation, iso is not. 0946 */ 0947 float ExpoBlendingThread::getAverageSceneLuminance(const QUrl& url) 0948 { 0949 if (!d->meta.load(url.toLocalFile())) 0950 { 0951 return -1; 0952 } 0953 0954 if (!d->meta.hasExif()) 0955 { 0956 return -1; 0957 } 0958 0959 long num = 1, den = 1; 0960 0961 // default not valid values 0962 0963 float expo = -1.0; 0964 float iso = -1.0; 0965 float fnum = -1.0; 0966 QVariant rationals; 0967 0968 if (d->meta.getExifTagRational("Exif.Photo.ExposureTime", num, den)) 0969 { 0970 if (den) 0971 { 0972 expo = (float)(num) / (float)(den); 0973 } 0974 } 0975 else if (getXmpRational("Xmp.exif.ExposureTime", num, den, &d->meta)) 0976 { 0977 if (den) 0978 { 0979 expo = (float)(num) / (float)(den); 0980 } 0981 } 0982 else if (d->meta.getExifTagRational("Exif.Photo.ShutterSpeedValue", num, den)) 0983 { 0984 long nmr = 1, div = 1; 0985 double tmp = 0.0; 0986 0987 if (den) 0988 { 0989 tmp = exp(log(2.0) * (float)(num) / (float)(den)); 0990 } 0991 0992 if (tmp > 1.0) 0993 { 0994 div = (long)(tmp + 0.5); 0995 } 0996 else 0997 { 0998 nmr = (long)(1 / tmp + 0.5); 0999 } 1000 1001 if (div) 1002 { 1003 expo = (float)(nmr) / (float)(div); 1004 } 1005 } 1006 else if (getXmpRational("Xmp.exif.ShutterSpeedValue", num, den, &d->meta)) 1007 { 1008 long nmr = 1, div = 1; 1009 double tmp = 0.0; 1010 1011 if (den) 1012 { 1013 tmp = exp(log(2.0) * (float)(num) / (float)(den)); 1014 } 1015 1016 if (tmp > 1.0) 1017 { 1018 div = (long)(tmp + 0.5); 1019 } 1020 else 1021 { 1022 nmr = (long)(1 / tmp + 0.5); 1023 } 1024 1025 if (div) 1026 { 1027 expo = (float)(nmr) / (float)(div); 1028 } 1029 } 1030 1031 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << url.fileName() << ": expo =" << expo; 1032 1033 if (d->meta.getExifTagRational("Exif.Photo.FNumber", num, den)) 1034 { 1035 if (den) 1036 { 1037 fnum = (float)(num) / (float)(den); 1038 } 1039 1040 } 1041 else if (getXmpRational("Xmp.exif.FNumber", num, den, &d->meta)) 1042 { 1043 if (den) 1044 { 1045 fnum = (float)(num) / (float)(den); 1046 } 1047 } 1048 else if (d->meta.getExifTagRational("Exif.Photo.ApertureValue", num, den)) 1049 { 1050 if (den) 1051 { 1052 fnum = (float)(exp(log(2.0) * (float)(num) / (float)(den) / 2.0)); 1053 } 1054 } 1055 else if (getXmpRational("Xmp.exif.ApertureValue", num, den, &d->meta)) 1056 { 1057 if (den) 1058 { 1059 fnum = (float)(exp(log(2.0) * (float)(num) / (float)(den) / 2.0)); 1060 } 1061 } 1062 1063 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << url.fileName() << ": fnum =" << fnum; 1064 1065 // Some cameras/lens DO print the fnum but with value 0, and this is not allowed for ev computation purposes. 1066 1067 if (fnum == 0.0) 1068 { 1069 return -1.0; 1070 } 1071 1072 // If iso is found use that value, otherwise assume a value of iso=100. (again, some cameras do not print iso in exif). 1073 1074 if (d->meta.getExifTagRational("Exif.Photo.ISOSpeedRatings", num, den)) 1075 { 1076 if (den) 1077 { 1078 iso = (float)(num) / (float)(den); 1079 } 1080 } 1081 else if (getXmpRational("Xmp.exif.ISOSpeedRatings", num, den, &d->meta)) 1082 { 1083 if (den) 1084 { 1085 iso = (float)(num) / (float)(den); 1086 } 1087 } 1088 else 1089 { 1090 iso = 100.0; 1091 } 1092 1093 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << url.fileName() << ": iso =" << iso; 1094 1095 // At this point the three variables have to be != -1 1096 1097 if (expo != -1.0 && iso != -1.0 && fnum != -1.0) 1098 { 1099 float asl = (expo * iso) / (fnum * fnum * 12.07488f); 1100 qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << url.fileName() << ": ASL ==>" << asl; 1101 1102 return asl; 1103 } 1104 1105 return -1.0; 1106 } 1107 1108 bool ExpoBlendingThread::getXmpRational(const char* xmpTagName, long& num, long& den, MetaEngine* const meta) 1109 { 1110 QVariant rationals = meta->getXmpTagVariant(xmpTagName); 1111 1112 if (!rationals.isNull()) 1113 { 1114 QVariantList list = rationals.toList(); 1115 1116 if (list.size() == 2) 1117 { 1118 num = list[0].toInt(); 1119 den = list[1].toInt(); 1120 1121 return true; 1122 } 1123 } 1124 1125 return false; 1126 } 1127 1128 } // namespace DigikamGenericExpoBlendingPlugin 1129 1130 #include "moc_expoblendingthread.cpp"