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
73enum 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
101using TagPos = QHash<quint16, quint32>;
102using KnownTags = QHash<quint16, ExifTagType>;
103using 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
111static 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
150static 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
168static 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
183static 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 */
197static 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 */
216static 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 */
231static bool checkHeader(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 */
262static 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
276static 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
305template <class T>
306static 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
317inline 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
323template<class T>
324static 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
336static 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
344static 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
371static 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 */
401static 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
457template<class T>
458static 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
472template<class T>
473static 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
486static 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 */
515static 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
612MicroExif::MicroExif()
613{
614
615}
616
617void MicroExif::clear()
618{
619 m_tiffTags.clear();
620 m_exifTags.clear();
621 m_gpsTags.clear();
622}
623
624bool MicroExif::isEmpty() const
625{
626 return m_tiffTags.isEmpty() && m_exifTags.isEmpty() && m_gpsTags.isEmpty();
627}
628
629double 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
638void 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
649double 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
658void 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
669QColorSpace 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
676void 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
682void 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
688qint32 MicroExif::width() const
689{
690 return m_tiffTags.value(TIFF_IMAGEWIDTH).toUInt();
691}
692
693void MicroExif::setWidth(qint32 w)
694{
695 m_tiffTags.insert(TIFF_IMAGEWIDTH, value: w);
696 m_exifTags.insert(EXIF_PIXELXDIM, value: w);
697}
698
699qint32 MicroExif::height() const
700{
701 return m_tiffTags.value(TIFF_IMAGEHEIGHT).toUInt();
702}
703
704void MicroExif::setHeight(qint32 h)
705{
706 m_tiffTags.insert(TIFF_IMAGEHEIGHT, value: h);
707 m_exifTags.insert(EXIF_PIXELYDIM, value: h);
708}
709
710quint16 MicroExif::orientation() const
711{
712 return m_tiffTags.value(TIFF_ORIENT).toUInt();
713}
714
715void 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
723QImageIOHandler::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
748void 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
781QString MicroExif::software() const
782{
783 return tiffString(TIFF_SOFTWARE);
784}
785
786void MicroExif::setSoftware(const QString &s)
787{
788 setTiffString(TIFF_SOFTWARE, s);
789}
790
791QString MicroExif::description() const
792{
793 return tiffString(TIFF_IMAGEDESCRIPTION);
794}
795
796void MicroExif::setDescription(const QString &s)
797{
798 setTiffString(TIFF_IMAGEDESCRIPTION, s);
799}
800
801QString MicroExif::artist() const
802{
803 return tiffString(TIFF_ARTIST);
804}
805
806void MicroExif::setArtist(const QString &s)
807{
808 setTiffString(TIFF_ARTIST, s);
809}
810
811QString MicroExif::copyright() const
812{
813 return tiffString(TIFF_COPYRIGHT);
814}
815
816void MicroExif::setCopyright(const QString &s)
817{
818 setTiffString(TIFF_COPYRIGHT, s);
819}
820
821QString MicroExif::make() const
822{
823 return tiffString(TIFF_MAKE);
824}
825
826void MicroExif::setMake(const QString &s)
827{
828 setTiffString(TIFF_MAKE, s);
829}
830
831QString MicroExif::model() const
832{
833 return tiffString(TIFF_MODEL);
834}
835
836void MicroExif::setModel(const QString &s)
837{
838 setTiffString(TIFF_MODEL, s);
839}
840
841QString MicroExif::serialNumber() const
842{
843 return tiffString(EXIF_BODYSERIALNUMBER);
844}
845
846void MicroExif::setSerialNumber(const QString &s)
847{
848 setTiffString(EXIF_BODYSERIALNUMBER, s);
849}
850
851QString MicroExif::lensMake() const
852{
853 return tiffString(EXIF_LENSMAKE);
854}
855
856void MicroExif::setLensMake(const QString &s)
857{
858 setTiffString(EXIF_LENSMAKE, s);
859}
860
861QString MicroExif::lensModel() const
862{
863 return tiffString(EXIF_LENSMODEL);
864}
865
866void MicroExif::setLensModel(const QString &s)
867{
868 setTiffString(EXIF_LENSMODEL, s);
869}
870
871QString MicroExif::lensSerialNumber() const
872{
873 return tiffString(EXIF_LENSSERIALNUMBER);
874}
875
876void MicroExif::setLensSerialNumber(const QString &s)
877{
878 setTiffString(EXIF_LENSSERIALNUMBER, s);
879}
880
881QDateTime 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
890void 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
901QDateTime 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
910void 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
921QDateTime 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
930void 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
941QString MicroExif::title() const
942{
943 return exifString(EXIF_IMAGETITLE);
944}
945
946void MicroExif::setImageTitle(const QString &s)
947{
948 setExifString(EXIF_IMAGETITLE, s);
949}
950
951QUuid 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
961void 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
969double 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
983void 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
998double 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
1012void 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
1027double 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
1038void 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
1048double 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
1060void 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
1070QByteArray 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
1081QByteArray 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
1096bool 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
1103QByteArray 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
1118bool 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
1125bool 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
1141void 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
1196bool 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
1205MicroExif MicroExif::fromByteArray(const QByteArray &ba, bool searchHeader)
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
1224MicroExif MicroExif::fromRawData(const char *data, size_t size, bool searchHeader)
1225{
1226 if (data == nullptr || size == 0)
1227 return {};
1228 return fromByteArray(ba: QByteArray::fromRawData(data, size), searchHeader);
1229}
1230
1231MicroExif 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
1263MicroExif 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
1325void MicroExif::setTiffString(quint16 tagId, const QString &s)
1326{
1327 MicroExif::setString(tags&: m_tiffTags, tagId, s);
1328}
1329
1330QString MicroExif::tiffString(quint16 tagId) const
1331{
1332 return MicroExif::string(tags: m_tiffTags, tagId);
1333}
1334
1335void MicroExif::setExifString(quint16 tagId, const QString &s)
1336{
1337 MicroExif::setString(tags&: m_exifTags, tagId, s);
1338}
1339
1340QString MicroExif::exifString(quint16 tagId) const
1341{
1342 return MicroExif::string(tags: m_exifTags, tagId);
1343}
1344
1345void MicroExif::setGpsString(quint16 tagId, const QString &s)
1346{
1347 MicroExif::setString(tags&: m_gpsTags, tagId, s);
1348}
1349
1350QString MicroExif::gpsString(quint16 tagId) const
1351{
1352 return MicroExif::string(tags: m_gpsTags, tagId);
1353}
1354
1355bool MicroExif::writeHeader(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
1366bool 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
1383void 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
1399void 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
1407QString MicroExif::string(const Tags &tags, quint16 tagId)
1408{
1409 return tags.value(key: tagId).toString();
1410}
1411

source code of kimageformats/src/imageformats/microexif.cpp