File indexing completed on 2025-01-05 03:56:23

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2006-02-23
0007  * Description : item metadata interface - comments helpers.
0008  *
0009  * SPDX-FileCopyrightText: 2006-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0010  * SPDX-FileCopyrightText: 2006-2013 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
0011  * SPDX-FileCopyrightText: 2011      by Leif Huhn <leif at dkstat dot com>
0012  *
0013  * SPDX-License-Identifier: GPL-2.0-or-later
0014  *
0015  * ============================================================ */
0016 
0017 #include "dmetadata.h"
0018 
0019 // Qt includes
0020 
0021 #include <QLocale>
0022 
0023 // Local includes
0024 
0025 #include "digikam_version.h"
0026 #include "digikam_globals.h"
0027 #include "digikam_debug.h"
0028 
0029 namespace Digikam
0030 {
0031 
0032 CaptionsMap DMetadata::getItemComments(const DMetadataSettingsContainer& settings) const
0033 {
0034     CaptionsMap            captionsMap;
0035     MetaEngine::AltLangMap authorsMap;
0036     MetaEngine::AltLangMap datesMap;
0037     MetaEngine::AltLangMap commentsMap;
0038     QString                commonAuthor;
0039 
0040     // In first try to get captions properties from digiKam XMP namespace
0041 
0042     if (supportXmp())
0043     {
0044         authorsMap = getXmpTagStringListLangAlt("Xmp.digiKam.CaptionsAuthorNames",    false);
0045         datesMap   = getXmpTagStringListLangAlt("Xmp.digiKam.CaptionsDateTimeStamps", false);
0046 
0047         if (authorsMap.isEmpty() && commonAuthor.isEmpty())
0048         {
0049             QString xmpAuthors = getXmpTagString("Xmp.acdsee.author", false);
0050 
0051             if (!xmpAuthors.isEmpty())
0052             {
0053                 authorsMap.insert(QLatin1String("x-default"), xmpAuthors);
0054             }
0055         }
0056     }
0057 
0058     // Get author name from IPTC DescriptionWriter. Private namespace above gets precedence.
0059 
0060     QVariant descriptionWriter = getMetadataField(MetadataInfo::DescriptionWriter);
0061 
0062     if (!descriptionWriter.isNull())
0063     {
0064         commonAuthor = descriptionWriter.toString();
0065     }
0066 
0067     // In first, we check XMP alternative language tags to create map of values.
0068 
0069     bool xmpSupported  = hasXmp();
0070     bool iptcSupported = hasIptc();
0071     bool exivSupported = hasExif();
0072 
0073     Q_FOREACH (const NamespaceEntry& entry, settings.getReadMapping(NamespaceEntry::DM_COMMENT_CONTAINER()))
0074     {
0075         if (entry.isDisabled)
0076         {
0077             continue;
0078         }
0079 
0080         QString commentString;
0081         const std::string myStr = entry.namespaceName.toStdString();
0082         const char* nameSpace   = myStr.data();
0083 
0084         switch (entry.subspace)
0085         {
0086             case NamespaceEntry::XMP:
0087             {
0088                 switch (entry.specialOpts)
0089                 {
0090                     case NamespaceEntry::COMMENT_ALTLANG:
0091                     {
0092                         if (xmpSupported)
0093                         {
0094                             commentString = getXmpTagStringLangAlt(nameSpace, QString(), false);
0095                         }
0096 
0097                         break;
0098                     }
0099 
0100                     case NamespaceEntry::COMMENT_ATLLANGLIST:
0101                     {
0102                         if (xmpSupported)
0103                         {
0104                             commentsMap = getXmpTagStringListLangAlt(nameSpace, false);
0105                         }
0106 
0107                         break;
0108                     }
0109 
0110                     case NamespaceEntry::COMMENT_XMP:
0111                     {
0112                         if (xmpSupported)
0113                         {
0114                             commentString = getXmpTagString(nameSpace, false);
0115                         }
0116 
0117                         break;
0118                     }
0119 
0120                     case NamespaceEntry::COMMENT_JPEG:
0121                     {
0122                         // Now, we trying to get image comments, outside of XMP.
0123                         // For JPEG, string is extracted from JFIF Comments section.
0124                         // For PNG, string is extracted from iTXt chunk.
0125 
0126                         commentString = getCommentsDecoded();
0127                     }
0128 
0129                     default:
0130                     {
0131                         break;
0132                     }
0133                 }
0134 
0135                 break;
0136             }
0137 
0138             case NamespaceEntry::IPTC:
0139             {
0140                 if (iptcSupported)
0141                 {
0142                     commentString = getIptcTagString(nameSpace, false);
0143                 }
0144 
0145                 break;
0146             }
0147 
0148             case NamespaceEntry::EXIF:
0149             {
0150                 if (exivSupported)
0151                 {
0152                     commentString = getExifTagComment(nameSpace);
0153 
0154                     if (commentString.isEmpty() && !entry.alternativeName.isEmpty())
0155                     {
0156                         const std::string altStr   = entry.alternativeName.toStdString();
0157                         const char* alternateSpace = altStr.data();
0158 
0159                         commentString              = getExifTagComment(alternateSpace);
0160                     }
0161                 }
0162 
0163                 break;
0164             }
0165 
0166             default:
0167             {
0168                 break;
0169             }
0170         }
0171 
0172         if (!commentString.isEmpty() && !commentString.trimmed().isEmpty())
0173         {
0174             commentsMap.insert(QLatin1String("x-default"), commentString);
0175             captionsMap.setData(commentsMap, authorsMap, commonAuthor, datesMap);
0176 
0177             return captionsMap;
0178         }
0179 
0180         if (!commentsMap.isEmpty())
0181         {
0182             captionsMap.setData(commentsMap, authorsMap, commonAuthor, datesMap);
0183 
0184             return captionsMap;
0185         }
0186     }
0187 
0188     return captionsMap;
0189 }
0190 
0191 bool DMetadata::setItemComments(const CaptionsMap& comments, const DMetadataSettingsContainer& settings) const
0192 {
0193 /*
0194     // See bug #139313: An empty string is also a valid value
0195 
0196     if (comments.isEmpty())
0197     {
0198           return false;
0199     }
0200 */
0201 
0202 /*
0203     qCDebug(DIGIKAM_METAENGINE_LOG) << getFilePath() << " ==> Comment: " << comments;
0204 */
0205     // In first, set captions properties to digiKam XMP namespace
0206 
0207     if (supportXmp())
0208     {
0209         if (!setXmpTagStringListLangAlt("Xmp.digiKam.CaptionsAuthorNames", comments.authorsList()))
0210         {
0211             return false;
0212         }
0213 
0214         if (!setXmpTagStringListLangAlt("Xmp.digiKam.CaptionsDateTimeStamps", comments.datesList()))
0215         {
0216             return false;
0217         }
0218     }
0219 
0220     QString defaultComment        = comments.value(QLatin1String("x-default")).caption;
0221     QList<NamespaceEntry> toWrite = settings.getReadMapping(NamespaceEntry::DM_COMMENT_CONTAINER());
0222 
0223     if (!settings.unifyReadWrite())
0224     {
0225         toWrite = settings.getWriteMapping(NamespaceEntry::DM_COMMENT_CONTAINER());
0226     }
0227 
0228     for (const NamespaceEntry& entry : qAsConst(toWrite))
0229     {
0230         if (entry.isDisabled)
0231         {
0232             continue;
0233         }
0234 
0235         const std::string myStr = entry.namespaceName.toStdString();
0236         const char* nameSpace   = myStr.data();
0237 
0238         switch (entry.subspace)
0239         {
0240             case NamespaceEntry::XMP:
0241             {
0242                 if (!supportXmp())
0243                 {
0244                     continue;
0245                 }
0246 
0247                 switch (entry.specialOpts)
0248                 {
0249                     case NamespaceEntry::COMMENT_ALTLANG:
0250                     {
0251                         removeXmpTag(nameSpace);
0252 
0253                         if (!defaultComment.isNull())
0254                         {
0255                             if (!setXmpTagStringLangAlt(nameSpace, defaultComment, QString()))
0256                             {
0257                                 qCDebug(DIGIKAM_METAENGINE_LOG) << "Setting image comment failed" << nameSpace;
0258                                 return false;
0259                             }
0260                         }
0261 
0262                         break;
0263                     }
0264 
0265                     case NamespaceEntry::COMMENT_ATLLANGLIST:
0266                     {
0267                         // NOTE : setXmpTagStringListLangAlt remove xmp tag before to add new values
0268 
0269                         if (!setXmpTagStringListLangAlt(nameSpace, comments.toAltLangMap()))
0270                         {
0271                             return false;
0272                         }
0273 
0274                         break;
0275                     }
0276 
0277                     case NamespaceEntry::COMMENT_XMP:
0278                     {
0279                         removeXmpTag(nameSpace);
0280 
0281                         if (!defaultComment.isNull())
0282                         {
0283                             if (!setXmpTagString(nameSpace, defaultComment))
0284                             {
0285                                 return false;
0286                             }
0287                         }
0288 
0289                         if (entry.namespaceName == QLatin1String("Xmp.acdsee.notes"))
0290                         {
0291                             QString defaultAuthor = comments.value(QLatin1String("x-default")).author;
0292                             removeXmpTag("Xmp.acdsee.author");
0293 
0294                             if (!defaultAuthor.isNull())
0295                             {
0296                                 if (!setXmpTagString("Xmp.acdsee.author", defaultAuthor))
0297                                 {
0298                                     return false;
0299                                 }
0300                             }
0301                         }
0302 
0303                         break;
0304                     }
0305 
0306                     case NamespaceEntry::COMMENT_JPEG:
0307                     {
0308                         // In first we set image comments, outside of Exif, XMP, and IPTC.
0309 
0310                         if (!setComments(defaultComment.toUtf8()))
0311                         {
0312                             return false;
0313                         }
0314 
0315                         break;
0316                     }
0317 
0318                     default:
0319                     {
0320                         break;
0321                     }
0322                 }
0323 
0324                 break;
0325             }
0326 
0327             case NamespaceEntry::IPTC:
0328             {
0329                 removeIptcTag(nameSpace);
0330 
0331                 if (!defaultComment.isNull())
0332                 {
0333                     defaultComment.truncate(2000);
0334 
0335                     if (!setIptcTagString(nameSpace, defaultComment))
0336                     {
0337                         return false;
0338                     }
0339                 }
0340 
0341                 break;
0342             }
0343 
0344             case NamespaceEntry::EXIF:
0345             {
0346                 if (entry.namespaceName == QLatin1String("Exif.Image.XPComment"))
0347                 {
0348                     if      (writeWithExifTool() && !defaultComment.isEmpty())
0349                     {
0350                         QByteArray xpData  = QByteArray((char*)defaultComment.utf16(), defaultComment.size() * 2);
0351                         xpData.append("\x00\x00");
0352 
0353                         if (!setExifTagData(nameSpace, xpData))
0354                         {
0355                             return false;
0356                         }
0357                     }
0358                     else if (removeExifTag(nameSpace))
0359                     {
0360                         qCDebug(DIGIKAM_METAENGINE_LOG) << "Remove image comment" << nameSpace;
0361                     }
0362                 }
0363                 else if (entry.namespaceName == QLatin1String("Exif.Photo.UserComment"))
0364                 {
0365                     if (!setExifComment(defaultComment, false))
0366                     {
0367                         return false;
0368                     }
0369                 }
0370                 else if (!setExifTagString(nameSpace, defaultComment))
0371                 {
0372                     return false;
0373                 }
0374 
0375                 break;
0376             }
0377 
0378             default:
0379             {
0380                 break;
0381             }
0382         }
0383     }
0384 
0385     return true;
0386 }
0387 
0388 CaptionsMap DMetadata::getItemTitles(const DMetadataSettingsContainer& settings) const
0389 {
0390     CaptionsMap            captionsMap;
0391     MetaEngine::AltLangMap authorsMap;
0392     MetaEngine::AltLangMap datesMap;
0393     MetaEngine::AltLangMap titlesMap;
0394     QString                commonAuthor;
0395 
0396     // Get author name from IPTC DescriptionWriter. Private namespace above gets precedence.
0397 
0398     QVariant descriptionWriter = getMetadataField(MetadataInfo::DescriptionWriter);
0399 
0400     if (!descriptionWriter.isNull())
0401     {
0402         commonAuthor = descriptionWriter.toString();
0403     }
0404 
0405     // In first, we check XMP alternative language tags to create map of values.
0406 
0407     bool xmpSupported  = hasXmp();
0408     bool iptcSupported = hasIptc();
0409     bool exivSupported = hasExif();
0410 
0411     Q_FOREACH (const NamespaceEntry& entry, settings.getReadMapping(NamespaceEntry::DM_TITLE_CONTAINER()))
0412     {
0413         if (entry.isDisabled)
0414         {
0415             continue;
0416         }
0417 
0418         QString titleString;
0419         const std::string myStr = entry.namespaceName.toStdString();
0420         const char* nameSpace   = myStr.data();
0421 
0422         switch (entry.subspace)
0423         {
0424             case NamespaceEntry::XMP:
0425             {
0426                 switch (entry.specialOpts)
0427                 {
0428                     case NamespaceEntry::COMMENT_ATLLANGLIST:
0429                     {
0430                         if (xmpSupported)
0431                         {
0432                             titlesMap = getXmpTagStringListLangAlt(nameSpace, false);
0433                         }
0434 
0435                         break;
0436                     }
0437 
0438                     case NamespaceEntry::COMMENT_XMP:
0439                     {
0440                         if (xmpSupported)
0441                         {
0442                             titleString = getXmpTagString(nameSpace, false);
0443                         }
0444 
0445                         break;
0446                     }
0447 
0448                     default:
0449                     {
0450                         break;
0451                     }
0452                 }
0453 
0454                 break;
0455             }
0456 
0457             case NamespaceEntry::IPTC:
0458             {
0459                 if (iptcSupported)
0460                 {
0461                     titleString = getIptcTagString(nameSpace, false);
0462                 }
0463 
0464                 break;
0465             }
0466 
0467             case NamespaceEntry::EXIF:
0468             {
0469                 if (exivSupported)
0470                 {
0471                     titleString = getExifTagString(nameSpace);
0472                 }
0473 
0474                 break;
0475             }
0476 
0477             default:
0478             {
0479                 break;
0480             }
0481         }
0482 
0483         if (!titleString.isEmpty() && !titleString.trimmed().isEmpty())
0484         {
0485             titlesMap.insert(QLatin1String("x-default"), titleString);
0486             captionsMap.setData(titlesMap, authorsMap, commonAuthor, datesMap);
0487 
0488             return captionsMap;
0489         }
0490 
0491         if (!titlesMap.isEmpty())
0492         {
0493             captionsMap.setData(titlesMap, authorsMap, commonAuthor, datesMap);
0494 
0495             return captionsMap;
0496         }
0497     }
0498 
0499     return captionsMap;
0500 }
0501 
0502 bool DMetadata::setItemTitles(const CaptionsMap& titles, const DMetadataSettingsContainer& settings) const
0503 {
0504 /*
0505     qCDebug(DIGIKAM_METAENGINE_LOG) << getFilePath() << " ==> Title: " << titles;
0506 */
0507     QString defaultTitle          = titles[QLatin1String("x-default")].caption;
0508     QList<NamespaceEntry> toWrite = settings.getReadMapping(NamespaceEntry::DM_TITLE_CONTAINER());
0509 
0510     if (!settings.unifyReadWrite())
0511     {
0512         toWrite = settings.getWriteMapping(NamespaceEntry::DM_TITLE_CONTAINER());
0513     }
0514 
0515     for (const NamespaceEntry& entry : qAsConst(toWrite))
0516     {
0517         if (entry.isDisabled)
0518         {
0519             continue;
0520         }
0521 
0522         const std::string myStr = entry.namespaceName.toStdString();
0523         const char* nameSpace   = myStr.data();
0524 
0525         switch (entry.subspace)
0526         {
0527             case NamespaceEntry::XMP:
0528             {
0529                 if (!supportXmp())
0530                 {
0531                     continue;
0532                 }
0533 
0534                 switch (entry.specialOpts)
0535                 {
0536                     case NamespaceEntry::COMMENT_ATLLANGLIST:
0537                     {
0538                         // NOTE : setXmpTagStringListLangAlt remove xmp tag before to add new values
0539 
0540                         if (!setXmpTagStringListLangAlt(nameSpace, titles.toAltLangMap()))
0541                         {
0542                             return false;
0543                         }
0544 
0545                         break;
0546                     }
0547 
0548                     case NamespaceEntry::COMMENT_XMP:
0549                     {
0550                         removeXmpTag(nameSpace);
0551 
0552                         if (!defaultTitle.isNull())
0553                         {
0554                             if (!setXmpTagString(nameSpace, defaultTitle))
0555                             {
0556                                 return false;
0557                             }
0558                         }
0559 
0560                         break;
0561                     }
0562 
0563                     default:
0564                     {
0565                         break;
0566                     }
0567                 }
0568 
0569                 break;
0570             }
0571 
0572             case NamespaceEntry::IPTC:
0573             {
0574                 removeIptcTag(nameSpace);
0575 
0576                 if (!defaultTitle.isNull())
0577                 {
0578                     // Note that Caption IPTC tag is limited to 64 char and ASCII charset.
0579 
0580                     defaultTitle.truncate(64);
0581 
0582                     // See if we have any non printable chars in there. If so, skip IPTC
0583                     // to avoid confusing other apps and web services with invalid tags.
0584 
0585                     bool hasInvalidChar = false;
0586 
0587                     for (QString::const_iterator c = defaultTitle.constBegin() ; c != defaultTitle.constEnd() ; ++c)
0588                     {
0589                         if (!(*c).isPrint())
0590                         {
0591                             hasInvalidChar = true;
0592                             break;
0593                         }
0594                     }
0595 
0596                     if (!hasInvalidChar)
0597                     {
0598                         if (!setIptcTagString(nameSpace, defaultTitle))
0599                         {
0600                             return false;
0601                         }
0602                     }
0603                 }
0604 
0605                 break;
0606             }
0607 
0608             case NamespaceEntry::EXIF:
0609             {
0610                 if (entry.namespaceName == QLatin1String("Exif.Image.XPTitle"))
0611                 {
0612                     if      (writeWithExifTool() && !defaultTitle.isEmpty())
0613                     {
0614                         QByteArray xpData  = QByteArray((char*)defaultTitle.utf16(), defaultTitle.size() * 2);
0615                         xpData.append("\x00\x00");
0616 
0617                         if (!setExifTagData(nameSpace, xpData))
0618                         {
0619                             return false;
0620                         }
0621                     }
0622                     else if (removeExifTag(nameSpace))
0623                     {
0624                         qCDebug(DIGIKAM_METAENGINE_LOG) << "Remove image title" << nameSpace;
0625                     }
0626                 }
0627                 else if (!setExifTagString(nameSpace, defaultTitle))
0628                 {
0629                     return false;
0630                 }
0631 
0632                 break;
0633             }
0634 
0635             default:
0636             {
0637                 break;
0638             }
0639         }
0640     }
0641 
0642     return true;
0643 }
0644 
0645 MetaEngine::AltLangMap DMetadata::toAltLangMap(const QVariant& var)
0646 {
0647     MetaEngine::AltLangMap map;
0648 
0649     if (var.isNull())
0650     {
0651         return map;
0652     }
0653 
0654 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
0655 
0656     switch (var.typeId())
0657 
0658 #else
0659 
0660     switch (var.type())
0661 
0662 #endif
0663 
0664     {
0665         case QVariant::String:
0666         {
0667             map.insert(QLatin1String("x-default"), var.toString());
0668             break;
0669         }
0670 
0671         case QVariant::Map:
0672         {
0673             QMap<QString, QVariant> varMap = var.toMap();
0674 
0675             for (QMap<QString, QVariant>::const_iterator it = varMap.constBegin() ; it != varMap.constEnd() ; ++it)
0676             {
0677                 map.insert(it.key(), it.value().toString());
0678             }
0679 
0680             break;
0681         }
0682 
0683         default:
0684         {
0685             break;
0686         }
0687     }
0688 
0689     return map;
0690 }
0691 
0692 } // namespace Digikam