File indexing completed on 2025-01-05 03:56:24
0001 /* ============================================================ 0002 * 0003 * This file is a part of digiKam project 0004 * https://www.digikam.org 0005 * 0006 * Date : 2020-12-23 0007 * Description : item metadata interface - ImageMagick helpers. 0008 * 0009 * SPDX-FileCopyrightText: 2020-2024 by Gilles Caulier <caulier dot gilles at gmail dot com> 0010 * 0011 * SPDX-License-Identifier: GPL-2.0-or-later 0012 * 0013 * ============================================================ */ 0014 0015 #include "dmetadata.h" 0016 0017 // Qt includes 0018 0019 #include <QByteArray> 0020 #include <QString> 0021 #include <QStringList> 0022 #include <QFileInfo> 0023 0024 // Local includes 0025 0026 #include "digikam_config.h" 0027 #include "digikam_debug.h" 0028 #include "drawdecoder.h" 0029 0030 // ImageMagick includes 0031 0032 #ifdef HAVE_IMAGE_MAGICK 0033 0034 // Pragma directives to reduce warnings from ImageMagick header files. 0035 # if !defined(Q_OS_DARWIN) && defined(Q_CC_GNU) 0036 # pragma GCC diagnostic push 0037 # pragma GCC diagnostic ignored "-Wignored-qualifiers" 0038 # pragma GCC diagnostic ignored "-Wzero-as-null-pointer-constant" 0039 # endif 0040 0041 # if defined(Q_CC_CLANG) 0042 # pragma clang diagnostic push 0043 # pragma clang diagnostic ignored "-Wignored-qualifiers" 0044 # pragma clang diagnostic ignored "-Wkeyword-macro" 0045 # endif 0046 0047 # include <Magick++.h> 0048 0049 using namespace Magick; 0050 using namespace MagickCore; 0051 0052 // Restore warnings 0053 # if !defined(Q_OS_DARWIN) && defined(Q_CC_GNU) 0054 # pragma GCC diagnostic pop 0055 # endif 0056 0057 # if defined(Q_CC_CLANG) 0058 # pragma clang diagnostic pop 0059 # endif 0060 0061 #endif // HAVE_IMAGE_MAGICK 0062 0063 namespace Digikam 0064 { 0065 0066 bool DMetadata::loadUsingImageMagick(const QString& filePath) 0067 { 0068 bool ret = false; 0069 0070 #ifdef HAVE_IMAGE_MAGICK 0071 0072 QFileInfo fileInfo(filePath); 0073 0074 // Ignore null file size to prevent IM crash. See bug #457061 0075 0076 if (fileInfo.size() == 0) 0077 { 0078 return ret; 0079 } 0080 0081 QString rawFilesExt = DRawDecoder::rawFiles(); 0082 QString ext = fileInfo.suffix().toUpper(); 0083 0084 if ( 0085 !fileInfo.exists() || ext.isEmpty() || 0086 rawFilesExt.toUpper().contains(ext) || // Ignore RAW files 0087 (ext == QLatin1String("HEIF")) || // Ignore HEIF files 0088 (ext == QLatin1String("HEIC")) || // Ignore HEIC files 0089 (ext == QLatin1String("HIF")) || // Ignore HIF files 0090 (ext == QLatin1String("XCF")) || // Ignore XCF files 0091 (ext == QLatin1String("SVG")) || // Ignore SVG files 0092 (ext == QLatin1String("PDF")) // Ignore PDF files 0093 ) 0094 { 0095 return false; 0096 } 0097 0098 MagickCore::ImageInfo* image_info = nullptr; 0099 ExceptionInfo ex = *AcquireExceptionInfo(); 0100 0101 // Syntax description of IM percent escape formats: https://imagemagick.org/script/escape.php 0102 // WARNING: this string is limited to 1024 characters internally in Image Magick. 0103 // Over this size, this will corrupt memory at run time in Image Magick core. 0104 // It's recommended to use reduced option forms in favor of long versions. 0105 0106 const QString filters = QLatin1String 0107 ( 0108 // Standard XMP namespaces Exiv2 Type IM long option Exiftool tags 0109 0110 "Xmp.tiff.ImageLength=%h\n" // Text %[height] MIFF.rows 0111 "Xmp.tiff.ImageWidth=%w\n" // Text %[width] MIFF.columns 0112 "Xmp.tiff.Compression=%C\n" // Text %[compression] MIFF.compression 0113 "Xmp.tiff.BitsPerSample=%[bit-depth]\n" // Seq - - 0114 "Xmp.tiff.ImageDescription=%[caption]\n" // LangAlt - - 0115 "Xmp.tiff.Orientation=%[orientation]\n" // Text - - 0116 "Xmp.tiff.DateTime=%[date:create]\n" // Text - - 0117 "Xmp.tiff.XResolution=%x\n" // Text %[resolution.x] MIFF.resolution 0118 "Xmp.tiff.YResolution=%y\n" // Text %[resolution.y] MIFF.resolution 0119 "Xmp.tiff.ResolutionUnit=%U\n" // Text %[units] - 0120 "Xmp.exif.UserComment=%c\n" // LangAlt %[comment] - 0121 "Xmp.exif.ColorSpace=%r\n" // Text %[colorspace] MIFF.colorspace 0122 "Xmp.exifEX.Gamma=%[gamma]\n" // Text - MIFF.gamma 0123 "Xmp.xmp.ModifyDate=%[date:modify]\n" // Text - - 0124 "Xmp.xmp.Label=%l\n" // Text %[label] MIFF.label 0125 0126 // ImageMagick Attributes namespace. 0127 // See Exiftool MIFF namespace for details: https://exiftool.org/TagNames/MIFF.html 0128 0129 "Xmp.MIFF.Version=%[version]\n" // Text - - 0130 "Xmp.MIFF.Copyright=%[copyright]\n" // Text - - 0131 "Xmp.MIFF.BaseName=%t\n" // Text %[basename] - 0132 "Xmp.MIFF.Extension=%e\n" // Text %[extension] - 0133 "Xmp.MIFF.Codec=%m\n" // Text %[magick] - 0134 "Xmp.MIFF.Channels=%[channels]\n" // Text - - 0135 "Xmp.MIFF.Interlace=%[interlace]\n" // Text - - 0136 "Xmp.MIFF.Transparency=%A\n" // Text - - 0137 "Xmp.MIFF.Profiles=%[profiles]\n" // Text - - 0138 "Xmp.MIFF.ProfileICC=%[profile:icc]\n" // Text - MIFF.profile-icc 0139 "Xmp.MIFF.ProfileICM=%[profile:icm]\n" // Text - - 0140 "Xmp.MIFF.XPrintSize=%[printsize.x]\n" // Text - - 0141 "Xmp.MIFF.YPrintSize=%[printsize.y]\n" // Text - - 0142 "Xmp.MIFF.Size=%B\n" // Text %[size] - 0143 "Xmp.MIFF.Quality=%Q\n" // Text %[quality] - 0144 "Xmp.MIFF.Rendering=%[rendering-intent]\n" // Text MIFF.rendering-intent 0145 "Xmp.MIFF.Scene=%n\n" // Text %[scene] MIFF.Scene 0146 /* 0147 NOTE: values calculated which introduce non negligible time latency: 0148 0149 %k (not specified in IM doc) // Text %[colors] MIFF.colors 0150 %[entropy] (specified as CALCULATED in doc) // Text - 0151 %[kurtosis] (specified as CALCULATED in doc) // Text - 0152 %[max] (specified as CALCULATED in doc) // Text - 0153 %[mean] (specified as CALCULATED in doc) // Text - 0154 %[median] (specified as CALCULATED in doc) // Text - 0155 %[min] (specified as CALCULATED in doc) // Text - 0156 %[opaque] (specified as CALCULATED in doc) // Text - 0157 %[skewness] (specified as CALCULATED in doc) // Text - 0158 %[standard-deviation] (specified as CALCULATED in doc) // Text - 0159 %[type] (specified as CALCULATED in doc) // Text - 0160 %# (specified as CALCULATED in doc) // Text - MIFF.signature 0161 %@ (specified as CALCULATED in doc) // Text %[bounding-box] - 0162 */ 0163 0164 // ImageMagick Properties namespace (MIFP) 0165 0166 "%[*]\n" // Text - - 0167 ); 0168 0169 try 0170 { 0171 if (filters.size() >= 1024) 0172 { 0173 qCWarning(DIGIKAM_METAENGINE_LOG) << "Size of percent escape format passed to Image Magick interface" 0174 "is largest than 1024 bytes and metadata cannot be parsed!"; 0175 return ret; 0176 } 0177 0178 // Allocate metadata container for IM. 0179 0180 const int msize = 256; // Number of internal metadata entries prepared for IM. 0181 char** metadata = new char*[msize]; 0182 0183 for (int i = 0 ; i < msize ; ++i) 0184 { 0185 // NOTE: prepare metadata items with null pointers which will be allocated with malloc on demand by ImageMagick. 0186 0187 metadata[i] = nullptr; 0188 } 0189 0190 // Prepare image info for IM isentification 0191 0192 image_info = CloneImageInfo((ImageInfo*)nullptr); 0193 qstrncpy(image_info->filename, filePath.toLatin1().constData(), sizeof(image_info->filename)); 0194 image_info->ping = MagickTrue; 0195 image_info->verbose = MagickTrue; 0196 image_info->debug = MagickTrue; 0197 0198 // Insert percent escape format for IM identification call. 0199 0200 int identargc = 4; // Number or arguments passed to IM identification call. 0201 char** identargv = new char*[identargc]; // Container to store arguments. 0202 identargv[0] = (char*)"identify"; // String content is not important but must be present. 0203 identargv[1] = (char*)"-format"; // We will pass percent escape options. 0204 QByteArray ba = filters.toLatin1(); 0205 identargv[2] = ba.data(); // Percent escape format description. 0206 ba = filePath.toLatin1(); 0207 identargv[3] = ba.data(); // The file path to parse (even if this also passed through IM::ImageInfo container). 0208 0209 // Call ImageMagick core identification. 0210 // This is a fast IM C API call, not the IM CLI tool process. 0211 /* 0212 // NOTE: to hack with CLI IM tool 0213 qCDebug(DIGIKAM_METAENGINE_LOG) << "IM identify escape format string (" << filters.size() << "bytes):" << QT_ENDL << filters << QT_ENDL; 0214 */ 0215 if (IdentifyImageCommand(image_info, identargc, identargv, metadata, &ex) == MagickTrue) 0216 { 0217 // Post process metadata 0218 0219 registerXmpNameSpace(QLatin1String("https://imagemagick.org/MIFF/"), QLatin1String("MIFF")); // Magick Image File Format (Attributes) 0220 registerXmpNameSpace(QLatin1String("https://imagemagick.org/MIFP/"), QLatin1String("MIFP")); // Magick Image File Properties 0221 0222 QString output; 0223 int lbytes = 0; 0224 0225 for (int i = 0 ; i < msize ; ++i) 0226 { 0227 if (metadata[i]) 0228 { 0229 QString tmp = QString::fromUtf8(metadata[i]); 0230 output.append(tmp); 0231 lbytes += tmp.size(); 0232 Q_UNUSED(lbytes); 0233 /* 0234 qCDebug(DIGIKAM_METAENGINE_LOG) << "Append new metadata line of" << tmp.size() << "bytes"; 0235 qCDebug(DIGIKAM_METAENGINE_LOG) << metadata[i]; 0236 */ 0237 } 0238 } 0239 0240 QStringList lines = output.split(QLatin1Char('\n')); 0241 /* 0242 qCDebug(DIGIKAM_METAENGINE_LOG) << "Total metadata lines parsed:" << lines.count() << "Total bytes:" << lbytes; 0243 */ 0244 QString key; 0245 QString val; 0246 0247 Q_FOREACH (const QString& tupple, lines) 0248 { 0249 key = tupple.section(QLatin1Char('='), 0, 0); 0250 val = tupple.section(QLatin1Char('='), 1, 1); 0251 0252 if (val.startsWith(QLatin1Char('\''))) 0253 { 0254 val = val.section(QLatin1Char('\''), 1, 1); // Drop tag description string on the right side (stage1). 0255 val = val.remove(QLatin1Char('\'')); 0256 } 0257 0258 if (val.contains(QLatin1String(" / "))) 0259 { 0260 val = val.section(QLatin1String(" / "), 0, 0); // Drop tag description string on the right side (stage2). 0261 } 0262 0263 if (key.isEmpty() || 0264 key.startsWith(QLatin1String("comment")) || 0265 key.startsWith(QLatin1String("date:create")) || 0266 key.startsWith(QLatin1String("date:modify"))) 0267 { 0268 // These tags are already handled with Exif or key or val are empty. 0269 0270 continue; 0271 } 0272 0273 if (val.isEmpty()) 0274 { 0275 val = QLatin1String("None"); // Mimic IM "none" strings, not i18n 0276 } 0277 0278 if (!key.startsWith(QLatin1String("Xmp."))) 0279 { 0280 // Create a dedicated XMP namespace to store ImageMagick properties. 0281 0282 key = QLatin1String("Xmp.MIFP.") + key.remove(QLatin1Char(':')); 0283 } 0284 0285 key = key.remove(QLatin1Char('-')); 0286 key = key.remove(QLatin1Char('_')); 0287 val = val.simplified(); 0288 0289 // Register XMP tags in container 0290 0291 if (key == QLatin1String("Xmp.tiff.ImageDescription") || 0292 key == QLatin1String("Xmp.exif.UserComment")) 0293 { 0294 setXmpTagStringLangAlt(key.toLatin1().constData(), val, QString()); 0295 } 0296 else if (key == QLatin1String("Xmp.tiff.BitsPerSample")) 0297 { 0298 setXmpTagStringSeq(key.toLatin1().constData(), QStringList() << val); 0299 } 0300 else 0301 { 0302 setXmpTagString(key.toLatin1().constData(), val); 0303 } 0304 } 0305 0306 QString dt = getXmpTagString("Xmp.tiff.DateTime"); 0307 0308 if (!dt.isEmpty()) 0309 { 0310 setXmpTagString("Xmp.exif.DateTimeOriginal", dt); 0311 setXmpTagString("Xmp.xmp.CreateDate", dt); 0312 } 0313 0314 ret = true; 0315 } 0316 else 0317 { 0318 qCWarning(DIGIKAM_METAENGINE_LOG) << "Cannot parse metadata from [" 0319 << filePath 0320 << "] with ImageMagick identify"; 0321 ret = false; 0322 } 0323 0324 // Clear memory 0325 0326 DestroyImageInfo(image_info); 0327 0328 for (int i = 0 ; i < msize ; ++i) 0329 { 0330 if (metadata[i]) 0331 { 0332 // Note: IM use malloc(), not new operator. Do not use delete operator here. 0333 0334 free (metadata[i]); 0335 } 0336 } 0337 0338 delete [] metadata; 0339 delete [] identargv; 0340 } 0341 catch (Exception& error_) 0342 { 0343 qCWarning(DIGIKAM_METAENGINE_LOG) << "Cannot read metadata from [" 0344 << filePath 0345 << "] due to ImageMagick exception:" 0346 << error_.what(); 0347 ret = false; 0348 } 0349 0350 #else // HAVE_IMAGE_MAGICK 0351 0352 Q_UNUSED(filePath); 0353 0354 #endif // HAVE_IMAGE_MAGICK 0355 0356 return ret; 0357 } 0358 0359 } // namespace Digikam