diff --git a/README.md b/README.md index ee5607c..ad83477 100644 --- a/README.md +++ b/README.md @@ -125,8 +125,9 @@ About the image: example a verbal description of the image. - `Copyright`: Copyright notice of the person or organization that claims the copyright to the image. -- `CreationDate`: Creation date and time in ISO 8601 format without - milliseconds (e.g. 2024-03-23T15:30:43). +- `CreationDate`: When the image was created or captured. Date and time in + 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. - `DocumentName`: The name of the document from which this image was scanned. @@ -136,6 +137,9 @@ About the image: north of the equator (e.g. 27.717). - `Longitude`: Floating-point number indicating the longitude in degrees east of Greenwich (e.g. 85.317). +- `ModificationDate`: Last modification date and time in ISO 8601 format + without milliseconds (e.g. 2024-03-23T15:30:43). This value should be + updated every time the image is saved. - `Owner`: Name of the owner of the image. - `Software`: Name and version number of the software package(s) used to create the image. @@ -189,6 +193,7 @@ are created automatically: - `Software`: Created using `applicationName` and `applicationVersion` methods of [`QCoreApplication`](https://doc.qt.io/qt-6/qcoreapplication.html). - `CreationDate`: Set to current time and date. +- `ModificationDate`: Set to current time and date. ### ICC profile support diff --git a/autotests/read/avif/metadata.avif.json b/autotests/read/avif/metadata.avif.json index 6aeffa1..a55909c 100644 --- a/autotests/read/avif/metadata.avif.json +++ b/autotests/read/avif/metadata.avif.json @@ -3,7 +3,7 @@ "fileName" : "metadata.png", "metadata" : [ { - "key" : "CreationDate", + "key" : "ModificationDate", "value" : "2025-02-19T08:27:22+01:00" }, { diff --git a/autotests/read/jxl/gimp_exif.jxl.json b/autotests/read/jxl/gimp_exif.jxl.json index aefe7c4..36b764d 100644 --- a/autotests/read/jxl/gimp_exif.jxl.json +++ b/autotests/read/jxl/gimp_exif.jxl.json @@ -3,7 +3,7 @@ "fileName" : "gimp_exif.png", "metadata" : [ { - "key" : "CreationDate", + "key" : "ModificationDate", "value" : "2025-01-05T10:18:16" }, { diff --git a/autotests/read/jxr/metadata.jxr b/autotests/read/jxr/metadata.jxr new file mode 100644 index 0000000..d82d5de Binary files /dev/null and b/autotests/read/jxr/metadata.jxr differ diff --git a/autotests/read/jxr/metadata.jxr.json b/autotests/read/jxr/metadata.jxr.json new file mode 100644 index 0000000..e4f4f19 --- /dev/null +++ b/autotests/read/jxr/metadata.jxr.json @@ -0,0 +1,59 @@ +[ + { + "fileName" : "metadata.png", + "metadata" : [ + { + "key" : "CreationDate", + "value" : "2025-01-14T10:34:51Z" + }, + { + "key" : "Software" , + "value" : "LIFE Pro 2.18.10" + }, + { + "key" : "Altitude", + "value" : "34" + }, + { + "key" : "Author", + "value" : "KDE Project" + }, + { + "key" : "Copyright", + "value" : "@2025 KDE Project" + }, + { + "key" : "Description", + "value" : "TV broadcast test image." + }, + { + "key" : "Latitude", + "value" : "44.6478" + }, + { + "key" : "LensManufacturer", + "value" : "KDE Glasses" + }, + { + "key" : "LensModel", + "value" : "A1234" + }, + { + "key" : "Longitude", + "value" : "10.9254" + }, + { + "key" : "Manufacturer", + "value" : "KFramework" + }, + { + "key" : "Model", + "value" : "KImageFormats" + } + ], + "resolution" : { + "dotsPerMeterX" : 11811, + "dotsPerMeterY" : 5906 + } + } +] diff --git a/autotests/read/jxr/metadata.png b/autotests/read/jxr/metadata.png new file mode 100644 index 0000000..91d792b Binary files /dev/null and b/autotests/read/jxr/metadata.png differ diff --git a/autotests/read/psd/mch-16bits.psd.json b/autotests/read/psd/mch-16bits.psd.json index 92bcd3e..acaddc9 100644 --- a/autotests/read/psd/mch-16bits.psd.json +++ b/autotests/read/psd/mch-16bits.psd.json @@ -8,7 +8,7 @@ }, "metadata" : [ { - "key" : "CreationDate", + "key" : "ModificationDate", "value" : "2022-11-11T14:27:52" }, { diff --git a/autotests/read/psd/mch-8bits.psd.json b/autotests/read/psd/mch-8bits.psd.json index e4f488e..a204450 100644 --- a/autotests/read/psd/mch-8bits.psd.json +++ b/autotests/read/psd/mch-8bits.psd.json @@ -8,7 +8,7 @@ }, "metadata" : [ { - "key" : "CreationDate", + "key" : "ModificationDate", "value" : "2022-11-11T14:27:39" }, { diff --git a/autotests/read/psd/metadata.psd.json b/autotests/read/psd/metadata.psd.json index de4ccfe..a08c26c 100644 --- a/autotests/read/psd/metadata.psd.json +++ b/autotests/read/psd/metadata.psd.json @@ -3,7 +3,7 @@ "fileName" : "metadata.png", "metadata" : [ { - "key" : "CreationDate", + "key" : "ModificationDate", "value" : "2025-01-14T13:53:32+01:00" }, { diff --git a/autotests/write/basic/avif.json b/autotests/write/basic/avif.json index c640941..61ca8fe 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" : "ModificationDate", + "value" : "2025-02-14T15:58:44+01:00" + }, { "key" : "Software" , "value" : "Adobe Photoshop 26.2 (Windows)" diff --git a/autotests/write/basic/jxl.json b/autotests/write/basic/jxl.json index c9c840f..7877b96 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" : "ModificationDate", + "value" : "2025-02-14T15:58:44+01:00" + }, { "key" : "Software" , "value" : "Adobe Photoshop 26.2 (Windows)" diff --git a/autotests/write/basic/jxr.json b/autotests/write/basic/jxr.json index 872aff1..154f30c 100644 --- a/autotests/write/basic/jxr.json +++ b/autotests/write/basic/jxr.json @@ -5,6 +5,14 @@ "key" : "CreationDate", "value" : "2025-01-14T13:53:32+01:00" }, + { + "key" : "ModificationDate", + "value" : "2025-02-14T15:58:44+01:00" + }, + { + "key" : "Altitude", + "value" : "34" + }, { "key" : "Author", "value" : "KDE Project" @@ -17,6 +25,22 @@ "key" : "Description", "value" : "TV broadcast test image." }, + { + "key" : "Latitude", + "value" : "44.6478" + }, + { + "key" : "LensManufacturer", + "value" : "KDE Glasses" + }, + { + "key" : "LensModel", + "value" : "A1234" + }, + { + "key" : "Longitude", + "value" : "10.9254" + }, { "key" : "Manufacturer", "value" : "KFramework" diff --git a/src/imageformats/CMakeLists.txt b/src/imageformats/CMakeLists.txt index c1b473e..58b4a6b 100644 --- a/src/imageformats/CMakeLists.txt +++ b/src/imageformats/CMakeLists.txt @@ -140,7 +140,7 @@ endif() ################################## if (LibJXR_FOUND) - kimageformats_add_plugin(kimg_jxr SOURCES jxr.cpp) + kimageformats_add_plugin(kimg_jxr SOURCES jxr.cpp microexif.cpp) kde_enable_exceptions() target_include_directories(kimg_jxr PRIVATE ${LIBJXR_INCLUDE_DIRS}) target_link_libraries(kimg_jxr PRIVATE ${LIBJXR_LIBRARIES}) diff --git a/src/imageformats/jxr.cpp b/src/imageformats/jxr.cpp index 3a103ca..a38bef5 100644 --- a/src/imageformats/jxr.cpp +++ b/src/imageformats/jxr.cpp @@ -15,6 +15,7 @@ */ #include "jxr_p.h" +#include "microexif_p.h" #include "util_p.h" #include @@ -83,9 +84,10 @@ Q_LOGGING_CATEGORY(LOG_JXRPLUGIN, "kf.imageformats.plugins.jxr", QtWarningMsg) class JXRHandlerPrivate : public QSharedData { private: - QSharedPointer tempDir; - mutable QSharedPointer jxrFile; - mutable QHash txtMeta; + QSharedPointer m_tempDir; + QSharedPointer m_jxrFile; + MicroExif m_exif; + mutable QHash m_txtMeta; public: PKFactory *pFactory = nullptr; @@ -95,7 +97,7 @@ public: JXRHandlerPrivate() { - tempDir = QSharedPointer(new QTemporaryDir); + m_tempDir = QSharedPointer(new QTemporaryDir); if (PKCreateFactory(&pFactory, PK_SDK_VERSION) == WMP_errSuccess) { PKCreateCodecFactory(&pCodecFactory, WMP_SDK_VERSION); } @@ -123,7 +125,7 @@ public: QString fileName() const { - return jxrFile->fileName(); + return m_jxrFile->fileName(); } /* *** READ *** */ @@ -318,11 +320,20 @@ public: } /*! - * \brief setTextMetadata - * Set the text metadata into \a image + * \brief exifData + * \return The EXIF data. + */ + MicroExif exifData() const + { + return m_exif; + } + + /*! + * \brief setMetadata + * Set the metadata into \a image * \param image Image on which to write metadata */ - void setTextMetadata(QImage& image) + void setMetadata(QImage& image) { auto xmp = xmpData(); if (!xmp.isEmpty()) { @@ -344,10 +355,6 @@ public: if (!model.isEmpty()) { image.setText(QStringLiteral(META_KEY_MODEL), model); } - auto cDate = dateTime(); - if (!cDate.isEmpty()) { - image.setText(QStringLiteral(META_KEY_CREATIONDATE), cDate); - } auto author = artist(); if (!author.isEmpty()) { image.setText(QStringLiteral(META_KEY_AUTHOR), author); @@ -368,20 +375,23 @@ public: if (!docn.isEmpty()) { image.setText(QStringLiteral(META_KEY_DOCUMENTNAME), docn); } + auto exif = exifData(); + if (!exif.isEmpty()) { + exif.toImageMetadata(image); + } } #define META_TEXT(name, key) \ QString name() const \ { \ readTextMeta(); \ - return txtMeta.value(QStringLiteral(key)); \ + return m_txtMeta.value(QStringLiteral(key)); \ } META_TEXT(description, META_KEY_DESCRIPTION) META_TEXT(cameraMake, META_KEY_MANUFACTURER) META_TEXT(cameraModel, META_KEY_MODEL) META_TEXT(software, META_KEY_SOFTWARE) - META_TEXT(dateTime, META_KEY_CREATIONDATE) META_TEXT(artist, META_KEY_AUTHOR) META_TEXT(copyright, META_KEY_COPYRIGHT) META_TEXT(caption, META_KEY_TITLE) @@ -400,9 +410,9 @@ public: bool initForWriting() { // I have to use QFile because, on Windows, the QTemporary file is locked (even if I close it) - auto fileName = QStringLiteral("%1.jxr").arg(tempDir->filePath(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8))); + auto fileName = QStringLiteral("%1.jxr").arg(m_tempDir->filePath(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8))); QSharedPointer file(new QFile(fileName)); - jxrFile = file; + m_jxrFile = file; return initEncoder(); } @@ -422,7 +432,7 @@ public: return false; } - if (!deviceCopy(device, jxrFile.data())) { + if (!deviceCopy(device, m_jxrFile.data())) { qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::finalizeWriting() error while writing in the target device"; return false; } @@ -580,7 +590,6 @@ public: META_CTEXT(META_KEY_MODEL, pvarCameraModel) META_CTEXT(META_KEY_AUTHOR, pvarArtist) META_CTEXT(META_KEY_COPYRIGHT, pvarCopyright) - META_CTEXT(META_KEY_CREATIONDATE, pvarDateTime) META_CTEXT(META_KEY_DOCUMENTNAME, pvarDocumentName) META_CTEXT(META_KEY_HOSTCOMPUTER, pvarHostComputer) META_WTEXT(META_KEY_TITLE, pvarCaption) @@ -595,12 +604,33 @@ public: meta.pvarSoftware.VT.pszVal = software.data(); } + // Date and Time (TIFF format) + auto cDate = QDateTime::fromString(image.text(QStringLiteral(META_KEY_MODIFICATIONDATE)), Qt::ISODate); + auto sDate = cDate.isValid() ? cDate.toString(QStringLiteral("yyyy:MM:dd HH:mm:ss")).toLatin1() : QByteArray(); + if (!sDate.isEmpty()) { + meta.pvarDateTime.vt = DPKVT_LPSTR; + meta.pvarDateTime.VT.pszVal = sDate.data(); + } + auto xmp = image.text(QStringLiteral(META_KEY_XMP_ADOBE)).toUtf8(); if (!xmp.isNull()) { - if (auto err = PKImageEncode_SetXMPMetadata_WMP(pEncoder, reinterpret_cast(xmp.data()), xmp.size())) { + if (auto err = PKImageEncode_SetXMPMetadata_WMP(pEncoder, reinterpret_cast(xmp.constData()), xmp.size())) { qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting XMP data:" << err; } } + + auto exif = MicroExif::fromImage(image); + if (!exif.isEmpty()) { + auto exifIfd = exif.exifIfdByteArray(QDataStream::LittleEndian); + if (auto err = PKImageEncode_SetEXIFMetadata_WMP(pEncoder, reinterpret_cast(exifIfd.constData()), exifIfd.size())) { + qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting EXIF data:" << err; + } + auto gpsIfd = exif.gpsIfdByteArray(QDataStream::LittleEndian); + if (auto err = PKImageEncode_SetGPSInfoMetadata_WMP(pEncoder, reinterpret_cast(gpsIfd.constData()), gpsIfd.size())) { + qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting GPS data:" << err; + } + } + if (auto err = pEncoder->SetDescriptiveMetadata(pEncoder, &meta)) { qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting descriptive data:" << err; } @@ -710,11 +740,11 @@ private: if (device == nullptr) { return false; } - if (!jxrFile.isNull()) { + if (!m_jxrFile.isNull()) { return true; } // I have to use QFile because, on Windows, the QTemporary file is locked (even if I close it) - auto fileName = QStringLiteral("%1.jxr").arg(tempDir->filePath(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8))); + auto fileName = QStringLiteral("%1.jxr").arg(m_tempDir->filePath(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8))); QSharedPointer file(new QFile(fileName)); if (!file->open(QFile::WriteOnly)) { return false; @@ -724,7 +754,8 @@ private: return false; } file->close(); - jxrFile = file; + m_exif = MicroExif::fromDevice(file.data()); + m_jxrFile = file; return true; } @@ -762,7 +793,7 @@ private: if (pDecoder == nullptr) { return false; } - if (!txtMeta.isEmpty()) { + if (!m_txtMeta.isEmpty()) { return true; } @@ -773,15 +804,14 @@ private: #define META_TEXT(name, field) \ if (meta.field.vt == DPKVT_LPSTR) \ - txtMeta.insert(QStringLiteral(name), QString::fromUtf8(meta.field.VT.pszVal)); \ + m_txtMeta.insert(QStringLiteral(name), QString::fromUtf8(meta.field.VT.pszVal)); \ else if (meta.field.vt == DPKVT_LPWSTR) \ - txtMeta.insert(QStringLiteral(name), QString::fromUtf16(reinterpret_cast(meta.field.VT.pwszVal))); + m_txtMeta.insert(QStringLiteral(name), QString::fromUtf16(reinterpret_cast(meta.field.VT.pwszVal))); META_TEXT(META_KEY_DESCRIPTION, pvarImageDescription) META_TEXT(META_KEY_MANUFACTURER, pvarCameraMake) META_TEXT(META_KEY_MODEL, pvarCameraModel) META_TEXT(META_KEY_SOFTWARE, pvarSoftware) - META_TEXT(META_KEY_CREATIONDATE, pvarDateTime) META_TEXT(META_KEY_AUTHOR, pvarArtist) META_TEXT(META_KEY_COPYRIGHT, pvarCopyright) META_TEXT(META_KEY_TITLE, pvarCaption) @@ -867,7 +897,7 @@ bool JXRHandler::read(QImage *outImage) // Metadata (e.g.: icc profile, description, etc...) img.setColorSpace(d->colorSpace()); - d->setTextMetadata(img); + d->setMetadata(img); #ifndef JXR_DENY_FLOAT_IMAGE // JXR float are stored in scRGB. diff --git a/src/imageformats/microexif.cpp b/src/imageformats/microexif.cpp index 78e129e..c45b856 100644 --- a/src/imageformats/microexif.cpp +++ b/src/imageformats/microexif.cpp @@ -37,7 +37,12 @@ // EXIF 3 specs #define EXIF_EXIFIFD 0x8769 #define EXIF_GPSIFD 0x8825 +#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_COLORSPACE 0xA001 #define EXIF_PIXELXDIM 0xA002 #define EXIF_PIXELYDIM 0xA003 @@ -47,7 +52,6 @@ #define EXIF_LENSMODEL 0xA434 #define EXIF_LENSSERIALNUMBER 0xA435 #define EXIF_IMAGETITLE 0xA436 -#define EXIF_EXIFVERSION 0x9000 #define EXIF_VAL_COLORSPACE_SRGB 1 #define EXIF_VAL_COLORSPACE_UNCAL 0xFFFF @@ -119,7 +123,11 @@ static const KnownTags staticTagTypes = { TagInfo(TIFF_COPYRIGHT, ExifTagType::Ascii), TagInfo(EXIF_EXIFIFD, ExifTagType::Long), TagInfo(EXIF_GPSIFD, ExifTagType::Long), + TagInfo(EXIF_DATETIMEORIGINAL, ExifTagType::Ascii), + TagInfo(EXIF_OFFSETTIMEDIGITIZED, ExifTagType::Ascii), TagInfo(EXIF_OFFSETTIME, ExifTagType::Ascii), + TagInfo(EXIF_OFFSETTIMEORIGINAL, ExifTagType::Ascii), + TagInfo(EXIF_OFFSETTIMEDIGITIZED, ExifTagType::Ascii), TagInfo(EXIF_COLORSPACE, ExifTagType::Short), TagInfo(EXIF_PIXELXDIM, ExifTagType::Long), TagInfo(EXIF_PIXELYDIM, ExifTagType::Long), @@ -230,8 +238,8 @@ static bool checkHeader(QDataStream &ds) quint16 version; ds >> version; - if (version != 0x2A) - return false; + if (version != 0x002A && version != 0x01BC) + return false; // not TIFF or JXR quint32 offset; ds >> offset; @@ -852,6 +860,46 @@ void MicroExif::setDateTime(const QDateTime &dt) 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); @@ -956,6 +1004,50 @@ QByteArray MicroExif::toByteArray(const QDataStream::ByteOrder &byteOrder) const return ba; } +QByteArray MicroExif::exifIfdByteArray(const QDataStream::ByteOrder &byteOrder) const +{ + QByteArray ba; + { + QDataStream ds(&ba, QIODevice::WriteOnly); + ds.setByteOrder(byteOrder); + auto exifTags = m_exifTags; + exifTags.insert(EXIF_EXIFVERSION, QByteArray("0300")); + TagPos positions; + if (!writeIfd(ds, 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 +{ + 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, 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 { if (device == nullptr || device->isSequential() || isEmpty()) @@ -993,8 +1085,13 @@ void MicroExif::toImageMetadata(QImage &targetImage, bool replaceExisting) const } // set date and time - if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_CREATIONDATE)).isEmpty()) { + 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)); } @@ -1095,12 +1192,18 @@ MicroExif MicroExif::fromImage(const QImage &image) exif.setSoftware(sw.trimmed()); } - // TIFF Creation date and time - auto dt = QDateTime::fromString(image.text(QStringLiteral(META_KEY_CREATIONDATE)), Qt::ISODate); + // 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); diff --git a/src/imageformats/microexif_p.h b/src/imageformats/microexif_p.h index f5a7863..b715516 100644 --- a/src/imageformats/microexif_p.h +++ b/src/imageformats/microexif_p.h @@ -201,6 +201,20 @@ public: QDateTime dateTime() const; void setDateTime(const QDateTime& dt); + /*! + * \brief dateTimeOriginal + * \return The date and time when the original image data was generated. + */ + QDateTime dateTimeOriginal() const; + void setDateTimeOriginal(const QDateTime& dt); + + /*! + * \brief dateTimeDigitized + * \return The date and time when the image was stored as digital data. + */ + QDateTime dateTimeDigitized() const; + void setDateTimeDigitized(const QDateTime& dt); + /*! * \brief title * \return The title of the image. @@ -239,17 +253,58 @@ public: /*! * \brief toByteArray + * Converts the class to RAW data. The raw data contains: + * - TIFF header + * - MAIN IFD + * - EXIF IFD + * - GPS IFD * \param byteOrder Sets the serialization byte order for EXIF data. * \return A byte array containing the serialized data. + * \sa write */ QByteArray toByteArray(const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER) const; + /*! + * \brief exifIfdByteArray + * Convert the EXIF IFD only to RAW data. Useful when you want to add EXIF data to an existing TIFF container. + * \param byteOrder Sets the serialization byte order for the data. + * \return A byte array containing the serialized data. + */ + QByteArray exifIfdByteArray(const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER) const; + /*! + * \brief setExifIfdByteArray + * \param ba The RAW data of EXIF IFD. + * \param byteOrder Sets the serialization byte order of the data. + * \return True on success, otherwise false. + */ + bool setExifIfdByteArray(const QByteArray& ba, const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER); + + /*! + * \brief gpsIfdByteArray + * Convert the GPS IFD only to RAW data. Useful when you want to add GPS data to an existing TIFF container. + * \param byteOrder Sets the serialization byte order for the data. + * \return A byte array containing the serialized data. + */ + QByteArray gpsIfdByteArray(const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER) const; + /*! + * \brief setGpsIfdByteArray + * \param ba The RAW data of GPS IFD. + * \param byteOrder Sets the serialization byte order of the data. + * \return True on success, otherwise false. + */ + bool setGpsIfdByteArray(const QByteArray& ba, const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER); + /*! * \brief write - * Serialize the class on a device. + * Serialize the class on a device. The serialized data contains: + * - TIFF header + * - MAIN IFD + * - EXIF IFD + * - GPS IFD * \param device A random access device. * \param byteOrder Sets the serialization byte order for EXIF data. * \return True on success, otherwise false. + * \sa toByteArray */ bool write(QIODevice *device, const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER) const; diff --git a/src/imageformats/util_p.h b/src/imageformats/util_p.h index 27c1082..225c0b5 100644 --- a/src/imageformats/util_p.h +++ b/src/imageformats/util_p.h @@ -24,6 +24,7 @@ #define META_KEY_HOSTCOMPUTER "HostComputer" #define META_KEY_LATITUDE "Latitude" #define META_KEY_LONGITUDE "Longitude" +#define META_KEY_MODIFICATIONDATE "ModificationDate" #define META_KEY_OWNER "Owner" #define META_KEY_SOFTWARE "Software" #define META_KEY_TITLE "Title"