1 | /* |
2 | This file is part of the KDE project |
3 | SPDX-FileCopyrightText: 2025 Mirco Miranda <mircomir@outlook.com> |
4 | |
5 | SPDX-License-Identifier: LGPL-2.1-or-later |
6 | */ |
7 | |
8 | #include "microexif_p.h" |
9 | #include "util_p.h" |
10 | |
11 | #include <QBuffer> |
12 | #include <QCoreApplication> |
13 | #include <QDataStream> |
14 | #include <QHash> |
15 | #include <QStringDecoder> |
16 | #include <QTimeZone> |
17 | |
18 | // TIFF 6 specs |
19 | #define TIFF_IMAGEWIDTH 0x100 |
20 | #define TIFF_IMAGEHEIGHT 0x101 |
21 | #define TIFF_BITSPERSAMPLE 0x102 |
22 | #define TIFF_IMAGEDESCRIPTION 0x10E |
23 | #define TIFF_MAKE 0x10F |
24 | #define TIFF_MODEL 0x110 |
25 | #define TIFF_ORIENT 0x0112 |
26 | #define TIFF_XRES 0x011A |
27 | #define TIFF_YRES 0x011B |
28 | #define TIFF_URES 0x0128 |
29 | #define TIFF_SOFTWARE 0x0131 |
30 | #define TIFF_ARTIST 0x013B |
31 | #define TIFF_DATETIME 0x0132 |
32 | #define TIFF_COPYRIGHT 0x8298 |
33 | |
34 | #define TIFF_VAL_URES_NOABSOLUTE 1 |
35 | #define TIFF_VAL_URES_INCH 2 |
36 | #define TIFF_VAL_URES_CENTIMETER 3 |
37 | |
38 | // EXIF 3 specs |
39 | #define EXIF_EXIFIFD 0x8769 |
40 | #define EXIF_GPSIFD 0x8825 |
41 | #define EXIF_EXIFVERSION 0x9000 |
42 | #define EXIF_DATETIMEORIGINAL 0x9003 |
43 | #define EXIF_DATETIMEDIGITIZED 0x9004 |
44 | #define EXIF_OFFSETTIME 0x9010 |
45 | #define EXIF_OFFSETTIMEORIGINAL 0x9011 |
46 | #define EXIF_OFFSETTIMEDIGITIZED 0x9012 |
47 | #define EXIF_COLORSPACE 0xA001 |
48 | #define EXIF_PIXELXDIM 0xA002 |
49 | #define EXIF_PIXELYDIM 0xA003 |
50 | #define EXIF_IMAGEUNIQUEID 0xA420 |
51 | #define EXIF_BODYSERIALNUMBER 0xA431 |
52 | #define EXIF_LENSMAKE 0xA433 |
53 | #define EXIF_LENSMODEL 0xA434 |
54 | #define EXIF_LENSSERIALNUMBER 0xA435 |
55 | #define EXIF_IMAGETITLE 0xA436 |
56 | |
57 | #define EXIF_VAL_COLORSPACE_SRGB 1 |
58 | #define EXIF_VAL_COLORSPACE_UNCAL 0xFFFF |
59 | |
60 | #define GPS_GPSVERSION 0 |
61 | #define GPS_LATITUDEREF 1 |
62 | #define GPS_LATITUDE 2 |
63 | #define GPS_LONGITUDEREF 3 |
64 | #define GPS_LONGITUDE 4 |
65 | #define GPS_ALTITUDEREF 5 |
66 | #define GPS_ALTITUDE 6 |
67 | #define GPS_IMGDIRECTIONREF 16 |
68 | #define GPS_IMGDIRECTION 17 |
69 | #define EXIF_TAG_VALUE(n, byteSize) (((n) << 6) | ((byteSize) & 0x3F)) |
70 | #define EXIF_TAG_SIZEOF(dataType) (quint16(dataType) & 0x3F) |
71 | #define EXIF_TAG_DATATYPE(dataType) (quint16(dataType) >> 6) |
72 | |
73 | enum class ExifTagType : quint16 { |
74 | // Base data types |
75 | Byte = EXIF_TAG_VALUE(1, 1), |
76 | Ascii = EXIF_TAG_VALUE(2, 1), |
77 | Short = EXIF_TAG_VALUE(3, 2), |
78 | Long = EXIF_TAG_VALUE(4, 4), |
79 | Rational = EXIF_TAG_VALUE(5, 8), |
80 | |
81 | // Extended data types |
82 | SByte = EXIF_TAG_VALUE(6, 1), |
83 | Undefined = EXIF_TAG_VALUE(7, 1), |
84 | SShort = EXIF_TAG_VALUE(8, 2), |
85 | SLong = EXIF_TAG_VALUE(9, 4), |
86 | SRational = EXIF_TAG_VALUE(10, 8), |
87 | |
88 | Float = EXIF_TAG_VALUE(11, 4), // not used in EXIF specs |
89 | Double = EXIF_TAG_VALUE(12, 8), // not used in EXIF specs |
90 | Ifd = EXIF_TAG_VALUE(13, 4), // not used in EXIF specs |
91 | |
92 | // BigTiff data types (EXIF specs are 32-bits only) |
93 | Long8 = EXIF_TAG_VALUE(16, 8), // not used in EXIF specs |
94 | SLong8 = EXIF_TAG_VALUE(17, 8), // not used in EXIF specs |
95 | Ifd8 = EXIF_TAG_VALUE(18, 8), // not used in EXIF specs |
96 | |
97 | // Exif 3.0 only |
98 | Utf8 = EXIF_TAG_VALUE(129, 1) |
99 | }; |
100 | |
101 | using TagPos = QHash<quint16, quint32>; |
102 | using KnownTags = QHash<quint16, ExifTagType>; |
103 | using TagInfo = std::pair<quint16, ExifTagType>; |
104 | |
105 | /*! |
106 | * \brief staticTagTypes |
107 | * The supported tags. |
108 | * \note EXIF tags are an extension of TIFF tags, so I'm writing them all together. |
109 | */ |
110 | // clang-format off |
111 | static const KnownTags staticTagTypes = { |
112 | TagInfo(TIFF_IMAGEWIDTH, ExifTagType::Long), |
113 | TagInfo(TIFF_IMAGEHEIGHT, ExifTagType::Long), |
114 | TagInfo(TIFF_BITSPERSAMPLE, ExifTagType::Short), |
115 | TagInfo(TIFF_IMAGEDESCRIPTION, ExifTagType::Utf8), |
116 | TagInfo(TIFF_MAKE, ExifTagType::Utf8), |
117 | TagInfo(TIFF_MODEL, ExifTagType::Utf8), |
118 | TagInfo(TIFF_ORIENT, ExifTagType::Short), |
119 | TagInfo(TIFF_XRES, ExifTagType::Rational), |
120 | TagInfo(TIFF_YRES, ExifTagType::Rational), |
121 | TagInfo(TIFF_URES, ExifTagType::Short), |
122 | TagInfo(TIFF_SOFTWARE, ExifTagType::Utf8), |
123 | TagInfo(TIFF_ARTIST, ExifTagType::Utf8), |
124 | TagInfo(TIFF_DATETIME, ExifTagType::Ascii), |
125 | TagInfo(TIFF_COPYRIGHT, ExifTagType::Utf8), |
126 | TagInfo(EXIF_EXIFIFD, ExifTagType::Long), |
127 | TagInfo(EXIF_GPSIFD, ExifTagType::Long), |
128 | TagInfo(EXIF_DATETIMEORIGINAL, ExifTagType::Ascii), |
129 | TagInfo(EXIF_OFFSETTIMEDIGITIZED, ExifTagType::Ascii), |
130 | TagInfo(EXIF_OFFSETTIME, ExifTagType::Ascii), |
131 | TagInfo(EXIF_OFFSETTIMEORIGINAL, ExifTagType::Ascii), |
132 | TagInfo(EXIF_OFFSETTIMEDIGITIZED, ExifTagType::Ascii), |
133 | TagInfo(EXIF_COLORSPACE, ExifTagType::Short), |
134 | TagInfo(EXIF_PIXELXDIM, ExifTagType::Long), |
135 | TagInfo(EXIF_PIXELYDIM, ExifTagType::Long), |
136 | TagInfo(EXIF_IMAGEUNIQUEID, ExifTagType::Ascii), |
137 | TagInfo(EXIF_BODYSERIALNUMBER, ExifTagType::Ascii), |
138 | TagInfo(EXIF_LENSMAKE, ExifTagType::Utf8), |
139 | TagInfo(EXIF_LENSMODEL, ExifTagType::Utf8), |
140 | TagInfo(EXIF_LENSSERIALNUMBER, ExifTagType::Ascii), |
141 | TagInfo(EXIF_IMAGETITLE, ExifTagType::Utf8), |
142 | TagInfo(EXIF_EXIFVERSION, ExifTagType::Undefined) |
143 | }; |
144 | // clang-format on |
145 | |
146 | /*! |
147 | * \brief staticGpsTagTypes |
148 | */ |
149 | // clang-format off |
150 | static const KnownTags staticGpsTagTypes = { |
151 | TagInfo(GPS_GPSVERSION, ExifTagType::Byte), |
152 | TagInfo(GPS_LATITUDEREF, ExifTagType::Ascii), |
153 | TagInfo(GPS_LATITUDE, ExifTagType::Rational), |
154 | TagInfo(GPS_LONGITUDEREF, ExifTagType::Ascii), |
155 | TagInfo(GPS_LONGITUDE, ExifTagType::Rational), |
156 | TagInfo(GPS_ALTITUDEREF, ExifTagType::Byte), |
157 | TagInfo(GPS_ALTITUDE, ExifTagType::Rational), |
158 | TagInfo(GPS_IMGDIRECTIONREF, ExifTagType::Ascii), |
159 | TagInfo(GPS_IMGDIRECTION, ExifTagType::Rational) |
160 | }; |
161 | // clang-format on |
162 | |
163 | /*! |
164 | * \brief tiffStrMap |
165 | * TIFF string <-> metadata |
166 | */ |
167 | // clang-format off |
168 | static const QList<std::pair<quint16, QString>> tiffStrMap = { |
169 | std::pair<quint16, QString>(TIFF_IMAGEDESCRIPTION, QStringLiteral(META_KEY_DESCRIPTION)), |
170 | std::pair<quint16, QString>(TIFF_ARTIST, QStringLiteral(META_KEY_AUTHOR)), |
171 | std::pair<quint16, QString>(TIFF_SOFTWARE, QStringLiteral(META_KEY_SOFTWARE)), |
172 | std::pair<quint16, QString>(TIFF_COPYRIGHT, QStringLiteral(META_KEY_COPYRIGHT)), |
173 | std::pair<quint16, QString>(TIFF_MAKE, QStringLiteral(META_KEY_MANUFACTURER)), |
174 | std::pair<quint16, QString>(TIFF_MODEL, QStringLiteral(META_KEY_MODEL)) |
175 | }; |
176 | // clang-format on |
177 | |
178 | /*! |
179 | * \brief exifStrMap |
180 | * EXIF string <-> metadata |
181 | */ |
182 | // clang-format off |
183 | static const QList<std::pair<quint16, QString>> exifStrMap = { |
184 | std::pair<quint16, QString>(EXIF_BODYSERIALNUMBER, QStringLiteral(META_KEY_SERIALNUMBER)), |
185 | std::pair<quint16, QString>(EXIF_LENSMAKE, QStringLiteral(META_KEY_LENS_MANUFACTURER)), |
186 | std::pair<quint16, QString>(EXIF_LENSMODEL, QStringLiteral(META_KEY_LENS_MODEL)), |
187 | std::pair<quint16, QString>(EXIF_LENSSERIALNUMBER, QStringLiteral(META_KEY_LENS_SERIALNUMBER)), |
188 | std::pair<quint16, QString>(EXIF_IMAGETITLE, QStringLiteral(META_KEY_TITLE)), |
189 | }; |
190 | // clang-format on |
191 | |
192 | /*! |
193 | * \brief timeOffset |
194 | * \param offset The EXIF string of the offset from UTC. |
195 | * \return The offset in minutes. |
196 | */ |
197 | static qint16 timeOffset(const QString& offset) |
198 | { |
199 | if (offset.size() != 6 || offset.at(i: 3) != u':') |
200 | return 0; |
201 | auto ok = false; |
202 | auto hh = offset.left(n: 3).toInt(ok: &ok); |
203 | if (!ok) |
204 | return 0; |
205 | auto mm = offset.mid(position: 4, n: 2).toInt(ok: &ok) * (hh < 0 ? -1 : 1); |
206 | if (!ok) |
207 | return 0; |
208 | return qint16(hh * 60 + mm); |
209 | } |
210 | |
211 | /*! |
212 | * \brief timeOffset |
213 | * \param offset Offset from UTC in minutes. |
214 | * \return The EXIF string of the offset. |
215 | */ |
216 | static QString timeOffset(qint16 offset) |
217 | { |
218 | auto absOff = quint16(std::abs(x: offset)); |
219 | return QStringLiteral("%1%2:%3" ) |
220 | .arg(a: offset < 0 ? QStringLiteral("-" ) : QStringLiteral("+" )) |
221 | .arg(a: absOff / 60, fieldWidth: 2, base: 10, fillChar: QChar(u'0')) |
222 | .arg(a: absOff % 60, fieldWidth: 2, base: 10, fillChar: QChar(u'0')); |
223 | } |
224 | |
225 | |
226 | /*! |
227 | * \brief checkHeader |
228 | * \param ds The data stream |
229 | * \return True if header is a valid EXIF, otherwise false. |
230 | */ |
231 | static bool (QDataStream &ds) |
232 | { |
233 | quint16 order; |
234 | ds >> order; |
235 | if (order == 0x4949) { |
236 | ds.setByteOrder(QDataStream::LittleEndian); |
237 | } else if (order == 0x4d4d) { |
238 | ds.setByteOrder(QDataStream::BigEndian); |
239 | } else { |
240 | return false; |
241 | } |
242 | |
243 | quint16 version; |
244 | ds >> version; |
245 | if (version != 0x002A && version != 0x01BC) |
246 | return false; // not TIFF or JXR |
247 | |
248 | quint32 offset; |
249 | ds >> offset; |
250 | offset -= 8; |
251 | if (ds.skipRawData(len: offset) != offset) |
252 | return false; |
253 | |
254 | return ds.status() == QDataStream::Ok; |
255 | } |
256 | |
257 | /*! |
258 | * \brief updatePos |
259 | * Write the current stram position in \a pos position as uint32. |
260 | * \return True on success, otherwise false; |
261 | */ |
262 | static bool updatePos(QDataStream &ds, quint32 pos) |
263 | { |
264 | auto dev = ds.device(); |
265 | if (pos != 0) { |
266 | auto p = dev->pos(); |
267 | if (!dev->seek(pos)) |
268 | return false; |
269 | ds << quint32(p); |
270 | if (!dev->seek(pos: p)) |
271 | return false; |
272 | } |
273 | return ds.status() == QDataStream::Ok; |
274 | } |
275 | |
276 | static qint32 countBytes(const ExifTagType &dataType, const QVariant &value) |
277 | { |
278 | auto count = 1; |
279 | if (dataType == ExifTagType::Ascii) { |
280 | count = value.toString().toLatin1().size() + 1; // ASCIIZ |
281 | } else if (dataType == ExifTagType::Utf8) { |
282 | count = value.toString().toUtf8().size() + 1; // ASCIIZ |
283 | } else if (dataType == ExifTagType::Undefined) { |
284 | count = value.toByteArray().size(); |
285 | } else if (dataType == ExifTagType::Byte) { |
286 | count = value.value<QList<quint8>>().size(); |
287 | } else if (dataType == ExifTagType::Short) { |
288 | count = value.value<QList<quint16>>().size(); |
289 | } else if (dataType == ExifTagType::Long || dataType == ExifTagType::Ifd) { |
290 | count = value.value<QList<quint32>>().size(); |
291 | } else if (dataType == ExifTagType::SByte) { |
292 | count = value.value<QList<qint8>>().size(); |
293 | } else if (dataType == ExifTagType::SShort) { |
294 | count = value.value<QList<qint16>>().size(); |
295 | } else if (dataType == ExifTagType::SLong) { |
296 | count = value.value<QList<qint32>>().size(); |
297 | } else if (dataType == ExifTagType::Rational || dataType == ExifTagType::SRational || dataType == ExifTagType::Double) { |
298 | count = value.value<QList<double>>().size(); |
299 | } else if (dataType == ExifTagType::Float) { |
300 | count = value.value<QList<float>>().size(); |
301 | } |
302 | return std::max(a: 1, b: count); |
303 | } |
304 | |
305 | template <class T> |
306 | static void writeList(QDataStream &ds, const QVariant &value) |
307 | { |
308 | auto l = value.value<QList<T>>(); |
309 | if (l.isEmpty()) |
310 | l.append(value.toInt()); |
311 | for (;l.size() < qsizetype(4 / sizeof(T));) |
312 | l.append(T()); |
313 | for (auto &&v : l) |
314 | ds << v; |
315 | } |
316 | |
317 | inline qint32 rationalPrecision(double v) |
318 | { |
319 | v = qAbs(t: v); |
320 | return 8 - qBound(min: 0, val: v < 1 ? 8 : int(std::log10(x: v)), max: 8); |
321 | } |
322 | |
323 | template<class T> |
324 | static void writeRationalList(QDataStream &ds, const QVariant &value) |
325 | { |
326 | auto l = value.value<QList<double>>(); |
327 | if (l.isEmpty()) |
328 | l.append(t: value.toDouble()); |
329 | for (auto &&v : l) { |
330 | auto den = std::pow(x: 10, y: rationalPrecision(v)); |
331 | ds << T(qRound(d: v * den)); |
332 | ds << T(den); |
333 | } |
334 | } |
335 | |
336 | static void writeByteArray(QDataStream &ds, const QByteArray &ba) |
337 | { |
338 | for (auto &&v : ba) |
339 | ds << v; |
340 | for (auto n = ba.size(); n < 4; ++n) |
341 | ds << char(); |
342 | } |
343 | |
344 | static void writeData(QDataStream &ds, const QVariant &value, const ExifTagType& dataType) |
345 | { |
346 | if (dataType == ExifTagType::Ascii) { |
347 | writeByteArray(ds, ba: value.toString().toLatin1().append(c: char())); |
348 | } else if (dataType == ExifTagType::Utf8) { |
349 | writeByteArray(ds, ba: value.toString().toUtf8().append(c: char())); |
350 | } else if (dataType == ExifTagType::Undefined) { |
351 | writeByteArray(ds, ba: value.toByteArray()); |
352 | } else if (dataType == ExifTagType::Byte) { |
353 | writeList<quint8>(ds, value); |
354 | } else if (dataType == ExifTagType::SByte) { |
355 | writeList<qint8>(ds, value); |
356 | } else if (dataType == ExifTagType::Short) { |
357 | writeList<quint16>(ds, value); |
358 | } else if (dataType == ExifTagType::SShort) { |
359 | writeList<qint16>(ds, value); |
360 | } else if (dataType == ExifTagType::Long || dataType == ExifTagType::Ifd) { |
361 | writeList<quint32>(ds, value); |
362 | } else if (dataType == ExifTagType::SLong) { |
363 | writeList<qint32>(ds, value); |
364 | } else if (dataType == ExifTagType::Rational) { |
365 | writeRationalList<quint32>(ds, value); |
366 | } else if (dataType == ExifTagType::SRational) { |
367 | writeRationalList<qint32>(ds, value); |
368 | } |
369 | } |
370 | |
371 | static ExifTagType updateDataType(const ExifTagType &dataType, const QVariant &value, const MicroExif::Version &ver) |
372 | { |
373 | if (dataType != ExifTagType::Utf8) |
374 | return dataType; |
375 | |
376 | if (ver == MicroExif::V2) |
377 | return ExifTagType::Ascii; |
378 | |
379 | // Note that in EXIF specs, UTF-8 is backward compatible with ASCII: all UTF-8 tags can also be ASCII. |
380 | // To maximize compatibility, I check if the string can be encoded in ASCII. |
381 | auto txt = value.toString(); |
382 | |
383 | // Exif ASCII data type allow only values up to 127 (7-bit ASCII). |
384 | auto u8 = txt.toUtf8(); |
385 | for (auto &&c : u8) { |
386 | if (uchar(c) > 127) |
387 | return dataType; |
388 | } |
389 | |
390 | return ExifTagType::Ascii; |
391 | } |
392 | |
393 | /*! |
394 | * \brief writeIfd |
395 | * \param ds The stream. |
396 | * \param tags The list of tags to write. |
397 | * \param pos The position of the TAG value to update with this IFD position. |
398 | * \param knownTags List of known and supported tags. |
399 | * \return True on success, otherwise false. |
400 | */ |
401 | static bool writeIfd(QDataStream &ds, |
402 | const MicroExif::Version &ver, |
403 | const MicroExif::Tags &tags, |
404 | TagPos &positions, |
405 | quint32 pos = 0, |
406 | const KnownTags &knownTags = staticTagTypes) |
407 | { |
408 | if (tags.isEmpty()) |
409 | return true; |
410 | if (!updatePos(ds, pos)) |
411 | return false; |
412 | |
413 | auto keys = tags.keys(); |
414 | auto entries = quint16(keys.size()); |
415 | ds << entries; |
416 | for (auto &&key : keys) { |
417 | if (!knownTags.contains(key)) { |
418 | continue; |
419 | } |
420 | auto value = tags.value(key); |
421 | auto dataType = updateDataType(dataType: knownTags.value(key), value, ver); |
422 | auto count = countBytes(dataType, value); |
423 | |
424 | ds << quint16(key); |
425 | ds << quint16(EXIF_TAG_DATATYPE(dataType)); |
426 | ds << quint32(count); |
427 | positions.insert(key, value: quint32(ds.device()->pos())); |
428 | auto valueSize = count * EXIF_TAG_SIZEOF(dataType); |
429 | if (valueSize > 4) { |
430 | ds << quint32(); |
431 | } else { |
432 | writeData(ds, value, dataType); |
433 | } |
434 | } |
435 | // no more IFDs |
436 | ds << quint32(); |
437 | |
438 | // write data larger than 4 bytes |
439 | for (auto &&key : keys) { |
440 | if (!knownTags.contains(key)) { |
441 | continue; |
442 | } |
443 | auto value = tags.value(key); |
444 | auto dataType = updateDataType(dataType: knownTags.value(key), value, ver); |
445 | auto count = countBytes(dataType, value); |
446 | auto valueSize = count * EXIF_TAG_SIZEOF(dataType); |
447 | if (valueSize <= 4) |
448 | continue; |
449 | if (!updatePos(ds, pos: positions.value(key))) |
450 | return false; |
451 | writeData(ds, value, dataType); |
452 | } |
453 | |
454 | return ds.status() == QDataStream::Ok; |
455 | } |
456 | |
457 | template<class T> |
458 | static QList<T> readList(QDataStream &ds, quint32 count) |
459 | { |
460 | QList<T> l; |
461 | T c; |
462 | for (quint32 i = 0; i < count; ++i) { |
463 | ds >> c; |
464 | l.append(c); |
465 | } |
466 | for (auto n = count; n < quint32(4 / sizeof(T)); ++n) { |
467 | ds >> c; |
468 | } |
469 | return l; |
470 | } |
471 | |
472 | template<class T> |
473 | static QList<double> readRationalList(QDataStream &ds, quint32 count) |
474 | { |
475 | QList<double> l; |
476 | for (quint32 i = 0; i < count; ++i) { |
477 | T num; |
478 | ds >> num; |
479 | T den; |
480 | ds >> den; |
481 | l.append(den == 0 ? 0 : double(num) / double(den)); |
482 | } |
483 | return l; |
484 | } |
485 | |
486 | static QByteArray readBytes(QDataStream &ds, quint32 count, bool asciiz) |
487 | { |
488 | QByteArray l; |
489 | if (count == 0) { |
490 | return l; |
491 | } |
492 | char c; |
493 | for (quint32 i = 0; i < count; ++i) { |
494 | ds >> c; |
495 | l.append(c); |
496 | } |
497 | if (asciiz && l.at(i: l.size() - 1) == 0) { |
498 | l.removeLast(); |
499 | } |
500 | for (auto n = count; n < 4; ++n) { |
501 | ds >> c; |
502 | } |
503 | return l; |
504 | } |
505 | |
506 | /*! |
507 | * \brief readIfd |
508 | * \param ds The stream. |
509 | * \param tags Where to sotro the read tags. |
510 | * \param pos The position of the IFD. |
511 | * \param knownTags List of known and supported tags. |
512 | * \param nextIfd The position of next IFD (0 if none). |
513 | * \return True on succes, otherwise false. |
514 | */ |
515 | static bool readIfd(QDataStream &ds, MicroExif::Tags &tags, quint32 pos = 0, const KnownTags &knownTags = staticTagTypes, quint32 *nextIfd = nullptr) |
516 | { |
517 | auto localNextIfd = quint32(); |
518 | if (nextIfd == nullptr) |
519 | nextIfd = &localNextIfd; |
520 | *nextIfd = 0; |
521 | |
522 | auto device = ds.device(); |
523 | if (pos && !device->seek(pos)) |
524 | return false; |
525 | |
526 | quint16 entries; |
527 | ds >> entries; |
528 | if (ds.status() != QDataStream::Ok) |
529 | return false; |
530 | |
531 | for (quint16 i = 0; i < entries; ++i) { |
532 | quint16 tagId; |
533 | ds >> tagId; |
534 | quint16 dataType; |
535 | ds >> dataType; |
536 | quint32 count; |
537 | ds >> count; |
538 | if (ds.status() != QDataStream::Ok) |
539 | return false; |
540 | |
541 | // search for supported values only |
542 | if (!knownTags.contains(key: tagId)) { |
543 | quint32 value; |
544 | ds >> value; |
545 | continue; |
546 | } |
547 | |
548 | // read TAG data |
549 | auto toRead = qint64(count) * EXIF_TAG_SIZEOF(knownTags.value(tagId)); |
550 | if (toRead > qint64(device->size())) |
551 | return false; |
552 | |
553 | auto curPos = qint64(); |
554 | if (toRead > 4) { |
555 | quint32 value; |
556 | ds >> value; |
557 | curPos = device->pos(); |
558 | if (!device->seek(pos: value)) |
559 | return false; |
560 | } |
561 | |
562 | if (dataType == EXIF_TAG_DATATYPE(ExifTagType::Ascii) || dataType == EXIF_TAG_DATATYPE(ExifTagType::Utf8)) { |
563 | auto l = readBytes(ds, count, asciiz: true); |
564 | if (!l.isEmpty()) { |
565 | // It seems that converting to Latin 1 never detects errors so, using UTF-8. |
566 | // Note that if the dataType is ASCII, by EXIF specification, it must use only the |
567 | // first 128 values ​​so the UTF-8 conversion is correct. |
568 | auto dec = QStringDecoder(QStringDecoder::Utf8); |
569 | // QStringDecoder raise an error only after converting to QString |
570 | auto ut8 = QString(dec(l)); |
571 | // If there are errors in the conversion to UTF-8, then I try with latin1 (extended ASCII) |
572 | tags.insert(key: tagId, value: dec.hasError() ? QString::fromLatin1(ba: l) : ut8); |
573 | } |
574 | } else if (dataType == EXIF_TAG_DATATYPE(ExifTagType::Undefined)) { |
575 | auto l = readBytes(ds, count, asciiz: false); |
576 | if (!l.isEmpty()) |
577 | tags.insert(key: tagId, value: l); |
578 | } else if (dataType == EXIF_TAG_DATATYPE(ExifTagType::Byte)) { |
579 | auto l = readList<quint8>(ds, count); |
580 | tags.insert(key: tagId, value: l.size() == 1 ? QVariant(l.first()) : QVariant::fromValue(value: l)); |
581 | } else if (dataType == EXIF_TAG_DATATYPE(ExifTagType::SByte)) { |
582 | auto l = readList<qint8>(ds, count); |
583 | tags.insert(key: tagId, value: l.size() == 1 ? QVariant(l.first()) : QVariant::fromValue(value: l)); |
584 | } else if (dataType == EXIF_TAG_DATATYPE(ExifTagType::Short)) { |
585 | auto l = readList<quint16>(ds, count); |
586 | tags.insert(key: tagId, value: l.size() == 1 ? QVariant(l.first()) : QVariant::fromValue(value: l)); |
587 | } else if (dataType == EXIF_TAG_DATATYPE(ExifTagType::SShort)) { |
588 | auto l = readList<qint16>(ds, count); |
589 | tags.insert(key: tagId, value: l.size() == 1 ? QVariant(l.first()) : QVariant::fromValue(value: l)); |
590 | } else if (dataType == EXIF_TAG_DATATYPE(ExifTagType::Long) || dataType == EXIF_TAG_DATATYPE(ExifTagType::Ifd)) { |
591 | auto l = readList<quint32>(ds, count); |
592 | tags.insert(key: tagId, value: l.size() == 1 ? QVariant(l.first()) : QVariant::fromValue(value: l)); |
593 | } else if (dataType == EXIF_TAG_DATATYPE(ExifTagType::SLong)) { |
594 | auto l = readList<qint32>(ds, count); |
595 | tags.insert(key: tagId, value: l.size() == 1 ? QVariant(l.first()) : QVariant::fromValue(value: l)); |
596 | } else if (dataType == EXIF_TAG_DATATYPE(ExifTagType::Rational)) { |
597 | auto l = readRationalList<quint32>(ds, count); |
598 | tags.insert(key: tagId, value: l.size() == 1 ? QVariant(l.first()) : QVariant::fromValue(value: l)); |
599 | } else if (dataType == EXIF_TAG_DATATYPE(ExifTagType::SRational)) { |
600 | auto l = readRationalList<qint32>(ds, count); |
601 | tags.insert(key: tagId, value: l.size() == 1 ? QVariant(l.first()) : QVariant::fromValue(value: l)); |
602 | } |
603 | |
604 | if (curPos > 0 && !device->seek(pos: curPos)) |
605 | return false; |
606 | } |
607 | ds >> *nextIfd; |
608 | |
609 | return true; |
610 | } |
611 | |
612 | MicroExif::MicroExif() |
613 | { |
614 | |
615 | } |
616 | |
617 | void MicroExif::clear() |
618 | { |
619 | m_tiffTags.clear(); |
620 | m_exifTags.clear(); |
621 | m_gpsTags.clear(); |
622 | } |
623 | |
624 | bool MicroExif::isEmpty() const |
625 | { |
626 | return m_tiffTags.isEmpty() && m_exifTags.isEmpty() && m_gpsTags.isEmpty(); |
627 | } |
628 | |
629 | double MicroExif::horizontalResolution() const |
630 | { |
631 | auto u = m_tiffTags.value(TIFF_URES).toUInt(); |
632 | auto v = m_tiffTags.value(TIFF_XRES).toDouble(); |
633 | if (u == TIFF_VAL_URES_CENTIMETER) |
634 | return v * 2.54; |
635 | return v; |
636 | } |
637 | |
638 | void MicroExif::setHorizontalResolution(double hres) |
639 | { |
640 | auto u = m_tiffTags.value(TIFF_URES).toUInt(); |
641 | if (u == TIFF_VAL_URES_CENTIMETER) { |
642 | hres /= 2.54; |
643 | } else if (u < TIFF_VAL_URES_INCH) { |
644 | m_tiffTags.insert(TIFF_URES, TIFF_VAL_URES_INCH); |
645 | } |
646 | m_tiffTags.insert(TIFF_XRES, value: hres); |
647 | } |
648 | |
649 | double MicroExif::verticalResolution() const |
650 | { |
651 | auto u = m_tiffTags.value(TIFF_URES).toUInt(); |
652 | auto v = m_tiffTags.value(TIFF_YRES).toDouble(); |
653 | if (u == TIFF_VAL_URES_CENTIMETER) |
654 | return v * 2.54; |
655 | return v; |
656 | } |
657 | |
658 | void MicroExif::setVerticalResolution(double vres) |
659 | { |
660 | auto u = m_tiffTags.value(TIFF_URES).toUInt(); |
661 | if (u == TIFF_VAL_URES_CENTIMETER) { |
662 | vres /= 2.54; |
663 | } else if (u < TIFF_VAL_URES_INCH) { |
664 | m_tiffTags.insert(TIFF_URES, TIFF_VAL_URES_INCH); |
665 | } |
666 | m_tiffTags.insert(TIFF_YRES, value: vres); |
667 | } |
668 | |
669 | QColorSpace MicroExif::colosSpace() const |
670 | { |
671 | if (m_exifTags.value(EXIF_COLORSPACE).toUInt() == EXIF_VAL_COLORSPACE_SRGB) |
672 | return QColorSpace(QColorSpace::SRgb); |
673 | return QColorSpace(); |
674 | } |
675 | |
676 | void MicroExif::setColorSpace(const QColorSpace &cs) |
677 | { |
678 | auto srgb = cs.transferFunction() == QColorSpace::TransferFunction::SRgb && cs.primaries() == QColorSpace::Primaries::SRgb; |
679 | m_exifTags.insert(EXIF_COLORSPACE, value: srgb ? EXIF_VAL_COLORSPACE_SRGB : EXIF_VAL_COLORSPACE_UNCAL); |
680 | } |
681 | |
682 | void MicroExif::setColorSpace(const QColorSpace::NamedColorSpace &csName) |
683 | { |
684 | auto srgb = csName == QColorSpace::SRgb; |
685 | m_exifTags.insert(EXIF_COLORSPACE, value: srgb ? EXIF_VAL_COLORSPACE_SRGB : EXIF_VAL_COLORSPACE_UNCAL); |
686 | } |
687 | |
688 | qint32 MicroExif::width() const |
689 | { |
690 | return m_tiffTags.value(TIFF_IMAGEWIDTH).toUInt(); |
691 | } |
692 | |
693 | void MicroExif::setWidth(qint32 w) |
694 | { |
695 | m_tiffTags.insert(TIFF_IMAGEWIDTH, value: w); |
696 | m_exifTags.insert(EXIF_PIXELXDIM, value: w); |
697 | } |
698 | |
699 | qint32 MicroExif::height() const |
700 | { |
701 | return m_tiffTags.value(TIFF_IMAGEHEIGHT).toUInt(); |
702 | } |
703 | |
704 | void MicroExif::setHeight(qint32 h) |
705 | { |
706 | m_tiffTags.insert(TIFF_IMAGEHEIGHT, value: h); |
707 | m_exifTags.insert(EXIF_PIXELYDIM, value: h); |
708 | } |
709 | |
710 | quint16 MicroExif::orientation() const |
711 | { |
712 | return m_tiffTags.value(TIFF_ORIENT).toUInt(); |
713 | } |
714 | |
715 | void MicroExif::setOrientation(quint16 orient) |
716 | { |
717 | if (orient < 1 || orient > 8) |
718 | m_tiffTags.remove(TIFF_ORIENT); |
719 | else |
720 | m_tiffTags.insert(TIFF_ORIENT, value: orient); |
721 | } |
722 | |
723 | QImageIOHandler::Transformation MicroExif::transformation() const |
724 | { |
725 | switch (orientation()) { |
726 | case 1: |
727 | return QImageIOHandler::TransformationNone; |
728 | case 2: |
729 | return QImageIOHandler::TransformationMirror; |
730 | case 3: |
731 | return QImageIOHandler::TransformationRotate180; |
732 | case 4: |
733 | return QImageIOHandler::TransformationFlip; |
734 | case 5: |
735 | return QImageIOHandler::TransformationFlipAndRotate90; |
736 | case 6: |
737 | return QImageIOHandler::TransformationRotate90; |
738 | case 7: |
739 | return QImageIOHandler::TransformationMirrorAndRotate90; |
740 | case 8: |
741 | return QImageIOHandler::TransformationRotate270; |
742 | default: |
743 | break; |
744 | }; |
745 | return QImageIOHandler::TransformationNone; |
746 | } |
747 | |
748 | void MicroExif::setTransformation(const QImageIOHandler::Transformation &t) |
749 | { |
750 | switch (t) { |
751 | case QImageIOHandler::TransformationNone: |
752 | setOrientation(1); |
753 | break; |
754 | case QImageIOHandler::TransformationMirror: |
755 | setOrientation(2); |
756 | break; |
757 | case QImageIOHandler::TransformationRotate180: |
758 | setOrientation(3); |
759 | break; |
760 | case QImageIOHandler::TransformationFlip: |
761 | setOrientation(4); |
762 | break; |
763 | case QImageIOHandler::TransformationFlipAndRotate90: |
764 | setOrientation(5); |
765 | break; |
766 | case QImageIOHandler::TransformationRotate90: |
767 | setOrientation(6); |
768 | break; |
769 | case QImageIOHandler::TransformationMirrorAndRotate90: |
770 | setOrientation(7); |
771 | break; |
772 | case QImageIOHandler::TransformationRotate270: |
773 | setOrientation(8); |
774 | break; |
775 | default: |
776 | break; |
777 | } |
778 | setOrientation(0); // no orientation set |
779 | } |
780 | |
781 | QString MicroExif::software() const |
782 | { |
783 | return tiffString(TIFF_SOFTWARE); |
784 | } |
785 | |
786 | void MicroExif::setSoftware(const QString &s) |
787 | { |
788 | setTiffString(TIFF_SOFTWARE, s); |
789 | } |
790 | |
791 | QString MicroExif::description() const |
792 | { |
793 | return tiffString(TIFF_IMAGEDESCRIPTION); |
794 | } |
795 | |
796 | void MicroExif::setDescription(const QString &s) |
797 | { |
798 | setTiffString(TIFF_IMAGEDESCRIPTION, s); |
799 | } |
800 | |
801 | QString MicroExif::artist() const |
802 | { |
803 | return tiffString(TIFF_ARTIST); |
804 | } |
805 | |
806 | void MicroExif::setArtist(const QString &s) |
807 | { |
808 | setTiffString(TIFF_ARTIST, s); |
809 | } |
810 | |
811 | QString MicroExif::copyright() const |
812 | { |
813 | return tiffString(TIFF_COPYRIGHT); |
814 | } |
815 | |
816 | void MicroExif::setCopyright(const QString &s) |
817 | { |
818 | setTiffString(TIFF_COPYRIGHT, s); |
819 | } |
820 | |
821 | QString MicroExif::make() const |
822 | { |
823 | return tiffString(TIFF_MAKE); |
824 | } |
825 | |
826 | void MicroExif::setMake(const QString &s) |
827 | { |
828 | setTiffString(TIFF_MAKE, s); |
829 | } |
830 | |
831 | QString MicroExif::model() const |
832 | { |
833 | return tiffString(TIFF_MODEL); |
834 | } |
835 | |
836 | void MicroExif::setModel(const QString &s) |
837 | { |
838 | setTiffString(TIFF_MODEL, s); |
839 | } |
840 | |
841 | QString MicroExif::serialNumber() const |
842 | { |
843 | return tiffString(EXIF_BODYSERIALNUMBER); |
844 | } |
845 | |
846 | void MicroExif::setSerialNumber(const QString &s) |
847 | { |
848 | setTiffString(EXIF_BODYSERIALNUMBER, s); |
849 | } |
850 | |
851 | QString MicroExif::lensMake() const |
852 | { |
853 | return tiffString(EXIF_LENSMAKE); |
854 | } |
855 | |
856 | void MicroExif::setLensMake(const QString &s) |
857 | { |
858 | setTiffString(EXIF_LENSMAKE, s); |
859 | } |
860 | |
861 | QString MicroExif::lensModel() const |
862 | { |
863 | return tiffString(EXIF_LENSMODEL); |
864 | } |
865 | |
866 | void MicroExif::setLensModel(const QString &s) |
867 | { |
868 | setTiffString(EXIF_LENSMODEL, s); |
869 | } |
870 | |
871 | QString MicroExif::lensSerialNumber() const |
872 | { |
873 | return tiffString(EXIF_LENSSERIALNUMBER); |
874 | } |
875 | |
876 | void MicroExif::setLensSerialNumber(const QString &s) |
877 | { |
878 | setTiffString(EXIF_LENSSERIALNUMBER, s); |
879 | } |
880 | |
881 | QDateTime MicroExif::dateTime() const |
882 | { |
883 | auto dt = QDateTime::fromString(string: tiffString(TIFF_DATETIME), QStringLiteral("yyyy:MM:dd HH:mm:ss" )); |
884 | auto ofTag = exifString(EXIF_OFFSETTIME); |
885 | if (dt.isValid() && !ofTag.isEmpty()) |
886 | dt.setTimeZone(toZone: QTimeZone::fromSecondsAheadOfUtc(offset: timeOffset(offset: ofTag) * 60)); |
887 | return(dt); |
888 | } |
889 | |
890 | void MicroExif::setDateTime(const QDateTime &dt) |
891 | { |
892 | if (!dt.isValid()) { |
893 | m_tiffTags.remove(TIFF_DATETIME); |
894 | m_exifTags.remove(EXIF_OFFSETTIME); |
895 | return; |
896 | } |
897 | setTiffString(TIFF_DATETIME, s: dt.toString(QStringLiteral("yyyy:MM:dd HH:mm:ss" ))); |
898 | setExifString(EXIF_OFFSETTIME, s: timeOffset(offset: dt.offsetFromUtc() / 60)); |
899 | } |
900 | |
901 | QDateTime MicroExif::dateTimeOriginal() const |
902 | { |
903 | auto dt = QDateTime::fromString(string: exifString(EXIF_DATETIMEORIGINAL), QStringLiteral("yyyy:MM:dd HH:mm:ss" )); |
904 | auto ofTag = exifString(EXIF_OFFSETTIMEORIGINAL); |
905 | if (dt.isValid() && !ofTag.isEmpty()) |
906 | dt.setTimeZone(toZone: QTimeZone::fromSecondsAheadOfUtc(offset: timeOffset(offset: ofTag) * 60)); |
907 | return(dt); |
908 | } |
909 | |
910 | void MicroExif::setDateTimeOriginal(const QDateTime &dt) |
911 | { |
912 | if (!dt.isValid()) { |
913 | m_exifTags.remove(EXIF_DATETIMEORIGINAL); |
914 | m_exifTags.remove(EXIF_OFFSETTIMEORIGINAL); |
915 | return; |
916 | } |
917 | setExifString(EXIF_DATETIMEORIGINAL, s: dt.toString(QStringLiteral("yyyy:MM:dd HH:mm:ss" ))); |
918 | setExifString(EXIF_OFFSETTIMEORIGINAL, s: timeOffset(offset: dt.offsetFromUtc() / 60)); |
919 | } |
920 | |
921 | QDateTime MicroExif::dateTimeDigitized() const |
922 | { |
923 | auto dt = QDateTime::fromString(string: exifString(EXIF_DATETIMEDIGITIZED), QStringLiteral("yyyy:MM:dd HH:mm:ss" )); |
924 | auto ofTag = exifString(EXIF_OFFSETTIMEDIGITIZED); |
925 | if (dt.isValid() && !ofTag.isEmpty()) |
926 | dt.setTimeZone(toZone: QTimeZone::fromSecondsAheadOfUtc(offset: timeOffset(offset: ofTag) * 60)); |
927 | return(dt); |
928 | } |
929 | |
930 | void MicroExif::setDateTimeDigitized(const QDateTime &dt) |
931 | { |
932 | if (!dt.isValid()) { |
933 | m_exifTags.remove(EXIF_DATETIMEDIGITIZED); |
934 | m_exifTags.remove(EXIF_OFFSETTIMEDIGITIZED); |
935 | return; |
936 | } |
937 | setExifString(EXIF_DATETIMEDIGITIZED, s: dt.toString(QStringLiteral("yyyy:MM:dd HH:mm:ss" ))); |
938 | setExifString(EXIF_OFFSETTIMEDIGITIZED, s: timeOffset(offset: dt.offsetFromUtc() / 60)); |
939 | } |
940 | |
941 | QString MicroExif::title() const |
942 | { |
943 | return exifString(EXIF_IMAGETITLE); |
944 | } |
945 | |
946 | void MicroExif::setImageTitle(const QString &s) |
947 | { |
948 | setExifString(EXIF_IMAGETITLE, s); |
949 | } |
950 | |
951 | QUuid MicroExif::uniqueId() const |
952 | { |
953 | auto s = exifString(EXIF_IMAGEUNIQUEID); |
954 | if (s.length() == 32) { |
955 | auto tmp = QStringLiteral("%1-%2-%3-%4-%5" ).arg(args: s.left(n: 8), args: s.mid(position: 8, n: 4), args: s.mid(position: 12, n: 4), args: s.mid(position: 16, n: 4), args: s.mid(position: 20)); |
956 | return QUuid(tmp); |
957 | } |
958 | return {}; |
959 | } |
960 | |
961 | void MicroExif::setUniqueId(const QUuid &uuid) |
962 | { |
963 | if (uuid.isNull()) |
964 | setExifString(EXIF_IMAGEUNIQUEID, s: QString()); |
965 | else |
966 | setExifString(EXIF_IMAGEUNIQUEID, s: uuid.toString(mode: QUuid::WithoutBraces).replace(QStringLiteral("-" ), after: QString())); |
967 | } |
968 | |
969 | double MicroExif::latitude() const |
970 | { |
971 | auto ref = gpsString(GPS_LATITUDEREF).toUpper(); |
972 | if (ref != QStringLiteral("N" ) && ref != QStringLiteral("S" )) |
973 | return qQNaN(); |
974 | auto lat = m_gpsTags.value(GPS_LATITUDE).value<QList<double>>(); |
975 | if (lat.size() != 3) |
976 | return qQNaN(); |
977 | auto degree = lat.at(i: 0) + lat.at(i: 1) / 60 + lat.at(i: 2) / 3600; |
978 | if (degree < -90.0 || degree > 90.0) |
979 | return qQNaN(); |
980 | return ref == QStringLiteral("N" ) ? degree : -degree; |
981 | } |
982 | |
983 | void MicroExif::setLatitude(double degree) |
984 | { |
985 | if (qIsNaN(d: degree)) { |
986 | m_gpsTags.remove(GPS_LATITUDEREF); |
987 | m_gpsTags.remove(GPS_LATITUDE); |
988 | } |
989 | if (degree < -90.0 || degree > 90.0) |
990 | return; // invalid latitude |
991 | auto adeg = qAbs(t: degree); |
992 | auto min = (adeg - int(adeg)) * 60; |
993 | auto sec = (min - int(min)) * 60; |
994 | m_gpsTags.insert(GPS_LATITUDEREF, value: degree < 0 ? QStringLiteral("S" ) : QStringLiteral("N" )); |
995 | m_gpsTags.insert(GPS_LATITUDE, value: QVariant::fromValue(value: QList<double>() << int(adeg) << int(min) << sec)); |
996 | } |
997 | |
998 | double MicroExif::longitude() const |
999 | { |
1000 | auto ref = gpsString(GPS_LONGITUDEREF).toUpper(); |
1001 | if (ref != QStringLiteral("E" ) && ref != QStringLiteral("W" )) |
1002 | return qQNaN(); |
1003 | auto lon = m_gpsTags.value(GPS_LONGITUDE).value<QList<double>>(); |
1004 | if (lon.size() != 3) |
1005 | return qQNaN(); |
1006 | auto degree = lon.at(i: 0) + lon.at(i: 1) / 60 + lon.at(i: 2) / 3600; |
1007 | if (degree < -180.0 || degree > 180.0) |
1008 | return qQNaN(); |
1009 | return ref == QStringLiteral("E" ) ? degree : -degree; |
1010 | } |
1011 | |
1012 | void MicroExif::setLongitude(double degree) |
1013 | { |
1014 | if (qIsNaN(d: degree)) { |
1015 | m_gpsTags.remove(GPS_LONGITUDEREF); |
1016 | m_gpsTags.remove(GPS_LONGITUDE); |
1017 | } |
1018 | if (degree < -180.0 || degree > 180.0) |
1019 | return; // invalid longitude |
1020 | auto adeg = qAbs(t: degree); |
1021 | auto min = (adeg - int(adeg)) * 60; |
1022 | auto sec = (min - int(min)) * 60; |
1023 | m_gpsTags.insert(GPS_LONGITUDEREF, value: degree < 0 ? QStringLiteral("W" ) : QStringLiteral("E" )); |
1024 | m_gpsTags.insert(GPS_LONGITUDE, value: QVariant::fromValue(value: QList<double>() << int(adeg) << int(min) << sec)); |
1025 | } |
1026 | |
1027 | double MicroExif::altitude() const |
1028 | { |
1029 | auto ref = m_gpsTags.value(GPS_ALTITUDEREF); |
1030 | if (ref.isNull()) |
1031 | return qQNaN(); |
1032 | if (!m_gpsTags.contains(GPS_ALTITUDE)) |
1033 | return qQNaN(); |
1034 | auto alt = m_gpsTags.value(GPS_ALTITUDE).toDouble(); |
1035 | return (ref.toInt() == 0 || ref.toInt() == 2) ? alt : -alt; |
1036 | } |
1037 | |
1038 | void MicroExif::setAltitude(double meters) |
1039 | { |
1040 | if (qIsNaN(d: meters)) { |
1041 | m_gpsTags.remove(GPS_ALTITUDEREF); |
1042 | m_gpsTags.remove(GPS_ALTITUDE); |
1043 | } |
1044 | m_gpsTags.insert(GPS_ALTITUDEREF, value: quint8(meters < 0 ? 1 : 0)); |
1045 | m_gpsTags.insert(GPS_ALTITUDE, value: meters); |
1046 | } |
1047 | |
1048 | double MicroExif::imageDirection(bool *isMagnetic) const |
1049 | { |
1050 | auto tmp = false; |
1051 | if (isMagnetic == nullptr) |
1052 | isMagnetic = &tmp; |
1053 | if (!m_gpsTags.contains(GPS_IMGDIRECTION)) |
1054 | return qQNaN(); |
1055 | auto ref = gpsString(GPS_IMGDIRECTIONREF).toUpper(); |
1056 | *isMagnetic = (ref == QStringLiteral("M" )); |
1057 | return m_gpsTags.value(GPS_IMGDIRECTION).toDouble(); |
1058 | } |
1059 | |
1060 | void MicroExif::setImageDirection(double degree, bool isMagnetic) |
1061 | { |
1062 | if (qIsNaN(d: degree)) { |
1063 | m_gpsTags.remove(GPS_IMGDIRECTIONREF); |
1064 | m_gpsTags.remove(GPS_IMGDIRECTION); |
1065 | } |
1066 | m_gpsTags.insert(GPS_IMGDIRECTIONREF, value: isMagnetic ? QStringLiteral("M" ) : QStringLiteral("T" )); |
1067 | m_gpsTags.insert(GPS_IMGDIRECTION, value: degree); |
1068 | } |
1069 | |
1070 | QByteArray MicroExif::toByteArray(const QDataStream::ByteOrder &byteOrder, const Version &version) const |
1071 | { |
1072 | QByteArray ba; |
1073 | { |
1074 | QBuffer buf(&ba); |
1075 | if (!write(device: &buf, byteOrder, version)) |
1076 | return {}; |
1077 | } |
1078 | return ba; |
1079 | } |
1080 | |
1081 | QByteArray MicroExif::exifIfdByteArray(const QDataStream::ByteOrder &byteOrder, const Version &version) const |
1082 | { |
1083 | QByteArray ba; |
1084 | { |
1085 | QDataStream ds(&ba, QIODevice::WriteOnly); |
1086 | ds.setByteOrder(byteOrder); |
1087 | auto exifTags = m_exifTags; |
1088 | exifTags.insert(EXIF_EXIFVERSION, value: version == Version::V3 ? QByteArray("0300" ) : QByteArray("0232" )); |
1089 | TagPos positions; |
1090 | if (!writeIfd(ds, ver: version, tags: exifTags, positions)) |
1091 | return {}; |
1092 | } |
1093 | return ba; |
1094 | } |
1095 | |
1096 | bool MicroExif::setExifIfdByteArray(const QByteArray &ba, const QDataStream::ByteOrder &byteOrder) |
1097 | { |
1098 | QDataStream ds(ba); |
1099 | ds.setByteOrder(byteOrder); |
1100 | return readIfd(ds, tags&: m_exifTags, pos: 0, knownTags: staticTagTypes); |
1101 | } |
1102 | |
1103 | QByteArray MicroExif::gpsIfdByteArray(const QDataStream::ByteOrder &byteOrder, const Version &version) const |
1104 | { |
1105 | QByteArray ba; |
1106 | { |
1107 | QDataStream ds(&ba, QIODevice::WriteOnly); |
1108 | ds.setByteOrder(byteOrder); |
1109 | auto gpsTags = m_gpsTags; |
1110 | gpsTags.insert(GPS_GPSVERSION, value: QByteArray("2400" )); |
1111 | TagPos positions; |
1112 | if (!writeIfd(ds, ver: version, tags: gpsTags, positions, pos: 0, knownTags: staticGpsTagTypes)) |
1113 | return {}; |
1114 | return ba; |
1115 | } |
1116 | } |
1117 | |
1118 | bool MicroExif::setGpsIfdByteArray(const QByteArray &ba, const QDataStream::ByteOrder &byteOrder) |
1119 | { |
1120 | QDataStream ds(ba); |
1121 | ds.setByteOrder(byteOrder); |
1122 | return readIfd(ds, tags&: m_gpsTags, pos: 0, knownTags: staticGpsTagTypes); |
1123 | } |
1124 | |
1125 | bool MicroExif::write(QIODevice *device, const QDataStream::ByteOrder &byteOrder, const Version &version) const |
1126 | { |
1127 | if (device == nullptr || device->isSequential() || isEmpty()) |
1128 | return false; |
1129 | if (device->open(mode: QBuffer::WriteOnly)) { |
1130 | QDataStream ds(device); |
1131 | ds.setByteOrder(byteOrder); |
1132 | if (!writeHeader(ds)) |
1133 | return false; |
1134 | if (!writeIfds(ds, version)) |
1135 | return false; |
1136 | } |
1137 | device->close(); |
1138 | return true; |
1139 | } |
1140 | |
1141 | void MicroExif::updateImageMetadata(QImage &targetImage, bool replaceExisting) const |
1142 | { |
1143 | // set TIFF strings |
1144 | for (auto &&p : tiffStrMap) { |
1145 | if (!replaceExisting && !targetImage.text(key: p.second).isEmpty()) |
1146 | continue; |
1147 | auto s = tiffString(tagId: p.first); |
1148 | if (!s.isEmpty()) |
1149 | targetImage.setText(key: p.second, value: s); |
1150 | } |
1151 | |
1152 | // set EXIF strings |
1153 | for (auto &&p : exifStrMap) { |
1154 | if (!replaceExisting && !targetImage.text(key: p.second).isEmpty()) |
1155 | continue; |
1156 | auto s = exifString(tagId: p.first); |
1157 | if (!s.isEmpty()) |
1158 | targetImage.setText(key: p.second, value: s); |
1159 | } |
1160 | |
1161 | // set date and time |
1162 | if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_MODIFICATIONDATE)).isEmpty()) { |
1163 | auto dt = dateTime(); |
1164 | if (dt.isValid()) |
1165 | targetImage.setText(QStringLiteral(META_KEY_MODIFICATIONDATE), value: dt.toString(format: Qt::ISODate)); |
1166 | } |
1167 | if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_CREATIONDATE)).isEmpty()) { |
1168 | auto dt = dateTimeOriginal(); |
1169 | if (dt.isValid()) |
1170 | targetImage.setText(QStringLiteral(META_KEY_CREATIONDATE), value: dt.toString(format: Qt::ISODate)); |
1171 | } |
1172 | |
1173 | // set GPS info |
1174 | if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_ALTITUDE)).isEmpty()) { |
1175 | auto v = altitude(); |
1176 | if (!qIsNaN(d: v)) |
1177 | targetImage.setText(QStringLiteral(META_KEY_ALTITUDE), QStringLiteral("%1" ).arg(a: v, fieldWidth: 0, format: 'g', precision: 9)); |
1178 | } |
1179 | if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_LATITUDE)).isEmpty()) { |
1180 | auto v = latitude(); |
1181 | if (!qIsNaN(d: v)) |
1182 | targetImage.setText(QStringLiteral(META_KEY_LATITUDE), QStringLiteral("%1" ).arg(a: v, fieldWidth: 0, format: 'g', precision: 9)); |
1183 | } |
1184 | if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_LONGITUDE)).isEmpty()) { |
1185 | auto v = longitude(); |
1186 | if (!qIsNaN(d: v)) |
1187 | targetImage.setText(QStringLiteral(META_KEY_LONGITUDE), QStringLiteral("%1" ).arg(a: v, fieldWidth: 0, format: 'g', precision: 9)); |
1188 | } |
1189 | if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_DIRECTION)).isEmpty()) { |
1190 | auto v = imageDirection(); |
1191 | if (!qIsNaN(d: v)) |
1192 | targetImage.setText(QStringLiteral(META_KEY_DIRECTION), QStringLiteral("%1" ).arg(a: v, fieldWidth: 0, format: 'g', precision: 9)); |
1193 | } |
1194 | } |
1195 | |
1196 | bool MicroExif::updateImageResolution(QImage &targetImage) |
1197 | { |
1198 | if (horizontalResolution() > 0) |
1199 | targetImage.setDotsPerMeterX(qRound(d: horizontalResolution() / 25.4 * 1000)); |
1200 | if (verticalResolution() > 0) |
1201 | targetImage.setDotsPerMeterY(qRound(d: verticalResolution() / 25.4 * 1000)); |
1202 | return (horizontalResolution() > 0) || (verticalResolution() > 0); |
1203 | } |
1204 | |
1205 | MicroExif MicroExif::fromByteArray(const QByteArray &ba, bool ) |
1206 | { |
1207 | auto ba0(ba); |
1208 | if (searchHeader) { |
1209 | auto idxLE = ba0.indexOf(bv: QByteArray("II" )); |
1210 | auto idxBE = ba0.indexOf(bv: QByteArray("MM" )); |
1211 | auto idx = -1; |
1212 | if (idxLE > -1 && idxBE > -1) |
1213 | idx = std::min(a: idxLE, b: idxBE); |
1214 | else |
1215 | idx = idxLE > -1 ? idxLE : idxBE; |
1216 | if(idx > 0) |
1217 | ba0 = ba0.mid(index: idx); |
1218 | } |
1219 | QBuffer buf; |
1220 | buf.setData(ba0); |
1221 | return fromDevice(device: &buf); |
1222 | } |
1223 | |
1224 | MicroExif MicroExif::fromRawData(const char *data, size_t size, bool ) |
1225 | { |
1226 | if (data == nullptr || size == 0) |
1227 | return {}; |
1228 | return fromByteArray(ba: QByteArray::fromRawData(data, size), searchHeader); |
1229 | } |
1230 | |
1231 | MicroExif MicroExif::fromDevice(QIODevice *device) |
1232 | { |
1233 | if (device == nullptr || device->isSequential()) |
1234 | return {}; |
1235 | if (!device->open(mode: QBuffer::ReadOnly)) |
1236 | return {}; |
1237 | |
1238 | QDataStream ds(device); |
1239 | if (!checkHeader(ds)) |
1240 | return {}; |
1241 | |
1242 | MicroExif exif; |
1243 | |
1244 | // read TIFF ifd |
1245 | if (!readIfd(ds, tags&: exif.m_tiffTags)) |
1246 | return {}; |
1247 | |
1248 | // read EXIF ifd |
1249 | if (auto pos = exif.m_tiffTags.value(EXIF_EXIFIFD).toUInt()) { |
1250 | if (!readIfd(ds, tags&: exif.m_exifTags, pos)) |
1251 | return {}; |
1252 | } |
1253 | |
1254 | // read GPS ifd |
1255 | if (auto pos = exif.m_tiffTags.value(EXIF_GPSIFD).toUInt()) { |
1256 | if (!readIfd(ds, tags&: exif.m_gpsTags, pos, knownTags: staticGpsTagTypes)) |
1257 | return {}; |
1258 | } |
1259 | |
1260 | return exif; |
1261 | } |
1262 | |
1263 | MicroExif MicroExif::fromImage(const QImage &image) |
1264 | { |
1265 | MicroExif exif; |
1266 | if (image.isNull()) |
1267 | return exif; |
1268 | |
1269 | // Image properties |
1270 | exif.setWidth(image.width()); |
1271 | exif.setHeight(image.height()); |
1272 | exif.setHorizontalResolution(image.dotsPerMeterX() * 25.4 / 1000); |
1273 | exif.setVerticalResolution(image.dotsPerMeterY() * 25.4 / 1000); |
1274 | exif.setColorSpace(image.colorSpace()); |
1275 | |
1276 | // TIFF strings |
1277 | for (auto &&p : tiffStrMap) { |
1278 | exif.setTiffString(tagId: p.first, s: image.text(key: p.second)); |
1279 | } |
1280 | |
1281 | // EXIF strings |
1282 | for (auto &&p : exifStrMap) { |
1283 | exif.setExifString(tagId: p.first, s: image.text(key: p.second)); |
1284 | } |
1285 | |
1286 | // TIFF Software |
1287 | if (exif.software().isEmpty()) { |
1288 | auto sw = QCoreApplication::applicationName(); |
1289 | auto ver = sw = QCoreApplication::applicationVersion(); |
1290 | if (!sw.isEmpty() && !ver.isEmpty()) |
1291 | sw.append(QStringLiteral(" %1" ).arg(a: ver)); |
1292 | exif.setSoftware(sw.trimmed()); |
1293 | } |
1294 | |
1295 | // TIFF date and time |
1296 | auto dt = QDateTime::fromString(string: image.text(QStringLiteral(META_KEY_MODIFICATIONDATE)), format: Qt::ISODate); |
1297 | if (!dt.isValid()) |
1298 | dt = QDateTime::currentDateTime(); |
1299 | exif.setDateTime(dt); |
1300 | |
1301 | // EXIF original date and time |
1302 | dt = QDateTime::fromString(string: image.text(QStringLiteral(META_KEY_CREATIONDATE)), format: Qt::ISODate); |
1303 | if (!dt.isValid()) |
1304 | dt = QDateTime::currentDateTime(); |
1305 | exif.setDateTimeOriginal(dt); |
1306 | |
1307 | // GPS Info |
1308 | auto ok = false; |
1309 | auto alt = image.text(QStringLiteral(META_KEY_ALTITUDE)).toDouble(ok: &ok); |
1310 | if (ok) |
1311 | exif.setAltitude(alt); |
1312 | auto lat = image.text(QStringLiteral(META_KEY_LATITUDE)).toDouble(ok: &ok); |
1313 | if (ok) |
1314 | exif.setLatitude(lat); |
1315 | auto lon = image.text(QStringLiteral(META_KEY_LONGITUDE)).toDouble(ok: &ok); |
1316 | if (ok) |
1317 | exif.setLongitude(lon); |
1318 | auto dir = image.text(QStringLiteral(META_KEY_DIRECTION)).toDouble(ok: &ok); |
1319 | if (ok) |
1320 | exif.setImageDirection(degree: dir); |
1321 | |
1322 | return exif; |
1323 | } |
1324 | |
1325 | void MicroExif::setTiffString(quint16 tagId, const QString &s) |
1326 | { |
1327 | MicroExif::setString(tags&: m_tiffTags, tagId, s); |
1328 | } |
1329 | |
1330 | QString MicroExif::tiffString(quint16 tagId) const |
1331 | { |
1332 | return MicroExif::string(tags: m_tiffTags, tagId); |
1333 | } |
1334 | |
1335 | void MicroExif::setExifString(quint16 tagId, const QString &s) |
1336 | { |
1337 | MicroExif::setString(tags&: m_exifTags, tagId, s); |
1338 | } |
1339 | |
1340 | QString MicroExif::exifString(quint16 tagId) const |
1341 | { |
1342 | return MicroExif::string(tags: m_exifTags, tagId); |
1343 | } |
1344 | |
1345 | void MicroExif::setGpsString(quint16 tagId, const QString &s) |
1346 | { |
1347 | MicroExif::setString(tags&: m_gpsTags, tagId, s); |
1348 | } |
1349 | |
1350 | QString MicroExif::gpsString(quint16 tagId) const |
1351 | { |
1352 | return MicroExif::string(tags: m_gpsTags, tagId); |
1353 | } |
1354 | |
1355 | bool MicroExif::(QDataStream &ds) const |
1356 | { |
1357 | if (ds.byteOrder() == QDataStream::LittleEndian) |
1358 | ds << quint16(0x4949); // II |
1359 | else |
1360 | ds << quint16(0x4d4d); // MM |
1361 | ds << quint16(0x002a); // Tiff V6 |
1362 | ds << quint32(8); // IFD offset |
1363 | return ds.status() == QDataStream::Ok; |
1364 | } |
1365 | |
1366 | bool MicroExif::writeIfds(QDataStream &ds, const Version &version) const |
1367 | { |
1368 | auto tiffTags = m_tiffTags; |
1369 | auto exifTags = m_exifTags; |
1370 | auto gpsTags = m_gpsTags; |
1371 | updateTags(tiffTags, exifTags, gpsTags, version); |
1372 | |
1373 | TagPos positions; |
1374 | if (!writeIfd(ds, ver: version, tags: tiffTags, positions)) |
1375 | return false; |
1376 | if (!writeIfd(ds, ver: version, tags: exifTags, positions, pos: positions.value(EXIF_EXIFIFD))) |
1377 | return false; |
1378 | if (!writeIfd(ds, ver: version, tags: gpsTags, positions, pos: positions.value(EXIF_GPSIFD), knownTags: staticGpsTagTypes)) |
1379 | return false; |
1380 | return true; |
1381 | } |
1382 | |
1383 | void MicroExif::updateTags(Tags &tiffTags, Tags &exifTags, Tags &gpsTags, const Version &version) const |
1384 | { |
1385 | if (exifTags.isEmpty()) { |
1386 | tiffTags.remove(EXIF_EXIFIFD); |
1387 | } else { |
1388 | tiffTags.insert(EXIF_EXIFIFD, value: quint32()); |
1389 | exifTags.insert(EXIF_EXIFVERSION, value: version == Version::V3 ? QByteArray("0300" ) : QByteArray("0232" )); |
1390 | } |
1391 | if (gpsTags.isEmpty()) { |
1392 | tiffTags.remove(EXIF_GPSIFD); |
1393 | } else { |
1394 | tiffTags.insert(EXIF_GPSIFD, value: quint32()); |
1395 | gpsTags.insert(GPS_GPSVERSION, value: QByteArray("2400" )); |
1396 | } |
1397 | } |
1398 | |
1399 | void MicroExif::setString(Tags &tags, quint16 tagId, const QString &s) |
1400 | { |
1401 | if (s.isEmpty()) |
1402 | tags.remove(key: tagId); |
1403 | else |
1404 | tags.insert(key: tagId, value: s); |
1405 | } |
1406 | |
1407 | QString MicroExif::string(const Tags &tags, quint16 tagId) |
1408 | { |
1409 | return tags.value(key: tagId).toString(); |
1410 | } |
1411 | |