From 7742537f8c42edfd678be2cba413209fd69fe42f Mon Sep 17 00:00:00 2001 From: Mirco Miranda Date: Sun, 2 Mar 2025 10:18:13 +0100 Subject: [PATCH] MicroExif: API improvements and minor bugfixes --- README.md | 2 ++ autotests/write/basic/avif.json | 4 +++ autotests/write/basic/heif.json | 4 +++ autotests/write/basic/jxl.json | 4 +++ autotests/write/basic/jxr.json | 4 +++ src/imageformats/avif.cpp | 9 ++--- src/imageformats/heif.cpp | 9 ++--- src/imageformats/jxl.cpp | 9 ++--- src/imageformats/jxr.cpp | 2 +- src/imageformats/microexif.cpp | 61 +++++++++++++++++++++++++++++++-- src/imageformats/microexif_p.h | 23 ++++++++++--- src/imageformats/psd.cpp | 2 +- src/imageformats/util_p.h | 1 + 13 files changed, 103 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index ad83477..539dfe2 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,8 @@ About the image: ISO 8601 format without milliseconds (e.g. 2024-03-23T15:30:43). This value should be kept unchanged when present. - `Description`: A string that describes the subject of the image. +- `Direction`: Floating-point number indicating the direction of the image + when it was captured in degrees (e.g. 123.3). - `DocumentName`: The name of the document from which this image was scanned. - `HostComputer`: The computer and/or operating system in use at the time diff --git a/autotests/write/basic/avif.json b/autotests/write/basic/avif.json index 61ca8fe..1e99fb5 100644 --- a/autotests/write/basic/avif.json +++ b/autotests/write/basic/avif.json @@ -5,6 +5,10 @@ "key" : "CreationDate", "value" : "2025-01-14T13:53:32+01:00" }, + { + "key" : "Direction", + "value" : "123.7" + }, { "key" : "ModificationDate", "value" : "2025-02-14T15:58:44+01:00" diff --git a/autotests/write/basic/heif.json b/autotests/write/basic/heif.json index 39de6e7..479d742 100644 --- a/autotests/write/basic/heif.json +++ b/autotests/write/basic/heif.json @@ -5,6 +5,10 @@ "key" : "CreationDate", "value" : "2025-01-14T13:53:32+01:00" }, + { + "key" : "Direction", + "value" : "123.7" + }, { "key" : "ModificationDate", "value" : "2025-02-14T15:58:44+01:00" diff --git a/autotests/write/basic/jxl.json b/autotests/write/basic/jxl.json index 7877b96..c5ba2db 100644 --- a/autotests/write/basic/jxl.json +++ b/autotests/write/basic/jxl.json @@ -5,6 +5,10 @@ "key" : "CreationDate", "value" : "2025-01-14T13:53:32+01:00" }, + { + "key" : "Direction", + "value" : "123.7" + }, { "key" : "ModificationDate", "value" : "2025-02-14T15:58:44+01:00" diff --git a/autotests/write/basic/jxr.json b/autotests/write/basic/jxr.json index 154f30c..31afca5 100644 --- a/autotests/write/basic/jxr.json +++ b/autotests/write/basic/jxr.json @@ -5,6 +5,10 @@ "key" : "CreationDate", "value" : "2025-01-14T13:53:32+01:00" }, + { + "key" : "Direction", + "value" : "123.7" + }, { "key" : "ModificationDate", "value" : "2025-02-14T15:58:44+01:00" diff --git a/src/imageformats/avif.cpp b/src/imageformats/avif.cpp index 616580d..c438ea0 100644 --- a/src/imageformats/avif.cpp +++ b/src/imageformats/avif.cpp @@ -534,13 +534,8 @@ bool QAVIFHandler::decode_one_frame() if (m_decoder->image->exif.size) { auto exif = MicroExif::fromRawData(reinterpret_cast(m_decoder->image->exif.data), m_decoder->image->exif.size); - // set image resolution - if (exif.horizontalResolution() > 0) - m_current_image.setDotsPerMeterX(qRound(exif.horizontalResolution() / 25.4 * 1000)); - if (exif.verticalResolution() > 0) - m_current_image.setDotsPerMeterY(qRound(exif.verticalResolution() / 25.4 * 1000)); - // set image metadata - exif.toImageMetadata(m_current_image); + exif.updateImageResolution(m_current_image); + exif.updateImageMetadata(m_current_image); } if (m_decoder->image->xmp.size) { diff --git a/src/imageformats/heif.cpp b/src/imageformats/heif.cpp index 049ed4a..46bd4c5 100644 --- a/src/imageformats/heif.cpp +++ b/src/imageformats/heif.cpp @@ -955,13 +955,8 @@ bool HEIFHandler::ensureDecoder() } else if (isExif) { auto exif = MicroExif::fromByteArray(ba.mid(4)); if (!exif.isEmpty()) { - // set image resolution - if (exif.horizontalResolution() > 0) - m_current_image.setDotsPerMeterX(qRound(exif.horizontalResolution() / 25.4 * 1000)); - if (exif.verticalResolution() > 0) - m_current_image.setDotsPerMeterY(qRound(exif.verticalResolution() / 25.4 * 1000)); - // set image metadata - exif.toImageMetadata(m_current_image); + exif.updateImageResolution(m_current_image); + exif.updateImageMetadata(m_current_image); } } } diff --git a/src/imageformats/jxl.cpp b/src/imageformats/jxl.cpp index 87409c4..4bda057 100644 --- a/src/imageformats/jxl.cpp +++ b/src/imageformats/jxl.cpp @@ -791,13 +791,8 @@ bool QJpegXLHandler::decode_one_frame() if (!m_exif.isEmpty()) { auto exif = MicroExif::fromByteArray(m_exif); - // set image resolution - if (exif.horizontalResolution() > 0) - m_current_image.setDotsPerMeterX(qRound(exif.horizontalResolution() / 25.4 * 1000)); - if (exif.verticalResolution() > 0) - m_current_image.setDotsPerMeterY(qRound(exif.verticalResolution() / 25.4 * 1000)); - // set image metadata - exif.toImageMetadata(m_current_image); + exif.updateImageResolution(m_current_image); + exif.updateImageMetadata(m_current_image); } m_next_image_delay = m_framedelays[m_currentimage_index]; diff --git a/src/imageformats/jxr.cpp b/src/imageformats/jxr.cpp index ec8ddd9..8b2e352 100644 --- a/src/imageformats/jxr.cpp +++ b/src/imageformats/jxr.cpp @@ -473,7 +473,7 @@ public: } auto exif = exifData(); if (!exif.isEmpty()) { - exif.toImageMetadata(image); + exif.updateImageMetadata(image); } } diff --git a/src/imageformats/microexif.cpp b/src/imageformats/microexif.cpp index c45b856..5cebcb8 100644 --- a/src/imageformats/microexif.cpp +++ b/src/imageformats/microexif.cpp @@ -63,7 +63,8 @@ #define GPS_LONGITUDE 4 #define GPS_ALTITUDEREF 5 #define GPS_ALTITUDE 6 - +#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) @@ -152,7 +153,9 @@ static const KnownTags staticGpsTagTypes = { TagInfo(GPS_LONGITUDEREF, ExifTagType::Ascii), TagInfo(GPS_LONGITUDE, ExifTagType::Rational), TagInfo(GPS_ALTITUDEREF, ExifTagType::Byte), - TagInfo(GPS_ALTITUDE, ExifTagType::Rational) + TagInfo(GPS_ALTITUDE, ExifTagType::Rational), + TagInfo(GPS_IMGDIRECTIONREF, ExifTagType::Ascii), + TagInfo(GPS_IMGDIRECTION, ExifTagType::Rational) }; // clang-format on @@ -944,6 +947,10 @@ double MicroExif::latitude() const 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); @@ -969,6 +976,10 @@ double MicroExif::longitude() const 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); @@ -983,16 +994,44 @@ 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::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 { QByteArray ba; @@ -1064,7 +1103,7 @@ bool MicroExif::write(QIODevice *device, const QDataStream::ByteOrder &byteOrder return true; } -void MicroExif::toImageMetadata(QImage &targetImage, bool replaceExisting) const +void MicroExif::updateImageMetadata(QImage &targetImage, bool replaceExisting) const { // set TIFF strings for (auto &&p : tiffStrMap) { @@ -1112,6 +1151,19 @@ void MicroExif::toImageMetadata(QImage &targetImage, bool replaceExisting) const 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)); + } +} + +void MicroExif::updateImageResolution(QImage &targetImage) +{ + if (horizontalResolution() > 0) + targetImage.setDotsPerMeterX(qRound(horizontalResolution() / 25.4 * 1000)); + if (verticalResolution() > 0) + targetImage.setDotsPerMeterY(qRound(verticalResolution() / 25.4 * 1000)); } MicroExif MicroExif::fromByteArray(const QByteArray &ba) @@ -1215,6 +1267,9 @@ MicroExif MicroExif::fromImage(const QImage &image) 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); return exif; } diff --git a/src/imageformats/microexif_p.h b/src/imageformats/microexif_p.h index b715516..e469259 100644 --- a/src/imageformats/microexif_p.h +++ b/src/imageformats/microexif_p.h @@ -31,8 +31,6 @@ * * This class is a partial (or rather minimal) implementation and is only used * to avoid including external libraries when only a few tags are needed. - * - * It reads/writes the main IFD only. */ class MicroExif { @@ -251,6 +249,14 @@ public: double altitude() const; void setAltitude(double meters); + /*! + * \brief imageDirection + * \param isMagnetic Set to true if the direction is relative to magnetic north, false if it is relative to true north. Leave nullptr if is not of interest. + * \return Floating-point number indicating the direction of the image when it was captured. The range of values is from 0.00 to 359.99 or NaN if not set. + */ + double imageDirection(bool *isMagnetic = nullptr) const; + void setImageDirection(double degree, bool isMagnetic = false); + /*! * \brief toByteArray * Converts the class to RAW data. The raw data contains: @@ -309,12 +315,19 @@ public: bool write(QIODevice *device, const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER) const; /*! - * \brief toImageMetadata - * Helper to set metadata in an image. + * \brief updateImageMetadata + * Helper to set EXIF metadata to the image. * \param targetImage The image to set metadata on. * \param replaceExisting Replaces any existing metadata. */ - void toImageMetadata(QImage& targetImage, bool replaceExisting = false) const; + void updateImageMetadata(QImage &targetImage, bool replaceExisting = false) const; + + /*! + * \brief updateImageResolution + * Helper to set the EXIF resolution to the image. Resolution is set only if valid. + * \param targetImage The image to set resolution on. + */ + void updateImageResolution(QImage &targetImage); /*! * \brief fromByteArray diff --git a/src/imageformats/psd.cpp b/src/imageformats/psd.cpp index ff490bb..40ae9c3 100644 --- a/src/imageformats/psd.cpp +++ b/src/imageformats/psd.cpp @@ -545,7 +545,7 @@ static bool setExifData(QImage &img, const MicroExif &exif) { if (exif.isEmpty()) return false; - exif.toImageMetadata(img); + exif.updateImageMetadata(img); return true; } diff --git a/src/imageformats/util_p.h b/src/imageformats/util_p.h index 225c0b5..29a6381 100644 --- a/src/imageformats/util_p.h +++ b/src/imageformats/util_p.h @@ -20,6 +20,7 @@ #define META_KEY_COPYRIGHT "Copyright" #define META_KEY_CREATIONDATE "CreationDate" #define META_KEY_DESCRIPTION "Description" +#define META_KEY_DIRECTION "Direction" #define META_KEY_DOCUMENTNAME "DocumentName" #define META_KEY_HOSTCOMPUTER "HostComputer" #define META_KEY_LATITUDE "Latitude"