/*
    This file is part of the KDE project
    SPDX-FileCopyrightText: 2025 Mirco Miranda <mircomir@outlook.com>

    SPDX-License-Identifier: LGPL-2.1-or-later
*/

#include "microexif_p.h"
#include "util_p.h"

#include <QBuffer>
#include <QCoreApplication>
#include <QDataStream>
#include <QHash>
#include <QStringDecoder>
#include <QTimeZone>

// TIFF 6 specs
#define TIFF_IMAGEWIDTH 0x100
#define TIFF_IMAGEHEIGHT 0x101
#define TIFF_BITSPERSAMPLE 0x102
#define TIFF_IMAGEDESCRIPTION 0x10E
#define TIFF_MAKE 0x10F
#define TIFF_MODEL 0x110
#define TIFF_ORIENT 0x0112
#define TIFF_XRES 0x011A
#define TIFF_YRES 0x011B
#define TIFF_URES 0x0128
#define TIFF_SOFTWARE 0x0131
#define TIFF_ARTIST 0x013B
#define TIFF_DATETIME 0x0132
#define TIFF_COPYRIGHT 0x8298

#define TIFF_VAL_URES_NOABSOLUTE 1
#define TIFF_VAL_URES_INCH 2
#define TIFF_VAL_URES_CENTIMETER 3

// EXIF 3 specs
#define EXIF_EXPOSURETIME 0x829A
#define EXIF_FNUMBER 0x829D
#define EXIF_EXIFIFD 0x8769
#define EXIF_EXPOSUREPROGRAM 0x8822
#define EXIF_GPSIFD 0x8825
#define EXIF_ISOSPEEDRATINGS 0x8827
#define EXIF_EXIFVERSION 0x9000
#define EXIF_DATETIMEORIGINAL 0x9003
#define EXIF_DATETIMEDIGITIZED 0x9004
#define EXIF_OFFSETTIME 0x9010
#define EXIF_OFFSETTIMEORIGINAL 0x9011
#define EXIF_OFFSETTIMEDIGITIZED 0x9012
#define EXIF_FLASH 0x9209
#define EXIF_FOCALLENGTH 0x920A
#define EXIF_COLORSPACE 0xA001
#define EXIF_PIXELXDIM 0xA002
#define EXIF_PIXELYDIM 0xA003
#define EXIF_EXPOSUREMODE 0xA402
#define EXIF_WHITEBALANCE 0xA403
#define EXIF_DIGITALZOOMRATIO 0xA404
#define EXIF_IMAGEUNIQUEID 0xA420
#define EXIF_BODYSERIALNUMBER 0xA431
#define EXIF_LENSMAKE 0xA433
#define EXIF_LENSMODEL 0xA434
#define EXIF_LENSSERIALNUMBER 0xA435
#define EXIF_IMAGETITLE 0xA436

#define EXIF_VAL_COLORSPACE_SRGB 1
#define EXIF_VAL_COLORSPACE_UNCAL 0xFFFF

#define GPS_GPSVERSION 0
#define GPS_LATITUDEREF 1
#define GPS_LATITUDE 2
#define GPS_LONGITUDEREF 3
#define GPS_LONGITUDE 4
#define GPS_ALTITUDEREF 5
#define GPS_ALTITUDE 6
#define GPS_SPEEDREF 12
#define GPS_SPEED 13
#define GPS_IMGDIRECTIONREF 16
#define GPS_IMGDIRECTION 17
#define EXIF_TAG_VALUE(n, byteSize) (((n) << 6) | ((byteSize) & 0x3F))
#define EXIF_TAG_SIZEOF(dataType) (quint16(dataType) & 0x3F)
#define EXIF_TAG_DATATYPE(dataType) (quint16(dataType) >> 6)

enum class ExifTagType : quint16 {
    // Base data types
    Byte = EXIF_TAG_VALUE(1, 1),
    Ascii = EXIF_TAG_VALUE(2, 1),
    Short = EXIF_TAG_VALUE(3, 2),
    Long = EXIF_TAG_VALUE(4, 4),
    Rational = EXIF_TAG_VALUE(5, 8),

    // Extended data types
    SByte = EXIF_TAG_VALUE(6, 1),
    Undefined = EXIF_TAG_VALUE(7, 1),
    SShort = EXIF_TAG_VALUE(8, 2),
    SLong = EXIF_TAG_VALUE(9, 4),
    SRational = EXIF_TAG_VALUE(10, 8),

    Float = EXIF_TAG_VALUE(11, 4), // not used in EXIF specs
    Double = EXIF_TAG_VALUE(12, 8), // not used in EXIF specs
    Ifd = EXIF_TAG_VALUE(13, 4), // not used in EXIF specs

    // BigTiff data types (EXIF specs are 32-bits only)
    Long8 = EXIF_TAG_VALUE(16, 8), // not used in EXIF specs
    SLong8 = EXIF_TAG_VALUE(17, 8), // not used in EXIF specs
    Ifd8 = EXIF_TAG_VALUE(18, 8), // not used in EXIF specs

    // Exif 3.0 only
    Utf8 = EXIF_TAG_VALUE(129, 1)
};

using TagPos = QHash<quint16, quint32>;
using KnownTags = QHash<quint16, ExifTagType>;
using TagInfo = std::pair<quint16, ExifTagType>;

/*!
 * \brief staticTagTypes
 * The supported tags.
 * \note EXIF tags are an extension of TIFF tags, so I'm writing them all together.
 */
// clang-format off
static const KnownTags staticTagTypes = {
    TagInfo(TIFF_IMAGEWIDTH, ExifTagType::Long),
    TagInfo(TIFF_IMAGEHEIGHT, ExifTagType::Long),
    TagInfo(TIFF_BITSPERSAMPLE, ExifTagType::Short),
    TagInfo(TIFF_IMAGEDESCRIPTION, ExifTagType::Utf8),
    TagInfo(TIFF_MAKE, ExifTagType::Utf8),
    TagInfo(TIFF_MODEL, ExifTagType::Utf8),
    TagInfo(TIFF_ORIENT, ExifTagType::Short),
    TagInfo(TIFF_XRES, ExifTagType::Rational),
    TagInfo(TIFF_YRES, ExifTagType::Rational),
    TagInfo(TIFF_URES, ExifTagType::Short),
    TagInfo(TIFF_SOFTWARE, ExifTagType::Utf8),
    TagInfo(TIFF_ARTIST, ExifTagType::Utf8),
    TagInfo(TIFF_DATETIME, ExifTagType::Ascii),
    TagInfo(TIFF_COPYRIGHT, ExifTagType::Utf8),
    TagInfo(EXIF_EXPOSURETIME, ExifTagType::Rational),
    TagInfo(EXIF_FNUMBER, ExifTagType::Rational),
    TagInfo(EXIF_EXIFIFD, ExifTagType::Long),
    TagInfo(EXIF_EXPOSUREPROGRAM, ExifTagType::Short),
    TagInfo(EXIF_GPSIFD, ExifTagType::Long),
    TagInfo(EXIF_ISOSPEEDRATINGS, ExifTagType::Short),
    TagInfo(EXIF_DATETIMEORIGINAL, ExifTagType::Ascii),
    TagInfo(EXIF_DATETIMEDIGITIZED, ExifTagType::Ascii),
    TagInfo(EXIF_OFFSETTIME, ExifTagType::Ascii),
    TagInfo(EXIF_OFFSETTIMEORIGINAL, ExifTagType::Ascii),
    TagInfo(EXIF_OFFSETTIMEDIGITIZED, ExifTagType::Ascii),
    TagInfo(EXIF_FLASH, ExifTagType::Short),
    TagInfo(EXIF_FOCALLENGTH, ExifTagType::Rational),
    TagInfo(EXIF_COLORSPACE, ExifTagType::Short),
    TagInfo(EXIF_PIXELXDIM, ExifTagType::Long),
    TagInfo(EXIF_PIXELYDIM, ExifTagType::Long),
    TagInfo(EXIF_EXPOSUREMODE, ExifTagType::Short),
    TagInfo(EXIF_WHITEBALANCE, ExifTagType::Short),
    TagInfo(EXIF_DIGITALZOOMRATIO, ExifTagType::Rational),
    TagInfo(EXIF_IMAGEUNIQUEID, ExifTagType::Ascii),
    TagInfo(EXIF_BODYSERIALNUMBER, ExifTagType::Ascii),
    TagInfo(EXIF_LENSMAKE, ExifTagType::Utf8),
    TagInfo(EXIF_LENSMODEL, ExifTagType::Utf8),
    TagInfo(EXIF_LENSSERIALNUMBER, ExifTagType::Ascii),
    TagInfo(EXIF_IMAGETITLE, ExifTagType::Utf8),
    TagInfo(EXIF_EXIFVERSION, ExifTagType::Undefined)
};
// clang-format on

