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

KDE's Doxygen guidelines are available online.