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

KDE's Doxygen guidelines are available online.