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