/*!
 * \brief staticGpsTagTypes
 */
// clang-format off
static const KnownTags staticGpsTagTypes = {
    TagInfo(GPS_GPSVERSION, ExifTagType::Byte),
    TagInfo(GPS_LATITUDEREF, ExifTagType::Ascii),
    TagInfo(GPS_LATITUDE, ExifTagType::Rational),
    TagInfo(GPS_LONGITUDEREF, ExifTagType::Ascii),
    TagInfo(GPS_LONGITUDE, ExifTagType::Rational),
    TagInfo(GPS_ALTITUDEREF, ExifTagType::Byte),
    TagInfo(GPS_ALTITUDE, ExifTagType::Rational),
    TagInfo(GPS_SPEEDREF, ExifTagType::Ascii),
    TagInfo(GPS_SPEED, ExifTagType::Rational),
    TagInfo(GPS_IMGDIRECTIONREF, ExifTagType::Ascii),
    TagInfo(GPS_IMGDIRECTION, ExifTagType::Rational)
};
// clang-format on

/*!
 * \brief tiffStrMap
 * TIFF string <-> metadata
 */
// clang-format off
static const QList<std::pair<quint16, QString>> tiffStrMap = {
    std::pair<quint16, QString>(TIFF_IMAGEDESCRIPTION, QStringLiteral(META_KEY_DESCRIPTION)),
    std::pair<quint16, QString>(TIFF_ARTIST, QStringLiteral(META_KEY_AUTHOR)),
    std::pair<quint16, QString>(TIFF_SOFTWARE, QStringLiteral(META_KEY_SOFTWARE)),
    std::pair<quint16, QString>(TIFF_COPYRIGHT, QStringLiteral(META_KEY_COPYRIGHT)),
    std::pair<quint16, QString>(TIFF_MAKE, QStringLiteral(META_KEY_MANUFACTURER)),
    std::pair<quint16, QString>(TIFF_MODEL, QStringLiteral(META_KEY_MODEL))
};
// clang-format on

/*!
 * \brief exifStrMap
 * EXIF string <-> metadata
 */
// clang-format off
static const QList<std::pair<quint16, QString>> exifStrMap = {
    std::pair<quint16, QString>(EXIF_BODYSERIALNUMBER, QStringLiteral(META_KEY_SERIALNUMBER)),
    std::pair<quint16, QString>(EXIF_LENSMAKE, QStringLiteral(META_KEY_LENS_MANUFACTURER)),
    std::pair<quint16, QString>(EXIF_LENSMODEL, QStringLiteral(META_KEY_LENS_MODEL)),
    std::pair<quint16, QString>(EXIF_LENSSERIALNUMBER, QStringLiteral(META_KEY_LENS_SERIALNUMBER)),
    std::pair<quint16, QString>(EXIF_IMAGETITLE, QStringLiteral(META_KEY_TITLE)),
};
// clang-format on

/*!
 * \brief timeOffset
 * \param offset The EXIF string of the offset from UTC.
 * \return The offset in minutes.
 */
static qint16 timeOffset(const QString& offset)
{
    if (offset.size() != 6 || offset.at(3) != u':')
        return 0;
    auto ok = false;
    auto hh = offset.left(3).toInt(&ok);
    if (!ok)
        return 0;
    auto mm = offset.mid(4, 2).toInt(&ok) * (hh < 0 ? -1 : 1);
    if (!ok)
        return 0;
    return qint16(hh * 60 + mm);
}

/*!
 * \brief timeOffset
 * \param offset Offset from UTC in minutes.
 * \return The EXIF string of the offset.
 */
static QString timeOffset(qint16 offset)
{
    auto absOff = quint16(std::abs(offset));
    return QStringLiteral("%1%2:%3")
        .arg(offset < 0 ? QStringLiteral("-") : QStringLiteral("+"))
        .arg(absOff / 60, 2, 10, QChar(u'0'))
        .arg(absOff % 60, 2, 10, QChar(u'0'));
}


/*!
 * \brief checkHeader
 * \param ds The data stream
 * \return True if header is a valid EXIF, otherwise false.
 */
static bool checkHeader(QDataStream &ds)
{
    quint16 order;
    ds >> order;
    if (order == 0x4949) {
        ds.setByteOrder(QDataStream::LittleEndian);
    } else if (order == 0x4d4d) {
        ds.setByteOrder(QDataStream::BigEndian);
    } else {
        return false;
    }

    quint16 version;
    ds >> version;
    if (version != 0x002A && version != 0x01BC)
        return false; // not TIFF or JXR

    quint32 offset;
    ds >> offset;
    offset -= 8;
    if (ds.skipRawData(offset) != offset)
        return false;

    return ds.status() == QDataStream::Ok;
}

/*!
 * \brief updatePos
 * Write the current stream position in \a pos position as uint32.
 * \return True on success, otherwise false;
 */
static bool updatePos(QDataStream &ds, quint32 pos)
{
    auto dev = ds.device();
    if (pos != 0) {
        auto p = dev->pos();
        if (!dev->seek(pos))
            return false;
        ds << quint32(p);
        if (!dev->seek(p))
            return false;
    }
    return ds.status() == QDataStream::Ok;
}

static qint32 countBytes(const ExifTagType &dataType, const QVariant &value)
{
    auto count = 1;
    if (dataType == ExifTagType::Ascii) {
        count = value.toString().toLatin1().size() + 1; // ASCIIZ
    } else if (dataType == ExifTagType::Utf8) {
        count = value.toString().toUtf8().size() + 1; // ASCIIZ
    } else if (dataType == ExifTagType::Undefined) {
        count = value.toByteArray().size();
    } else if (dataType == ExifTagType::Byte) {
        count = value.value<QList<quint8>>().size();
    } else if (dataType == ExifTagType::Short) {
        count = value.value<QList<quint16>>().size();
    } else if (dataType == ExifTagType::Long || dataType == ExifTagType::Ifd) {
        count = value.value<QList<quint32>>().size();
    } else if (dataType == ExifTagType::SByte) {
        count = value.value<QList<qint8>>().size();
    } else if (dataType == ExifTagType::SShort) {
        count = value.value<QList<qint16>>().size();
    } else if (dataType == ExifTagType::SLong) {
        count = value.value<QList<qint32>>().size();
    } else if (dataType == ExifTagType::Rational || dataType == ExifTagType::SRational || dataType == ExifTagType::Double) {
        count = value.value<QList<double>>().size();
    } else if (dataType == ExifTagType::Float) {
        count = value.value<QList<float>>().size();
    }
    return std::max(1, count);
}

template <class T>
static void writeList(QDataStream &ds, const QVariant &value)
{
    auto l = value.value<QList<T>>();
    if (l.isEmpty())
        l.append(value.toInt());
    for (;l.size() < qsizetype(4 / sizeof(T));)
        l.append(T());
    for (auto &&v : l)
        ds << v;
}

inline qint32 rationalPrecision(double v)
{
    v = qAbs(v);
    return v < 1 ? 8 : 8 - qBound(0, int(std::log10(v)), 8);
}

template<class T>
static void writeRationalList(QDataStream &ds, const QVariant &value)
{
    auto l = value.value<QList<double>>();
    if (l.isEmpty())
        l.append(value.toDouble());
    for (auto &&v : l) {
        auto den = std::pow(10, rationalPrecision(v));
        ds << T(qRound(v * den));
        ds << T(den);
    }
}

