diff --git a/autotests/write/basic/avif.json b/autotests/write/basic/avif.json index 1e99fb5..d91d0d8 100644 --- a/autotests/write/basic/avif.json +++ b/autotests/write/basic/avif.json @@ -31,7 +31,7 @@ }, { "key" : "Description", - "value" : "TV broadcast test image." + "value" : "テレビ放送テスト映像。(TV broadcast test image.)" }, { "key" : "Latitude", diff --git a/autotests/write/basic/heif.json b/autotests/write/basic/heif.json index 479d742..a89919f 100644 --- a/autotests/write/basic/heif.json +++ b/autotests/write/basic/heif.json @@ -31,7 +31,7 @@ }, { "key" : "Description", - "value" : "TV broadcast test image." + "value" : "テレビ放送テスト映像。(TV broadcast test image.)" }, { "key" : "Latitude", diff --git a/autotests/write/basic/jxl.json b/autotests/write/basic/jxl.json index c5ba2db..24e9600 100644 --- a/autotests/write/basic/jxl.json +++ b/autotests/write/basic/jxl.json @@ -31,7 +31,7 @@ }, { "key" : "Description", - "value" : "TV broadcast test image." + "value" : "テレビ放送テスト映像。(TV broadcast test image.)" }, { "key" : "Latitude", diff --git a/src/imageformats/jxr.cpp b/src/imageformats/jxr.cpp index 8b2e352..fbc2bcf 100644 --- a/src/imageformats/jxr.cpp +++ b/src/imageformats/jxr.cpp @@ -720,11 +720,11 @@ public: auto exif = MicroExif::fromImage(image); if (!exif.isEmpty()) { - auto exifIfd = exif.exifIfdByteArray(QDataStream::LittleEndian); + auto exifIfd = exif.exifIfdByteArray(QDataStream::LittleEndian, MicroExif::V2); 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); + auto gpsIfd = exif.gpsIfdByteArray(QDataStream::LittleEndian, MicroExif::V2); if (auto err = PKImageEncode_SetGPSInfoMetadata_WMP(pEncoder, reinterpret_cast(gpsIfd.constData()), gpsIfd.size())) { qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting GPS data:" << err; } diff --git a/src/imageformats/microexif.cpp b/src/imageformats/microexif.cpp index 4a113bf..785e617 100644 --- a/src/imageformats/microexif.cpp +++ b/src/imageformats/microexif.cpp @@ -111,17 +111,17 @@ static const KnownTags staticTagTypes = { TagInfo(TIFF_IMAGEWIDTH, ExifTagType::Long), TagInfo(TIFF_IMAGEHEIGHT, ExifTagType::Long), TagInfo(TIFF_BITSPERSAMPLE, ExifTagType::Short), - TagInfo(TIFF_IMAGEDESCRIPTION, ExifTagType::Ascii), - TagInfo(TIFF_MAKE, ExifTagType::Ascii), - TagInfo(TIFF_MODEL, ExifTagType::Ascii), + 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::Ascii), - TagInfo(TIFF_ARTIST, ExifTagType::Ascii), + TagInfo(TIFF_SOFTWARE, ExifTagType::Utf8), + TagInfo(TIFF_ARTIST, ExifTagType::Utf8), TagInfo(TIFF_DATETIME, ExifTagType::Ascii), - TagInfo(TIFF_COPYRIGHT, ExifTagType::Ascii), + TagInfo(TIFF_COPYRIGHT, ExifTagType::Utf8), TagInfo(EXIF_EXIFIFD, ExifTagType::Long), TagInfo(EXIF_GPSIFD, ExifTagType::Long), TagInfo(EXIF_DATETIMEORIGINAL, ExifTagType::Ascii), @@ -134,10 +134,10 @@ static const KnownTags staticTagTypes = { TagInfo(EXIF_PIXELYDIM, ExifTagType::Long), TagInfo(EXIF_IMAGEUNIQUEID, ExifTagType::Ascii), TagInfo(EXIF_BODYSERIALNUMBER, ExifTagType::Ascii), - TagInfo(EXIF_LENSMAKE, ExifTagType::Ascii), - TagInfo(EXIF_LENSMODEL, ExifTagType::Ascii), + TagInfo(EXIF_LENSMAKE, ExifTagType::Utf8), + TagInfo(EXIF_LENSMODEL, ExifTagType::Utf8), TagInfo(EXIF_LENSSERIALNUMBER, ExifTagType::Ascii), - TagInfo(EXIF_IMAGETITLE, ExifTagType::Ascii), + TagInfo(EXIF_IMAGETITLE, ExifTagType::Utf8), TagInfo(EXIF_EXIFVERSION, ExifTagType::Undefined) }; // clang-format on @@ -367,6 +367,30 @@ static void writeData(QDataStream &ds, const QVariant &value, const ExifTagType& } } +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(); + + // I try to implement a rudimentary check without going through Qt's string conversion classes: + // a UTF-8 string if it uses only characters in the first 127 of the ASCII table is encoded as + // a Latin 1. Each character above 128, whether it is an extended ASCII character or a character + // of another nature, uses 2 or more bytes. Since the EXIF ​​specifications state that the ASCII + // type must use only the first 127 characters, I only need to do the size comparison to understand + // which type to use. + if (txt.toLatin1().size() == txt.toUtf8().size()) + return ExifTagType::Ascii; + + return dataType; +} + /*! * \brief writeIfd * \param ds The stream. @@ -375,7 +399,12 @@ static void writeData(QDataStream &ds, const QVariant &value, const ExifTagType& * \param knownTags List of known and supported tags. * \return True on success, otherwise false. */ -static bool writeIfd(QDataStream &ds, const MicroExif::Tags &tags, TagPos &positions, quint32 pos = 0, const KnownTags &knownTags = staticTagTypes) +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; @@ -390,7 +419,7 @@ static bool writeIfd(QDataStream &ds, const MicroExif::Tags &tags, TagPos &posit continue; } auto value = tags.value(key); - auto dataType = knownTags.value(key); + auto dataType = updateDataType(knownTags.value(key), value, ver); auto count = countBytes(dataType, value); ds << quint16(key); @@ -412,9 +441,8 @@ static bool writeIfd(QDataStream &ds, const MicroExif::Tags &tags, TagPos &posit if (!knownTags.contains(key)) { continue; } - auto value = tags.value(key); - auto dataType = knownTags.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) @@ -1032,7 +1060,7 @@ void MicroExif::setImageDirection(double degree, bool isMagnetic) m_gpsTags.insert(GPS_IMGDIRECTION, degree); } -QByteArray MicroExif::toByteArray(const QDataStream::ByteOrder &byteOrder) const +QByteArray MicroExif::toByteArray(const QDataStream::ByteOrder &byteOrder, const Version &version) const { QByteArray ba; { @@ -1043,16 +1071,16 @@ QByteArray MicroExif::toByteArray(const QDataStream::ByteOrder &byteOrder) const return ba; } -QByteArray MicroExif::exifIfdByteArray(const QDataStream::ByteOrder &byteOrder) const +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, QByteArray("0300")); + exifTags.insert(EXIF_EXIFVERSION, version == Version::V3 ? QByteArray("0300") : QByteArray("0232")); TagPos positions; - if (!writeIfd(ds, exifTags, positions)) + if (!writeIfd(ds, version, exifTags, positions)) return {}; } return ba; @@ -1065,7 +1093,7 @@ bool MicroExif::setExifIfdByteArray(const QByteArray &ba, const QDataStream::Byt return readIfd(ds, m_exifTags, 0, staticTagTypes); } -QByteArray MicroExif::gpsIfdByteArray(const QDataStream::ByteOrder &byteOrder) const +QByteArray MicroExif::gpsIfdByteArray(const QDataStream::ByteOrder &byteOrder, const Version &version) const { QByteArray ba; { @@ -1074,7 +1102,7 @@ QByteArray MicroExif::gpsIfdByteArray(const QDataStream::ByteOrder &byteOrder) c auto gpsTags = m_gpsTags; gpsTags.insert(GPS_GPSVERSION, QByteArray("2400")); TagPos positions; - if (!writeIfd(ds, gpsTags, positions, 0, staticGpsTagTypes)) + if (!writeIfd(ds, version, gpsTags, positions, 0, staticGpsTagTypes)) return {}; return ba; } @@ -1087,7 +1115,7 @@ bool MicroExif::setGpsIfdByteArray(const QByteArray &ba, const QDataStream::Byte return readIfd(ds, m_gpsTags, 0, staticGpsTagTypes); } -bool MicroExif::write(QIODevice *device, const QDataStream::ByteOrder &byteOrder) const +bool MicroExif::write(QIODevice *device, const QDataStream::ByteOrder &byteOrder, const Version &version) const { if (device == nullptr || device->isSequential() || isEmpty()) return false; @@ -1096,7 +1124,7 @@ bool MicroExif::write(QIODevice *device, const QDataStream::ByteOrder &byteOrder ds.setByteOrder(byteOrder); if (!writeHeader(ds)) return false; - if (!writeIfds(ds)) + if (!writeIfds(ds, version)) return false; } device->close(); @@ -1327,30 +1355,30 @@ bool MicroExif::writeHeader(QDataStream &ds) const return ds.status() == QDataStream::Ok; } -bool MicroExif::writeIfds(QDataStream &ds) const +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); + updateTags(tiffTags, exifTags, gpsTags, version); TagPos positions; - if (!writeIfd(ds, tiffTags, positions)) + if (!writeIfd(ds, version, tiffTags, positions)) return false; - if (!writeIfd(ds, exifTags, positions, positions.value(EXIF_EXIFIFD))) + if (!writeIfd(ds, version, exifTags, positions, positions.value(EXIF_EXIFIFD))) return false; - if (!writeIfd(ds, gpsTags, positions, positions.value(EXIF_GPSIFD), staticGpsTagTypes)) + 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 +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, QByteArray("0300")); + exifTags.insert(EXIF_EXIFVERSION, version == Version::V3 ? QByteArray("0300") : QByteArray("0232")); } if (gpsTags.isEmpty()) { tiffTags.remove(EXIF_GPSIFD); diff --git a/src/imageformats/microexif_p.h b/src/imageformats/microexif_p.h index 6014d48..6e89b02 100644 --- a/src/imageformats/microexif_p.h +++ b/src/imageformats/microexif_p.h @@ -37,6 +37,15 @@ class MicroExif public: using Tags = QMap; + /*! + * \brief The Version enum + * Exif specs version used when writing. + */ + enum Version { + V2, // V2.xx + V3 // V3.xx, use of UTF-8 data type (default) + }; + /*! * \brief MicroExif * Constructs an empty class. @@ -265,18 +274,20 @@ public: * - EXIF IFD * - GPS IFD * \param byteOrder Sets the serialization byte order for EXIF data. + * \param version The EXIF specs version to use. * \return A byte array containing the serialized data. * \sa write */ - QByteArray toByteArray(const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER) const; + QByteArray toByteArray(const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER, const Version &version = Version::V3) 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. + * \param version The EXIF specs version to use. * \return A byte array containing the serialized data. */ - QByteArray exifIfdByteArray(const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER) const; + QByteArray exifIfdByteArray(const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER, const Version &version = Version::V3) const; /*! * \brief setExifIfdByteArray * \param ba The RAW data of EXIF IFD. @@ -289,9 +300,10 @@ public: * \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. + * \param version The EXIF specs version to use. * \return A byte array containing the serialized data. */ - QByteArray gpsIfdByteArray(const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER) const; + QByteArray gpsIfdByteArray(const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER, const Version &version = Version::V3) const; /*! * \brief setGpsIfdByteArray * \param ba The RAW data of GPS IFD. @@ -309,10 +321,11 @@ public: * - GPS IFD * \param device A random access device. * \param byteOrder Sets the serialization byte order for EXIF data. + * \param version The EXIF specs version to use. * \return True on success, otherwise false. * \sa toByteArray */ - bool write(QIODevice *device, const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER) const; + bool write(QIODevice *device, const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER, const Version &version = Version::V3) const; /*! * \brief updateImageMetadata @@ -373,8 +386,8 @@ private: void setGpsString(quint16 tagId, const QString& s); QString gpsString(quint16 tagId) const; bool writeHeader(QDataStream &ds) const; - bool writeIfds(QDataStream &ds) const; - void updateTags(Tags &tiffTags, Tags &exifTags, Tags &gpsTags) const; + bool writeIfds(QDataStream &ds, const Version &version) const; + void updateTags(Tags &tiffTags, Tags &exifTags, Tags &gpsTags, const Version &version) const; static void setString(Tags &tags, quint16 tagId, const QString &s); static QString string(const Tags &tags, quint16 tagId);