1 // SPDX-FileCopyrightText: 2003-2020 The KPhotoAlbum Development Team
2 // SPDX-FileCopyrightText: 2021 Johannes Zarl-Zierl <johannes@zarl-zierl.at>
3 //
4 // SPDX-License-Identifier: GPL-2.0-or-later
5 
6 #include "Info.h"
7 
8 #include <kpabase/Logging.h>
9 
10 #include <kpabase/FileName.h>
11 #include <kpabase/SettingsData.h>
12 #include <kpabase/StringSet.h>
13 
14 #include <QFile>
15 #include <QFileInfo>
16 #include <QTextCodec>
17 #include <exiv2/exv_conf.h>
18 #include <exiv2/image.hpp>
19 #include <exiv2/version.hpp>
20 
21 using namespace Exif;
22 
23 namespace
24 {
cStringWithEncoding(const char * c_str,const QString & charset)25 QString cStringWithEncoding(const char *c_str, const QString &charset)
26 {
27     QTextCodec *codec = QTextCodec::codecForName(charset.toLatin1());
28     if (!codec)
29         codec = QTextCodec::codecForLocale();
30     return codec->toUnicode(c_str);
31 }
32 
33 } // namespace
34 
35 Info *Info::s_instance = nullptr;
36 
info(const DB::FileName & fileName,StringSet wantedKeys,bool returnFullExifName,const QString & charset)37 QMap<QString, QStringList> Info::info(const DB::FileName &fileName, StringSet wantedKeys, bool returnFullExifName, const QString &charset)
38 {
39     QMap<QString, QStringList> result;
40 
41     try {
42         Metadata data = metadata(exifInfoFile(fileName));
43 
44         for (Exiv2::ExifData::const_iterator i = data.exif.begin(); i != data.exif.end(); ++i) {
45             QString key = QString::fromLocal8Bit(i->key().c_str());
46             m_keys.insert(key);
47 
48             if (wantedKeys.contains(key)) {
49                 QString text = key;
50                 if (!returnFullExifName)
51                     text = key.split(QLatin1String(".")).last();
52 
53                 std::ostringstream stream;
54                 stream << *i;
55                 QString str(cStringWithEncoding(stream.str().c_str(), charset));
56                 result[text] += str;
57             }
58         }
59 
60         for (Exiv2::IptcData::const_iterator i = data.iptc.begin(); i != data.iptc.end(); ++i) {
61             QString key = QString::fromLatin1(i->key().c_str());
62             m_keys.insert(key);
63 
64             if (wantedKeys.contains(key)) {
65                 QString text = key;
66                 if (!returnFullExifName)
67                     text = key.split(QString::fromLatin1(".")).last();
68 
69                 std::ostringstream stream;
70                 stream << *i;
71                 QString str(cStringWithEncoding(stream.str().c_str(), charset));
72                 result[text] += str;
73             }
74         }
75     } catch (...) {
76     }
77 
78     return result;
79 }
80 
instance()81 Info *Info::instance()
82 {
83     if (!s_instance)
84         s_instance = new Info;
85     return s_instance;
86 }
87 
availableKeys()88 StringSet Info::availableKeys()
89 {
90     return m_keys;
91 }
92 
infoForViewer(const DB::FileName & fileName,const QString & charset)93 QMap<QString, QStringList> Info::infoForViewer(const DB::FileName &fileName, const QString &charset)
94 {
95     return info(fileName, ::Settings::SettingsData::instance()->exifForViewer(), false, charset);
96 }
97 
infoForDialog(const DB::FileName & fileName,const QString & charset)98 QMap<QString, QStringList> Info::infoForDialog(const DB::FileName &fileName, const QString &charset)
99 {
100     auto keys = ::Settings::SettingsData::instance()->exifForDialog();
101     if (keys.isEmpty())
102         keys = standardKeys();
103     return info(fileName, keys, true, charset);
104 }
105 
standardKeys()106 StringSet Info::standardKeys()
107 {
108     static StringSet res;
109 
110     if (!res.empty())
111         return res;
112 
113     QList<const Exiv2::TagInfo *> tags;
114     std::ostringstream s;
115 
116     const Exiv2::GroupInfo *gi = Exiv2::ExifTags::groupList();
117     while (gi->tagList_ != nullptr) {
118         Exiv2::TagListFct tl = gi->tagList_;
119         const Exiv2::TagInfo *ti = tl();
120 
121         while (ti->tag_ != 0xFFFF) {
122             tags << ti;
123             ++ti;
124         }
125         ++gi;
126     }
127 
128     for (QList<const Exiv2::TagInfo *>::iterator it = tags.begin(); it != tags.end(); ++it) {
129         while ((*it)->tag_ != 0xffff) {
130             res.insert(QString::fromLatin1(Exiv2::ExifKey(**it).key().c_str()));
131             ++(*it);
132         }
133     }
134 
135     // IPTC tags use yet another format...
136     Exiv2::IptcDataSets::dataSetList(s);
137 
138     QStringList lines = QString(QLatin1String(s.str().c_str())).split(QChar::fromLatin1('\n'));
139     for (QStringList::const_iterator it = lines.constBegin(); it != lines.constEnd(); ++it) {
140         if (it->isEmpty())
141             continue;
142         QStringList fields = it->split(QChar::fromLatin1('\t'));
143         if (fields.size() == 7) {
144             QString id = fields[4];
145             if (id.endsWith(QChar::fromLatin1(',')))
146                 id.chop(1);
147             res.insert(id);
148         } else {
149             fields = it->split(QLatin1String(", "));
150             if (fields.size() >= 11) {
151                 res.insert(fields[8]);
152             } else {
153                 qCWarning(ExifLog) << "Unparsable output from exiv2 library: " << *it;
154                 continue;
155             }
156         }
157     }
158     return res;
159 }
160 
Info()161 Info::Info()
162 {
163     m_keys = standardKeys();
164 }
165 
writeExifInfoToFile(const DB::FileName & srcName,const QString & destName,const QString & imageDescription)166 void Exif::writeExifInfoToFile(const DB::FileName &srcName, const QString &destName, const QString &imageDescription)
167 {
168     // Load Exif from source image
169     Exiv2::Image::AutoPtr image = Exiv2::ImageFactory::open(QFile::encodeName(srcName.absolute()).data());
170     image->readMetadata();
171     Exiv2::ExifData data = image->exifData();
172 
173     // Modify Exif information from database.
174     data["Exif.Image.ImageDescription"] = imageDescription.toLocal8Bit().data();
175 
176     image = Exiv2::ImageFactory::open(QFile::encodeName(destName).data());
177     image->setExifData(data);
178     image->writeMetadata();
179 }
180 
181 /**
182  * Some Canon cameras stores Exif info in files ending in .thm, so we need to use those files for fetching Exif info
183  * if they exists.
184  */
exifInfoFile(const DB::FileName & fileName)185 DB::FileName Exif::Info::exifInfoFile(const DB::FileName &fileName)
186 {
187     QString dirName = QFileInfo(fileName.relative()).path();
188     QString baseName = QFileInfo(fileName.relative()).baseName();
189     DB::FileName name = DB::FileName::fromRelativePath(dirName + QString::fromLatin1("/") + baseName + QString::fromLatin1(".thm"));
190     if (name.exists())
191         return name;
192 
193     name = DB::FileName::fromRelativePath(dirName + QString::fromLatin1("/") + baseName + QString::fromLatin1(".THM"));
194     if (name.exists())
195         return name;
196 
197     return fileName;
198 }
199 
metadata(const DB::FileName & fileName)200 Exif::Metadata Exif::Info::metadata(const DB::FileName &fileName)
201 {
202     try {
203         Exif::Metadata result;
204         Exiv2::Image::AutoPtr image = Exiv2::ImageFactory::open(QFile::encodeName(fileName.absolute()).data());
205         Q_ASSERT(image.get() != nullptr);
206         image->readMetadata();
207         result.exif = image->exifData();
208         result.iptc = image->iptcData();
209         result.comment = image->comment();
210         return result;
211     } catch (...) {
212     }
213     return Exif::Metadata();
214 }
215 
216 // vi:expandtab:tabstop=4 shiftwidth=4:
217