static void writeByteArray(QDataStream &ds, const QByteArray &ba)
{
    for (auto &&v : ba)
        ds << v;
    for (auto n = ba.size(); n < 4; ++n)
        ds << char();
}

static void writeData(QDataStream &ds, const QVariant &value, const ExifTagType& dataType)
{
    if (dataType == ExifTagType::Ascii) {
        writeByteArray(ds, value.toString().toLatin1().append(char()));
    } else if (dataType == ExifTagType::Utf8) {
        writeByteArray(ds, value.toString().toUtf8().append(char()));
    } else if (dataType == ExifTagType::Undefined) {
        writeByteArray(ds, value.toByteArray());
    } else if (dataType == ExifTagType::Byte) {
        writeList<quint8>(ds, value);
    } else if (dataType == ExifTagType::SByte) {
        writeList<qint8>(ds, value);
    } else if (dataType == ExifTagType::Short) {
        writeList<quint16>(ds, value);
    } else if (dataType == ExifTagType::SShort) {
        writeList<qint16>(ds, value);
    } else if (dataType == ExifTagType::Long || dataType == ExifTagType::Ifd) {
        writeList<quint32>(ds, value);
    } else if (dataType == ExifTagType::SLong) {
        writeList<qint32>(ds, value);
    } else if (dataType == ExifTagType::Rational) {
        writeRationalList<quint32>(ds, value);
    } else if (dataType == ExifTagType::SRational) {
        writeRationalList<qint32>(ds, value);
    }
}

static ExifTagType updateDataType(const ExifTagType &dataType, const QVariant &value, const MicroExif::Version &ver)
{
    if (dataType != ExifTagType::Utf8)
        return dataType;

    if (ver == MicroExif::V2)
        return ExifTagType::Ascii;

    // Note that in EXIF specs, UTF-8 is backward compatible with ASCII: all UTF-8 tags can also be ASCII.
    // To maximize compatibility, I check if the string can be encoded in ASCII.
    auto txt = value.toString();

    // Exif ASCII data type allow only values up to 127 (7-bit ASCII).
    auto u8 = txt.toUtf8();
    for (auto &&c : u8) {
        if (uchar(c) > 127)
            return dataType;
    }

    return ExifTagType::Ascii;
}

/*!
 * \brief writeIfd
 * \param ds The stream.
 * \param tags The list of tags to write.
 * \param pos The position of the TAG value to update with this IFD position.
 * \param knownTags List of known and supported tags.
 * \return True on success, otherwise false.
 */
static bool writeIfd(QDataStream &ds,
                     const MicroExif::Version &ver,
                     const MicroExif::Tags &tags,
                     TagPos &positions,
                     quint32 pos = 0,
                     const KnownTags &knownTags = staticTagTypes)
{
    if (tags.isEmpty())
        return true;
    if (!updatePos(ds, pos))
        return false;

    auto keys = tags.keys();
    auto entries = quint16(keys.size());
    ds << entries;
    for (auto &&key : keys) {
        if (!knownTags.contains(key)) {
            continue;
        }
        auto value = tags.value(key);
        auto dataType = updateDataType(knownTags.value(key), value, ver);
        auto count = countBytes(dataType, value);

        ds << quint16(key);
        ds << quint16(EXIF_TAG_DATATYPE(dataType));
        ds << quint32(count);
        positions.insert(key, quint32(ds.device()->pos()));
        auto valueSize = count * EXIF_TAG_SIZEOF(dataType);
        if (valueSize > 4) {
            ds << quint32();
        } else {
            writeData(ds, value, dataType);
        }
    }
    // no more IFDs
    ds << quint32();

    // write data larger than 4 bytes
    for (auto &&key : keys) {
        if (!knownTags.contains(key)) {
            continue;
        }
        auto value = tags.value(key);
        auto dataType = updateDataType(knownTags.value(key), value, ver);
        auto count = countBytes(dataType, value);
        auto valueSize = count * EXIF_TAG_SIZEOF(dataType);
        if (valueSize <= 4)
            continue;
        if (!updatePos(ds, positions.value(key)))
            return false;
        writeData(ds, value, dataType);
    }

    return ds.status() ==  QDataStream::Ok;
}

template<class T>
static QList<T> readList(QDataStream &ds, quint32 count)
{
    QList<T> l;
    T c;
    for (quint32 i = 0; i < count; ++i) {
        ds >> c;
        l.append(c);
    }
    for (auto n = count; n < quint32(4 / sizeof(T)); ++n) {
        ds >> c;
    }
    return l;
}

template<class T>
static QList<double> readRationalList(QDataStream &ds, quint32 count)
{
    QList<double> l;
    for (quint32 i = 0; i < count; ++i) {
        T num;
        ds >> num;
        T den;
        ds >> den;
        l.append(den == 0 ? 0 : double(num) / double(den));
    }
    return l;
}

static QByteArray readBytes(QDataStream &ds, quint32 count, bool asciiz)
{
    QByteArray l;
    if (count == 0) {
        return l;
    }
    char c;
    for (quint32 i = 0; i < count; ++i) {
        ds >> c;
        l.append(c);
    }
    if (asciiz && l.at(l.size() - 1) == 0) {
        l.removeLast();
    }
    for (auto n = count; n < 4; ++n) {
        ds >> c;
    }
    return l;
}

/*!
 * \brief readIfd
 * \param ds The stream.
 * \param tags Where to sotro the read tags.
 * \param pos The position of the IFD.
 * \param knownTags List of known and supported tags.
 * \param nextIfd The position of next IFD (0 if none).
 * \return True on success, otherwise false.
 */
static bool readIfd(QDataStream &ds, MicroExif::Tags &tags, quint32 pos = 0, const KnownTags &knownTags = staticTagTypes, quint32 *nextIfd = nullptr)
{
    auto localNextIfd = quint32();
    if (nextIfd == nullptr)
        nextIfd = &localNextIfd;
    *nextIfd = 0;

    auto device = ds.device();
    if (pos && !device->seek(pos))
        return false;

    quint16 entries;
    ds >> entries;
    if (ds.status() != QDataStream::Ok)
        return false;

    for (quint16 i = 0; i < entries; ++i) {
        quint16 tagId;
        ds >> tagId;
        quint16 dataType;
        ds >> dataType;
        quint32 count;
        ds >> count;
        if (ds.status() != QDataStream::Ok)
            return false;

        // search for supported values only
        if (!knownTags.contains(tagId)) {
            quint32 value;
            ds >> value;
            continue;
        }

        // read TAG data
        auto toRead = qint64(count) * EXIF_TAG_SIZEOF(knownTags.value(tagId));
        if (toRead > qint64(device->size()))
            return false;

        auto curPos = qint64();
        if (toRead > 4) {
            quint32 value;
            ds >> value;
            curPos = device->pos();
            if (!device->seek(value))
                return false;
        }

        if (dataType == EXIF_TAG_DATATYPE(ExifTagType::Ascii) || dataType == EXIF_TAG_DATATYPE(ExifTagType::Utf8)) {
            auto l = readBytes(ds, count, true);
            if (!l.isEmpty()) {
                // It seems that converting to Latin 1 never detects errors so, using UTF-8.
                // Note that if the dataType is ASCII, by EXIF specification, it must use only the
                // first 128 values ​​so the UTF-8 conversion is correct.
                auto dec = QStringDecoder(QStringDecoder::Utf8);
                // QStringDecoder raise an error only after converting to QString
                auto ut8 = QString(dec(l));
                // If there are errors in the conversion to UTF-8, then I try with latin1 (extended ASCII)
                tags.insert(tagId, dec.hasError() ? QString::fromLatin1(l) : ut8);
            }
        } else if (dataType == EXIF_TAG_DATATYPE(ExifTagType::Undefined)) {
            auto l = readBytes(ds, count, false);
            if (!l.isEmpty())
                tags.insert(tagId, l);
        } else if (dataType == EXIF_TAG_DATATYPE(ExifTagType::Byte)) {
            auto l = readList<quint8>(ds, count);
            tags.insert(tagId, l.size() == 1 ? QVariant(l.first()) : QVariant::fromValue(l));
        } else if (dataType == EXIF_TAG_DATATYPE(ExifTagType::SByte)) {
            auto l = readList<qint8>(ds, count);
            tags.insert(tagId, l.size() == 1 ? QVariant(l.first()) : QVariant::fromValue(l));
        } else if (dataType == EXIF_TAG_DATATYPE(ExifTagType::Short)) {
            auto l = readList<quint16>(ds, count);
            tags.insert(tagId, l.size() == 1 ? QVariant(l.first()) : QVariant::fromValue(l));
        } else if (dataType == EXIF_TAG_DATATYPE(ExifTagType::SShort)) {
            auto l = readList<qint16>(ds, count);
            tags.insert(tagId, l.size() == 1 ? QVariant(l.first()) : QVariant::fromValue(l));
        } else if (dataType == EXIF_TAG_DATATYPE(ExifTagType::Long) || dataType == EXIF_TAG_DATATYPE(ExifTagType::Ifd)) {
            auto l = readList<quint32>(ds, count);
            tags.insert(tagId, l.size() == 1 ? QVariant(l.first()) : QVariant::fromValue(l));
        } else if (dataType == EXIF_TAG_DATATYPE(ExifTagType::SLong)) {
            auto l = readList<qint32>(ds, count);
            tags.insert(tagId, l.size() == 1 ? QVariant(l.first()) : QVariant::fromValue(l));
        } else if (dataType == EXIF_TAG_DATATYPE(ExifTagType::Rational)) {
            auto l = readRationalList<quint32>(ds, count);
            tags.insert(tagId, l.size() == 1 ? QVariant(l.first()) : QVariant::fromValue(l));
        } else if (dataType == EXIF_TAG_DATATYPE(ExifTagType::SRational)) {
            auto l = readRationalList<qint32>(ds, count);
            tags.insert(tagId, l.size() == 1 ? QVariant(l.first()) : QVariant::fromValue(l));
        }

        if (curPos > 0 && !device->seek(curPos))
            return false;
    }
    ds >> *nextIfd;

    return true;
}

