File indexing completed on 2025-01-19 03:56:00

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2006-09-15
0007  * Description : Exiv2 library interface.
0008  *               File I/O methods
0009  *
0010  * SPDX-FileCopyrightText: 2006-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0011  * SPDX-FileCopyrightText: 2006-2013 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
0012  *
0013  * SPDX-License-Identifier: GPL-2.0-or-later
0014  *
0015  * ============================================================ */
0016 
0017 #include "metaengine_p.h"
0018 
0019 // Local includes
0020 
0021 #include "digikam_debug.h"
0022 #include "digikam_config.h"
0023 #include "digikam_version.h"
0024 
0025 #if defined(Q_CC_CLANG)
0026 #   pragma clang diagnostic push
0027 #   pragma clang diagnostic ignored "-Wdeprecated-declarations"
0028 #endif
0029 
0030 namespace Digikam
0031 {
0032 
0033 void MetaEngine::setFilePath(const QString& path)
0034 {
0035     d->filePath = path;
0036 }
0037 
0038 QString MetaEngine::getFilePath() const
0039 {
0040     return d->filePath;
0041 }
0042 
0043 QString MetaEngine::sidecarFilePathForFile(const QString& path)
0044 {
0045     if (path.isEmpty())
0046     {
0047         return QString();
0048     }
0049 
0050     if (MetaEngineSettings::instance()->settings().useCompatibleFileName)
0051     {
0052         QFileInfo info(path);
0053         QString pathBaseXmp = path;
0054         pathBaseXmp.chop(info.suffix().size());
0055 
0056         return pathBaseXmp + QLatin1String("xmp");
0057     }
0058 
0059     return path + QLatin1String(".xmp");
0060 }
0061 
0062 QUrl MetaEngine::sidecarUrl(const QUrl& url)
0063 {
0064     return sidecarUrl(url.toLocalFile());
0065 }
0066 
0067 QUrl MetaEngine::sidecarUrl(const QString& path)
0068 {
0069     return QUrl::fromLocalFile(sidecarFilePathForFile(path));
0070 }
0071 
0072 QString MetaEngine::sidecarPath(const QString& path)
0073 {
0074     return sidecarFilePathForFile(path);
0075 }
0076 
0077 bool MetaEngine::hasSidecar(const QString& path)
0078 {
0079     return QFileInfo::exists(sidecarFilePathForFile(path));
0080 }
0081 
0082 QString MetaEngine::backendName(Backend t)
0083 {
0084     switch (t)
0085     {
0086         case LibRawBackend:
0087         {
0088             return QLatin1String("LibRaw");
0089         }
0090 
0091         case LibHeifBackend:
0092         {
0093             return QLatin1String("LibHeif");
0094         }
0095 
0096         case ImageMagickBackend:
0097         {
0098             return QLatin1String("ImageMagick");
0099         }
0100 
0101         case FFMpegBackend:
0102         {
0103             return QLatin1String("FFMpeg");
0104         }
0105 
0106         case ExifToolBackend:
0107         {
0108             return QLatin1String("ExifTool");
0109         }
0110 
0111         case NoBackend:
0112         {
0113             return QLatin1String("No Backend");
0114         }
0115 
0116         default:
0117         {
0118             return QLatin1String("Exiv2");
0119         }
0120     }
0121 }
0122 
0123 bool MetaEngine::load(const QString& filePath, Backend* backend)
0124 {
0125     if (backend)
0126     {
0127         *backend = NoBackend;
0128     }
0129 
0130     if (filePath.isEmpty())
0131     {
0132         return false;
0133     }
0134 
0135     d->filePath      = filePath;
0136     bool hasLoaded   = false;
0137 
0138     QMutexLocker lock(&s_metaEngineMutex);
0139 
0140     s_metaEngineWarnOrError = false;
0141 
0142     try
0143     {
0144         Exiv2::Image::AutoPtr image;
0145 
0146 #if defined Q_OS_WIN && defined EXV_UNICODE_PATH
0147 
0148         image        = Exiv2::ImageFactory::open((const wchar_t*)filePath.utf16());
0149 
0150 #elif defined Q_OS_WIN
0151 
0152         image        = Exiv2::ImageFactory::open(QFile::encodeName(filePath).constData());
0153 
0154 #else
0155 
0156         image        = Exiv2::ImageFactory::open(filePath.toUtf8().constData());
0157 
0158 #endif
0159 
0160         image->readMetadata();
0161 
0162         // Size and mimetype ---------------------------------
0163 
0164         d->pixelSize = QSize(image->pixelWidth(), image->pixelHeight());
0165         d->mimeType  = QString::fromStdString(image->mimeType());
0166 
0167         // Image comments ---------------------------------
0168 
0169         d->itemComments() = image->comment();
0170 
0171         // Exif metadata ----------------------------------
0172 
0173         d->exifMetadata() = image->exifData();
0174 
0175         // Iptc metadata ----------------------------------
0176 
0177         d->iptcMetadata() = image->iptcData();
0178 
0179 #ifdef _XMP_SUPPORT_
0180 
0181         // Xmp metadata -----------------------------------
0182         d->xmpMetadata() = image->xmpData();
0183 
0184 #endif // _XMP_SUPPORT_
0185 
0186         if (s_metaEngineWarnOrError)
0187         {
0188             d->itemComments().clear();
0189             d->exifMetadata().clear();
0190             d->iptcMetadata().clear();
0191 
0192 #ifdef _XMP_SUPPORT_
0193 
0194             d->xmpMetadata().clear();
0195 
0196 #endif // _XMP_SUPPORT_
0197 
0198             return false;
0199         }
0200 
0201         if (backend)
0202         {
0203             *backend = Exiv2Backend;
0204         }
0205 
0206         hasLoaded = true;
0207 
0208     }
0209     catch (Exiv2::AnyError& e)
0210     {
0211         d->printExiv2ExceptionError(QString::fromUtf8("Cannot load metadata from file with Exiv2 backend: %1").arg(getFilePath()), e);
0212     }
0213     catch (...)
0214     {
0215         qCCritical(DIGIKAM_METAENGINE_LOG) << "Default exception from Exiv2";
0216     }
0217 
0218     hasLoaded |= loadFromSidecarAndMerge(filePath);
0219 
0220     return hasLoaded;
0221 }
0222 
0223 bool MetaEngine::loadFromSidecarAndMerge(const QString& filePath)
0224 {
0225     if (filePath.isEmpty())
0226     {
0227         return false;
0228     }
0229 
0230     d->filePath    = filePath;
0231     bool hasLoaded = false;
0232 
0233 #ifdef _XMP_SUPPORT_
0234 
0235     QMutexLocker lock(&s_metaEngineMutex);
0236 
0237     try
0238     {
0239         if (d->useXMPSidecar4Reading)
0240         {
0241             QString xmpSidecarPath = sidecarFilePathForFile(filePath);
0242             QFileInfo xmpSidecarFileInfo(xmpSidecarPath);
0243 
0244             Exiv2::Image::AutoPtr xmpsidecar;
0245 
0246             if (xmpSidecarFileInfo.exists() && xmpSidecarFileInfo.isReadable())
0247             {
0248                 // Read sidecar data
0249 
0250 #if defined Q_OS_WIN && defined EXV_UNICODE_PATH
0251 
0252                 xmpsidecar = Exiv2::ImageFactory::open((const wchar_t*)xmpSidecarPath.utf16());
0253 
0254 #elif defined Q_OS_WIN
0255 
0256                 xmpsidecar = Exiv2::ImageFactory::open(QFile::encodeName(xmpSidecarPath).constData());
0257 
0258 #else
0259 
0260                 xmpsidecar = Exiv2::ImageFactory::open(xmpSidecarPath.toUtf8().constData());
0261 
0262 #endif
0263 
0264                 xmpsidecar->readMetadata();
0265 
0266                 // Merge
0267 
0268 #if EXIV2_TEST_VERSION(0,27,99)
0269 
0270                 d->loadSidecarData(std::move(xmpsidecar));
0271 
0272 #else
0273 
0274                 d->loadSidecarData(xmpsidecar);
0275 
0276 #endif
0277 
0278                 hasLoaded = true;
0279             }
0280         }
0281     }
0282     catch (Exiv2::AnyError& e)
0283     {
0284         d->printExiv2ExceptionError(QString::fromUtf8("Cannot load XMP sidecar from file with Exiv2 backend: %1").arg(getFilePath()), e);
0285     }
0286     catch (...)
0287     {
0288         qCCritical(DIGIKAM_METAENGINE_LOG) << "Default exception from Exiv2";
0289     }
0290 
0291 #endif // _XMP_SUPPORT_
0292 
0293     return hasLoaded;
0294 }
0295 
0296 bool MetaEngine::save(const QString& imageFilePath, bool setVersion) const
0297 {
0298     if (setVersion && !setProgramId())
0299     {
0300         return false;
0301     }
0302 
0303     // If our image is really a symlink, we should follow the symlink so that
0304     // when we delete the file and rewrite it, we are honoring the symlink
0305     // (rather than just deleting it and putting a file there).
0306     //
0307     // However, this may be surprising to the user when they are writing sidecar
0308     // files.  They might expect them to show up where the symlink is.  So, we
0309     // shouldn't follow the link when figuring out what the filename for the
0310     // sidecar should be.
0311     //
0312     // Note, we are not yet handling the case where the sidecar itself is a
0313     // symlink.
0314 
0315     QString regularFilePath = imageFilePath; // imageFilePath might be a
0316                                              // symlink.  Below we will change
0317                                              // regularFile to the pointed to
0318                                              // file if so.
0319     QFileInfo givenFileInfo(imageFilePath);
0320 
0321     if (givenFileInfo.isSymLink())
0322     {
0323         qCDebug(DIGIKAM_METAENGINE_LOG) << "filePath" << imageFilePath << "is a symlink."
0324                                         << "Using target" << givenFileInfo.canonicalFilePath();
0325 
0326         regularFilePath = givenFileInfo.canonicalFilePath(); // Walk all the symlinks
0327     }
0328 
0329     bool writeToFile                     = false;
0330     bool writeToSidecar                  = false;
0331     bool writeToSidecarIfFileNotPossible = false;
0332     bool writtenToFile                   = false;
0333     bool writtenToSidecar                = false;
0334 
0335     qCDebug(DIGIKAM_METAENGINE_LOG) << "MetaEngine::metadataWritingMode" << d->metadataWritingMode;
0336 
0337     switch (d->metadataWritingMode)
0338     {
0339         case WRITE_TO_SIDECAR_ONLY:
0340         {
0341             writeToSidecar = true;
0342             break;
0343         }
0344 
0345         case WRITE_TO_FILE_ONLY:
0346         {
0347             writeToFile    = true;
0348             break;
0349         }
0350 
0351         case WRITE_TO_SIDECAR_AND_FILE:
0352         {
0353             writeToFile    = true;
0354             writeToSidecar = true;
0355             break;
0356         }
0357 
0358         case WRITE_TO_SIDECAR_ONLY_FOR_READ_ONLY_FILES:
0359         {
0360             writeToFile                     = true;
0361             writeToSidecarIfFileNotPossible = true;
0362             break;
0363         }
0364     }
0365 
0366     // NOTE: see B.K.O #137770 & #138540 : never touch the file if is read only.
0367     QFileInfo finfo(regularFilePath);
0368     QFileInfo dinfo(finfo.path());
0369 
0370     if (writeToFile)
0371     {
0372         if (!dinfo.isWritable())
0373         {
0374             qCDebug(DIGIKAM_METAENGINE_LOG) << "Dir" << dinfo.filePath() << "is read-only. Metadata not saved.";
0375             writtenToFile = false;
0376         }
0377         else
0378         {
0379             qCDebug(DIGIKAM_METAENGINE_LOG) << "Will write Metadata to file" << finfo.absoluteFilePath();
0380             writtenToFile = d->saveToFile(finfo);
0381         }
0382 
0383         if (writtenToFile)
0384         {
0385             qCDebug(DIGIKAM_METAENGINE_LOG) << "Metadata for file" << finfo.fileName() << "written to file.";
0386         }
0387     }
0388 
0389     if (writeToSidecar || (writeToSidecarIfFileNotPossible && !writtenToFile))
0390     {
0391         qCDebug(DIGIKAM_METAENGINE_LOG) << "Will write XMP sidecar for file" << finfo.fileName();
0392 
0393         if (!dinfo.isWritable())
0394         {
0395             writtenToSidecar = d->saveToXMPSidecar(QFileInfo(imageFilePath));
0396         }
0397         else
0398         {
0399             writtenToSidecar = d->saveToXMPSidecar(QFileInfo(regularFilePath));
0400         }
0401 
0402         if (writtenToSidecar)
0403         {
0404             qCDebug(DIGIKAM_METAENGINE_LOG) << "Metadata for file" << finfo.fileName() << "written to XMP sidecar.";
0405         }
0406     }
0407 
0408     return (writtenToFile || writtenToSidecar);
0409 }
0410 
0411 bool MetaEngine::applyChanges(bool setVersion) const
0412 {
0413     if (d->filePath.isEmpty())
0414     {
0415         qCDebug(DIGIKAM_METAENGINE_LOG) << "Failed to apply changes: file path is empty!";
0416         return false;
0417     }
0418 
0419     return save(d->filePath, setVersion);
0420 }
0421 
0422 bool MetaEngine::exportChanges(const QString& exvTmpFile) const
0423 {
0424     if (exvTmpFile.isEmpty())
0425     {
0426         qCDebug(DIGIKAM_METAENGINE_LOG) << "Failed to export changes: temp path is empty!";
0427         return false;
0428     }
0429 
0430     QMutexLocker lock(&s_metaEngineMutex);
0431 
0432     try
0433     {
0434         // Create target EXV container.
0435 
0436         Exiv2::Image::AutoPtr targetExv = Exiv2::ImageFactory::create(Exiv2::ImageType::exv, exvTmpFile.toStdString());
0437         targetExv->setComment(d->itemComments());
0438         targetExv->setExifData(d->exifMetadata());
0439         targetExv->setIptcData(d->iptcMetadata());
0440 
0441 #ifdef _XMP_SUPPORT_
0442 
0443         targetExv->setXmpData(d->xmpMetadata());
0444 
0445 #endif // _XMP_SUPPORT_
0446 
0447         targetExv->writeMetadata();
0448 
0449         return true;
0450     }
0451     catch (Exiv2::AnyError& e)
0452     {
0453         d->printExiv2ExceptionError(QLatin1String("Cannot export changes with Exiv2 backend: "), e);
0454     }
0455     catch (...)
0456     {
0457         qCCritical(DIGIKAM_METAENGINE_LOG) << "Default exception from Exiv2";
0458     }
0459 
0460     return false;
0461 }
0462 
0463 } // namespace Digikam
0464 
0465 #if defined(Q_CC_CLANG)
0466 #   pragma clang diagnostic pop
0467 #endif