From 227534a3c7bebbdf7ba9bd55e2540a91be43b245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Tue, 4 Nov 2025 13:24:45 +0100 Subject: [PATCH 1/2] exif: implement reading user-defined EXIF tags from files The metadata customization dialogue does offer Exif.* tags, but they were not actually read from EXIF. This commit fixes that behaviour. --- src/common/exif.cc | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/common/exif.cc b/src/common/exif.cc index 0a9aa5f80e70..beeb88c03af9 100644 --- a/src/common/exif.cc +++ b/src/common/exif.cc @@ -2352,6 +2352,50 @@ static bool _exif_decode_exif_data(dt_image_t *img, Exiv2::ExifData &exifData) } }; + dt_pthread_mutex_lock(&darktable.metadata_threadsafe); + for(GList *iter = dt_metadata_get_list(); iter; iter = iter->next) + { + dt_metadata_t *metadata = (dt_metadata_t *)iter->data; + if(!FIND_EXIF_TAG(metadata->tagname)) + { + continue; + } + + int ival = pos->toLong(); + std::string str = pos->print(&exifData); + char *value = g_locale_to_utf8(str.c_str(), str.length(), NULL, NULL, NULL); + if(value == NULL) + { + // need non-const char* for g_strstrip + value = g_strdup(str.c_str()); + } + g_strstrip(value); + + gchar *str_value = g_strdup_printf("(%d)", ival); + if(g_strcmp0(value, str_value) == 0) + { + // no string mapping available in exiv2, so we use exiv2's + // default string conversion. + g_free(value); + str = pos->toString(); + // for consistency with the handling above. don't want to mix two + // allocators, that causes me headaches. + value = g_strdup(str.c_str()); + } + g_free(str_value); + + char *adr = value; + // Skip any lang="" or charset=xxx + while(!strncmp(value, "lang=", 5) || !strncmp(value, "charset=", 8)) + { + while(*value != ' ' && *value) value++; + while(*value == ' ') value++; + } + dt_metadata_set_import(img->id, metadata->tagname, value); + g_free(adr); + } + dt_pthread_mutex_unlock(&darktable.metadata_threadsafe); + img->exif_inited = TRUE; return true; } From bfdae9db80ce1fffb889a86c994f166f30c71def Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Tue, 4 Nov 2025 17:13:43 +0100 Subject: [PATCH 2/2] exif: do not blanket-delete all custom metadata on Xmp read Through the call chain dt_image_import -> _image_import_internal -> dt_exif_xmp_read -> _exif_decode_xmp_data, the function _exif_decode_xmp_data would be called with exif_read == FALSE, causing it to delete all metadata which has previously been read in _exif_decode_exif_data (see the parent commit). The intent was likely to be able to update descriptions etc. which had been written through other programs in the Xmp files in darktable. In order to continue to support this use case, we do still remove Xmp-related tags if we don't find them in the sidecar, but only those (via a string match). --- src/common/exif.cc | 7 ++++++- src/common/metadata.c | 28 ++++++++++++++++++++++++++++ src/common/metadata.h | 2 ++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/common/exif.cc b/src/common/exif.cc index beeb88c03af9..f6102b4b5898 100644 --- a/src/common/exif.cc +++ b/src/common/exif.cc @@ -610,7 +610,6 @@ static bool _exif_decode_xmp_data(dt_image_t *img, if(version == -1 || version > 0) { dt_pthread_mutex_lock(&darktable.metadata_threadsafe); - if(!exif_read) dt_metadata_clear(imgs, FALSE); for(GList *iter = dt_metadata_get_list(); iter; iter = iter->next) { @@ -629,6 +628,12 @@ static bool _exif_decode_xmp_data(dt_image_t *img, dt_metadata_set_import(img->id, metadata->tagname, value); free(adr); } + else if(!exif_read && strstr(metadata->tagname, "Xmp.") == metadata->tagname) + { + // Only remove Xmp. metadata fields, do not touch metadata from other + // sources (e.g. Exif.*). + dt_metadata_unset(img->id, metadata->tagname, FALSE); + } } dt_pthread_mutex_unlock(&darktable.metadata_threadsafe); } diff --git a/src/common/metadata.c b/src/common/metadata.c index 0e5f2cab9838..49c47e011e7e 100644 --- a/src/common/metadata.c +++ b/src/common/metadata.c @@ -838,6 +838,34 @@ void dt_metadata_set_list_id(const GList *img, } } +void dt_metadata_unset(const dt_imgid_t imgid, const char *key, const gboolean undo_on) +{ + if(!key || !dt_is_valid_imgid(imgid)) return; + + int keyid = dt_metadata_get_keyid(key); + if(keyid == -1) return; + + GList *imgs = NULL; + imgs = g_list_prepend(imgs, GINT_TO_POINTER(imgid)); + GList *undo = NULL; + if(undo_on) dt_undo_start_group(darktable.undo, DT_UNDO_METADATA); + + const gchar *ckey = g_strdup_printf("%d", keyid); + GList *metadata = NULL; + metadata = g_list_append(metadata, (gpointer)ckey); + metadata = g_list_append(metadata, NULL); + + _metadata_execute(imgs, metadata, &undo, undo_on, DT_MA_REMOVE); + + g_list_free_full(metadata, g_free); + g_list_free(imgs); + if(undo_on) + { + dt_undo_record(darktable.undo, NULL, DT_UNDO_METADATA, undo, _pop_undo, _metadata_undo_data_free); + dt_undo_end_group(darktable.undo); + } +} + gboolean dt_metadata_already_imported(const char *filename, const char *datetime) { if(!filename || !datetime) diff --git a/src/common/metadata.h b/src/common/metadata.h index c2f6b3c4e297..33453218c504 100644 --- a/src/common/metadata.h +++ b/src/common/metadata.h @@ -98,6 +98,8 @@ void dt_metadata_set_list(const GList *imgs, GList *key_value, const gboolean un if clear_on TRUE the image metadata are cleared before attaching the new ones*/ void dt_metadata_set_list_id(const GList *img, const GList *metadata, const gboolean clear_on, const gboolean undo_on); +/** Unset a specific metadata key for a specific image. Noop if the key isn't set. */ +void dt_metadata_unset(const dt_imgid_t imgid, const char *key, const gboolean undo_on); /** Get metadata (named keys) for a specific image, or all selected for an invalid imgid For keys which return a string, the caller has to make sure that it is freed after usage. With mutex lock. */