MicroExif::MicroExif()
{

}

void MicroExif::clear()
{
    m_tiffTags.clear();
    m_exifTags.clear();
    m_gpsTags.clear();
}

bool MicroExif::isEmpty() const
{
    return m_tiffTags.isEmpty() && m_exifTags.isEmpty() && m_gpsTags.isEmpty();
}

double MicroExif::horizontalResolution() const
{
    auto u = m_tiffTags.value(TIFF_URES).toUInt();
    auto v = m_tiffTags.value(TIFF_XRES).toDouble();
    if (u == TIFF_VAL_URES_CENTIMETER)
        return v * 2.54;
    return v;
}

void MicroExif::setHorizontalResolution(double hres)
{
    auto u = m_tiffTags.value(TIFF_URES).toUInt();
    if (u == TIFF_VAL_URES_CENTIMETER) {
        hres /= 2.54;
    } else if (u < TIFF_VAL_URES_INCH) {
        m_tiffTags.insert(TIFF_URES, TIFF_VAL_URES_INCH);
    }
    m_tiffTags.insert(TIFF_XRES, hres);
}

double MicroExif::verticalResolution() const
{
    auto u = m_tiffTags.value(TIFF_URES).toUInt();
    auto v = m_tiffTags.value(TIFF_YRES).toDouble();
    if (u == TIFF_VAL_URES_CENTIMETER)
        return v * 2.54;
    return v;
}

void MicroExif::setVerticalResolution(double vres)
{
    auto u = m_tiffTags.value(TIFF_URES).toUInt();
    if (u == TIFF_VAL_URES_CENTIMETER) {
        vres /= 2.54;
    } else if (u < TIFF_VAL_URES_INCH) {
        m_tiffTags.insert(TIFF_URES, TIFF_VAL_URES_INCH);
    }
    m_tiffTags.insert(TIFF_YRES, vres);
}

QColorSpace MicroExif::colosSpace() const
{
    if (m_exifTags.value(EXIF_COLORSPACE).toUInt() == EXIF_VAL_COLORSPACE_SRGB)
        return QColorSpace(QColorSpace::SRgb);
    return QColorSpace();
}

void MicroExif::setColorSpace(const QColorSpace &cs)
{
    auto srgb = cs.transferFunction() == QColorSpace::TransferFunction::SRgb && cs.primaries() == QColorSpace::Primaries::SRgb;
    m_exifTags.insert(EXIF_COLORSPACE, srgb ? EXIF_VAL_COLORSPACE_SRGB : EXIF_VAL_COLORSPACE_UNCAL);
}

void MicroExif::setColorSpace(const QColorSpace::NamedColorSpace &csName)
{
    auto srgb = csName == QColorSpace::SRgb;
    m_exifTags.insert(EXIF_COLORSPACE, srgb ? EXIF_VAL_COLORSPACE_SRGB : EXIF_VAL_COLORSPACE_UNCAL);
}

qint32 MicroExif::width() const
{
    return m_tiffTags.value(TIFF_IMAGEWIDTH).toUInt();
}

void MicroExif::setWidth(qint32 w)
{
    m_tiffTags.insert(TIFF_IMAGEWIDTH, w);
    m_exifTags.insert(EXIF_PIXELXDIM, w);
}

qint32 MicroExif::height() const
{
    return m_tiffTags.value(TIFF_IMAGEHEIGHT).toUInt();
}

void MicroExif::setHeight(qint32 h)
{
    m_tiffTags.insert(TIFF_IMAGEHEIGHT, h);
    m_exifTags.insert(EXIF_PIXELYDIM, h);
}

quint16 MicroExif::orientation() const
{
    return m_tiffTags.value(TIFF_ORIENT).toUInt();
}

void MicroExif::setOrientation(quint16 orient)
{
    if (orient < 1 || orient > 8)
        m_tiffTags.remove(TIFF_ORIENT);
    else
        m_tiffTags.insert(TIFF_ORIENT, orient);
}

QImageIOHandler::Transformation MicroExif::transformation() const
{
    switch (orientation()) {
    case 1:
        return QImageIOHandler::TransformationNone;
    case 2:
        return QImageIOHandler::TransformationMirror;
    case 3:
        return QImageIOHandler::TransformationRotate180;
    case 4:
        return QImageIOHandler::TransformationFlip;
    case 5:
        return QImageIOHandler::TransformationFlipAndRotate90;
    case 6:
        return QImageIOHandler::TransformationRotate90;
    case 7:
        return QImageIOHandler::TransformationMirrorAndRotate90;
    case 8:
        return QImageIOHandler::TransformationRotate270;
    default:
        break;
    };
    return QImageIOHandler::TransformationNone;
}

void MicroExif::setTransformation(const QImageIOHandler::Transformation &t)
{
    switch (t) {
    case QImageIOHandler::TransformationNone:
        setOrientation(1);
        break;
    case QImageIOHandler::TransformationMirror:
        setOrientation(2);
        break;
    case QImageIOHandler::TransformationRotate180:
        setOrientation(3);
        break;
    case QImageIOHandler::TransformationFlip:
        setOrientation(4);
        break;
    case QImageIOHandler::TransformationFlipAndRotate90:
        setOrientation(5);
        break;
    case QImageIOHandler::TransformationRotate90:
        setOrientation(6);
        break;
    case QImageIOHandler::TransformationMirrorAndRotate90:
        setOrientation(7);
        break;
    case QImageIOHandler::TransformationRotate270:
        setOrientation(8);
        break;
    default:
        break;
    }
    setOrientation(0); // no orientation set
}

QString MicroExif::software() const
{
    return tiffString(TIFF_SOFTWARE);
}

void MicroExif::setSoftware(const QString &s)
{
    setTiffString(TIFF_SOFTWARE, s);
}

QString MicroExif::description() const
{
    return tiffString(TIFF_IMAGEDESCRIPTION);
}

