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

KDE's Doxygen guidelines are available online.