From 7979be7484d7e7a9b44ab6bf87977ad57b8d07bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Fri, 31 Oct 2025 10:39:48 +0100 Subject: [PATCH] Add support for extracting, storing, and filtering on Fujifilm Film Modes Fujifilm cameras of the X and GF series have support for so-called Film Simulation Modes. These are said to mimic the look of certain types of analogue film. I have wished for the ability to filter on film sim mode for quite a while. In addition, I would like to, in the future, add specific dtstyles for the various modes and allow to apply them by default via apply_camera_style. This commits provides the foundation for that. It adds the ability to filter by and view the film mode in the UI. It also exports the film mode to Lua, so that apply_camera_style could in the future use that. --- src/common/collection.c | 40 +++++++++++++++++++++++++++++++--------- src/common/collection.h | 1 + src/common/database.c | 17 ++++++++++++++++- src/common/exif.cc | 15 +++++++++++++++ src/common/image.c | 14 ++++++++++---- src/common/image.h | 2 ++ src/common/image_cache.c | 10 ++++++++-- src/libs/collect.c | 32 +++++++++++++++++++++----------- src/libs/filtering.c | 3 ++- src/libs/filters/misc.c | 5 +++++ src/libs/metadata_view.c | 10 +++++++++- src/lua/image.c | 1 + 12 files changed, 121 insertions(+), 29 deletions(-) diff --git a/src/common/collection.c b/src/common/collection.c index c3d66064c29a..db24f16b47cb 100644 --- a/src/common/collection.c +++ b/src/common/collection.c @@ -659,6 +659,8 @@ const char *dt_collection_name_untranslated(const dt_collection_properties_t pro return N_("exposure program"); case DT_COLLECTION_PROP_METERING_MODE: return N_("metering mode"); + case DT_COLLECTION_PROP_FILM_MODE: + return N_("film mode"); case DT_COLLECTION_PROP_LAST: return NULL; default: @@ -1828,6 +1830,30 @@ static gchar *get_query_string(const dt_collection_properties_t property, const dt_util_str_cat(&query, ")"); break; + case DT_COLLECTION_PROP_FILM_MODE: // film mode + query = g_strdup("("); + // handle the possibility of multiple values + elems = _strsplit_quotes(escaped_text, ",", -1); + for(int i = 0; i < g_strv_length(elems); i++) + { + if(!g_strcmp0(elems[i], _("unnamed"))) + { + dt_util_str_cat(&query, + "%sfilm_mode_id IN (SELECT id FROM main.film_mode WHERE name IS NULL OR TRIM(name)='')", + i > 0 ? " OR " : ""); + } + else + { + gchar *film_mode = _add_wildcards(elems[i]); + dt_util_str_cat(&query, "%sfilm_mode_id IN (SELECT id FROM main.film_mode WHERE name LIKE '%s')", + i > 0 ? " OR " : "", film_mode); + g_free(film_mode); + } + } + g_strfreev(elems); + dt_util_str_cat(&query, ")"); + break; + case DT_COLLECTION_PROP_GROUP_ID: // group id query = g_strdup("("); // handle the possibility of multiple values @@ -2605,15 +2631,11 @@ void dt_collection_update_query(const dt_collection_t *collection, snprintf(confname, sizeof(confname), "plugins/lighttable/collect/mode%1d", i); const int mode = dt_conf_get_int(confname); - if(*text - && g_strcmp0(text, _("unnamed")) != 0 - && (property == DT_COLLECTION_PROP_CAMERA - || property == DT_COLLECTION_PROP_LENS - || property == DT_COLLECTION_PROP_WHITEBALANCE - || property == DT_COLLECTION_PROP_FLASH - || property == DT_COLLECTION_PROP_EXPOSURE_PROGRAM - || property == DT_COLLECTION_PROP_METERING_MODE - || property == DT_COLLECTION_PROP_GROUP_ID)) + if(*text && g_strcmp0(text, _("unnamed")) != 0 + && (property == DT_COLLECTION_PROP_CAMERA || property == DT_COLLECTION_PROP_LENS + || property == DT_COLLECTION_PROP_WHITEBALANCE || property == DT_COLLECTION_PROP_FLASH + || property == DT_COLLECTION_PROP_EXPOSURE_PROGRAM || property == DT_COLLECTION_PROP_METERING_MODE + || property == DT_COLLECTION_PROP_FILM_MODE || property == DT_COLLECTION_PROP_GROUP_ID)) { gchar *text_quoted = g_strdup_printf("\"%s\"", text); g_free(text); diff --git a/src/common/collection.h b/src/common/collection.h index 9e8a533e078d..0f08f020c5dd 100644 --- a/src/common/collection.h +++ b/src/common/collection.h @@ -121,6 +121,7 @@ typedef enum dt_collection_properties_t // all new collection types need to be added before DT_COLLECTION_PROP_LAST, // which separates actual collection types from special flag values + DT_COLLECTION_PROP_FILM_MODE, DT_COLLECTION_PROP_LAST, DT_COLLECTION_PROP_UNDEF, diff --git a/src/common/database.c b/src/common/database.c index 92ef8c5a271e..193d48438b51 100644 --- a/src/common/database.c +++ b/src/common/database.c @@ -51,7 +51,7 @@ #define LAST_FULL_DATABASE_VERSION_DATA 10 // You HAVE TO bump THESE versions whenever you add an update branches to _upgrade_*_schema_step()! -#define CURRENT_DATABASE_VERSION_LIBRARY 57 +#define CURRENT_DATABASE_VERSION_LIBRARY 58 #define CURRENT_DATABASE_VERSION_DATA 13 #define USE_NESTED_TRANSACTIONS @@ -2967,6 +2967,21 @@ static int _upgrade_library_schema_step(dt_database_t *db, "[init] can't add `flash_tagvalue' column to images table in database\n"); new_version = 57; } + else if(version == 57) + { + + TRY_EXEC("CREATE TABLE film_mode" + " (id INTEGER PRIMARY KEY AUTOINCREMENT," + " name VARCHAR)", + "can't create table film_mode"); + TRY_EXEC("CREATE UNIQUE INDEX film_mode_name ON film_mode (name)", + "can't create index `film_mode_name' on table `film_mode'"); + + TRY_EXEC("ALTER TABLE main.images ADD COLUMN film_mode_id INTEGER REFERENCES film_mode (id) ON DELETE " + "RESTRICT ON UPDATE CASCADE", + "[init] can't add `film_mode' column to images table in database\n"); + new_version = 58; + } else new_version = version; // should be the fallback so that calling code sees that we are in an infinite loop diff --git a/src/common/exif.cc b/src/common/exif.cc index 0d51a142f80a..39dd078fc030 100644 --- a/src/common/exif.cc +++ b/src/common/exif.cc @@ -2335,6 +2335,21 @@ static bool _exif_decode_exif_data(dt_image_t *img, Exiv2::ExifData &exifData) } }; + if(FIND_EXIF_TAG("Exif.Fujifilm.FilmMode")) + { + const int value = pos->toLong(); + _exif_value_str(value, img->exif_film_mode, sizeof(img->exif_film_mode), pos, exifData); + } + else if(FIND_EXIF_TAG("Exif.Fujifilm.Color")) + { + // Exif.Fujifilm.Color contains the monochrome mode (sepia, normal b&w, + // color filters, acros) if FilmMode is not set. + // In the camera UI, those are all in the same menu, so it makes sense to + // merge them into the same tag in darktable. + const int value = pos->toLong(); + _exif_value_str(value, img->exif_film_mode, sizeof(img->exif_film_mode), pos, exifData); + } + img->exif_inited = TRUE; return true; } diff --git a/src/common/image.c b/src/common/image.c index 01a676e67788..206057b2b2ec 100644 --- a/src/common/image.c +++ b/src/common/image.c @@ -1318,7 +1318,7 @@ static dt_imgid_t _image_duplicate_with_version_ext(const dt_imgid_t imgid, " orientation, longitude, latitude, altitude, color_matrix," " colorspace, version, max_version," " history_end, position, aspect_ratio, exposure_bias, import_timestamp," - " whitebalance_id, flash_id, exposure_program_id, metering_mode_id, flash_tagvalue)" + " whitebalance_id, flash_id, exposure_program_id, metering_mode_id, flash_tagvalue, film_mode_id)" " SELECT NULL, group_id, film_id, width, height, filename," " maker_id, model_id, camera_id, lens_id," " exposure, aperture, iso, focal_length, focus_distance, datetime_taken," @@ -1326,7 +1326,7 @@ static dt_imgid_t _image_duplicate_with_version_ext(const dt_imgid_t imgid, " raw_black, raw_maximum, orientation," " longitude, latitude, altitude, color_matrix, colorspace, NULL, NULL, 0, ?1," " aspect_ratio, exposure_bias, import_timestamp," - " whitebalance_id, flash_id, exposure_program_id, metering_mode_id, flash_tagvalue" + " whitebalance_id, flash_id, exposure_program_id, metering_mode_id, flash_tagvalue, film_mode_id" " FROM main.images WHERE id = ?2", -1, &stmt, NULL); // clang-format on @@ -2141,6 +2141,7 @@ void dt_image_init(dt_image_t *img) memset(img->exif_flash, 0, sizeof(img->exif_flash)); memset(img->exif_exposure_program, 0, sizeof(img->exif_exposure_program)); memset(img->exif_metering_mode, 0, sizeof(img->exif_metering_mode)); + memset(img->exif_film_mode, 0, sizeof(img->exif_metering_mode)); memset(&img->exif_datetime_taken, 0, sizeof(img->exif_datetime_taken)); memset(img->camera_maker, 0, sizeof(img->camera_maker)); memset(img->camera_model, 0, sizeof(img->camera_model)); @@ -2548,7 +2549,7 @@ dt_imgid_t dt_image_copy_rename(const dt_imgid_t imgid, " raw_black, raw_maximum, orientation," " longitude, latitude, altitude, color_matrix, colorspace, version, max_version," " position, aspect_ratio, exposure_bias," - " whitebalance_id, flash_id, exposure_program_id, metering_mode_id, flash_tagvalue)" + " whitebalance_id, flash_id, exposure_program_id, metering_mode_id, flash_tagvalue, film_mode_id)" " SELECT NULL, group_id, ?1 as film_id, width, height, ?2 as filename," " maker_id, model_id, lens_id," " exposure, aperture, iso, focal_length, focus_distance, datetime_taken," @@ -2556,7 +2557,7 @@ dt_imgid_t dt_image_copy_rename(const dt_imgid_t imgid, " orientation, longitude, latitude, altitude," " color_matrix, colorspace, -1, -1," " ?3, aspect_ratio, exposure_bias," - " whitebalance_id, flash_id, exposure_program_id, metering_mode_id, flash_tagvalue" + " whitebalance_id, flash_id, exposure_program_id, metering_mode_id, flash_tagvalue, film_mode_id" " FROM main.images" " WHERE id = ?4", -1, &stmt, NULL); @@ -3349,6 +3350,11 @@ int32_t dt_image_get_metering_mode_id(const char *name) return _image_get_set_name_id("metering_mode", name); } +int32_t dt_image_get_film_mode_id(const char *name) +{ + return _image_get_set_name_id("film_mode", name); +} + int32_t dt_image_get_camera_id(const char *maker, const char *model) { sqlite3_stmt *stmt; diff --git a/src/common/image.h b/src/common/image.h index 9430757ec956..5f87082327a9 100644 --- a/src/common/image.h +++ b/src/common/image.h @@ -271,6 +271,7 @@ typedef struct dt_image_t char exif_flash[64]; char exif_exposure_program[64]; char exif_metering_mode[64]; + char exif_film_mode[64]; GTimeSpan exif_datetime_taken; dt_image_correction_type_t exif_correction_type; @@ -636,6 +637,7 @@ int32_t dt_image_get_whitebalance_id(const char *name); int32_t dt_image_get_flash_id(const char *name); int32_t dt_image_get_exposure_program_id(const char *name); int32_t dt_image_get_metering_mode_id(const char *name); +int32_t dt_image_get_film_mode_id(const char *name); int32_t dt_image_get_camera_id(const char *maker, const char *model); G_END_DECLS diff --git a/src/common/image_cache.c b/src/common/image_cache.c index d489743ab4a0..f1dd4081467a 100644 --- a/src/common/image_cache.c +++ b/src/common/image_cache.c @@ -49,7 +49,7 @@ static void _image_cache_allocate(void *data, " raw_black, raw_maximum, aspect_ratio, exposure_bias," " import_timestamp, change_timestamp, export_timestamp, print_timestamp," " output_width, output_height, cm.maker, cm.model, cm.alias," - " wb.name, fl.name, ep.name, mm.name, flash_tagvalue" + " wb.name, fl.name, ep.name, mm.name, flash_tagvalue, fm.name" " FROM main.images AS mi" " LEFT JOIN main.cameras AS cm ON cm.id = mi.camera_id" " LEFT JOIN main.makers AS mk ON mk.id = mi.maker_id" @@ -59,6 +59,7 @@ static void _image_cache_allocate(void *data, " LEFT JOIN main.flash AS fl ON fl.id = mi.flash_id" " LEFT JOIN main.exposure_program AS ep ON ep.id = mi.exposure_program_id" " LEFT JOIN main.metering_mode AS mm ON mm.id = mi.metering_mode_id" + " LEFT JOIN main.film_mode AS fm ON fm.id = mi.film_mode_id" " WHERE mi.id = ?1", -1, &stmt, NULL); // clang-format on @@ -155,6 +156,8 @@ static void _image_cache_allocate(void *data, if(str) g_strlcpy(img->exif_metering_mode, str, sizeof(img->exif_metering_mode)); img->exif_flash_tagvalue = sqlite3_column_int(stmt, 42); + str = (char *)sqlite3_column_text(stmt, 43); + if(str) g_strlcpy(img->exif_film_mode, str, sizeof(img->exif_film_mode)); dt_color_harmony_get(entry->key, &img->color_harmony_guide); @@ -345,7 +348,8 @@ void dt_image_cache_write_release_info(dt_image_t *img, " import_timestamp = ?28, change_timestamp = ?29, export_timestamp = ?30," " print_timestamp = ?31, output_width = ?32, output_height = ?33," " whitebalance_id = ?36, flash_id = ?37," - " exposure_program_id = ?38, metering_mode_id = ?39, flash_tagvalue = ?41" + " exposure_program_id = ?38, metering_mode_id = ?39, flash_tagvalue = ?41," + " film_mode_id = ?42" " WHERE id = ?40", -1, &stmt, NULL); @@ -356,6 +360,7 @@ void dt_image_cache_write_release_info(dt_image_t *img, const int32_t flash_id = dt_image_get_flash_id(img->exif_flash); const int32_t exposure_program_id = dt_image_get_exposure_program_id(img->exif_exposure_program); const int32_t metering_mode_id = dt_image_get_metering_mode_id(img->exif_metering_mode); + const int32_t film_mode_id = dt_image_get_film_mode_id(img->exif_film_mode); // also make sure we update the camera_id and possibly the associated data // in cameras table. @@ -412,6 +417,7 @@ void dt_image_cache_write_release_info(dt_image_t *img, DT_DEBUG_SQLITE3_BIND_INT(stmt, 39, metering_mode_id); DT_DEBUG_SQLITE3_BIND_INT(stmt, 40, img->id); DT_DEBUG_SQLITE3_BIND_INT(stmt, 41, img->exif_flash_tagvalue); + DT_DEBUG_SQLITE3_BIND_INT(stmt, 42, film_mode_id); const int rc = sqlite3_step(stmt); if(rc != SQLITE_DONE) diff --git a/src/libs/collect.c b/src/libs/collect.c index 30bab8d33dc7..4acefd07d948 100644 --- a/src/libs/collect.c +++ b/src/libs/collect.c @@ -2104,6 +2104,19 @@ static void _list_view(dt_lib_collect_rule_t *dr) // clang-format on break; + case DT_COLLECTION_PROP_FILM_MODE: // lens + // clang-format off + g_snprintf(query, sizeof(query), + "SELECT fm.name AS film_mode, 1, COUNT(*) AS count" + " FROM main.images AS mi, main.film_mode AS fm" + " WHERE mi.film_mode_id = fm.id" + " AND %s" + " GROUP BY LOWER(film_mode)" + " ORDER BY LOWER(film_mode) %s", where_ext, + sort_descending ? "DESC" : "ASC"); + // clang-format on + break; + case DT_COLLECTION_PROP_FILENAME: // filename // clang-format off g_snprintf(query, sizeof(query), @@ -2375,17 +2388,12 @@ static void _list_view(dt_lib_collect_rule_t *dr) // if needed, we restrict the tree to matching entries if(dr->typing - && (property == DT_COLLECTION_PROP_CAMERA - || property == DT_COLLECTION_PROP_FILENAME - || property == DT_COLLECTION_PROP_FILMROLL - || property == DT_COLLECTION_PROP_LENS - || property == DT_COLLECTION_PROP_APERTURE - || property == DT_COLLECTION_PROP_FOCAL_LENGTH - || property == DT_COLLECTION_PROP_ISO - || property == DT_COLLECTION_PROP_MODULE - || property == DT_COLLECTION_PROP_ORDER - || property == DT_COLLECTION_PROP_RATING - || property >= DT_COLLECTION_PROP_METADATA_OFFSET)) + && (property == DT_COLLECTION_PROP_CAMERA || property == DT_COLLECTION_PROP_FILENAME + || property == DT_COLLECTION_PROP_FILMROLL || property == DT_COLLECTION_PROP_LENS + || property == DT_COLLECTION_PROP_APERTURE || property == DT_COLLECTION_PROP_FOCAL_LENGTH + || property == DT_COLLECTION_PROP_ISO || property == DT_COLLECTION_PROP_MODULE + || property == DT_COLLECTION_PROP_ORDER || property == DT_COLLECTION_PROP_RATING + || property >= DT_COLLECTION_PROP_METADATA_OFFSET || property == DT_COLLECTION_PROP_FILM_MODE)) { gchar *needle = g_utf8_strdown(gtk_entry_get_text(GTK_ENTRY(dr->text)), -1); if(g_str_has_suffix(needle, "%")) @@ -3360,6 +3368,7 @@ static void _populate_collect_combo(GtkWidget *w) ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_FLASH); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_EXPOSURE_PROGRAM); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_METERING_MODE); + ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_FILM_MODE); dt_bauhaus_combobox_add_section(w, _("darktable")); ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_GROUP_ID); @@ -4004,6 +4013,7 @@ void init(struct dt_lib_module_t *self) luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_ASPECT_RATIO); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_EXPOSURE); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_EXPOSURE_BIAS); + luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_FILM_MODE); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_FILENAME); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_GEOTAGGING); luaA_enum_value(L, dt_collection_properties_t, DT_COLLECTION_PROP_LOCAL_COPY); diff --git a/src/libs/filtering.c b/src/libs/filtering.c index e6a9bfb5dafc..e30d75d1e3e1 100644 --- a/src/libs/filtering.c +++ b/src/libs/filtering.c @@ -248,7 +248,8 @@ static _filter_t filters[] { DT_COLLECTION_PROP_WHITEBALANCE, _misc_widget_init, _misc_update }, { DT_COLLECTION_PROP_FLASH, _misc_widget_init, _misc_update }, { DT_COLLECTION_PROP_EXPOSURE_PROGRAM, _misc_widget_init, _misc_update }, - { DT_COLLECTION_PROP_METERING_MODE, _misc_widget_init, _misc_update } }; + { DT_COLLECTION_PROP_METERING_MODE, _misc_widget_init, _misc_update }, + { DT_COLLECTION_PROP_FILM_MODE, _misc_widget_init, _misc_update } }; static _filter_t *_filters_get(const dt_collection_properties_t prop) { diff --git a/src/libs/filters/misc.c b/src/libs/filters/misc.c index 2b6172bc932d..0e396339f409 100644 --- a/src/libs/filters/misc.c +++ b/src/libs/filters/misc.c @@ -113,6 +113,11 @@ void _misc_tree_update(_widgets_misc_t *misc) table = g_strdup("metering_mode"); tooltip = g_strdup(_("no metering mode defined")); } + else if(misc->prop == DT_COLLECTION_PROP_FILM_MODE) + { + table = g_strdup("film_mode"); + tooltip = g_strdup(_("no film mode defined")); + } // SQL if(misc->prop == DT_COLLECTION_PROP_CAMERA) diff --git a/src/libs/metadata_view.c b/src/libs/metadata_view.c index f4e1abd11b09..d12d7d10690c 100644 --- a/src/libs/metadata_view.c +++ b/src/libs/metadata_view.c @@ -106,6 +106,7 @@ enum md_exif_datetime, md_exif_width, md_exif_height, + md_exif_film_mode, /* size of final image */ md_width, @@ -121,7 +122,7 @@ enum md_categories, /* xmp */ - md_xmp_metadata // keep this at last position! + md_xmp_metadata // keep this at last position! }; // We have to maintain the correspondence between the displayed metadata list @@ -162,6 +163,7 @@ static const char *_labels[] = { N_("datetime"), N_("width"), N_("height"), + N_("film mode"), N_("export width"), N_("export height"), @@ -594,6 +596,7 @@ void gui_update(dt_lib_module_t *self) "COUNT(DISTINCT datetime_taken), " "COUNT(DISTINCT width), " "COUNT(DISTINCT height), " + "COUNT(DISTINCT IFNULL(film_mode_id, '')), " "COUNT(DISTINCT IFNULL(output_width, '')), " //exported width "COUNT(DISTINCT IFNULL(output_height, '')), " //exported height "COUNT(DISTINCT IFNULL(latitude, '')), " @@ -901,6 +904,11 @@ void gui_update(dt_lib_module_t *self) } break; + case md_exif_film_mode: + (void)g_snprintf(text, sizeof(text), "%s", img->exif_film_mode); + _metadata_update_value(md_exif_film_mode, text, self); + break; + case md_width: (void)g_strlcpy(text, NODATA_STRING, sizeof(text)); if(img->final_width > 0) diff --git a/src/lua/image.c b/src/lua/image.c index 63399a40a7c3..42aa8ec42f5c 100644 --- a/src/lua/image.c +++ b/src/lua/image.c @@ -540,6 +540,7 @@ int dt_lua_init_image(lua_State *L) luaA_struct_member(L, dt_image_t, exif_flash, char_64); luaA_struct_member(L, dt_image_t, exif_exposure_program, char_64); luaA_struct_member(L, dt_image_t, exif_metering_mode, char_64); + luaA_struct_member(L, dt_image_t, exif_film_mode, char_64); luaA_struct_member(L, dt_image_t, filename, const char_filename_length); luaA_struct_member(L, dt_image_t, width, const int32_t); luaA_struct_member(L, dt_image_t, height, const int32_t);