void MicroExif::setDescription(const QString &s)
{
    setTiffString(TIFF_IMAGEDESCRIPTION, s);
}

QString MicroExif::artist() const
{
    return tiffString(TIFF_ARTIST);
}

void MicroExif::setArtist(const QString &s)
{
    setTiffString(TIFF_ARTIST, s);
}

QString MicroExif::copyright() const
{
    return tiffString(TIFF_COPYRIGHT);
}

void MicroExif::setCopyright(const QString &s)
{
    setTiffString(TIFF_COPYRIGHT, s);
}

QString MicroExif::make() const
{
    return tiffString(TIFF_MAKE);
}

void MicroExif::setMake(const QString &s)
{
    setTiffString(TIFF_MAKE, s);
}

QString MicroExif::model() const
{
    return tiffString(TIFF_MODEL);
}

void MicroExif::setModel(const QString &s)
{
    setTiffString(TIFF_MODEL, s);
}

QString MicroExif::serialNumber() const
{
    return tiffString(EXIF_BODYSERIALNUMBER);
}

void MicroExif::setSerialNumber(const QString &s)
{
    setTiffString(EXIF_BODYSERIALNUMBER, s);
}

QString MicroExif::lensMake() const
{
    return tiffString(EXIF_LENSMAKE);
}

void MicroExif::setLensMake(const QString &s)
{
    setTiffString(EXIF_LENSMAKE, s);
}

QString MicroExif::lensModel() const
{
    return tiffString(EXIF_LENSMODEL);
}

void MicroExif::setLensModel(const QString &s)
{
    setTiffString(EXIF_LENSMODEL, s);
}

QString MicroExif::lensSerialNumber() const
{
    return tiffString(EXIF_LENSSERIALNUMBER);
}

void MicroExif::setLensSerialNumber(const QString &s)
{
    setTiffString(EXIF_LENSSERIALNUMBER, s);
}

QDateTime MicroExif::dateTime() const
{
    auto dt = QDateTime::fromString(tiffString(TIFF_DATETIME), QStringLiteral("yyyy:MM:dd HH:mm:ss"));
    auto ofTag = exifString(EXIF_OFFSETTIME);
    if (dt.isValid() && !ofTag.isEmpty())
        dt.setTimeZone(QTimeZone::fromSecondsAheadOfUtc(timeOffset(ofTag) * 60));
    return dt;
}

void MicroExif::setDateTime(const QDateTime &dt)
{
    if (!dt.isValid()) {
        m_tiffTags.remove(TIFF_DATETIME);
        m_exifTags.remove(EXIF_OFFSETTIME);
        return;
    }
    setTiffString(TIFF_DATETIME, dt.toString(QStringLiteral("yyyy:MM:dd HH:mm:ss")));
    setExifString(EXIF_OFFSETTIME, timeOffset(dt.offsetFromUtc() / 60));
}

QDateTime MicroExif::dateTimeOriginal() const
{
    auto dt = QDateTime::fromString(exifString(EXIF_DATETIMEORIGINAL), QStringLiteral("yyyy:MM:dd HH:mm:ss"));
    auto ofTag = exifString(EXIF_OFFSETTIMEORIGINAL);
    if (dt.isValid() && !ofTag.isEmpty())
        dt.setTimeZone(QTimeZone::fromSecondsAheadOfUtc(timeOffset(ofTag) * 60));
    return dt;
}

void MicroExif::setDateTimeOriginal(const QDateTime &dt)
{
    if (!dt.isValid()) {
        m_exifTags.remove(EXIF_DATETIMEORIGINAL);
        m_exifTags.remove(EXIF_OFFSETTIMEORIGINAL);
        return;
    }
    setExifString(EXIF_DATETIMEORIGINAL, dt.toString(QStringLiteral("yyyy:MM:dd HH:mm:ss")));
    setExifString(EXIF_OFFSETTIMEORIGINAL, timeOffset(dt.offsetFromUtc() / 60));
}

QDateTime MicroExif::dateTimeDigitized() const
{
    auto dt = QDateTime::fromString(exifString(EXIF_DATETIMEDIGITIZED), QStringLiteral("yyyy:MM:dd HH:mm:ss"));
    auto ofTag = exifString(EXIF_OFFSETTIMEDIGITIZED);
    if (dt.isValid() && !ofTag.isEmpty())
        dt.setTimeZone(QTimeZone::fromSecondsAheadOfUtc(timeOffset(ofTag) * 60));
    return dt;
}

void MicroExif::setDateTimeDigitized(const QDateTime &dt)
{
    if (!dt.isValid()) {
        m_exifTags.remove(EXIF_DATETIMEDIGITIZED);
        m_exifTags.remove(EXIF_OFFSETTIMEDIGITIZED);
        return;
    }
    setExifString(EXIF_DATETIMEDIGITIZED, dt.toString(QStringLiteral("yyyy:MM:dd HH:mm:ss")));
    setExifString(EXIF_OFFSETTIMEDIGITIZED, timeOffset(dt.offsetFromUtc() / 60));
}

QString MicroExif::title() const
{
    return exifString(EXIF_IMAGETITLE);
}

void MicroExif::setImageTitle(const QString &s)
{
    setExifString(EXIF_IMAGETITLE, s);
}

QUuid MicroExif::uniqueId() const
{
    auto s = exifString(EXIF_IMAGEUNIQUEID);
    if (s.length() == 32) {
        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));
        return QUuid(tmp);
    }
    return {};
}

void MicroExif::setUniqueId(const QUuid &uuid)
{
    if (uuid.isNull())
        setExifString(EXIF_IMAGEUNIQUEID, QString());
    else
        setExifString(EXIF_IMAGEUNIQUEID, uuid.toString(QUuid::WithoutBraces).replace(QStringLiteral("-"), QString()));
}

double MicroExif::digitalZoomRatio() const
{
    if (!m_exifTags.contains(EXIF_DIGITALZOOMRATIO))
        return qQNaN();
    return m_exifTags.value(EXIF_DIGITALZOOMRATIO).toDouble();

}

void MicroExif::setDigitalZoomRatio(double zoom)
{
    if (qIsNaN(zoom))
        m_exifTags.remove(EXIF_DIGITALZOOMRATIO);
    else
        m_exifTags.insert(EXIF_DIGITALZOOMRATIO, zoom);
}

quint16 MicroExif::isoSpeedRatings() const
{
    return quint16(m_exifTags.value(EXIF_ISOSPEEDRATINGS).toUInt());
}

void MicroExif::setIsoSpeedRatings(quint16 iso)
{
    if (iso == 0)
        m_exifTags.remove(EXIF_ISOSPEEDRATINGS);
    else
        m_exifTags.insert(EXIF_ISOSPEEDRATINGS, iso);
}

ExposureMode MicroExif::exposureMode() const
{
    auto ok = false;
    auto v = m_exifTags.value(EXIF_EXPOSUREMODE).toUInt(&ok);
    return ok ? ExposureMode(v) : ExposureMode::NotSet;
}

void MicroExif::setExposureMode(const ExposureMode &em)
{
    if (em == ExposureMode::NotSet)
        m_exifTags.remove(EXIF_EXPOSUREMODE);
    else
        m_exifTags.insert(EXIF_EXPOSUREMODE, quint16(em));
}

ExposureProgram MicroExif::exposureProgram() const
{
    auto ok = false;
    auto v = m_exifTags.value(EXIF_EXPOSUREPROGRAM).toUInt(&ok);
    return ok ? ExposureProgram(v) : ExposureProgram::NotSet;
}

void MicroExif::setExposureProgram(const ExposureProgram &ep)
{
    if (ep == ExposureProgram::NotSet)
        m_exifTags.remove(EXIF_EXPOSUREPROGRAM);
    else
        m_exifTags.insert(EXIF_EXPOSUREPROGRAM, quint16(ep));
}

double MicroExif::exposureTime() const
{
    if (!m_exifTags.contains(EXIF_EXPOSURETIME))
        return qQNaN();
    return m_exifTags.value(EXIF_EXPOSURETIME).toDouble();
}

