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"