KImageFormats

microexif.cpp
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(3) != u':')
200 return 0;
201 auto ok = false;
202 auto hh = offset.left(3).toInt(&ok);
203 if (!ok)
204 return 0;
205 auto mm = offset.mid(4, 2).toInt(&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(offset));
219 return QStringLiteral("%1%2:%3")
220 .arg(offset < 0 ? QStringLiteral("-") : QStringLiteral("+"))
221 .arg(absOff / 60, 2, 10, QChar(u'0'))
222 .arg(absOff % 60, 2, 10, 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) {
237 } else if (order == 0x4d4d) {
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(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(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(1, 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(v);
320 return 8 - qBound(0, v < 1 ? 8 : int(std::log10(v)), 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(value.toDouble());
329 for (auto &&v : l) {
330 auto den = std::pow(10, rationalPrecision(v));
331 ds << T(qRound(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, value.toString().toLatin1().append(char()));
348 } else if (dataType == ExifTagType::Utf8) {
349 writeByteArray(ds, value.toString().toUtf8().append(char()));
350 } else if (dataType == ExifTagType::Undefined) {
351 writeByteArray(ds, 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(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, 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(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, 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{
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(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(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(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, 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.
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(tagId, dec.hasError() ? QString::fromLatin1(l) : ut8);
573 }
574 } else if (dataType == EXIF_TAG_DATATYPE(ExifTagType::Undefined)) {
575 auto l = readBytes(ds, count, false);
576 if (!l.isEmpty())
577 tags.insert(tagId, l);
578 } else if (dataType == EXIF_TAG_DATATYPE(ExifTagType::Byte)) {
579 auto l = readList<quint8>(ds, count);
580 tags.insert(tagId, l.size() == 1 ? QVariant(l.first()) : QVariant::fromValue(l));
581 } else if (dataType == EXIF_TAG_DATATYPE(ExifTagType::SByte)) {
582 auto l = readList<qint8>(ds, count);
583 tags.insert(tagId, l.size() == 1 ? QVariant(l.first()) : QVariant::fromValue(l));
584 } else if (dataType == EXIF_TAG_DATATYPE(ExifTagType::Short)) {
585 auto l = readList<quint16>(ds, count);
586 tags.insert(tagId, l.size() == 1 ? QVariant(l.first()) : QVariant::fromValue(l));
587 } else if (dataType == EXIF_TAG_DATATYPE(ExifTagType::SShort)) {
588 auto l = readList<qint16>(ds, count);
589 tags.insert(tagId, l.size() == 1 ? QVariant(l.first()) : QVariant::fromValue(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(tagId, l.size() == 1 ? QVariant(l.first()) : QVariant::fromValue(l));
593 } else if (dataType == EXIF_TAG_DATATYPE(ExifTagType::SLong)) {
594 auto l = readList<qint32>(ds, count);
595 tags.insert(tagId, l.size() == 1 ? QVariant(l.first()) : QVariant::fromValue(l));
596 } else if (dataType == EXIF_TAG_DATATYPE(ExifTagType::Rational)) {
597 auto l = readRationalList<quint32>(ds, count);
598 tags.insert(tagId, l.size() == 1 ? QVariant(l.first()) : QVariant::fromValue(l));
599 } else if (dataType == EXIF_TAG_DATATYPE(ExifTagType::SRational)) {
600 auto l = readRationalList<qint32>(ds, count);
601 tags.insert(tagId, l.size() == 1 ? QVariant(l.first()) : QVariant::fromValue(l));
602 }
603
604 if (curPos > 0 && !device->seek(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, 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, 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, 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, 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, w);
696 m_exifTags.insert(EXIF_PIXELXDIM, 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, h);
707 m_exifTags.insert(EXIF_PIXELYDIM, 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, orient);
721}
722
723QImageIOHandler::Transformation MicroExif::transformation() const
724{
725 switch (orientation()) {
726 case 1:
728 case 2:
730 case 3:
732 case 4:
734 case 5:
736 case 6:
738 case 7:
740 case 8:
742 default:
743 break;
744 };
746}
747
748void MicroExif::setTransformation(const QImageIOHandler::Transformation &t)
749{
750 switch (t) {
752 setOrientation(1);
753 break;
755 setOrientation(2);
756 break;
758 setOrientation(3);
759 break;
761 setOrientation(4);
762 break;
764 setOrientation(5);
765 break;
767 setOrientation(6);
768 break;
770 setOrientation(7);
771 break;
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(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(QTimeZone::fromSecondsAheadOfUtc(timeOffset(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, dt.toString(QStringLiteral("yyyy:MM:dd HH:mm:ss")));
898 setExifString(EXIF_OFFSETTIME, timeOffset(dt.offsetFromUtc() / 60));
899}
900
901QDateTime MicroExif::dateTimeOriginal() const
902{
903 auto dt = QDateTime::fromString(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(QTimeZone::fromSecondsAheadOfUtc(timeOffset(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, dt.toString(QStringLiteral("yyyy:MM:dd HH:mm:ss")));
918 setExifString(EXIF_OFFSETTIMEORIGINAL, timeOffset(dt.offsetFromUtc() / 60));
919}
920
921QDateTime MicroExif::dateTimeDigitized() const
922{
923 auto dt = QDateTime::fromString(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(QTimeZone::fromSecondsAheadOfUtc(timeOffset(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, dt.toString(QStringLiteral("yyyy:MM:dd HH:mm:ss")));
938 setExifString(EXIF_OFFSETTIMEDIGITIZED, timeOffset(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(s.left(8), s.mid(8, 4), s.mid(12, 4), s.mid(16, 4), s.mid(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, QString());
965 else
966 setExifString(EXIF_IMAGEUNIQUEID, uuid.toString(QUuid::WithoutBraces).replace(QStringLiteral("-"), 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(0) + lat.at(1) / 60 + lat.at(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(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(degree);
992 auto min = (adeg - int(adeg)) * 60;
993 auto sec = (min - int(min)) * 60;
994 m_gpsTags.insert(GPS_LATITUDEREF, degree < 0 ? QStringLiteral("S") : QStringLiteral("N"));
995 m_gpsTags.insert(GPS_LATITUDE, QVariant::fromValue(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(0) + lon.at(1) / 60 + lon.at(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(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(degree);
1021 auto min = (adeg - int(adeg)) * 60;
1022 auto sec = (min - int(min)) * 60;
1023 m_gpsTags.insert(GPS_LONGITUDEREF, degree < 0 ? QStringLiteral("W") : QStringLiteral("E"));
1024 m_gpsTags.insert(GPS_LONGITUDE, QVariant::fromValue(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(meters)) {
1041 m_gpsTags.remove(GPS_ALTITUDEREF);
1042 m_gpsTags.remove(GPS_ALTITUDE);
1043 }
1044 m_gpsTags.insert(GPS_ALTITUDEREF, quint8(meters < 0 ? 1 : 0));
1045 m_gpsTags.insert(GPS_ALTITUDE, 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(degree)) {
1063 m_gpsTags.remove(GPS_IMGDIRECTIONREF);
1064 m_gpsTags.remove(GPS_IMGDIRECTION);
1065 }
1066 m_gpsTags.insert(GPS_IMGDIRECTIONREF, isMagnetic ? QStringLiteral("M") : QStringLiteral("T"));
1067 m_gpsTags.insert(GPS_IMGDIRECTION, 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(&buf, byteOrder))
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, version == Version::V3 ? QByteArray("0300") : QByteArray("0232"));
1089 TagPos positions;
1090 if (!writeIfd(ds, version, 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, m_exifTags, 0, 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, QByteArray("2400"));
1111 TagPos positions;
1112 if (!writeIfd(ds, version, gpsTags, positions, 0, 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, m_gpsTags, 0, 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(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(p.second).isEmpty())
1146 continue;
1147 auto s = tiffString(p.first);
1148 if (!s.isEmpty())
1149 targetImage.setText(p.second, s);
1150 }
1151
1152 // set EXIF strings
1153 for (auto &&p : exifStrMap) {
1154 if (!replaceExisting && !targetImage.text(p.second).isEmpty())
1155 continue;
1156 auto s = exifString(p.first);
1157 if (!s.isEmpty())
1158 targetImage.setText(p.second, 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), dt.toString(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), dt.toString(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(v))
1177 targetImage.setText(QStringLiteral(META_KEY_ALTITUDE), QStringLiteral("%1").arg(v, 0, 'g', 9));
1178 }
1179 if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_LATITUDE)).isEmpty()) {
1180 auto v = latitude();
1181 if (!qIsNaN(v))
1182 targetImage.setText(QStringLiteral(META_KEY_LATITUDE), QStringLiteral("%1").arg(v, 0, 'g', 9));
1183 }
1184 if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_LONGITUDE)).isEmpty()) {
1185 auto v = longitude();
1186 if (!qIsNaN(v))
1187 targetImage.setText(QStringLiteral(META_KEY_LONGITUDE), QStringLiteral("%1").arg(v, 0, 'g', 9));
1188 }
1189 if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_DIRECTION)).isEmpty()) {
1190 auto v = imageDirection();
1191 if (!qIsNaN(v))
1192 targetImage.setText(QStringLiteral(META_KEY_DIRECTION), QStringLiteral("%1").arg(v, 0, 'g', 9));
1193 }
1194}
1195
1196void MicroExif::updateImageResolution(QImage &targetImage)
1197{
1198 if (horizontalResolution() > 0)
1199 targetImage.setDotsPerMeterX(qRound(horizontalResolution() / 25.4 * 1000));
1200 if (verticalResolution() > 0)
1201 targetImage.setDotsPerMeterY(qRound(verticalResolution() / 25.4 * 1000));
1202}
1203
1204MicroExif MicroExif::fromByteArray(const QByteArray &ba, bool searchHeader)
1205{
1206 auto ba0(ba);
1207 if (searchHeader) {
1208 auto idxLE = ba0.indexOf(QByteArray("II"));
1209 auto idxBE = ba0.indexOf(QByteArray("MM"));
1210 auto idx = -1;
1211 if (idxLE > -1 && idxBE > -1)
1212 idx = std::min(idxLE, idxBE);
1213 else
1214 idx = idxLE > -1 ? idxLE : idxBE;
1215 if(idx > 0)
1216 ba0 = ba0.mid(idx);
1217 }
1218 QBuffer buf;
1219 buf.setData(ba0);
1220 return fromDevice(&buf);
1221}
1222
1223MicroExif MicroExif::fromRawData(const char *data, size_t size, bool searchHeader)
1224{
1225 if (data == nullptr || size == 0)
1226 return {};
1227 return fromByteArray(QByteArray::fromRawData(data, size), searchHeader);
1228}
1229
1230MicroExif MicroExif::fromDevice(QIODevice *device)
1231{
1232 if (device == nullptr || device->isSequential())
1233 return {};
1234 if (!device->open(QBuffer::ReadOnly))
1235 return {};
1236
1237 QDataStream ds(device);
1238 if (!checkHeader(ds))
1239 return {};
1240
1241 MicroExif exif;
1242
1243 // read TIFF ifd
1244 if (!readIfd(ds, exif.m_tiffTags))
1245 return {};
1246
1247 // read EXIF ifd
1248 if (auto pos = exif.m_tiffTags.value(EXIF_EXIFIFD).toUInt()) {
1249 if (!readIfd(ds, exif.m_exifTags, pos))
1250 return {};
1251 }
1252
1253 // read GPS ifd
1254 if (auto pos = exif.m_tiffTags.value(EXIF_GPSIFD).toUInt()) {
1255 if (!readIfd(ds, exif.m_gpsTags, pos, staticGpsTagTypes))
1256 return {};
1257 }
1258
1259 return exif;
1260}
1261
1262MicroExif MicroExif::fromImage(const QImage &image)
1263{
1264 MicroExif exif;
1265 if (image.isNull())
1266 return exif;
1267
1268 // Image properties
1269 exif.setWidth(image.width());
1270 exif.setHeight(image.height());
1271 exif.setHorizontalResolution(image.dotsPerMeterX() * 25.4 / 1000);
1272 exif.setVerticalResolution(image.dotsPerMeterY() * 25.4 / 1000);
1273 exif.setColorSpace(image.colorSpace());
1274
1275 // TIFF strings
1276 for (auto &&p : tiffStrMap) {
1277 exif.setTiffString(p.first, image.text(p.second));
1278 }
1279
1280 // EXIF strings
1281 for (auto &&p : exifStrMap) {
1282 exif.setExifString(p.first, image.text(p.second));
1283 }
1284
1285 // TIFF Software
1286 if (exif.software().isEmpty()) {
1288 auto ver = sw = QCoreApplication::applicationVersion();
1289 if (!sw.isEmpty() && !ver.isEmpty())
1290 sw.append(QStringLiteral(" %1").arg(ver));
1291 exif.setSoftware(sw.trimmed());
1292 }
1293
1294 // TIFF date and time
1295 auto dt = QDateTime::fromString(image.text(QStringLiteral(META_KEY_MODIFICATIONDATE)), Qt::ISODate);
1296 if (!dt.isValid())
1298 exif.setDateTime(dt);
1299
1300 // EXIF original date and time
1301 dt = QDateTime::fromString(image.text(QStringLiteral(META_KEY_CREATIONDATE)), Qt::ISODate);
1302 if (!dt.isValid())
1304 exif.setDateTimeOriginal(dt);
1305
1306 // GPS Info
1307 auto ok = false;
1308 auto alt = image.text(QStringLiteral(META_KEY_ALTITUDE)).toDouble(&ok);
1309 if (ok)
1310 exif.setAltitude(alt);
1311 auto lat = image.text(QStringLiteral(META_KEY_LATITUDE)).toDouble(&ok);
1312 if (ok)
1313 exif.setLatitude(lat);
1314 auto lon = image.text(QStringLiteral(META_KEY_LONGITUDE)).toDouble(&ok);
1315 if (ok)
1316 exif.setLongitude(lon);
1317 auto dir = image.text(QStringLiteral(META_KEY_DIRECTION)).toDouble(&ok);
1318 if (ok)
1319 exif.setImageDirection(dir);
1320
1321 return exif;
1322}
1323
1324void MicroExif::setTiffString(quint16 tagId, const QString &s)
1325{
1326 MicroExif::setString(m_tiffTags, tagId, s);
1327}
1328
1329QString MicroExif::tiffString(quint16 tagId) const
1330{
1331 return MicroExif::string(m_tiffTags, tagId);
1332}
1333
1334void MicroExif::setExifString(quint16 tagId, const QString &s)
1335{
1336 MicroExif::setString(m_exifTags, tagId, s);
1337}
1338
1339QString MicroExif::exifString(quint16 tagId) const
1340{
1341 return MicroExif::string(m_exifTags, tagId);
1342}
1343
1344void MicroExif::setGpsString(quint16 tagId, const QString &s)
1345{
1346 MicroExif::setString(m_gpsTags, tagId, s);
1347}
1348
1349QString MicroExif::gpsString(quint16 tagId) const
1350{
1351 return MicroExif::string(m_gpsTags, tagId);
1352}
1353
1354bool MicroExif::writeHeader(QDataStream &ds) const
1355{
1357 ds << quint16(0x4949); // II
1358 else
1359 ds << quint16(0x4d4d); // MM
1360 ds << quint16(0x002a); // Tiff V6
1361 ds << quint32(8); // IFD offset
1362 return ds.status() == QDataStream::Ok;
1363}
1364
1365bool MicroExif::writeIfds(QDataStream &ds, const Version &version) const
1366{
1367 auto tiffTags = m_tiffTags;
1368 auto exifTags = m_exifTags;
1369 auto gpsTags = m_gpsTags;
1370 updateTags(tiffTags, exifTags, gpsTags, version);
1371
1372 TagPos positions;
1373 if (!writeIfd(ds, version, tiffTags, positions))
1374 return false;
1375 if (!writeIfd(ds, version, exifTags, positions, positions.value(EXIF_EXIFIFD)))
1376 return false;
1377 if (!writeIfd(ds, version, gpsTags, positions, positions.value(EXIF_GPSIFD), staticGpsTagTypes))
1378 return false;
1379 return true;
1380}
1381
1382void MicroExif::updateTags(Tags &tiffTags, Tags &exifTags, Tags &gpsTags, const Version &version) const
1383{
1384 if (exifTags.isEmpty()) {
1385 tiffTags.remove(EXIF_EXIFIFD);
1386 } else {
1387 tiffTags.insert(EXIF_EXIFIFD, quint32());
1388 exifTags.insert(EXIF_EXIFVERSION, version == Version::V3 ? QByteArray("0300") : QByteArray("0232"));
1389 }
1390 if (gpsTags.isEmpty()) {
1391 tiffTags.remove(EXIF_GPSIFD);
1392 } else {
1393 tiffTags.insert(EXIF_GPSIFD, quint32());
1394 gpsTags.insert(GPS_GPSVERSION, QByteArray("2400"));
1395 }
1396}
1397
1398void MicroExif::setString(Tags &tags, quint16 tagId, const QString &s)
1399{
1400 if (s.isEmpty())
1401 tags.remove(tagId);
1402 else
1403 tags.insert(tagId, s);
1404}
1405
1406QString MicroExif::string(const Tags &tags, quint16 tagId)
1407{
1408 return tags.value(tagId).toString();
1409}
KDB_EXPORT KDbVersionInfo version()
KIOCORE_EXPORT QString dir(const QString &fileClass)
void setData(const QByteArray &data)
QByteArray & append(QByteArrayView data)
char at(qsizetype i) const const
QByteArray first(qsizetype n) const const
QByteArray fromRawData(const char *data, qsizetype size)
bool isEmpty() const const
QByteArray & removeLast()
qsizetype size() const const
Primaries primaries() const const
TransferFunction transferFunction() const const
ByteOrder byteOrder() const const
QIODevice * device() const const
void setByteOrder(ByteOrder bo)
int skipRawData(int len)
Status status() const const
QDateTime currentDateTime()
QDateTime fromString(QStringView string, QStringView format, QCalendar cal)
bool isValid() const const
int offsetFromUtc() const const
void setTimeZone(const QTimeZone &toZone)
QString toString(QStringView format, QCalendar cal) const const
iterator insert(const Key &key, const T &value)
bool remove(const Key &key)
T value(const Key &key) const const
QColorSpace colorSpace() const const
int dotsPerMeterX() const const
int dotsPerMeterY() const const
int height() const const
bool isNull() const const
void setDotsPerMeterX(int x)
void setDotsPerMeterY(int y)
void setText(const QString &key, const QString &text)
QString text(const QString &key) const const
int width() const const
virtual void close()
virtual bool isSequential() const const
virtual bool open(QIODeviceBase::OpenMode mode)
virtual qint64 pos() const const
void append(QList< T > &&value)
const QChar at(qsizetype position) const const
QString fromLatin1(QByteArrayView str)
bool isEmpty() const const
QString left(qsizetype n) const const
QString mid(qsizetype position, qsizetype n) const const
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
qsizetype size() const const
double toDouble(bool *ok) const const
int toInt(bool *ok, int base) const const
QByteArray toLatin1() const const
QByteArray toUtf8() const const
QTextStream & dec(QTextStream &stream)
QTimeZone fromSecondsAheadOfUtc(int offset)
bool isNull() const const
QString toString(StringFormat mode) const const
QVariant fromValue(T &&value)
QByteArray toByteArray() const const
double toDouble(bool *ok) const const
int toInt(bool *ok) const const
QString toString() const const
T value() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Apr 25 2025 11:46:32 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.