void MicroExif::setExposureTime(double et)
{
    if (qIsNaN(et))
        m_exifTags.remove(EXIF_EXPOSURETIME);
    else
        m_exifTags.insert(EXIF_EXPOSURETIME, et);
}

double MicroExif::fNumber() const
{
    if (!m_exifTags.contains(EXIF_FNUMBER))
        return qQNaN();
    return m_exifTags.value(EXIF_FNUMBER).toDouble();
}

void MicroExif::setFNumber(double f)
{
    if (qIsNaN(f))
        m_exifTags.remove(EXIF_FNUMBER);
    else
        m_exifTags.insert(EXIF_FNUMBER, f);
}

double MicroExif::focalLength() const
{
    if (!m_exifTags.contains(EXIF_FOCALLENGTH))
        return qQNaN();
    return m_exifTags.value(EXIF_FOCALLENGTH).toDouble();
}

void MicroExif::setFocalLength(double fl)
{
    if (qIsNaN(fl))
        m_exifTags.remove(EXIF_FOCALLENGTH);
    else
        m_exifTags.insert(EXIF_FOCALLENGTH, fl);
}

FlashFlags MicroExif::flash() const
{
    return FlashFlags(m_exifTags.value(EXIF_FLASH).toUInt());
}

void MicroExif::setFlash(const FlashFlags &flash)
{
    if (flash == Flash::NotSet)
        m_exifTags.remove(EXIF_FLASH);
    else
        m_exifTags.insert(EXIF_FLASH, quint16(flash));
}

WhiteBalance MicroExif::whiteBalance() const
{
    auto ok = false;
    auto v = m_exifTags.value(EXIF_WHITEBALANCE).toUInt(&ok);
    return ok ? WhiteBalance(v) : WhiteBalance::NotSet;
}

void MicroExif::setWhiteBalance(const WhiteBalance &wb)
{
    if (wb == WhiteBalance::NotSet)
        m_exifTags.remove(EXIF_WHITEBALANCE);
    else
        m_exifTags.insert(EXIF_WHITEBALANCE, quint16(wb));
}

double MicroExif::latitude() const
{
    auto ref = gpsString(GPS_LATITUDEREF).toUpper();
    if (ref != QStringLiteral("N") && ref != QStringLiteral("S"))
        return qQNaN();
    auto lat = m_gpsTags.value(GPS_LATITUDE).value<QList<double>>();
    if (lat.size() != 3)
        return qQNaN();
    auto degree = lat.at(0) + lat.at(1) / 60 + lat.at(2) / 3600;
    if (degree < -90.0 || degree > 90.0)
        return qQNaN();
    return ref == QStringLiteral("N") ? degree : -degree;
}

void MicroExif::setLatitude(double degree)
{
    if (qIsNaN(degree)) {
        m_gpsTags.remove(GPS_LATITUDEREF);
        m_gpsTags.remove(GPS_LATITUDE);
    }
    if (degree < -90.0 || degree > 90.0)
        return; // invalid latitude
    auto adeg = qAbs(degree);
    auto min = (adeg - int(adeg)) * 60;
    auto sec = (min - int(min)) * 60;
    m_gpsTags.insert(GPS_LATITUDEREF, degree < 0 ? QStringLiteral("S") : QStringLiteral("N"));
    m_gpsTags.insert(GPS_LATITUDE, QVariant::fromValue(QList<double>() << int(adeg) << int(min) << sec));
}

double MicroExif::longitude() const
{
    auto ref = gpsString(GPS_LONGITUDEREF).toUpper();
    if (ref != QStringLiteral("E") && ref != QStringLiteral("W"))
        return qQNaN();
    auto lon = m_gpsTags.value(GPS_LONGITUDE).value<QList<double>>();
    if (lon.size() != 3)
        return qQNaN();
    auto degree = lon.at(0) + lon.at(1) / 60 + lon.at(2) / 3600;
    if (degree < -180.0 || degree > 180.0)
        return qQNaN();
    return ref == QStringLiteral("E") ? degree : -degree;
}

void MicroExif::setLongitude(double degree)
{
    if (qIsNaN(degree)) {
        m_gpsTags.remove(GPS_LONGITUDEREF);
        m_gpsTags.remove(GPS_LONGITUDE);
    }
    if (degree < -180.0 || degree > 180.0)
        return; // invalid longitude
    auto adeg = qAbs(degree);
    auto min = (adeg - int(adeg)) * 60;
    auto sec = (min - int(min)) * 60;
    m_gpsTags.insert(GPS_LONGITUDEREF, degree < 0 ? QStringLiteral("W") : QStringLiteral("E"));
    m_gpsTags.insert(GPS_LONGITUDE, QVariant::fromValue(QList<double>() << int(adeg) << int(min) << sec));
}

double MicroExif::altitude() const
{
    auto ref = m_gpsTags.value(GPS_ALTITUDEREF);
    if (ref.isNull())
        return qQNaN();
    if (!m_gpsTags.contains(GPS_ALTITUDE))
        return qQNaN();
    auto alt = m_gpsTags.value(GPS_ALTITUDE).toDouble();
    return (ref.toInt() == 0 || ref.toInt() == 2) ? alt : -alt;
}

void MicroExif::setAltitude(double meters)
{
    if (qIsNaN(meters)) {
        m_gpsTags.remove(GPS_ALTITUDEREF);
        m_gpsTags.remove(GPS_ALTITUDE);
    }
    m_gpsTags.insert(GPS_ALTITUDEREF, quint8(meters < 0 ? 1 : 0));
    m_gpsTags.insert(GPS_ALTITUDE, meters);
}

double MicroExif::imageSpeed() const
{
    if (!m_gpsTags.contains(GPS_SPEED))
        return qQNaN();
    auto ref = gpsString(GPS_SPEEDREF).toUpper();
    auto speed = m_gpsTags.value(GPS_SPEED).toDouble();
    if (ref == QStringLiteral("M"))
        speed *= 1.60934;
    else if (ref == QStringLiteral("N"))
        speed *= 1.852;
    return speed;
}

void MicroExif::setImageSpeed(double kmh)
{
    if (qIsNaN(kmh)) {
        m_gpsTags.remove(GPS_SPEEDREF);
        m_gpsTags.remove(GPS_SPEED);
        return;
    }
    m_gpsTags.insert(GPS_SPEEDREF, QStringLiteral("K"));
    m_gpsTags.insert(GPS_SPEED, kmh);
}

double MicroExif::imageDirection(bool *isMagnetic) const
{
    auto tmp = false;
    if (isMagnetic == nullptr)
        isMagnetic = &tmp;
    if (!m_gpsTags.contains(GPS_IMGDIRECTION))
        return qQNaN();
    auto ref = gpsString(GPS_IMGDIRECTIONREF).toUpper();
    *isMagnetic = (ref == QStringLiteral("M"));
    return m_gpsTags.value(GPS_IMGDIRECTION).toDouble();
}

void MicroExif::setImageDirection(double degree, bool isMagnetic)
{
    if (qIsNaN(degree)) {
        m_gpsTags.remove(GPS_IMGDIRECTIONREF);
        m_gpsTags.remove(GPS_IMGDIRECTION);
    }
    m_gpsTags.insert(GPS_IMGDIRECTIONREF, isMagnetic ? QStringLiteral("M") : QStringLiteral("T"));
    m_gpsTags.insert(GPS_IMGDIRECTION, degree);
}

QByteArray MicroExif::toByteArray(const QDataStream::ByteOrder &byteOrder, const Version &version) const
{
    QByteArray ba;
    {
        QBuffer buf(&ba);
        if (!write(&buf, byteOrder, version))
            return {};
    }
    return ba;
}

QByteArray MicroExif::exifIfdByteArray(const QDataStream::ByteOrder &byteOrder, const Version &version) const
{
    QByteArray ba;
    {
        QDataStream ds(&ba, QIODevice::WriteOnly);
        ds.setByteOrder(byteOrder);
        auto exifTags = m_exifTags;
        exifTags.insert(EXIF_EXIFVERSION, version == Version::V3 ? QByteArray("0300") : QByteArray("0232"));
        TagPos positions;
        if (!writeIfd(ds, version, exifTags, positions))
            return {};
    }
    return ba;
}

