File indexing completed on 2025-01-19 03:56:02
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 * Internal private container. 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 // Qt includes 0020 0021 #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) 0022 # include <QTextCodec> 0023 #endif 0024 0025 #include <qplatformdefs.h> 0026 0027 // Local includes 0028 0029 #include "digikam_debug.h" 0030 #include "digikam_config.h" 0031 #include "metaengine_data_p.h" 0032 #include "drawfiles.h" 0033 #include "exiftoolparser.h" 0034 #include "filereadwritelock.h" 0035 0036 // Pragma directives to reduce warnings from Exiv2. 0037 0038 #if defined(Q_CC_GNU) && !defined(Q_CC_CLANG) 0039 # pragma GCC diagnostic push 0040 # pragma GCC diagnostic ignored "-Wdeprecated-declarations" 0041 #endif 0042 0043 #if defined(Q_CC_CLANG) 0044 # pragma clang diagnostic push 0045 # pragma clang diagnostic ignored "-Wdeprecated-declarations" 0046 #endif 0047 0048 namespace Digikam 0049 { 0050 0051 /** 0052 * This mutex is used to protect all Exiv2 API calls when MetaEngine is used with multi-threads. 0053 */ 0054 QRecursiveMutex s_metaEngineMutex; 0055 0056 /** 0057 * Boolean value about Bmff based file support (CR3, HEIF, HEIC, and AVIF). 0058 * Initialized at run time by initializeExiv2(). 0059 */ 0060 bool s_metaEngineSupportBmff = false; 0061 0062 /** 0063 * Boolean value about Exiv2 warning or error in the Exiv2::LogMsg. 0064 * Changed in MetaEngine::Private::printExiv2MessageHandler. 0065 */ 0066 bool s_metaEngineWarnOrError = false; 0067 0068 MetaEngine::Private::Private(MetaEngine* const q) 0069 : writeWithExifTool (false), 0070 writeRawFiles (false), 0071 writeDngFiles (false), 0072 updateFileTimeStamp (false), 0073 useXMPSidecar4Reading (false), 0074 useCompatibleFileName (false), 0075 metadataWritingMode (WRITE_TO_FILE_ONLY), 0076 loadedFromSidecar (false), 0077 parent (q), 0078 data (new MetaEngineData::Private) 0079 { 0080 QMutexLocker lock(&s_metaEngineMutex); 0081 Exiv2::LogMsg::setHandler(MetaEngine::Private::printExiv2MessageHandler); 0082 } 0083 0084 MetaEngine::Private::~Private() 0085 { 0086 } 0087 0088 const Exiv2::ExifData& MetaEngine::Private::exifMetadata() const 0089 { 0090 return data.constData()->exifMetadata; 0091 } 0092 0093 const Exiv2::IptcData& MetaEngine::Private::iptcMetadata() const 0094 { 0095 return data.constData()->iptcMetadata; 0096 } 0097 0098 const std::string& MetaEngine::Private::itemComments() const 0099 { 0100 return data.constData()->imageComments; 0101 } 0102 0103 Exiv2::ExifData& MetaEngine::Private::exifMetadata() 0104 { 0105 return data.data()->exifMetadata; 0106 } 0107 0108 Exiv2::IptcData& MetaEngine::Private::iptcMetadata() 0109 { 0110 return data.data()->iptcMetadata; 0111 } 0112 0113 std::string& MetaEngine::Private::itemComments() 0114 { 0115 return data.data()->imageComments; 0116 } 0117 0118 #ifdef _XMP_SUPPORT_ 0119 0120 const Exiv2::XmpData& MetaEngine::Private::xmpMetadata() const 0121 { 0122 return data.constData()->xmpMetadata; 0123 } 0124 0125 Exiv2::XmpData& MetaEngine::Private::xmpMetadata() 0126 { 0127 return data.data()->xmpMetadata; 0128 } 0129 0130 #endif // _XMP_SUPPORT_ 0131 0132 void MetaEngine::Private::copyPrivateData(const Private* const other) 0133 { 0134 QMutexLocker lock(&s_metaEngineMutex); 0135 0136 data = other->data; 0137 filePath = other->filePath; 0138 writeRawFiles = other->writeRawFiles; 0139 writeDngFiles = other->writeDngFiles; 0140 updateFileTimeStamp = other->updateFileTimeStamp; 0141 useXMPSidecar4Reading = other->useXMPSidecar4Reading; 0142 useCompatibleFileName = other->useCompatibleFileName; 0143 metadataWritingMode = other->metadataWritingMode; 0144 } 0145 0146 bool MetaEngine::Private::saveToXMPSidecar(const QFileInfo& finfo) const 0147 { 0148 QString xmpFile = MetaEngine::sidecarFilePathForFile(finfo.filePath()); 0149 0150 if (xmpFile.isEmpty()) 0151 { 0152 return false; 0153 } 0154 0155 QFileInfo xmpInfo(xmpFile); 0156 QDateTime modTime = xmpInfo.exists() ? xmpInfo.fileTime(QFileDevice::FileModificationTime) 0157 : finfo.fileTime(QFileDevice::FileModificationTime); 0158 0159 QMutexLocker lock(&s_metaEngineMutex); 0160 0161 try 0162 { 0163 Exiv2::Image::AutoPtr image; 0164 0165 #if defined Q_OS_WIN && defined EXV_UNICODE_PATH 0166 0167 image = Exiv2::ImageFactory::create(Exiv2::ImageType::xmp, 0168 (const wchar_t*)xmpFile.utf16()); 0169 0170 #elif defined Q_OS_WIN 0171 0172 image = Exiv2::ImageFactory::create(Exiv2::ImageType::xmp, 0173 QFile::encodeName(xmpFile).constData()); 0174 0175 #else 0176 0177 image = Exiv2::ImageFactory::create(Exiv2::ImageType::xmp, 0178 xmpFile.toUtf8().constData()); 0179 0180 #endif 0181 0182 #if EXIV2_TEST_VERSION(0,27,99) 0183 0184 return saveUsingExiv2(xmpInfo, modTime, std::move(image)); 0185 0186 #else 0187 0188 return saveUsingExiv2(xmpInfo, modTime, image); 0189 0190 #endif 0191 0192 } 0193 catch (Exiv2::AnyError& e) 0194 { 0195 printExiv2ExceptionError(QLatin1String("Cannot save metadata to XMP sidecar with Exiv2 backend:"), e); 0196 0197 return false; 0198 } 0199 catch (...) 0200 { 0201 qCCritical(DIGIKAM_METAENGINE_LOG) << "Default exception from Exiv2"; 0202 0203 return false; 0204 } 0205 } 0206 0207 bool MetaEngine::Private::saveToFile(const QFileInfo& finfo) const 0208 { 0209 if (!finfo.isWritable()) 0210 { 0211 qCDebug(DIGIKAM_METAENGINE_LOG) << "File" << finfo.fileName() 0212 << "is read only. Metadata not written."; 0213 return false; 0214 } 0215 0216 QDateTime modTime = finfo.fileTime(QFileDevice::FileModificationTime); 0217 0218 QMutexLocker lock(&s_metaEngineMutex); 0219 0220 if (writeWithExifTool) 0221 { 0222 return saveUsingExifTool(finfo, modTime); 0223 } 0224 else 0225 { 0226 try 0227 { 0228 Exiv2::Image::AutoPtr image; 0229 0230 #if defined Q_OS_WIN && defined EXV_UNICODE_PATH 0231 0232 image = Exiv2::ImageFactory::open((const wchar_t*)finfo.filePath().utf16()); 0233 0234 #elif defined Q_OS_WIN 0235 0236 image = Exiv2::ImageFactory::open(QFile::encodeName(finfo.filePath()).constData()); 0237 0238 #else 0239 0240 image = Exiv2::ImageFactory::open(finfo.filePath().toUtf8().constData()); 0241 0242 #endif 0243 0244 #if EXIV2_TEST_VERSION(0,27,99) 0245 0246 return saveUsingExiv2(finfo, modTime, std::move(image)); 0247 0248 #else 0249 0250 return saveUsingExiv2(finfo, modTime, image); 0251 0252 #endif 0253 0254 } 0255 catch (Exiv2::AnyError& e) 0256 { 0257 printExiv2ExceptionError(QLatin1String("Cannot save metadata to image with Exiv2 backend:"), e); 0258 0259 return false; 0260 } 0261 catch (...) 0262 { 0263 qCCritical(DIGIKAM_METAENGINE_LOG) << "Default exception from Exiv2"; 0264 0265 return false; 0266 } 0267 } 0268 } 0269 0270 bool MetaEngine::Private::saveUsingExiv2(const QFileInfo& finfo, 0271 const QDateTime& modTime, 0272 Exiv2::Image::AutoPtr image) const 0273 { 0274 QMutexLocker lock(&s_metaEngineMutex); 0275 0276 QString ext = finfo.suffix().toLower(); 0277 0278 if (!ext.isEmpty()) 0279 { 0280 if (s_rawFileExtensions().contains(ext)) 0281 { 0282 // NOTE: never touch RAW files with Exiv2 as it's not safe. Use ExifTool backend instead. 0283 0284 return false; 0285 } 0286 0287 // Check for a PGF image, this would be destroyed by Exiv2. 0288 0289 if (ext == QLatin1String("pgf")) 0290 { 0291 return false; 0292 } 0293 } 0294 0295 try 0296 { 0297 Exiv2::AccessMode mode; 0298 bool wroteComment = false; 0299 bool wroteEXIF = false; 0300 bool wroteIPTC = false; 0301 bool wroteXMP = false; 0302 0303 // We need to load target file metadata to merge with new one. It's mandatory with TIFF format: 0304 // like all tiff file structure is based on Exif. 0305 0306 image->readMetadata(); 0307 0308 // Image Comments --------------------------------- 0309 0310 mode = image->checkMode(Exiv2::mdComment); 0311 0312 if ((mode == Exiv2::amWrite) || (mode == Exiv2::amReadWrite)) 0313 { 0314 image->setComment(itemComments()); 0315 wroteComment = true; 0316 } 0317 0318 qCDebug(DIGIKAM_METAENGINE_LOG) << "wroteComment: " << wroteComment; 0319 0320 // Exif metadata ---------------------------------- 0321 0322 mode = image->checkMode(Exiv2::mdExif); 0323 0324 if ((mode == Exiv2::amWrite) || (mode == Exiv2::amReadWrite)) 0325 { 0326 if (image->mimeType() == "image/tiff") 0327 { 0328 Exiv2::ExifData orgExif = image->exifData(); 0329 Exiv2::ExifData newExif; 0330 QStringList untouchedTags; 0331 0332 // With tiff image we cannot overwrite whole Exif data as well, because 0333 // image data are stored in Exif container. We need to take a care about 0334 // to not lost image data. 0335 0336 untouchedTags << QLatin1String("Exif.Image.ImageWidth"); 0337 untouchedTags << QLatin1String("Exif.Image.ImageLength"); 0338 untouchedTags << QLatin1String("Exif.Image.BitsPerSample"); 0339 untouchedTags << QLatin1String("Exif.Image.Compression"); 0340 untouchedTags << QLatin1String("Exif.Image.PhotometricInterpretation"); 0341 untouchedTags << QLatin1String("Exif.Image.FillOrder"); 0342 untouchedTags << QLatin1String("Exif.Image.SamplesPerPixel"); 0343 untouchedTags << QLatin1String("Exif.Image.StripOffsets"); 0344 untouchedTags << QLatin1String("Exif.Image.RowsPerStrip"); 0345 untouchedTags << QLatin1String("Exif.Image.StripByteCounts"); 0346 untouchedTags << QLatin1String("Exif.Image.XResolution"); 0347 untouchedTags << QLatin1String("Exif.Image.YResolution"); 0348 untouchedTags << QLatin1String("Exif.Image.PlanarConfiguration"); 0349 untouchedTags << QLatin1String("Exif.Image.ResolutionUnit"); 0350 0351 for (Exiv2::ExifData::const_iterator it = orgExif.begin() ; it != orgExif.end() ; ++it) 0352 { 0353 if (untouchedTags.contains(QLatin1String(it->key().c_str()))) 0354 { 0355 newExif[it->key().c_str()] = orgExif[it->key().c_str()]; 0356 } 0357 } 0358 0359 Exiv2::ExifData readedExif = exifMetadata(); 0360 0361 for (Exiv2::ExifData::const_iterator it = readedExif.begin() ; it != readedExif.end() ; ++it) 0362 { 0363 if (!untouchedTags.contains(QLatin1String(it->key().c_str()))) 0364 { 0365 newExif[it->key().c_str()] = readedExif[it->key().c_str()]; 0366 } 0367 } 0368 0369 image->setExifData(newExif); 0370 } 0371 else 0372 { 0373 image->setExifData(exifMetadata()); 0374 } 0375 0376 wroteEXIF = true; 0377 } 0378 0379 qCDebug(DIGIKAM_METAENGINE_LOG) << "wroteEXIF: " << wroteEXIF; 0380 0381 // Iptc metadata ---------------------------------- 0382 0383 mode = image->checkMode(Exiv2::mdIptc); 0384 0385 if ((mode == Exiv2::amWrite) || (mode == Exiv2::amReadWrite)) 0386 { 0387 image->setIptcData(iptcMetadata()); 0388 wroteIPTC = true; 0389 } 0390 0391 qCDebug(DIGIKAM_METAENGINE_LOG) << "wroteIPTC: " << wroteIPTC; 0392 0393 // Xmp metadata ----------------------------------- 0394 0395 mode = image->checkMode(Exiv2::mdXmp); 0396 0397 if ((mode == Exiv2::amWrite) || (mode == Exiv2::amReadWrite)) 0398 { 0399 0400 #ifdef _XMP_SUPPORT_ 0401 0402 image->setXmpData(xmpMetadata()); 0403 0404 if (image->imageType() == Exiv2::ImageType::xmp) 0405 { 0406 QList<QPair<QString, QString> > pairedTags; 0407 typedef QPair<QString, QString> StringPair; 0408 0409 // We have XMP tags interacting with IPTC tags when writing to sidecar files. 0410 // If an XMP tag does not exist remove the corresponding IPTC tag as well. 0411 // See: https://exiv2.org/conversion.html 0412 // See: BUG 462071 0413 0414 pairedTags << qMakePair(QLatin1String("Xmp.dc.subject"), 0415 QLatin1String("Iptc.Application2.Keywords")); 0416 0417 Q_FOREACH (const StringPair& pair, pairedTags) 0418 { 0419 Exiv2::XmpKey xmpKey(pair.first.toLatin1().constData()); 0420 Exiv2::XmpData::const_iterator it1 = image->xmpData().findKey(xmpKey); 0421 0422 if (it1 == image->xmpData().end()) 0423 { 0424 Exiv2::IptcKey iptcKey(pair.second.toLatin1().constData()); 0425 Exiv2::IptcData::iterator it2 = image->iptcData().findKey(iptcKey); 0426 0427 if (it2 != image->iptcData().end()) 0428 { 0429 image->iptcData().erase(it2); 0430 } 0431 } 0432 } 0433 } 0434 0435 wroteXMP = true; 0436 0437 #endif // _XMP_SUPPORT_ 0438 0439 } 0440 0441 qCDebug(DIGIKAM_METAENGINE_LOG) << "wroteXMP: " << wroteXMP; 0442 0443 // cppcheck-suppress knownConditionTrueFalse 0444 if (!wroteComment && !wroteEXIF && !wroteIPTC && !wroteXMP) 0445 { 0446 qCDebug(DIGIKAM_METAENGINE_LOG) << "Writing metadata is not supported for file" << finfo.fileName(); 0447 0448 return false; 0449 } 0450 else if (!wroteEXIF || !wroteIPTC || !wroteXMP) 0451 { 0452 qCDebug(DIGIKAM_METAENGINE_LOG) << "Support for writing metadata is limited for file" << finfo.fileName(); 0453 } 0454 0455 image->writeMetadata(); 0456 0457 if (!updateFileTimeStamp && modTime.isValid()) 0458 { 0459 // Don't touch modification timestamp of file. 0460 0461 QFile modFile(finfo.filePath()); 0462 0463 if (modFile.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::ExistingOnly)) 0464 { 0465 modFile.setFileTime(modTime, QFileDevice::FileModificationTime); 0466 0467 qCDebug(DIGIKAM_METAENGINE_LOG) << "File time stamp restored"; 0468 0469 modFile.close(); 0470 } 0471 } 0472 0473 return true; 0474 } 0475 catch (Exiv2::AnyError& e) 0476 { 0477 printExiv2ExceptionError(QLatin1String("Cannot save metadata with Exiv2 backend:"), e); 0478 } 0479 catch (...) 0480 { 0481 qCCritical(DIGIKAM_METAENGINE_LOG) << "Default exception from Exiv2"; 0482 } 0483 0484 return false; 0485 } 0486 0487 bool MetaEngine::Private::saveUsingExifTool(const QFileInfo& finfo, 0488 const QDateTime& modTime) const 0489 { 0490 QString ext = finfo.suffix().toLower(); 0491 0492 if (!writeDngFiles && (ext == QLatin1String("dng"))) 0493 { 0494 qCDebug(DIGIKAM_METAENGINE_LOG) << finfo.fileName() 0495 << "is a DNG file, " 0496 << "writing to such a file is disabled by current settings."; 0497 return false; 0498 } 0499 0500 if (!writeRawFiles && s_rawFileExtensions().remove(QLatin1String("dng")).contains(ext)) 0501 { 0502 qCDebug(DIGIKAM_METAENGINE_LOG) << finfo.fileName() 0503 << "is a RAW file, " 0504 << "writing to such a file is disabled by current settings."; 0505 return false; 0506 } 0507 0508 QScopedPointer<ExifToolParser> const parser(new ExifToolParser(nullptr)); 0509 0510 if (!parser->exifToolAvailable()) 0511 { 0512 qCWarning(DIGIKAM_METAENGINE_LOG) << "ExifTool is not available to save metadata..."; 0513 0514 return false; 0515 } 0516 0517 QString dir = QDir::temp().path(); 0518 SafeTemporaryFile* const temp = new SafeTemporaryFile(dir + QLatin1String("/MetaEngine-XXXXXX.exv")); 0519 temp->setAutoRemove(false); 0520 temp->open(); 0521 QString exvPath = temp->safeFilePath(); 0522 0523 // Crash fix: a QTemporaryFile is not properly closed until its destructor is called. 0524 0525 delete temp; 0526 0527 // --- 0528 0529 if (!parent->exportChanges(exvPath)) 0530 { 0531 qCWarning(DIGIKAM_METAENGINE_LOG) << "Exiv2 error exception for" << finfo.fileName() 0532 << "Writing of metadata aborted!"; 0533 0534 return false; 0535 } 0536 0537 bool hasCSet = (parent->getIptcTagData("Iptc.Envelope.CharacterSet") == "\33%G"); 0538 0539 if (!parser->applyChanges(finfo.filePath(), exvPath, 0540 parent->hasExif(), parent->hasXmp(), hasCSet)) 0541 { 0542 qCWarning(DIGIKAM_METAENGINE_LOG) << "Cannot apply changes with ExifTool on" << finfo.filePath(); 0543 QFile::remove(exvPath); 0544 0545 return false; 0546 } 0547 0548 if (!updateFileTimeStamp && modTime.isValid()) 0549 { 0550 // Don't touch modification timestamp of file. 0551 0552 QFile modFile(finfo.filePath()); 0553 0554 if (modFile.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::ExistingOnly)) 0555 { 0556 modFile.setFileTime(modTime, QFileDevice::FileModificationTime); 0557 0558 qCDebug(DIGIKAM_METAENGINE_LOG) << "File time stamp restored"; 0559 0560 modFile.close(); 0561 } 0562 } 0563 0564 QFile::remove(exvPath); 0565 0566 return true; 0567 } 0568 0569 void MetaEngine::Private::printExiv2ExceptionError(const QString& msg, Exiv2::AnyError& e) 0570 { 0571 qCCritical(DIGIKAM_METAENGINE_LOG) << msg.toLatin1().constData() 0572 << " (Error #" << (int)e.code() << ": " << QString::fromStdString(e.what()); 0573 } 0574 0575 void MetaEngine::Private::printExiv2MessageHandler(int lvl, const char* msg) 0576 { 0577 qCDebug(DIGIKAM_METAENGINE_LOG) << "Exiv2 (" << lvl << ") : " << msg; 0578 0579 s_metaEngineWarnOrError |= ((lvl == Exiv2::LogMsg::warn) || (lvl == Exiv2::LogMsg::error)); 0580 } 0581 0582 QString MetaEngine::Private::convertCommentValue(const Exiv2::Exifdatum& exifDatum) const 0583 { 0584 QMutexLocker lock(&s_metaEngineMutex); 0585 0586 try 0587 { 0588 std::string comment; 0589 std::string charset; 0590 0591 comment = exifDatum.toString(); 0592 0593 // libexiv2 will prepend "charset=SomeCharset " if charset is specified 0594 // Before conversion to QString, we must know the charset, so we stay with std::string for a while 0595 0596 if ((comment.length() > 8) && (comment.substr(0, 8) == "charset=")) 0597 { 0598 // the prepended charset specification is followed by a blank 0599 0600 std::string::size_type pos = comment.find_first_of(' '); 0601 0602 if (pos != std::string::npos) 0603 { 0604 // extract string between the = and the blank 0605 0606 charset = comment.substr(8, pos - 8); 0607 0608 // get the rest of the string after the charset specification 0609 0610 comment = comment.substr(pos + 1); 0611 } 0612 } 0613 0614 if (charset == "Unicode") 0615 { 0616 0617 #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) 0618 0619 QByteArray rawComment(exifDatum.size(), '\0'); 0620 exifDatum.copy((Exiv2::byte*)rawComment.data(), Exiv2::bigEndian); 0621 0622 // remove "UNICODE\0" 0623 0624 rawComment = rawComment.mid(8); 0625 0626 if ((rawComment.size() > 1) && (rawComment.at(1) == '\0')) 0627 { 0628 QString utf16String = QString::fromUtf16((ushort*)rawComment.data()); 0629 0630 if (utf16String.isValidUtf16()) 0631 { 0632 return QString::fromUtf8(utf16String.toUtf8()); 0633 } 0634 } 0635 0636 #endif 0637 0638 return QString::fromUtf8(comment.data()); 0639 } 0640 else if (charset == "Jis") 0641 { 0642 0643 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) 0644 0645 if (Exiv2::convertStringCharset(comment, charset.c_str(), "UTF-8")) 0646 { 0647 return QString::fromUtf8(comment); 0648 } 0649 else 0650 { 0651 return QLatin1String(""); 0652 } 0653 0654 #else 0655 0656 QTextCodec* const codec = QTextCodec::codecForName("JIS7"); 0657 0658 if (codec) 0659 { 0660 const char* tmp = comment.c_str(); 0661 0662 if (tmp) 0663 { 0664 return codec->toUnicode(tmp); 0665 } 0666 } 0667 0668 return QLatin1String(""); 0669 0670 #endif 0671 0672 } 0673 else if (charset == "Ascii") 0674 { 0675 return QString::fromStdString(comment); 0676 } 0677 else 0678 { 0679 return detectEncodingAndDecode(comment); 0680 } 0681 } 0682 catch (Exiv2::AnyError& e) 0683 { 0684 printExiv2ExceptionError(QLatin1String("Cannot convert Comment with Exiv2:"), e); 0685 } 0686 catch (...) 0687 { 0688 qCCritical(DIGIKAM_METAENGINE_LOG) << "Default exception from Exiv2"; 0689 } 0690 0691 return QString(); 0692 } 0693 0694 QString MetaEngine::Private::detectEncodingAndDecode(const std::string& value) const 0695 { 0696 // For charset autodetection, we could use sophisticated code 0697 // (Mozilla chardet, KHTML's autodetection, QTextCodec::codecForContent), 0698 // but that is probably too much. 0699 // We check for UTF8, Local encoding and ASCII. 0700 // Look like KEncodingDetector class can provide a full implementation for encoding detection. 0701 0702 if (value.empty()) 0703 { 0704 return QString(); 0705 } 0706 0707 if (isUtf8(value.c_str())) 0708 { 0709 return QString::fromStdString(value); 0710 } 0711 0712 // Utf8 has a pretty unique byte pattern. 0713 // That's not true for ASCII, it is not possible 0714 // to reliably autodetect different ISO-8859 charsets. 0715 // So we can use either local encoding, or latin1. 0716 0717 QString localString = QString::fromLocal8Bit(value.c_str()); 0718 0719 // To fix broken JFIF comment, replace non printable 0720 // characters from the string. See bug 39617 0721 0722 for (int i = 0 ; i < localString.size() ; ++i) 0723 { 0724 if (!localString.at(i).isPrint() && 0725 (localString.at(i) != QLatin1Char('\n')) && 0726 (localString.at(i) != QLatin1Char('\r'))) 0727 { 0728 localString[i] = QLatin1Char('_'); 0729 } 0730 } 0731 0732 return localString; 0733 } 0734 0735 bool MetaEngine::Private::isUtf8(const char* const buffer) const 0736 { 0737 int i, n; 0738 unsigned char c; 0739 bool gotone = false; 0740 0741 if (!buffer) 0742 { 0743 return true; 0744 } 0745 0746 // character never appears in text 0747 0748 #define F 0 0749 0750 // character appears in plain ASCII text 0751 0752 #define T 1 0753 0754 // character appears in ISO-8859 text 0755 0756 #define I 2 0757 0758 // character appears in non-ISO extended ASCII (Mac, IBM PC) 0759 0760 #define X 3 0761 0762 static const unsigned char text_chars[256] = 0763 { 0764 // BEL BS HT LF FF CR 0765 F, F, F, F, F, F, F, T, T, T, T, F, T, T, F, F, // 0x0X 0766 // ESC 0767 F, F, F, F, F, F, F, F, F, F, F, T, F, F, F, F, // 0x1X 0768 T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, // 0x2X 0769 T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, // 0x3X 0770 T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, // 0x4X 0771 T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, // 0x5X 0772 T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, // 0x6X 0773 T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, F, // 0x7X 0774 // NEL 0775 X, X, X, X, X, T, X, X, X, X, X, X, X, X, X, X, // 0x8X 0776 X, X, X, X, X, X, X, X, X, X, X, X, X, X, X, X, // 0x9X 0777 I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, // 0xaX 0778 I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, // 0xbX 0779 I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, // 0xcX 0780 I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, // 0xdX 0781 I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, // 0xeX 0782 I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I // 0xfX 0783 }; 0784 0785 for (i = 0 ; (c = buffer[i]) ; ++i) 0786 { 0787 if ((c & 0x80) == 0) 0788 { 0789 // 0xxxxxxx is plain ASCII 0790 0791 // Even if the whole file is valid UTF-8 sequences, 0792 // still reject it if it uses weird control characters. 0793 0794 if (text_chars[c] != T) 0795 { 0796 return false; 0797 } 0798 } 0799 else if ((c & 0x40) == 0) 0800 { 0801 // 10xxxxxx never 1st byte 0802 0803 return false; 0804 } 0805 else 0806 { 0807 // 11xxxxxx begins UTF-8 0808 0809 int following = 0; 0810 0811 if ((c & 0x20) == 0) 0812 { 0813 // 110xxxxx 0814 0815 following = 1; 0816 } 0817 else if ((c & 0x10) == 0) 0818 { 0819 // 1110xxxx 0820 0821 following = 2; 0822 } 0823 else if ((c & 0x08) == 0) 0824 { 0825 // 11110xxx 0826 0827 following = 3; 0828 } 0829 else if ((c & 0x04) == 0) 0830 { 0831 // 111110xx 0832 0833 following = 4; 0834 } 0835 else if ((c & 0x02) == 0) 0836 { 0837 // 1111110x 0838 0839 following = 5; 0840 } 0841 else 0842 { 0843 return false; 0844 } 0845 0846 for (n = 0 ; n < following ; ++n) 0847 { 0848 i++; 0849 0850 if (!(c = buffer[i])) 0851 { 0852 goto done; 0853 } 0854 0855 if (((c & 0x80) == 0) || (c & 0x40)) 0856 { 0857 return false; 0858 } 0859 } 0860 0861 gotone = true; 0862 } 0863 } 0864 0865 done: 0866 0867 return gotone; // don't claim it's UTF-8 if it's all 7-bit. 0868 } 0869 0870 #undef F 0871 #undef T 0872 #undef I 0873 #undef X 0874 0875 bool MetaEngine::Private::decodeGPSCoordinate(const char* exifTagName, double* const coordinate) const 0876 { 0877 QMutexLocker lock(&s_metaEngineMutex); 0878 0879 try 0880 { 0881 std::string exifkey(exifTagName); 0882 Exiv2::ExifKey gpsKey(exifkey); 0883 Exiv2::ExifData exifData(exifMetadata()); 0884 Exiv2::ExifData::const_iterator it = exifData.findKey(gpsKey); 0885 0886 if ((it != exifData.end()) && ((*it).count() == 3)) 0887 { 0888 double deg; 0889 double min; 0890 double sec; 0891 0892 deg = (double)((*it).toFloat(0)); 0893 0894 if (((*it).toRational(0).second == 0) || (deg == -1.0)) 0895 { 0896 return false; 0897 } 0898 0899 *coordinate = deg; 0900 0901 min = (double)((*it).toFloat(1)); 0902 0903 if (((*it).toRational(1).second == 0) || (min == -1.0)) 0904 { 0905 return false; 0906 } 0907 0908 *coordinate = *coordinate + min / 60.0; 0909 0910 sec = (double)((*it).toFloat(2)); 0911 0912 if (sec != -1.0) 0913 { 0914 *coordinate = *coordinate + sec / 3600.0; 0915 } 0916 0917 return true; 0918 } 0919 } 0920 catch (Exiv2::AnyError& e) 0921 { 0922 printExiv2ExceptionError(QLatin1String("Cannot get GPS tag with Exiv2:"), e); 0923 } 0924 catch (...) 0925 { 0926 qCCritical(DIGIKAM_METAENGINE_LOG) << "Default exception from Exiv2"; 0927 } 0928 0929 return false; 0930 } 0931 0932 int MetaEngine::Private::getXMPTagsListFromPrefix(const QString& pf, MetaEngine::TagsMap& tagsMap) const 0933 { 0934 int i = 0; 0935 0936 #ifdef _XMP_SUPPORT_ 0937 0938 QMutexLocker lock(&s_metaEngineMutex); 0939 0940 try 0941 { 0942 QList<const Exiv2::XmpPropertyInfo*> tags; 0943 tags << Exiv2::XmpProperties::propertyList(pf.toLatin1().data()); 0944 0945 for (QList<const Exiv2::XmpPropertyInfo*>::iterator it = tags.begin() ; it != tags.end() ; ++it) 0946 { 0947 while ((*it) && !QString::fromLatin1((*it)->name_).isNull()) 0948 { 0949 QString key = QLatin1String(Exiv2::XmpKey(pf.toLatin1().data(), (*it)->name_).key().c_str()); 0950 QStringList values; 0951 values << QLatin1String((*it)->name_) 0952 << QLatin1String((*it)->title_) 0953 << QLatin1String((*it)->desc_); 0954 tagsMap.insert(key, values); 0955 ++(*it); 0956 i++; 0957 } 0958 } 0959 } 0960 catch (Exiv2::AnyError& e) 0961 { 0962 printExiv2ExceptionError(QLatin1String("Cannot get Xmp tags list using Exiv2:"), e); 0963 } 0964 catch (...) 0965 { 0966 qCCritical(DIGIKAM_METAENGINE_LOG) << "Default exception from Exiv2"; 0967 } 0968 0969 #else 0970 0971 Q_UNUSED(pf); 0972 Q_UNUSED(tagsMap); 0973 0974 #endif // _XMP_SUPPORT_ 0975 0976 return i; 0977 } 0978 0979 #ifdef _XMP_SUPPORT_ 0980 0981 void MetaEngine::Private::loadSidecarData(Exiv2::Image::AutoPtr xmpsidecar) 0982 { 0983 // Having a sidecar is a special situation. 0984 // The sidecar data often "dominates", see in particular bug 309058 for important aspects: 0985 // If a field is removed from the sidecar, we must ignore (older) data for this field in the file. 0986 0987 // First: Ignore file XMP, only use sidecar XMP 0988 0989 xmpMetadata() = xmpsidecar->xmpData(); 0990 loadedFromSidecar = true; 0991 0992 // EXIF 0993 // Four groups of properties are mapped between EXIF and XMP: 0994 // Date/Time, Description, Copyright, Creator 0995 // A few more tags are defined "writeback" tags in the XMP specification, the sidecar value therefore overrides the Exif value. 0996 // The rest is kept side-by-side. 0997 // (to understand, remember that the xmpsidecar's Exif data is actually XMP data mapped back to Exif) 0998 0999 // Description, Copyright and Creator is dominated by the sidecar: Remove file Exif fields, if field not in XMP. 1000 1001 ExifMetaEngineMergeHelper exifDominatedHelper; 1002 exifDominatedHelper << QLatin1String("Exif.Image.ImageDescription") 1003 << QLatin1String("Exif.Photo.UserComment") 1004 << QLatin1String("Exif.Image.Copyright") 1005 << QLatin1String("Exif.Image.Artist"); 1006 exifDominatedHelper.exclusiveMerge(xmpsidecar->exifData(), exifMetadata()); 1007 1008 // Date/Time and "the few more" from the XMP spec are handled as writeback 1009 // Note that Date/Time mapping is slightly contradictory in latest specs. 1010 1011 ExifMetaEngineMergeHelper exifWritebackHelper; 1012 exifWritebackHelper << QLatin1String("Exif.Image.DateTime") 1013 << QLatin1String("Exif.Photo.DateTimeOriginal") 1014 << QLatin1String("Exif.Photo.DateTimeDigitized") 1015 << QLatin1String("Exif.Image.Orientation") 1016 << QLatin1String("Exif.Image.XResolution") 1017 << QLatin1String("Exif.Image.YResolution") 1018 << QLatin1String("Exif.Image.ResolutionUnit") 1019 << QLatin1String("Exif.Image.Software") 1020 << QLatin1String("Exif.Photo.RelatedSoundFile"); 1021 exifWritebackHelper.mergeFields(xmpsidecar->exifData(), exifMetadata()); 1022 1023 // IPTC 1024 // These fields cover almost all relevant IPTC data and are defined in the XMP specification for reconciliation. 1025 1026 IptcMetaEngineMergeHelper iptcDominatedHelper; 1027 iptcDominatedHelper << QLatin1String("Iptc.Application2.ObjectName") 1028 << QLatin1String("Iptc.Application2.Urgency") 1029 << QLatin1String("Iptc.Application2.Category") 1030 << QLatin1String("Iptc.Application2.SuppCategory") 1031 << QLatin1String("Iptc.Application2.Keywords") 1032 << QLatin1String("Iptc.Application2.SubLocation") 1033 << QLatin1String("Iptc.Application2.SpecialInstructions") 1034 << QLatin1String("Iptc.Application2.Byline") 1035 << QLatin1String("Iptc.Application2.BylineTitle") 1036 << QLatin1String("Iptc.Application2.City") 1037 << QLatin1String("Iptc.Application2.ProvinceState") 1038 << QLatin1String("Iptc.Application2.CountryCode") 1039 << QLatin1String("Iptc.Application2.CountryName") 1040 << QLatin1String("Iptc.Application2.TransmissionReference") 1041 << QLatin1String("Iptc.Application2.Headline") 1042 << QLatin1String("Iptc.Application2.Credit") 1043 << QLatin1String("Iptc.Application2.Source") 1044 << QLatin1String("Iptc.Application2.Copyright") 1045 << QLatin1String("Iptc.Application2.Caption") 1046 << QLatin1String("Iptc.Application2.Writer"); 1047 iptcDominatedHelper.exclusiveMerge(xmpsidecar->iptcData(), iptcMetadata()); 1048 1049 IptcMetaEngineMergeHelper iptcWritebackHelper; 1050 iptcWritebackHelper << QLatin1String("Iptc.Application2.DateCreated") 1051 << QLatin1String("Iptc.Application2.TimeCreated") 1052 << QLatin1String("Iptc.Application2.DigitizationDate") 1053 << QLatin1String("Iptc.Application2.DigitizationTime"); 1054 iptcWritebackHelper.mergeFields(xmpsidecar->iptcData(), iptcMetadata()); 1055 1056 /* 1057 * TODO: Exiv2 (referring to 0.23) does not correctly synchronize all times values as given below. 1058 * Time values and their synchronization: 1059 * Original Date/Time – Creation date of the intellectual content (e.g. the photograph), 1060 * rather than the creatio*n date of the content being shown 1061 * Exif DateTimeOriginal (36867, 0x9003) and SubSecTimeOriginal (37521, 0x9291) 1062 * IPTC DateCreated (IIM 2:55, 0x0237) and TimeCreated (IIM 2:60, 0x023C) 1063 * XMP (photoshop:DateCreated) 1064 * Digitized Date/Time – Creation date of the digital representation 1065 * Exif DateTimeDigitized (36868, 0x9004) and SubSecTimeDigitized (37522, 0x9292) 1066 * IPTC DigitalCreationDate (IIM 2:62, 0x023E) and DigitalCreationTime (IIM 2:63, 0x023F) 1067 * XMP (xmp:CreateDate) 1068 * Modification Date/Time – Modification date of the digital image file 1069 * Exif DateTime (306, 0x132) and SubSecTime (37520, 0x9290) 1070 * XMP (xmp:ModifyDate) 1071 */ 1072 } 1073 1074 #endif // _XMP_SUPPORT_ 1075 1076 QString MetaEngine::Private::extractIptcTagString(const Exiv2::IptcData& iptcData, const Exiv2::Iptcdatum& iptcTag) const 1077 { 1078 QString charSet = QLatin1String(iptcData.detectCharset()); 1079 QString value; 1080 1081 // NOTE: no need mutex lock here as this method is always called from a top level function by a parent mutex lock 1082 1083 try 1084 { 1085 if ((iptcTag.typeId() == Exiv2::string) && !charSet.isNull()) 1086 { 1087 // Perform Utf8 conversion from std::string 1088 // TODO: check if a parse of charset content can improve the string conversion if not Utf8 use. 1089 1090 value = QString::fromStdString(iptcTag.toString()); 1091 } 1092 else 1093 { 1094 // No characterset want mean ASCII-latin1 1095 // Decode the tag value with a user friendly output. 1096 1097 std::ostringstream os; 1098 os << iptcTag; 1099 value = QString::fromStdString(os.str()); 1100 } 1101 } 1102 catch (Exiv2::AnyError& e) 1103 { 1104 printExiv2ExceptionError(QLatin1String("Cannot decode Iptc tag string with right encoding using Exiv2:"), e); 1105 } 1106 catch (...) 1107 { 1108 qCCritical(DIGIKAM_METAENGINE_LOG) << "Default exception from Exiv2"; 1109 } 1110 1111 return value; 1112 } 1113 1114 } // namespace Digikam 1115 1116 // Restore warnings 1117 #if defined(Q_CC_GNU) && !defined(Q_CC_CLANG) 1118 # pragma GCC diagnostic pop 1119 #endif 1120 1121 #if defined(Q_CC_CLANG) 1122 # pragma clang diagnostic pop 1123 #endif