[frameworks/kimageformats] /: EXIF: add support for Windows Explorer tags
Mirco Miranda <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit c21b50d699bdbb43d499482dfe412f8a77697782 by Mirco Miranda, on behalf of Mirco Miranda.
Committed on 20/07/2026 at 05:27.
Pushed by mircomir into branch 'master'.
EXIF: add support for Windows Explorer tags
M +3 -0 README.md
M +- -- autotests/read/jxl/gimp_exif.jxl
M +8 -0 autotests/read/jxl/gimp_exif.jxl.json
M +- -- autotests/read/jxr/metadata.jxr
M +8 -0 autotests/read/jxr/metadata.jxr.json
M +8 -0 autotests/write/basic/jxl.json
M +4 -0 autotests/write/basic/jxr.json
M +11 -0 src/imageformats/jxr.cpp
M +93 -4 src/imageformats/microexif.cpp
M +22 -1 src/imageformats/microexif_p.h
M +2 -0 src/imageformats/util_p.h
https://invent.kde.org/frameworks/kimageformats/-/commit/c21b50d699bdbb43d499482dfe412f8a77697782
diff --git a/README.md b/README.md
index 6b37ef45..4110b14b 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 2ca16111..98edab09 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 1a963c64..58d24e46 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 bce9beef..3a9992dc 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 9153d4c2..4d9f98f7 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 f6c495d0..9e250be9 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 77882074..86e1cd79 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 c9473f51..331ce7c8 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 23dd3f4d..01901322 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<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)),
+ std::pair<quint16, QString>(EXIF_LENSSERIALNUMBER, QStringLiteral(META_KEY_LENS_SERIALNUMBER))
+ // std::pair<quint16, QString>(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<quint8> uba;
+ auto ba = QByteArrayView(reinterpret_cast<char*>(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<QList<quint8>>();
+ if (ba.isEmpty())
+ return {};
+ auto p16 = reinterpret_cast<char16_t*>(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 441d7136..cc5bdd04 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 fda946e1..c7e79ecd 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"