bool MicroExif::setExifIfdByteArray(const QByteArray &ba, const QDataStream::ByteOrder &byteOrder)
{
    QDataStream ds(ba);
    ds.setByteOrder(byteOrder);
    return readIfd(ds, m_exifTags, 0, staticTagTypes);
}

QByteArray MicroExif::gpsIfdByteArray(const QDataStream::ByteOrder &byteOrder, const Version &version) const
{
    QByteArray ba;
    {
        QDataStream ds(&ba, QIODevice::WriteOnly);
        ds.setByteOrder(byteOrder);
        auto gpsTags = m_gpsTags;
        gpsTags.insert(GPS_GPSVERSION, QByteArray("2400"));
        TagPos positions;
        if (!writeIfd(ds, version, gpsTags, positions, 0, staticGpsTagTypes))
            return {};
        return ba;
    }
}

bool MicroExif::setGpsIfdByteArray(const QByteArray &ba, const QDataStream::ByteOrder &byteOrder)
{
    QDataStream ds(ba);
    ds.setByteOrder(byteOrder);
    return readIfd(ds, m_gpsTags, 0, staticGpsTagTypes);
}

bool MicroExif::write(QIODevice *device, const QDataStream::ByteOrder &byteOrder, const Version &version) const
{
    if (device == nullptr || device->isSequential() || isEmpty())
        return false;
    if (device->open(QBuffer::WriteOnly)) {
        QDataStream ds(device);
        ds.setByteOrder(byteOrder);
        if (!writeHeader(ds))
            return false;
        if (!writeIfds(ds, version))
            return false;
    }
    device->close();
    return true;
}

void MicroExif::updateImageMetadata(QImage &targetImage, bool replaceExisting) const
{
    // set TIFF strings
    for (auto &&p : tiffStrMap) {
        if (!replaceExisting && !targetImage.text(p.second).isEmpty())
            continue;
        auto s = tiffString(p.first);
        if (!s.isEmpty())
            targetImage.setText(p.second, s);
    }

    // set EXIF strings
    for (auto &&p : exifStrMap) {
        if (!replaceExisting && !targetImage.text(p.second).isEmpty())
            continue;
        auto s = exifString(p.first);
        if (!s.isEmpty())
            targetImage.setText(p.second, s);
    }

    // set date and time
    if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_MODIFICATIONDATE)).isEmpty()) {
        auto dt = dateTime();
        if (dt.isValid())
            targetImage.setText(QStringLiteral(META_KEY_MODIFICATIONDATE), dt.toString(Qt::ISODate));
    }
    if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_CREATIONDATE)).isEmpty()) {
        auto dt = dateTimeOriginal();
        if (dt.isValid())
            targetImage.setText(QStringLiteral(META_KEY_CREATIONDATE), dt.toString(Qt::ISODate));
    }

    // set GPS info
    if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_ALTITUDE)).isEmpty()) {
        auto v = altitude();
        if (!qIsNaN(v))
            targetImage.setText(QStringLiteral(META_KEY_ALTITUDE), QStringLiteral("%1").arg(v, 0, 'g', 9));
    }
    if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_LATITUDE)).isEmpty()) {
        auto v = latitude();
        if (!qIsNaN(v))
            targetImage.setText(QStringLiteral(META_KEY_LATITUDE), QStringLiteral("%1").arg(v, 0, 'g', 9));
    }
    if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_LONGITUDE)).isEmpty()) {
        auto v = longitude();
        if (!qIsNaN(v))
            targetImage.setText(QStringLiteral(META_KEY_LONGITUDE), QStringLiteral("%1").arg(v, 0, 'g', 9));
    }
    if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_DIRECTION)).isEmpty()) {
        auto v = imageDirection();
        if (!qIsNaN(v))
            targetImage.setText(QStringLiteral(META_KEY_DIRECTION), QStringLiteral("%1").arg(v, 0, 'g', 9));
    }
    if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_SPEED)).isEmpty()) {
        auto v = imageSpeed();
        if (!qIsNaN(v))
            targetImage.setText(QStringLiteral(META_KEY_SPEED), QStringLiteral("%1").arg(v, 0, 'g', 9));
    }

    // shot info
    if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_DIGITALZOOMRATIO)).isEmpty()) {
        auto v = digitalZoomRatio();
        if (!qIsNaN(v))
            targetImage.setText(QStringLiteral(META_KEY_DIGITALZOOMRATIO), QStringLiteral("%1").arg(v, 0, 'g', 9));
    }
    if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_EXPOSUREMODE)).isEmpty()) {
        auto v = exposureMode();
        if (v != ExposureMode::NotSet)
            targetImage.setText(QStringLiteral(META_KEY_EXPOSUREMODE), QStringLiteral("%1").arg(quint16(v)));
    }
    if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_EXPOSUREPROGRAM)).isEmpty()) {
        auto v = exposureProgram();
        if (v != ExposureProgram::NotSet)
            targetImage.setText(QStringLiteral(META_KEY_EXPOSUREPROGRAM), QStringLiteral("%1").arg(quint16(v)));
    }
    if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_EXPOSURETIME)).isEmpty()) {
        auto v = exposureTime();
        if (!qIsNaN(v))
            targetImage.setText(QStringLiteral(META_KEY_EXPOSURETIME), QStringLiteral("%1").arg(v, 0, 'g', 9));
    }
    if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_FLASH)).isEmpty()) {
        auto v = flash();
        if (v != Flash::NotSet)
            targetImage.setText(QStringLiteral(META_KEY_FLASH), QStringLiteral("%1").arg(quint16(v)));
    }
    if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_FNUMBER)).isEmpty()) {
        auto v = fNumber();
        if (!qIsNaN(v))
            targetImage.setText(QStringLiteral(META_KEY_FNUMBER), QStringLiteral("%1").arg(v, 0, 'g', 9));
    }
    if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_FOCALLENGTH)).isEmpty()) {
        auto v = focalLength();
        if (!qIsNaN(v))
            targetImage.setText(QStringLiteral(META_KEY_FOCALLENGTH), QStringLiteral("%1").arg(v, 0, 'g', 9));
    }
    if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_ISOSPEEDRATINGS)).isEmpty()) {
        auto v = isoSpeedRatings();
        if (v != 0)
            targetImage.setText(QStringLiteral(META_KEY_ISOSPEEDRATINGS), QStringLiteral("%1").arg(v));
    }
    if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_WHITEBALANCE)).isEmpty()) {
        auto v = whiteBalance();
        if (v != WhiteBalance::NotSet)
            targetImage.setText(QStringLiteral(META_KEY_WHITEBALANCE), QStringLiteral("%1").arg(quint16(v)));
    }
}

bool MicroExif::updateImageResolution(QImage &targetImage)
{
    const auto hdpm = dpi2ppm(horizontalResolution());
    if (hdpm > 0) {
        targetImage.setDotsPerMeterX(hdpm);
    }
    const auto vdpm = dpi2ppm(verticalResolution());
    if (vdpm > 0) {
        targetImage.setDotsPerMeterY(vdpm);
    }
    return (hdpm > 0) || (vdpm > 0);
}

MicroExif MicroExif::fromByteArray(const QByteArray &ba, bool searchHeader)
{
    auto ba0(ba);
    if (searchHeader) {
        auto idxLE = ba0.indexOf(QByteArray("II"));
        auto idxBE = ba0.indexOf(QByteArray("MM"));
        auto idx = -1;
        if (idxLE > -1 && idxBE > -1)
            idx = std::min(idxLE, idxBE);
        else
            idx = idxLE > -1 ? idxLE : idxBE;
        if (idx > 0)
            ba0 = ba0.mid(idx);
    }
    QBuffer buf;
    buf.setData(ba0);
    return fromDevice(&buf);
}

