KFileMetaData

exiv2extractor.cpp
1/*
2 SPDX-FileCopyrightText: 2012 Vishesh Handa <me@vhanda.in>
3
4 SPDX-License-Identifier: LGPL-2.1-or-later
5*/
6
7
8#include "datetimeparser_p.h"
9#include "exiv2extractor.h"
10#include <QTimeZone>
11#include <cmath>
12#include <exiv2/exiv2.hpp>
13
14using namespace KFileMetaData;
15
16namespace {
17void add(ExtractionResult* result, const Exiv2::ExifData& data,
18 Property::Property prop, const Exiv2::ExifKey& key,
19 QMetaType::Type type);
20
21double fetchGpsDouble(const Exiv2::ExifData& data, const Exiv2::ExifKey& key);
22double fetchGpsAltitude(const Exiv2::ExifData& data);
23QByteArray fetchByteArray(const Exiv2::ExifData& data, const Exiv2::ExifKey& key);
24}
25
26Exiv2Extractor::Exiv2Extractor(QObject* parent)
27 : ExtractorPlugin(parent)
28{
29#ifdef EXV_ENABLE_BMFF
30#if !(EXIV2_TEST_VERSION(0, 28, 3))
31// Enabled by default since 0.28.3 when Exiv2 was built with BMFF support
32 Exiv2::enableBMFF(true);
33#endif
34#endif
35}
36
37namespace
38{
39static const QStringList supportedMimeTypes = {
40 QStringLiteral("image/bmp"),
41 QStringLiteral("image/gif"),
42 QStringLiteral("image/jp2"),
43 QStringLiteral("image/jpeg"),
44 QStringLiteral("image/pgf"),
45 QStringLiteral("image/png"),
46 QStringLiteral("image/tiff"),
47 QStringLiteral("image/webp"),
48#ifdef EXV_ENABLE_BMFF
49 QStringLiteral("image/avif"),
50 QStringLiteral("image/heif"),
51 QStringLiteral("image/jxl"),
52 QStringLiteral("image/x-canon-cr3"),
53#endif
54 QStringLiteral("image/x-exv"),
55 QStringLiteral("image/x-canon-cr2"),
56 QStringLiteral("image/x-canon-crw"),
57 QStringLiteral("image/x-fuji-raf"),
58 QStringLiteral("image/x-minolta-mrw"),
59 QStringLiteral("image/x-nikon-nef"),
60 QStringLiteral("image/x-olympus-orf"),
61 QStringLiteral("image/x-panasonic-rw2"),
62 QStringLiteral("image/x-pentax-pef"),
63 QStringLiteral("image/x-photoshop"),
64 QStringLiteral("image/x-samsung-srw"),
65 QStringLiteral("image/x-tga"),
66};
67
68QString toString(const Exiv2::Value& value)
69{
70 const std::string str = value.toString();
71 return QString::fromUtf8(str.c_str(), str.length());
72}
73
74QVariant toVariantDateTime(const Exiv2::Value& value)
75{
76 if (value.typeId() == Exiv2::asciiString) {
77 QDateTime val = Parser::dateTimeFromString(QString::fromStdString(value.toString()));
78 if (val.isValid()) {
79 // Datetime is stored in exif as local time.
81 return QVariant(val);
82 }
83 }
84
85 return QVariant();
86}
87
88QVariant toVariantLong(const Exiv2::Value& value)
89{
90 if (value.typeId() == Exiv2::unsignedLong || value.typeId() == Exiv2::signedLong) {
91#if EXIV2_TEST_VERSION(0,28,0)
92 qlonglong val = value.toInt64();
93#else
94 qlonglong val = value.toLong();
95#endif
96 return QVariant(val);
97 }
98
99 QString str(toString(value));
100 bool ok = false;
101 int val = str.toInt(&ok);
102 if (ok) {
103 return QVariant(val);
104 }
105
106 return QVariant();
107}
108
109QVariant toVariantDouble(const Exiv2::Value& value)
110{
111 if (value.typeId() == Exiv2::tiffFloat || value.typeId() == Exiv2::tiffDouble
112 || value.typeId() == Exiv2::unsignedRational || value.typeId() == Exiv2::signedRational) {
113 return QVariant(static_cast<double>(value.toFloat()));
114 }
115
116 QString str(toString(value));
117 bool ok = false;
118 double val = str.toDouble(&ok);
119 if (ok) {
120 return QVariant(val);
121 }
122
123 return QVariant();
124}
125
126QVariant toVariantString(const Exiv2::Value& value)
127{
128 QString str = toString(value);
129 if (!str.isEmpty()) {
130 return QVariant(str);
131 }
132
133 return QVariant();
134}
135
136QVariant toVariant(const Exiv2::Value& value, QMetaType::Type type) {
137 if (value.count() == 0) {
138 return QVariant();
139 }
140 switch (type) {
141 case QMetaType::Int:
142 return toVariantLong(value);
143
145 return toVariantDateTime(value);
146
148 return toVariantDouble(value);
149
151 default:
152 return toVariantString(value);
153 }
154}
155}
156
157QStringList Exiv2Extractor::mimetypes() const
158{
159 return supportedMimeTypes;
160}
161
162void Exiv2Extractor::extract(ExtractionResult* result)
163{
164 using namespace std::string_literals;
165
166 QByteArray arr = result->inputUrl().toUtf8();
167 std::string fileString(arr.data(), arr.length());
168
169#if EXIV2_TEST_VERSION(0, 28, 0)
170 Exiv2::Image::UniquePtr image;
171#else
172 Exiv2::Image::AutoPtr image;
173#endif
174 try {
175 image = Exiv2::ImageFactory::open(fileString);
176 } catch (const std::exception&) {
177 return;
178 }
179 if (!image.get()) {
180 return;
181 }
182
183 try {
184 image->readMetadata();
185 } catch (const std::exception&) {
186 return;
187 }
188 result->addType(Type::Image);
189
190 if (!(result->inputFlags() & ExtractionResult::ExtractMetaData)) {
191 return;
192 }
193
194 if (image->pixelHeight()) {
195 result->add(Property::Height, image->pixelHeight());
196 }
197
198 if (image->pixelWidth()) {
199 result->add(Property::Width, image->pixelWidth());
200 }
201
202 std::string comment = image->comment();
203 if (!comment.empty()) {
204 result->add(Property::Comment, QString::fromUtf8(comment.c_str(), comment.length()));
205 }
206
207 const Exiv2::ExifData& data = image->exifData();
208
209 using EK = Exiv2::ExifKey;
210 add(result, data, Property::Manufacturer, EK{"Exif.Image.Make"s}, QMetaType::QString);
211 add(result, data, Property::Model, EK{"Exif.Image.Model"s}, QMetaType::QString);
212 add(result, data, Property::Description, EK{"Exif.Image.ImageDescription"s}, QMetaType::QString);
213 add(result, data, Property::Artist, EK{"Exif.Image.Artist"s}, QMetaType::QString);
214 add(result, data, Property::Copyright, EK{"Exif.Image.Copyright"s}, QMetaType::QString);
215 add(result, data, Property::Generator, EK{"Exif.Image.Software"s}, QMetaType::QString);
216 add(result, data, Property::ImageDateTime, EK{"Exif.Image.DateTime"s}, QMetaType::QDateTime);
217 add(result, data, Property::ImageOrientation, EK{"Exif.Image.Orientation"s}, QMetaType::Int);
218 add(result, data, Property::PhotoFlash, EK{"Exif.Photo.Flash"s}, QMetaType::Int);
219 add(result, data, Property::PhotoPixelXDimension, EK{"Exif.Photo.PixelXDimension"s}, QMetaType::Int);
220 add(result, data, Property::PhotoPixelYDimension, EK{"Exif.Photo.PixelYDimension"s}, QMetaType::Int);
221 add(result, data, Property::PhotoDateTimeOriginal, EK{"Exif.Photo.DateTimeOriginal"s}, QMetaType::QDateTime);
222 add(result, data, Property::PhotoFocalLength, EK{"Exif.Photo.FocalLength"s}, QMetaType::Double);
223 add(result, data, Property::PhotoFocalLengthIn35mmFilm, EK{"Exif.Photo.FocalLengthIn35mmFilm"s}, QMetaType::Double);
224 add(result, data, Property::PhotoExposureTime, EK{"Exif.Photo.ExposureTime"s}, QMetaType::Double);
225 add(result, data, Property::PhotoExposureBiasValue, EK{"Exif.Photo.ExposureBiasValue"s}, QMetaType::Double);
226 add(result, data, Property::PhotoFNumber, EK{"Exif.Photo.FNumber"s}, QMetaType::Double);
227 add(result, data, Property::PhotoApertureValue, EK{"Exif.Photo.ApertureValue"s}, QMetaType::Double);
228 add(result, data, Property::PhotoWhiteBalance, EK{"Exif.Photo.WhiteBalance"s}, QMetaType::Int);
229 add(result, data, Property::PhotoMeteringMode, EK{"Exif.Photo.MeteringMode"s}, QMetaType::Int);
230 add(result, data, Property::PhotoISOSpeedRatings, EK{"Exif.Photo.ISOSpeedRatings"s}, QMetaType::Int);
231 add(result, data, Property::PhotoSaturation, EK{"Exif.Photo.Saturation"s}, QMetaType::Int);
232 add(result, data, Property::PhotoSharpness, EK{"Exif.Photo.Sharpness"s}, QMetaType::Int);
233 // https://exiv2.org/tags.html "Exif.Photo.ImageTitle" not natively supported, use tag value
234 add(result, data, Property::Title, EK{0xa436, "Photo"s}, QMetaType::QString);
235
236 double latitude = fetchGpsDouble(data, EK{"Exif.GPSInfo.GPSLatitude"s});
237 double longitude = fetchGpsDouble(data, EK{"Exif.GPSInfo.GPSLongitude"s});
238 double altitude = fetchGpsAltitude(data);
239
240 QByteArray latRef = fetchByteArray(data, EK{"Exif.GPSInfo.GPSLatitudeRef"s});
241 if (!latRef.isEmpty() && latRef[0] == 'S') {
242 latitude *= -1;
243 }
244
245 QByteArray longRef = fetchByteArray(data, EK{"Exif.GPSInfo.GPSLongitudeRef"s});
246 if (!longRef.isEmpty() && longRef[0] == 'W') {
247 longitude *= -1;
248 }
249
250 if (!std::isnan(latitude)) {
251 result->add(Property::PhotoGpsLatitude, latitude);
252 }
253
254 if (!std::isnan(longitude)) {
255 result->add(Property::PhotoGpsLongitude, longitude);
256 }
257
258 if (!std::isnan(altitude)) {
259 result->add(Property::PhotoGpsAltitude, altitude);
260 }
261
262 const Exiv2::XmpData& xmpData = image->xmpData();
263 for (const auto& entry : xmpData) {
264 if (entry.groupName() != "dc"s) {
265 continue;
266 }
267
268 std::map<std::string, Property::Property> propMap = {
269 { "subject"s, Property::Subject },
270 { "title"s, Property::Title},
271 { "description"s, Property::Description},
272 };
273 if (auto type = propMap.find(entry.tagName()); type != propMap.end()) {
274 auto xmpType = Exiv2::XmpValue::xmpArrayType(entry.value().typeId());
275 size_t limit = ((xmpType == Exiv2::XmpValue::xaBag) || (xmpType == Exiv2::XmpValue::xaSeq)) ? entry.count() : 1;
276 for (size_t i = 0; i < limit; i++) {
277 auto value = QString::fromStdString(entry.toString(i));
278 if (!value.isEmpty()) {
279 result->add(type->second, value);
280 }
281 }
282 }
283 }
284}
285
286namespace {
287void add(ExtractionResult* result, const Exiv2::ExifData& data,
288 Property::Property prop, const Exiv2::ExifKey& key,
289 QMetaType::Type type)
290{
291 Exiv2::ExifData::const_iterator it = data.findKey(key);
292 if (it != data.end()) {
293 QVariant value = toVariant(it->value(), type);
294 if (!value.isNull()) {
295 result->add(prop, value);
296 }
297 }
298}
299
300double fetchGpsDouble(const Exiv2::ExifData& data, const Exiv2::ExifKey& key)
301{
302 Exiv2::ExifData::const_iterator it = data.findKey(key);
303 if (it != data.end() && it->count() == 3) {
304 double n = 0.0;
305 double d = 0.0;
306
307 n = (*it).toRational(0).first;
308 d = (*it).toRational(0).second;
309
310 if (d == 0.0) {
311 return std::numeric_limits<double>::quiet_NaN();
312 }
313
314 double deg = n / d;
315
316 n = (*it).toRational(1).first;
317 d = (*it).toRational(1).second;
318
319 if (d == 0.0) {
320 return deg;
321 }
322
323 double min = n / d;
324 if (min != -1.0) {
325 deg += min / 60.0;
326 }
327
328 n = (*it).toRational(2).first;
329 d = (*it).toRational(2).second;
330
331 if (d == 0.0) {
332 return deg;
333 }
334
335 double sec = n / d;
336 if (sec != -1.0) {
337 deg += sec / 3600.0;
338 }
339
340 return deg;
341 }
342
343 return std::numeric_limits<double>::quiet_NaN();
344}
345
346double fetchGpsAltitude(const Exiv2::ExifData& data)
347{
348 using namespace std::string_literals;
349
350 double alt = std::numeric_limits<double>::quiet_NaN();
351 Exiv2::ExifData::const_iterator it = data.findKey(Exiv2::ExifKey("Exif.GPSInfo.GPSAltitude"s));
352 if (it != data.end() && it->count() > 0 &&
353 (it->value().typeId() == Exiv2::unsignedRational || it->value().typeId() == Exiv2::signedRational)) {
354 auto ratio = it->value().toRational();
355 if (ratio.second == 0) {
356 return alt;
357 }
358 it = data.findKey(Exiv2::ExifKey("Exif.GPSInfo.GPSAltitudeRef"s));
359 if (it != data.end() && it->count() > 0 &&
360 (it->value().typeId() == Exiv2::unsignedByte || it->value().typeId() == Exiv2::signedByte)) {
361#if EXIV2_TEST_VERSION(0,28,0)
362 auto altRef = it->value().toInt64();
363#else
364 auto altRef = it->value().toLong();
365#endif
366 if (altRef) {
367 alt = -1.0 * ratio.first / ratio.second;
368 } else {
369 alt = 1.0 * ratio.first / ratio.second;
370 }
371 }
372 }
373 return alt;
374}
375QByteArray fetchByteArray(const Exiv2::ExifData& data, const Exiv2::ExifKey& key)
376{
377 Exiv2::ExifData::const_iterator it = data.findKey(key);
378 if (it != data.end() && it->count() > 0) {
379 std::string str = it->value().toString();
380 return QByteArray(str.c_str(), str.size());
381 }
382
383 return QByteArray();
384}
385} // <anonymous> namespace
386
387#include "moc_exiv2extractor.cpp"
The ExtractionResult class is where all the data extracted by the indexer is saved.
QString inputUrl() const
The input URL which the plugins will use to locate the file.
virtual void addType(Type::Type type)=0
This function is called by the plugins.
virtual void add(Property::Property property, const QVariant &value)=0
This function is called by the plugins when they wish to add a key value pair which should be indexed...
Flags inputFlags() const
The flags which the extraction plugin should considering following when extracting metadata from the ...
The ExtractorPlugin is the base class for all file metadata extractors.
Type type(const QSqlDatabase &db)
char * toString(const EngineQuery &query)
The KFileMetaData namespace.
KGuiItem add()
char * data()
bool isEmpty() const const
qsizetype length() const const
bool isValid() const const
void setTimeZone(const QTimeZone &toZone)
QString fromStdString(const std::string &str)
QString fromUtf8(QByteArrayView str)
bool isEmpty() const const
QByteArray toUtf8() const const
QTimeZone fromSecondsAheadOfUtc(int offset)
bool isNull() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Mar 28 2025 11:56:37 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.