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