diff --git a/README.md b/README.md index 6b37ef4..4110b14 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,7 @@ About the image: scanned. - `HostComputer`: The computer and/or operating system in use at the time of image creation. +- `Keywords`: Keywords, separated by semicolons, that represent the image. - `Latitude`: Floating-point number indicating the latitude in degrees north of the equator (e.g. 27.717). - `Longitude`: Floating-point number indicating the longitude in degrees @@ -154,6 +155,8 @@ About the image: 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. +- `Rating`: Integer number indicating the image rating (usually between 1 + and 5). - `Software`: Name and version number of the software package(s) used to create the image. - `Speed`: Floating-point number indicating the speed of GPS receiver diff --git a/autotests/read/jxl/gimp_exif.jxl b/autotests/read/jxl/gimp_exif.jxl index 2ca1611..98edab0 100644 Binary files a/autotests/read/jxl/gimp_exif.jxl and b/autotests/read/jxl/gimp_exif.jxl differ diff --git a/autotests/read/jxl/gimp_exif.jxl.json b/autotests/read/jxl/gimp_exif.jxl.json index 1a963c6..58d24e4 100644 --- a/autotests/read/jxl/gimp_exif.jxl.json +++ b/autotests/read/jxl/gimp_exif.jxl.json @@ -12,6 +12,14 @@ "key" : "ModificationDate", "value" : "2025-01-05T10:18:16" }, + { + "key" : "Keywords", + "value" : "kde;w11" + }, + { + "key" : "Rating", + "value" : "4" + }, { "key" : "Software" , "value" : "GIMP 3.0.0-RC2" diff --git a/autotests/read/jxr/metadata.jxr b/autotests/read/jxr/metadata.jxr index bce9bee..3a9992d 100644 Binary files a/autotests/read/jxr/metadata.jxr and b/autotests/read/jxr/metadata.jxr differ diff --git a/autotests/read/jxr/metadata.jxr.json b/autotests/read/jxr/metadata.jxr.json index 9153d4c..4d9f98f 100644 --- a/autotests/read/jxr/metadata.jxr.json +++ b/autotests/read/jxr/metadata.jxr.json @@ -59,6 +59,14 @@ { "key" : "Model", "value" : "KImageFormats" + }, + { + "key" : "Keywords", + "value" : "broacast;test;kde" + }, + { + "key" : "Rating", + "value" : "5" } ], "resolution" : { diff --git a/autotests/write/basic/jxl.json b/autotests/write/basic/jxl.json index f6c495d..9e250be 100644 --- a/autotests/write/basic/jxl.json +++ b/autotests/write/basic/jxl.json @@ -37,6 +37,14 @@ "key" : "Description", "value" : "テレビ放送テスト映像。(TV broadcast test image.)" }, + { + "key" : "Keywords", + "value" : "kde;test" + }, + { + "key" : "Rating", + "value" : "5" + }, { "key" : "Latitude", "value" : "44.6478" diff --git a/autotests/write/basic/jxr.json b/autotests/write/basic/jxr.json index 7788207..86e1cd7 100644 --- a/autotests/write/basic/jxr.json +++ b/autotests/write/basic/jxr.json @@ -29,6 +29,10 @@ "key" : "Description", "value" : "TV broadcast test image." }, + { + "key" : "Rating", + "value" : "4" + }, { "key" : "Latitude", "value" : "44.6478" diff --git a/src/imageformats/jxr.cpp b/src/imageformats/jxr.cpp index c9473f5..331ce7c 100644 --- a/src/imageformats/jxr.cpp +++ b/src/imageformats/jxr.cpp @@ -739,6 +739,13 @@ public: DESCRIPTIVEMETADATA meta; memset(&meta, 0, sizeof(meta)); + auto ok = false; + auto rating = image.text(QStringLiteral(META_KEY_RATING)).toUInt(&ok); + if (ok) { + meta.pvarRatingStars.vt = DPKVT_UI2; + meta.pvarRatingStars.VT.uiVal = quint16(rating); + } + #define META_CTEXT(name, field) \ auto field = image.text(QStringLiteral(name)).toUtf8(); \ if (!field.isEmpty()) { \ @@ -1017,6 +1024,10 @@ private: return false; } + if (meta.pvarRatingStars.vt == DPKVT_UI2) { + m_txtMeta.insert(QStringLiteral(META_KEY_RATING), QStringLiteral("%1").arg(meta.pvarRatingStars.VT.uiVal)); + } + #define META_TEXT(name, field) \ if (meta.field.vt == DPKVT_LPSTR) \ m_txtMeta.insert(QStringLiteral(name), QString::fromUtf8(meta.field.VT.pszVal)); \ diff --git a/src/imageformats/microexif.cpp b/src/imageformats/microexif.cpp index 23dd3f4..0190132 100644 --- a/src/imageformats/microexif.cpp +++ b/src/imageformats/microexif.cpp @@ -30,6 +30,9 @@ #define TIFF_ARTIST 0x013B #define TIFF_DATETIME 0x0132 #define TIFF_COPYRIGHT 0x8298 +#define TIFF_XPRATING 0x4746 // added by Windows Explorer Image Properties +#define TIFF_XPTITLE 0x9C9B // added by Windows Explorer Image Properties +#define TIFF_XPKEYWORDS 0x9C9E // added by Windows Explorer Image Properties #define TIFF_VAL_URES_NOABSOLUTE 1 #define TIFF_VAL_URES_INCH 2 @@ -134,6 +137,9 @@ static const KnownTags staticTagTypes = { TagInfo(TIFF_ARTIST, ExifTagType::Utf8), TagInfo(TIFF_DATETIME, ExifTagType::Ascii), TagInfo(TIFF_COPYRIGHT, ExifTagType::Utf8), + TagInfo(TIFF_XPRATING, ExifTagType::Short), + TagInfo(TIFF_XPTITLE, ExifTagType::Byte), + TagInfo(TIFF_XPKEYWORDS, ExifTagType::Byte), TagInfo(EXIF_EXPOSURETIME, ExifTagType::Rational), TagInfo(EXIF_FNUMBER, ExifTagType::Rational), TagInfo(EXIF_EXIFIFD, ExifTagType::Long), @@ -206,8 +212,8 @@ static const QList> exifStrMap = { std::pair(EXIF_BODYSERIALNUMBER, QStringLiteral(META_KEY_SERIALNUMBER)), std::pair(EXIF_LENSMAKE, QStringLiteral(META_KEY_LENS_MANUFACTURER)), std::pair(EXIF_LENSMODEL, QStringLiteral(META_KEY_LENS_MODEL)), - std::pair(EXIF_LENSSERIALNUMBER, QStringLiteral(META_KEY_LENS_SERIALNUMBER)), - std::pair(EXIF_IMAGETITLE, QStringLiteral(META_KEY_TITLE)), + std::pair(EXIF_LENSSERIALNUMBER, QStringLiteral(META_KEY_LENS_SERIALNUMBER)) + // std::pair(EXIF_IMAGETITLE, QStringLiteral(META_KEY_TITLE)) // using functions due to XP title fallback }; // clang-format on @@ -842,6 +848,32 @@ void MicroExif::setCopyright(const QString &s) setTiffString(TIFF_COPYRIGHT, s); } +QStringList MicroExif::keywords() const +{ + auto val = utf16String(m_tiffTags, TIFF_XPKEYWORDS); + if (val.isEmpty()) + return {}; + return val.split(QChar(u';'), Qt::SkipEmptyParts); +} + +void MicroExif::setKeywords(const QStringList &k) +{ + setUtf16String(m_tiffTags, TIFF_XPKEYWORDS, k.join(QChar(u';'))); +} + +quint16 MicroExif::rating() const +{ + return m_tiffTags.value(TIFF_XPRATING).toUInt(); +} + +void MicroExif::setRating(quint16 rating) +{ + if (rating == 0) + m_tiffTags.remove(TIFF_XPRATING); + else + m_tiffTags.insert(TIFF_XPRATING, rating); +} + QString MicroExif::make() const { return tiffString(TIFF_MAKE); @@ -964,12 +996,18 @@ void MicroExif::setDateTimeDigitized(const QDateTime &dt) QString MicroExif::title() const { - return exifString(EXIF_IMAGETITLE); + auto s = exifString(EXIF_IMAGETITLE); + if (s.isEmpty()) { // fall back to XP title + s = utf16String(m_tiffTags, TIFF_XPTITLE); + } + return s; } -void MicroExif::setImageTitle(const QString &s) +void MicroExif::setTitle(const QString &s) { setExifString(EXIF_IMAGETITLE, s); + // Since there is an official title tag, I never write the non-standard Microsoft one. + setUtf16String(m_tiffTags, TIFF_XPTITLE, QString()); } QUuid MicroExif::uniqueId() const @@ -1337,6 +1375,11 @@ void MicroExif::updateImageMetadata(QImage &targetImage, bool replaceExisting) c if (!s.isEmpty()) targetImage.setText(p.second, s); } + if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_TITLE)).isEmpty()) { + auto s = title(); + if (!s.isEmpty()) + targetImage.setText(QStringLiteral(META_KEY_TITLE), s); + } // set date and time if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_MODIFICATIONDATE)).isEmpty()) { @@ -1423,6 +1466,18 @@ void MicroExif::updateImageMetadata(QImage &targetImage, bool replaceExisting) c if (v != WhiteBalance::NotSet) targetImage.setText(QStringLiteral(META_KEY_WHITEBALANCE), QStringLiteral("%1").arg(quint16(v))); } + + // set Microsoft tags + if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_KEYWORDS)).isEmpty()) { + auto keywords = this->keywords(); + if (!keywords.isEmpty()) + targetImage.setText(QStringLiteral(META_KEY_KEYWORDS), keywords.join(QChar(u';'))); + } + if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_RATING)).isEmpty()) { + auto v = rating(); + if (v != 0) + targetImage.setText(QStringLiteral(META_KEY_RATING), QStringLiteral("%1").arg(quint16(v))); + } } bool MicroExif::updateImageResolution(QImage &targetImage) @@ -1518,6 +1573,7 @@ MicroExif MicroExif::fromImage(const QImage &image) for (auto &&p : exifStrMap) { exif.setExifString(p.first, image.text(p.second)); } + exif.setTitle(image.text(QStringLiteral(META_KEY_TITLE))); // TIFF Software if (exif.software().isEmpty()) { @@ -1587,6 +1643,13 @@ MicroExif MicroExif::fromImage(const QImage &image) if (ok) exif.setWhiteBalance(WhiteBalance(whtb)); + // Microsoft tags + exif.setKeywords(image.text(QStringLiteral(META_KEY_KEYWORDS)).split(QChar(u';'), Qt::SkipEmptyParts)); + + auto rating = image.text(QStringLiteral(META_KEY_RATING)).toUInt(&ok); + if (ok) + exif.setRating(rating); + return exif; } @@ -1676,3 +1739,29 @@ QString MicroExif::string(const Tags &tags, quint16 tagId) { return tags.value(tagId).toString(); } + +void MicroExif::setUtf16String(Tags &tags, quint16 tagId, const QString &s) +{ + if (s.isEmpty()) { + tags.remove(tagId); + return; + } + auto s16 = s.toStdU16String(); + QList uba; + auto ba = QByteArrayView(reinterpret_cast(s16.data()), s.size() * 2); + for(auto&& c : ba) + uba.append(quint8(c)); + uba.append(quint8('\0')); + uba.append(quint8('\0')); + tags.insert(tagId, QVariant::fromValue(uba)); +} + +QString MicroExif::utf16String(const Tags &tags, quint16 tagId) +{ + auto ba = tags.value(tagId).value>(); + if (ba.isEmpty()) + return {}; + auto p16 = reinterpret_cast(ba.data()); + auto sz = std::max(ba.size() / 2 - 1, qsizetype()); + return QString::fromUtf16(p16, sz); +} diff --git a/src/imageformats/microexif_p.h b/src/imageformats/microexif_p.h index 441d713..cc5bdd0 100644 --- a/src/imageformats/microexif_p.h +++ b/src/imageformats/microexif_p.h @@ -251,6 +251,24 @@ public: QString copyright() const; void setCopyright(const QString& s); + /*! + * \brief keywords + * Keywords tag used by Windows. + * \note Non-standard, added by Windows Explorer. + * \return The list of tags. + */ + QStringList keywords() const; + void setKeywords(const QStringList& k); + + /*! + * \brief rating + * Rating tag used by Windows. + * \note Non-standard, added by Windows Explorer. + * \return The rating. Usually between 0 (not set) and 5. + */ + quint16 rating() const; + void setRating(quint16 rating); + /*! * \brief make * \return The manufacturer of the recording equipment. @@ -319,7 +337,7 @@ public: * \return The title of the image. */ QString title() const; - void setImageTitle(const QString &s); + void setTitle(const QString &s); /*! * \brief uniqueId @@ -555,6 +573,9 @@ private: static void setString(Tags &tags, quint16 tagId, const QString &s); static QString string(const Tags &tags, quint16 tagId); + static void setUtf16String(Tags &tags, quint16 tagId, const QString &s); + static QString utf16String(const Tags &tags, quint16 tagId); + private: Tags m_tiffTags; Tags m_exifTags; diff --git a/src/imageformats/util_p.h b/src/imageformats/util_p.h index fda946e..c7e79ec 100644 --- a/src/imageformats/util_p.h +++ b/src/imageformats/util_p.h @@ -44,10 +44,12 @@ #define META_KEY_DIRECTION "Direction" #define META_KEY_DOCUMENTNAME "DocumentName" #define META_KEY_HOSTCOMPUTER "HostComputer" +#define META_KEY_KEYWORDS "Keywords" #define META_KEY_LATITUDE "Latitude" #define META_KEY_LONGITUDE "Longitude" #define META_KEY_MODIFICATIONDATE "ModificationDate" #define META_KEY_OWNER "Owner" +#define META_KEY_RATING "Rating" #define META_KEY_SOFTWARE "Software" #define META_KEY_SPEED "Speed" #define META_KEY_TITLE "Title"