MicroExif MicroExif::fromRawData(const char *data, size_t size, bool searchHeader)
{
    if (data == nullptr || size == 0)
        return {};
    return fromByteArray(QByteArray::fromRawData(data, size), searchHeader);
}

MicroExif MicroExif::fromDevice(QIODevice *device)
{
    if (device == nullptr || device->isSequential())
        return {};
    if (!device->open(QBuffer::ReadOnly))
        return {};

    QDataStream ds(device);
    if (!checkHeader(ds))
        return {};

    MicroExif exif;

    // read TIFF ifd
    if (!readIfd(ds, exif.m_tiffTags))
        return {};

    // read EXIF ifd
    if (auto pos = exif.m_tiffTags.value(EXIF_EXIFIFD).toUInt()) {
        if (!readIfd(ds, exif.m_exifTags, pos))
            return {};
    }

    // read GPS ifd
    if (auto pos = exif.m_tiffTags.value(EXIF_GPSIFD).toUInt()) {
        if (!readIfd(ds, exif.m_gpsTags, pos, staticGpsTagTypes))
            return {};
    }

    return exif;
}

MicroExif MicroExif::fromImage(const QImage &image)
{
    MicroExif exif;
    if (image.isNull())
        return exif;

    // Image properties
    exif.setWidth(image.width());
    exif.setHeight(image.height());
    exif.setHorizontalResolution(dppm2dpi(image.dotsPerMeterX()));
    exif.setVerticalResolution(dppm2dpi(image.dotsPerMeterY()));
    exif.setColorSpace(image.colorSpace());

    // TIFF strings
    for (auto &&p : tiffStrMap) {
        exif.setTiffString(p.first, image.text(p.second));
    }

    // EXIF strings
    for (auto &&p : exifStrMap) {
        exif.setExifString(p.first, image.text(p.second));
    }

    // TIFF Software
    if (exif.software().isEmpty()) {
        auto sw = QCoreApplication::applicationName();
        auto ver = sw = QCoreApplication::applicationVersion();
        if (!sw.isEmpty() && !ver.isEmpty())
            sw.append(QStringLiteral(" %1").arg(ver));
        exif.setSoftware(sw.trimmed());
    }

    // TIFF date and time
    auto dt = QDateTime::fromString(image.text(QStringLiteral(META_KEY_MODIFICATIONDATE)), Qt::ISODate);
    if (!dt.isValid())
        dt = QDateTime::currentDateTime();
    exif.setDateTime(dt);

    // EXIF original date and time
    dt = QDateTime::fromString(image.text(QStringLiteral(META_KEY_CREATIONDATE)), Qt::ISODate);
    if (!dt.isValid())
        dt = QDateTime::currentDateTime();
    exif.setDateTimeOriginal(dt);

    // GPS info
    auto ok = false;
    auto alt = image.text(QStringLiteral(META_KEY_ALTITUDE)).toDouble(&ok);
    if (ok)
        exif.setAltitude(alt);
    auto lat = image.text(QStringLiteral(META_KEY_LATITUDE)).toDouble(&ok);
    if (ok)
        exif.setLatitude(lat);
    auto lon = image.text(QStringLiteral(META_KEY_LONGITUDE)).toDouble(&ok);
    if (ok)
        exif.setLongitude(lon);
    auto dir = image.text(QStringLiteral(META_KEY_DIRECTION)).toDouble(&ok);
    if (ok)
        exif.setImageDirection(dir);
    auto spd = image.text(QStringLiteral(META_KEY_SPEED)).toDouble(&ok);
    if (ok)
        exif.setImageSpeed(spd);

    // EXIF shot info
    auto zoom = image.text(QStringLiteral(META_KEY_DIGITALZOOMRATIO)).toDouble(&ok);
    if (ok)
        exif.setDigitalZoomRatio(zoom);
    auto expm = image.text(QStringLiteral(META_KEY_EXPOSUREMODE)).toUShort(&ok);
    if (ok)
        exif.setExposureMode(ExposureMode(expm));
    auto expp = image.text(QStringLiteral(META_KEY_EXPOSUREPROGRAM)).toUShort(&ok);
    if (ok)
        exif.setExposureProgram(ExposureProgram(expp));
    auto expt = image.text(QStringLiteral(META_KEY_EXPOSURETIME)).toDouble(&ok);
    if (ok)
        exif.setExposureTime(expt);
    auto flsh = image.text(QStringLiteral(META_KEY_FLASH)).toUShort(&ok);
    if (ok)
        exif.setFlash(FlashFlags(flsh));
    auto fnum = image.text(QStringLiteral(META_KEY_FNUMBER)).toDouble(&ok);
    if (ok)
        exif.setFNumber(fnum);
    auto flen = image.text(QStringLiteral(META_KEY_FOCALLENGTH)).toDouble(&ok);
    if (ok)
        exif.setFocalLength(flen);
    auto isos = image.text(QStringLiteral(META_KEY_ISOSPEEDRATINGS)).toUShort(&ok);
    if (ok)
        exif.setIsoSpeedRatings(isos);
    auto whtb = image.text(QStringLiteral(META_KEY_WHITEBALANCE)).toUShort(&ok);
    if (ok)
        exif.setWhiteBalance(WhiteBalance(whtb));

    return exif;
}

void MicroExif::setTiffString(quint16 tagId, const QString &s)
{
    MicroExif::setString(m_tiffTags, tagId, s);
}

QString MicroExif::tiffString(quint16 tagId) const
{
    return MicroExif::string(m_tiffTags, tagId);
}

void MicroExif::setExifString(quint16 tagId, const QString &s)
{
    MicroExif::setString(m_exifTags, tagId, s);
}

QString MicroExif::exifString(quint16 tagId) const
{
    return MicroExif::string(m_exifTags, tagId);
}

void MicroExif::setGpsString(quint16 tagId, const QString &s)
{
    MicroExif::setString(m_gpsTags, tagId, s);
}

QString MicroExif::gpsString(quint16 tagId) const
{
    return MicroExif::string(m_gpsTags, tagId);
}

bool MicroExif::writeHeader(QDataStream &ds) const
{
    if (ds.byteOrder() == QDataStream::LittleEndian)
        ds << quint16(0x4949); // II
    else
        ds << quint16(0x4d4d); // MM
    ds << quint16(0x002a); // Tiff V6
    ds << quint32(8); // IFD offset
    return ds.status() ==  QDataStream::Ok;
}

bool MicroExif::writeIfds(QDataStream &ds, const Version &version) const
{
    auto tiffTags = m_tiffTags;
    auto exifTags = m_exifTags;
    auto gpsTags = m_gpsTags;
    updateTags(tiffTags, exifTags, gpsTags, version);

    TagPos positions;
    if (!writeIfd(ds, version, tiffTags, positions))
        return false;
    if (!writeIfd(ds, version, exifTags, positions, positions.value(EXIF_EXIFIFD)))
        return false;
    if (!writeIfd(ds, version, gpsTags, positions, positions.value(EXIF_GPSIFD), staticGpsTagTypes))
        return false;
    return true;
}

void MicroExif::updateTags(Tags &tiffTags, Tags &exifTags, Tags &gpsTags, const Version &version) const
{
    if (exifTags.isEmpty()) {
        tiffTags.remove(EXIF_EXIFIFD);
    } else {
        tiffTags.insert(EXIF_EXIFIFD, quint32());
        exifTags.insert(EXIF_EXIFVERSION, version == Version::V3 ? QByteArray("0300") : QByteArray("0232"));
    }
    if (gpsTags.isEmpty()) {
        tiffTags.remove(EXIF_GPSIFD);
    } else {
        tiffTags.insert(EXIF_GPSIFD, quint32());
        gpsTags.insert(GPS_GPSVERSION, QByteArray("2400"));
    }
}

void MicroExif::setString(Tags &tags, quint16 tagId, const QString &s)
{
    if (s.isEmpty())
        tags.remove(tagId);
    else
        tags.insert(tagId, s);
}

QString MicroExif::string(const Tags &tags, quint16 tagId)
{
    return tags.value(tagId).toString();
}
