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

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2020-11-28
0007  * Description : ExifTool process stream parser.
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 "exiftoolparser_p.h"
0016 
0017 // Local includes
0018 
0019 #include "digikam_globals.h"
0020 
0021 namespace Digikam
0022 {
0023 
0024 void ExifToolParser::printExifToolOutput(const QByteArray& stdOut)
0025 {
0026     qCDebug(DIGIKAM_METAENGINE_LOG) << "ExifTool output:";
0027     qCDebug(DIGIKAM_METAENGINE_LOG) << "---";
0028     qCDebug(DIGIKAM_METAENGINE_LOG).noquote() << stdOut;
0029     qCDebug(DIGIKAM_METAENGINE_LOG) << "---";
0030 }
0031 
0032 void ExifToolParser::cmdCompleted(const ExifToolProcess::Result& result)
0033 {
0034     qCDebug(DIGIKAM_METAENGINE_LOG) << "ExifTool complete command for action"
0035                                     << d->actionString(result.cmdAction)
0036                                     << "with elasped time (ms):"
0037                                     << result.elapsed;
0038 
0039     ExifToolData exifToolData;
0040 
0041     switch (result.cmdAction)
0042     {
0043         case ExifToolProcess::LOAD_METADATA:
0044         {
0045             // Convert JSON array as QVariantMap
0046 
0047             QJsonDocument jsonDoc     = QJsonDocument::fromJson(result.output);
0048             QJsonArray    jsonArray   = jsonDoc.array();
0049 
0050             if (jsonArray.size() == 0)
0051             {
0052                 qCDebug(DIGIKAM_METAENGINE_LOG) << "Json Array size is null";
0053 
0054                 Q_EMIT signalExifToolDataAvailable();
0055 
0056                 if (d->async)
0057                 {
0058                     Q_EMIT signalExifToolAsyncData(exifToolData);
0059                 }
0060 
0061                 return;
0062             }
0063 
0064             QJsonObject   jsonObject  = jsonArray.at(0).toObject();
0065             QVariantMap   metadataMap = jsonObject.toVariantMap();
0066 
0067             qCDebug(DIGIKAM_METAENGINE_LOG) << "ExifTool Json map size:" << metadataMap.size();
0068 
0069             for (QVariantMap::const_iterator it = metadataMap.constBegin() ;
0070                 it != metadataMap.constEnd() ; ++it)
0071             {
0072                 QString     tagNameExifTool;
0073                 QString     tagType;
0074                 QStringList sections  = it.key().split(QLatin1Char(':'));
0075 
0076                 if      (sections.size() == 6)      // With ExifTool > 12.00 (at least under Windows or MacOS), groups are return with 6 sections.
0077                 {
0078                     tagNameExifTool = QString::fromLatin1("%1.%2.%3.%4")
0079                                           .arg(sections[0])
0080                                           .arg(sections[1])
0081                                           .arg(sections[2])
0082                                           .arg(sections[5]);
0083                     tagType         = sections[4];
0084                 }
0085                 else if (sections.size() == 5)      // ExifTool 12.00 under Linux return 5 or 4 sections.
0086                 {
0087                     tagNameExifTool = QString::fromLatin1("%1.%2.%3.%4")
0088                                           .arg(sections[0])
0089                                           .arg(sections[1])
0090                                           .arg(sections[2])
0091                                           .arg(sections[4]);
0092                     tagType         = sections[3];
0093                 }
0094                 else if (sections.size() == 4)
0095                 {
0096                     tagNameExifTool = QString::fromLatin1("%1.%2.%3.%4")
0097                                           .arg(sections[0])
0098                                           .arg(sections[1])
0099                                           .arg(sections[2])
0100                                           .arg(sections[3]);
0101                 }
0102                 else if (sections[0] == QLatin1String("SourceFile"))
0103                 {
0104                     d->currentPath = it.value().toString();
0105                     continue;
0106                 }
0107                 else
0108                 {
0109                     continue;
0110                 }
0111 
0112                 QVariantMap propsMap = it.value().toMap();
0113                 QString data;
0114 
0115 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
0116 
0117                 if (propsMap.find(QLatin1String("val")).value().typeId() == QVariant::List)
0118 
0119 #else
0120 
0121                 if (propsMap.find(QLatin1String("val")).value().type() == QVariant::List)
0122 
0123 #endif
0124 
0125                 {
0126                     QStringList list = propsMap.find(QLatin1String("val")).value().toStringList();
0127                     data             = list.join(QLatin1String(", "));
0128                 }
0129                 else
0130                 {
0131                     data             = propsMap.find(QLatin1String("val")).value().toString();
0132                 }
0133 
0134                 QString desc         = propsMap.find(QLatin1String("desc")).value().toString();
0135 
0136                 // Optional numerical value extraction, if any
0137 
0138                 QString num;
0139                 QVariantMap::iterator it2 = propsMap.find(QLatin1String("num"));
0140 
0141                 if (it2 != propsMap.end())
0142                 {
0143                     num = it2.value().toString();
0144                 }
0145 /*
0146                 qCDebug(DIGIKAM_METAENGINE_LOG) << "ExifTool json property:" << tagNameExifTool << data;
0147 */
0148 
0149                 if (data.startsWith(QLatin1String("(Binary data ")) &&
0150                     data.endsWith(QLatin1String(", use -b option to extract)")))
0151                 {
0152                     data = data.section(QLatin1Char(','), 0, 0);
0153                     data.remove(QLatin1Char('('));
0154                 }
0155 
0156                 if (exifToolData.contains(tagNameExifTool))
0157                 {
0158                     QString existData = exifToolData[tagNameExifTool][0].toString();
0159                     existData        += QLatin1String(", ") + data;
0160                     exifToolData[tagNameExifTool][0] = existData;
0161                 }
0162                 else
0163                 {
0164                     exifToolData.insert(tagNameExifTool, QVariantList()
0165                                                             << data        // ExifTool Raw data as string.
0166                                                             << tagType     // ExifTool data type.
0167                                                             << desc        // ExifTool tag description.
0168                                                             << num);       // ExifTool numeral value if any.
0169                 }
0170             }
0171 
0172             break;
0173         }
0174 
0175         case ExifToolProcess::LOAD_CHUNKS:
0176         {
0177             qCDebug(DIGIKAM_METAENGINE_LOG) << "EXV chunk size:" << result.output.size();
0178 
0179             exifToolData.insert(QLatin1String("EXV"), QVariantList() << result.output);     // Exv chunk as bytearray.
0180             break;
0181         }
0182 
0183         case ExifToolProcess::APPLY_CHANGES:
0184         {
0185             printExifToolOutput(result.output);
0186             break;
0187         }
0188 
0189         case ExifToolProcess::APPLY_CHANGES_EXV:
0190         {
0191             printExifToolOutput(result.output);
0192             break;
0193         }
0194 
0195         case ExifToolProcess::APPLY_METADATA_FILE:
0196         {
0197             printExifToolOutput(result.output);
0198             break;
0199         }
0200 
0201         case ExifToolProcess::COPY_TAGS:
0202         {
0203             printExifToolOutput(result.output);
0204             break;
0205         }
0206 
0207         case ExifToolProcess::TRANS_TAGS:
0208         {
0209             printExifToolOutput(result.output);
0210 
0211             if (!d->argsFile.isOpen() && d->argsFile.exists())
0212             {
0213                 d->argsFile.remove();
0214             }
0215 
0216             break;
0217         }
0218 
0219         case ExifToolProcess::READ_FORMATS:
0220         {
0221             // Remove first line
0222 
0223             QString out       = QString::fromUtf8(result.output).section(QLatin1Char('\n'), 1, -1);
0224 
0225             // Get extensions and descriptions as pair of strings
0226 
0227             QStringList lines = out.split(QLatin1Char('\n'), QT_SKIP_EMPTY_PARTS);
0228             QStringList lst;
0229             QString s;
0230 
0231             Q_FOREACH (const QString& ln, lines)
0232             {
0233                 s            = ln.simplified();
0234                 QString ext  = s.section(QLatin1Char(' '), 0, 0);
0235                 QString desc = s.section(QLatin1Char(' '), 1, -1);
0236                 lst << ext << desc;
0237             }
0238 
0239 
0240             exifToolData.insert(QLatin1String("READ_FORMATS"), QVariantList() << lst);
0241             break;
0242         }
0243 
0244         case ExifToolProcess::WRITE_FORMATS:
0245         {
0246             // Remove first line
0247 
0248             QString out       = QString::fromUtf8(result.output).section(QLatin1Char('\n'), 1, -1);
0249 
0250             // Get extensions and descriptions as pair of strings
0251 
0252             QStringList lines = out.split(QLatin1Char('\n'), QT_SKIP_EMPTY_PARTS);
0253             QStringList lst;
0254             QString s;
0255 
0256             Q_FOREACH (const QString& ln, lines)
0257             {
0258                 s            = ln.simplified();
0259                 QString ext  = s.section(QLatin1Char(' '), 0, 0);
0260                 QString desc = s.section(QLatin1Char(' '), 1, -1);
0261                 lst << ext << desc;
0262             }
0263 
0264             exifToolData.insert(QLatin1String("WRITE_FORMATS"), QVariantList() << lst);
0265             break;
0266         }
0267 
0268         case ExifToolProcess::TRANSLATIONS_LIST:
0269         {
0270             // Remove first line
0271 
0272             QString out       = QString::fromUtf8(result.output).section(QLatin1Char('\n'), 1, -1);
0273 
0274             // Get i18n list
0275 
0276             QStringList lines = out.split(QLatin1Char('\n'), QT_SKIP_EMPTY_PARTS);
0277             QStringList lst;
0278 
0279             Q_FOREACH (const QString& ln, lines)
0280             {
0281                 lst << ln.simplified().section(QLatin1String(" - "), 0, 0);
0282             }
0283 
0284             exifToolData.insert(QLatin1String("TRANSLATIONS_LIST"), QVariantList() << lst);
0285             break;
0286         }
0287 
0288         case ExifToolProcess::TAGS_DATABASE:
0289         {
0290             QString xml = QString::fromUtf8(result.output);
0291 
0292             QDomDocument doc;
0293 
0294             if (doc.setContent(xml))
0295             {
0296                 QDomElement docElem = doc.documentElement();
0297 
0298                 if (docElem.tagName() == QLatin1String("taginfo"))
0299                 {
0300                     for (QDomNode n1 = docElem.firstChild() ; !n1.isNull() ; n1 = n1.nextSibling())
0301                     {
0302                         QDomElement e1 = n1.toElement();
0303 
0304                         if (!e1.isNull())
0305                         {
0306                             if (e1.tagName() == QLatin1String("table"))                           // Top level group
0307                             {
0308                                 QString g0       = e1.attribute(QLatin1String("g0"));
0309                                 QString g1       = e1.attribute(QLatin1String("g1"));
0310                                 QString g2       = e1.attribute(QLatin1String("g2"));
0311                                 QString type;
0312                                 QString writable;
0313                                 QString tag;
0314                                 QString mainDesc;
0315                                 QString desc;
0316 
0317                                 for (QDomNode n2 = e1.firstChild() ; !n2.isNull() ; n2 = n2.nextSibling())
0318                                 {
0319                                     QDomElement e2 = n2.toElement();
0320 
0321                                     if (!e2.isNull())
0322                                     {
0323                                         if      (e2.tagName() == QLatin1String("desc"))          // Main description of group
0324                                         {
0325                                             if (e2.attribute(QLatin1String("lang")) == QLatin1String("en"))
0326                                             {
0327                                                 mainDesc = e2.text();
0328                                             }
0329 
0330                                             continue;
0331                                         }
0332                                         else if (e2.tagName() == QLatin1String("tag"))           // One tag from group
0333                                         {
0334                                             QString a1   = e2.attribute(QLatin1String("g1"));
0335                                             QString a2   = e2.attribute(QLatin1String("g2"));
0336                                             QString name = e2.attribute(QLatin1String("name"));
0337                                             tag          = QString::fromLatin1("%1.%2.%3.%4").arg(g0)
0338                                                                                              .arg(a1.isNull() ? g1 : a1)
0339                                                                                              .arg(a2.isNull() ? g2 : a2)
0340                                                                                              .arg(name);
0341                                             type         = e2.attribute(QLatin1String("type"));
0342                                             writable     = e2.attribute(QLatin1String("writable"));
0343 
0344                                             for (QDomNode n3 = e2.firstChild() ; !n3.isNull() ; n3 = n3.nextSibling())
0345                                             {
0346                                                 QDomElement e3 = n3.toElement();
0347 
0348                                                 if (!e3.isNull())
0349                                                 {
0350                                                     if (e3.tagName() == QLatin1String("desc"))  // Description of tag
0351                                                     {
0352                                                         if (e3.attribute(QLatin1String("lang")) == QLatin1String("en"))
0353                                                         {
0354                                                             desc = e3.text();
0355                                                             break;
0356                                                         }
0357                                                     }
0358                                                 }
0359                                             }
0360                                         }
0361                                     }
0362 
0363                                     exifToolData.insert(tag,
0364                                                         QVariantList()
0365                                                             << QString::fromLatin1("%1 - %2").arg(mainDesc).arg(desc)
0366                                                             << type
0367                                                             << writable
0368                                     );
0369                                 }
0370                             }
0371                         }
0372                     }
0373                 }
0374             }
0375 
0376             break;
0377         }
0378 
0379         case ExifToolProcess::VERSION_STRING:
0380         {
0381             QString out       = QString::fromUtf8(result.output);
0382             QStringList lines = out.split(QLatin1Char('\n'), QT_SKIP_EMPTY_PARTS);
0383 
0384             if (!lines.isEmpty())
0385             {
0386                 exifToolData.insert(QLatin1String("VERSION_STRING"), QVariantList() << lines.first());
0387             }
0388 
0389             break;
0390         }
0391 
0392         default:
0393         {
0394             break;
0395         }
0396     }
0397 
0398     qCDebug(DIGIKAM_METAENGINE_LOG) << "ExifTool parsed command for action" << d->actionString(result.cmdAction)
0399                                     << exifToolData.count() << "properties decoded";
0400 
0401     d->exifToolData = exifToolData;
0402 
0403     Q_EMIT signalExifToolDataAvailable();
0404 
0405     if (d->async)
0406     {
0407         Q_EMIT signalExifToolAsyncData(exifToolData);
0408     }
0409 }
0410 
0411 void ExifToolParser::errorOccurred(const ExifToolProcess::Result& result,
0412                                    QProcess::ProcessError error,
0413                                    const QString& description)
0414 {
0415     qCWarning(DIGIKAM_METAENGINE_LOG) << "ExifTool process for action"
0416                                       << d->actionString(result.cmdAction)
0417                                       << "exited with error:" << error;
0418 
0419     d->errorString = description;
0420 
0421     Q_EMIT signalExifToolDataAvailable();
0422 }
0423 
0424 void ExifToolParser::finished()
0425 {
0426     Q_EMIT signalExifToolDataAvailable();
0427 }
0428 
0429 void ExifToolParser::slotExifToolResult(int cmdId)
0430 {
0431     {
0432         QMutexLocker locker(&d->mutex);
0433 
0434         if (!d->asyncRunning.contains(cmdId))
0435         {
0436             return;
0437         }
0438 
0439         d->asyncRunning.removeAll(cmdId);
0440     }
0441 
0442     d->jumpToResultCommand(d->proc->getExifToolResult(cmdId), cmdId);
0443 }
0444 
0445 void ExifToolParser::setOutputStream(int cmdAction,
0446                                      const QByteArray& cmdOutputChannel,
0447                                      const QByteArray& /*cmdErrorChannel*/)
0448 {
0449     ExifToolProcess::Result result;
0450 
0451     result.cmdAction = cmdAction;
0452     result.output    = cmdOutputChannel;
0453 
0454     cmdCompleted(result);
0455 }
0456 
0457 } // namespace Digikam