1 /* ============================================================
2  *
3  * This file is a part of digiKam project
4  * https://www.digikam.org
5  *
6  * Date        : 2006-02-23
7  * Description : item metadata interface - tags helpers.
8  *
9  * Copyright (C) 2006-2021 by Gilles Caulier <caulier dot gilles at gmail dot com>
10  * Copyright (C) 2006-2013 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
11  * Copyright (C) 2011      by Leif Huhn <leif at dkstat dot com>
12  *
13  * This program is free software; you can redistribute it
14  * and/or modify it under the terms of the GNU General
15  * Public License as published by the Free Software Foundation;
16  * either version 2, or (at your option)
17  * any later version.
18  *
19  * This program is distributed in the hope that it will be useful,
20  * but WITHOUT ANY WARRANTY; without even the implied warranty of
21  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22  * GNU General Public License for more details.
23  *
24  * ============================================================ */
25 
26 #include "dmetadata.h"
27 
28 // Qt includes
29 
30 #include <QLocale>
31 
32 // Local includes
33 
34 #include "metaenginesettings.h"
35 #include "digikam_version.h"
36 #include "digikam_globals.h"
37 #include "digikam_debug.h"
38 
39 namespace Digikam
40 {
41 
getItemTagsPath(QStringList & tagsPath,const DMetadataSettingsContainer & settings) const42 bool DMetadata::getItemTagsPath(QStringList& tagsPath,
43                                  const DMetadataSettingsContainer& settings) const
44 {
45     foreach (const NamespaceEntry& entry, settings.getReadMapping(NamespaceEntry::DM_TAG_CONTAINER()))
46     {
47         if (entry.isDisabled)
48         {
49             continue;
50         }
51 
52         int index                                  = 0;
53         QString currentNamespace                   = entry.namespaceName;
54         NamespaceEntry::SpecialOptions currentOpts = entry.specialOpts;
55 
56         // Some namespaces have altenative paths, we must search them both
57 
58         switch (entry.subspace)
59         {
60             case NamespaceEntry::XMP:
61             {
62                 while (index < 2)
63                 {
64                     const std::string myStr = currentNamespace.toStdString();
65                     const char* nameSpace   = myStr.data();
66 
67                     switch (currentOpts)
68                     {
69                         case NamespaceEntry::TAG_XMPBAG:
70                         {
71                             tagsPath = getXmpTagStringBag(nameSpace, false);
72                             break;
73                         }
74 
75                         case NamespaceEntry::TAG_XMPSEQ:
76                         {
77                             tagsPath = getXmpTagStringSeq(nameSpace, false);
78                             break;
79                         }
80 
81                         case NamespaceEntry::TAG_ACDSEE:
82                         {
83                             getACDSeeTagsPath(tagsPath);
84                             break;
85                         }
86 
87                         // not used here, to suppress warnings
88                         case NamespaceEntry::COMMENT_XMP:
89                         case NamespaceEntry::COMMENT_ALTLANG:
90                         case NamespaceEntry::COMMENT_ATLLANGLIST:
91                         case NamespaceEntry::NO_OPTS:
92                         default:
93                         {
94                             break;
95                         }
96                     }
97 
98                     if      (!tagsPath.isEmpty())
99                     {
100                         if (entry.separator != QLatin1String("/"))
101                         {
102                             tagsPath.replaceInStrings(QLatin1String("/"), QLatin1String("\\"));
103                             tagsPath.replaceInStrings(entry.separator, QLatin1String("/"));
104                         }
105 
106                         return true;
107                     }
108                     else if (!entry.alternativeName.isEmpty())
109                     {
110                         currentNamespace = entry.alternativeName;
111                         currentOpts      = entry.secondNameOpts;
112                     }
113                     else
114                     {
115                         break; // no alternative namespace, go to next one
116                     }
117 
118                     index++;
119                 }
120 
121                 break;
122             }
123 
124             case NamespaceEntry::IPTC:
125             {
126                 // Try to get Tags Path list from IPTC keywords.
127                 // digiKam 0.9.x has used IPTC keywords to store Tags Path list.
128                 // This way is obsolete now since digiKam support XMP because IPTC
129                 // do not support UTF-8 and have strings size limitation. But we will
130                 // let the capability to import it for interworking issues.
131 
132                 tagsPath = getIptcKeywords();
133 
134                 if (!tagsPath.isEmpty())
135                 {
136                     // Work around to Imach tags path list hosted in IPTC with '.' as separator.
137 
138                     QStringList ntp = tagsPath.replaceInStrings(entry.separator, QLatin1String("/"));
139 
140                     // FIXME: The QStringList are always identical -> ntp == tagsPath.
141 
142                     if (ntp != tagsPath)
143                     {
144                         tagsPath = ntp;
145 
146                         //qCDebug(DIGIKAM_METAENGINE_LOG) << "Tags Path imported from Imach: " << tagsPath;
147                     }
148 
149                     return true;
150                 }
151 
152                 break;
153             }
154 
155             case NamespaceEntry::EXIF:
156             {
157                 // Try to get Tags Path list from Exif Windows keywords.
158 
159                 QString keyWords = getExifTagString("Exif.Image.XPKeywords", false);
160 
161                 if (!keyWords.isEmpty())
162                 {
163                     tagsPath = keyWords.split(entry.separator);
164 
165                     if (!tagsPath.isEmpty())
166                     {
167                         return true;
168                     }
169                 }
170 
171                 break;
172             }
173 
174             default:
175             {
176                 break;
177             }
178         }
179     }
180 
181     return false;
182 }
183 
setItemTagsPath(const QStringList & tagsPath,const DMetadataSettingsContainer & settings) const184 bool DMetadata::setItemTagsPath(const QStringList& tagsPath, const DMetadataSettingsContainer& settings) const
185 {
186     // NOTE : with digiKam 0.9.x, we have used IPTC Keywords for that.
187     // Now this way is obsolete, and we use XMP instead.
188 
189     // Set the new Tags path list. This is set, not add-to like setXmpKeywords.
190     // Unlike the other keyword fields, we do not need to merge existing entries.
191 
192     QList<NamespaceEntry> toWrite = settings.getReadMapping(NamespaceEntry::DM_TAG_CONTAINER());
193 
194     if (!settings.unifyReadWrite())
195     {
196         toWrite = settings.getWriteMapping(NamespaceEntry::DM_TAG_CONTAINER());
197     }
198 
199     for (const NamespaceEntry& entry : qAsConst(toWrite))
200     {
201         if (entry.isDisabled)
202         {
203             continue;
204         }
205 
206         QStringList newList;
207 
208         // Get keywords from tags path, for type tag
209 
210         for (const QString& tagPath : tagsPath)
211         {
212             newList.append(tagPath.split(QLatin1Char('/')).last());
213         }
214 
215         switch (entry.subspace)
216         {
217             case NamespaceEntry::XMP:
218             {
219                 if (!supportXmp())
220                 {
221                     continue;
222                 }
223 
224                 if (entry.tagPaths != NamespaceEntry::TAG)
225                 {
226                     newList = tagsPath;
227 
228                     if (entry.separator != QLatin1String("/"))
229                     {
230                         newList.replaceInStrings(QLatin1String("/"), entry.separator);
231                     }
232                 }
233 
234                 const std::string myStr = entry.namespaceName.toStdString();
235                 const char* nameSpace   = myStr.data();
236 
237                 switch (entry.specialOpts)
238                 {
239                     case NamespaceEntry::TAG_XMPSEQ:
240                     {
241                         if (!setXmpTagStringSeq(nameSpace, newList))
242                         {
243                             qCDebug(DIGIKAM_METAENGINE_LOG) << "Setting image paths failed" << nameSpace;
244                             return false;
245                         }
246 
247                         break;
248                     }
249 
250                     case NamespaceEntry::TAG_XMPBAG:
251                     {
252 
253                         if (!setXmpTagStringBag(nameSpace, newList))
254                         {
255                             qCDebug(DIGIKAM_METAENGINE_LOG) << "Setting image paths failed" << nameSpace;
256                             return false;
257                         }
258 
259                         break;
260                     }
261 
262                     case NamespaceEntry::TAG_ACDSEE:
263                     {
264 
265                         if (!setACDSeeTagsPath(newList))
266                         {
267                             qCDebug(DIGIKAM_METAENGINE_LOG) << "Setting image paths failed" << nameSpace;
268                             return false;
269                         }
270                     }
271 
272                     default:
273                     {
274                         break;
275                     }
276                 }
277 
278                 break;
279             }
280 
281             case NamespaceEntry::IPTC:
282 
283                 if (entry.namespaceName == QLatin1String("Iptc.Application2.Keywords"))
284                 {
285                     if (!setIptcKeywords(getIptcKeywords(), newList))
286                     {
287                         qCDebug(DIGIKAM_METAENGINE_LOG) << "Setting image paths failed" << entry.namespaceName;
288                         return false;
289                     }
290                 }
291 
292             default:
293             {
294                 break;
295             }
296         }
297     }
298 
299     return true;
300 }
301 
getACDSeeTagsPath(QStringList & tagsPath) const302 bool DMetadata::getACDSeeTagsPath(QStringList& tagsPath) const
303 {
304     // Try to get Tags Path list from ACDSee 8 Pro categories.
305 
306     QString xmlACDSee = getXmpTagString("Xmp.acdsee.categories", false);
307 
308     if (!xmlACDSee.isEmpty())
309     {
310         xmlACDSee.remove(QLatin1String("</Categories>"));
311         xmlACDSee.remove(QLatin1String("<Categories>"));
312         xmlACDSee.replace(QLatin1Char('/'), QLatin1Char('\\'));
313 
314         QStringList xmlTags = xmlACDSee.split(QLatin1String("<Category Assigned"));
315         int category        = 0;
316 
317         foreach (const QString& tags, xmlTags)
318         {
319             if (!tags.isEmpty())
320             {
321                 int count  = tags.count(QLatin1String("<\\Category>"));
322                 int length = tags.length() - (11 * count) - 5;
323 
324                 // cppcheck-suppress knownConditionTrueFalse
325                 if (category == 0)
326                 {
327                     tagsPath << tags.mid(5, length);
328                 }
329                 else
330                 {
331                     tagsPath.last().append(QLatin1Char('/') + tags.mid(5, length));
332                 }
333 
334                 category = category - count + 1;
335 
336                 if ((tags.left(5) == QLatin1String("=\"1\">")) && (category > 0))
337                 {
338                     tagsPath << tagsPath.last().section(QLatin1Char('/'), 0, category - 1);
339                 }
340             }
341         }
342 
343         if (!tagsPath.isEmpty())
344         {
345 /*
346             qCDebug(DIGIKAM_METAENGINE_LOG) << "Tags Path imported from ACDSee: " << tagsPath;
347 */
348             return true;
349         }
350     }
351 
352     return false;
353 }
354 
setACDSeeTagsPath(const QStringList & tagsPath) const355 bool DMetadata::setACDSeeTagsPath(const QStringList& tagsPath) const
356 {
357     // Converting Tags path list to ACDSee 8 Pro categories.
358 
359     const QString category(QLatin1String("<Category Assigned=\"%1\">"));
360     QStringList splitTags;
361     QStringList xmlTags;
362 
363     foreach (const QString& tags, tagsPath)
364     {
365         splitTags   = tags.split(QLatin1Char('/'));
366         int current = 0;
367 
368         for (int index = 0 ; index < splitTags.size() ; index++)
369         {
370             int tagIndex = xmlTags.indexOf(category.arg(0) + splitTags[index]);
371 
372             if (tagIndex == -1)
373             {
374                 tagIndex = xmlTags.indexOf(category.arg(1) + splitTags[index]);
375             }
376 
377             splitTags[index].insert(0, category.arg(index == splitTags.size() - 1 ? 1 : 0));
378 
379             if (tagIndex == -1)
380             {
381                 if (index == 0)
382                 {
383                     xmlTags << splitTags[index];
384                     xmlTags << QLatin1String("</Category>");
385                     current = xmlTags.size() - 1;
386                 }
387                 else
388                 {
389                     xmlTags.insert(current, splitTags[index]);
390                     xmlTags.insert(current + 1, QLatin1String("</Category>"));
391                     current++;
392                 }
393             }
394             else
395             {
396                 if (index == (splitTags.size() - 1))
397                 {
398                     xmlTags[tagIndex] = splitTags[index];
399                 }
400 
401                 current = tagIndex + 1;
402             }
403         }
404     }
405 
406     QString xmlACDSee = QLatin1String("<Categories>") + xmlTags.join(QLatin1String("")) + QLatin1String("</Categories>");
407 /*
408     qCDebug(DIGIKAM_METAENGINE_LOG) << "xmlACDSee" << xmlACDSee;
409 */
410     removeXmpTag("Xmp.acdsee.categories");
411 
412     if (!xmlTags.isEmpty())
413     {
414         if (!setXmpTagString("Xmp.acdsee.categories", xmlACDSee))
415         {
416             return false;
417         }
418     }
419 
420     return true;
421 }
422 
423 } // namespace Digikam
424