From 7531a292350d994f2106ed4329d042dfb5c673df Mon Sep 17 00:00:00 2001 From: Bill Ferguson Date: Sat, 30 May 2026 15:03:40 -0400 Subject: [PATCH 1/3] contrib/image_time - Use internal metadata if available instead of resorting to calling exiv2 or exiftool --- contrib/image_time.lua | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/contrib/image_time.lua b/contrib/image_time.lua index dbd15ba..fef5931 100644 --- a/contrib/image_time.lua +++ b/contrib/image_time.lua @@ -138,8 +138,9 @@ script_data.show = nil -- only required for libs since the destroy_method only h -local PS = dt.configuration.runnin_os == "windows" and "\\" or "/" -local ERROR = -1 +local PS = dt.configuration.runnin_os == "windows" and "\\" or "/" +local ERROR = -1 +local HAS_INTERNAL_METADATA = dt.metadata.exists("Exif.Image.DataTime") -- function to convert from exif time to system time local function exiftime2systime(exiftime) @@ -241,21 +242,30 @@ end local function _get_windows_image_file_creation_time(image) local datetime = nil - local p = io.popen("dir " .. image.path .. PS .. image.filename) - if p then - for line in p:lines() do - if string.match(line, ds.sanitize_lua(image.filename)) then - local mo, day, yr, hr, min, apm = string.match(line, "(%d+)/(%d+)/(%d+) (%d-):(%d+) (%S+)") - if apm == "PM" then - hr = hr + 12 - end - datetime = vars2exiftime(yr, mo, day, hr, min, 0) - end - end - p:close() + local datestring = nil + + if HAS_INTERNAL_METADATA then + datestring = image.DateString else - dt.print(string.format(_("unable to get information for %s"), image.filename)) - datetime = ERROR + local p = io.popen("dir " .. image.path .. PS .. image.filename) + if p then + for line in p:lines() do + if string.match(line, ds.sanitize_lua(image.filename)) then + datestring = line + end + end + p:close() + else + dt.print(string.format(_("unable to get information for %s"), image.filename)) + datetime = ERROR + end + end + if datestring then + local mo, day, yr, hr, min, apm = string.match(datestring, "(%d+)/(%d+)/(%d+) (%d-):(%d+) (%S+)") + if apm == "PM" then + hr = hr + 12 + end + datetime = vars2exiftime(yr, mo, day, hr, min, 0) end return datetime end From 53549f16b56e545881a88980eed81ebfa7ab763a Mon Sep 17 00:00:00 2001 From: Bill Ferguson Date: Sat, 30 May 2026 15:04:47 -0400 Subject: [PATCH 2/3] official/auto_straighten - Added check for Nikon cameras where rotation is opposite of Canon and thus requires the sign to be changed. --- official/auto_straighten.lua | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/official/auto_straighten.lua b/official/auto_straighten.lua index 2849370..3f22703 100644 --- a/official/auto_straighten.lua +++ b/official/auto_straighten.lua @@ -211,9 +211,14 @@ local function straighten_image(image) for line in pipe:lines() do local pr, val = string.match(line, "(.).+: (.+)") if pr == "R" then + local sign = 1.0 dt.print_log("roll angle is " .. val) log.msg(log.debug, "roll angle is " .. val) - roll = determine_correction(tonumber(val)) + -- check for Nidon and change the sign + if string.match(string.upper(image.exif_maker), "NIKON") then + sign = -1.0 + end + roll = determine_correction(tonumber(val) * sign) dt.print_log("corrected roll angle is " .. roll) log.msg(log.debug, "corrected roll angle is " .. roll) elseif pr == "P" then From f7faad096261bac110c283b4a2564fbe51f2ee03 Mon Sep 17 00:00:00 2001 From: Bill Ferguson Date: Sat, 30 May 2026 15:06:37 -0400 Subject: [PATCH 3/3] official/extract_burst_roll_images - added check for internal metadata so that can be used instead of spawning exiv2 or exiftool to detect RawBurstRollImages. Added select button to select burst roll images. Added action button to extract burst roll images from selected burst roll image container --- official/extract_burst_roll_images.lua | 179 ++++++++++++++++++++----- 1 file changed, 144 insertions(+), 35 deletions(-) diff --git a/official/extract_burst_roll_images.lua b/official/extract_burst_roll_images.lua index 90871b9..d487f45 100644 --- a/official/extract_burst_roll_images.lua +++ b/official/extract_burst_roll_images.lua @@ -127,6 +127,9 @@ local CS = dt.configuration.running_os == "windows" and "&" or ";" local EXIFTOOL = df.check_if_bin_exists("exiftool") local EXIV2 = df.check_if_bin_exists("exiv2") local DNGLAB = df.check_if_bin_exists("dnglab") +local EXIV2TAG = "Exif.Canon.RawBurstModeRoll" + +local HAS_INTERNAL_METADATA = dt.metadata.exists(EXIV2TAG) local DNGLAB_ARGS = " convert --image-index all --embed-raw false " local EXIV2_ARGS = " -K Exif.Canon.RawBurstModeRoll -pt " @@ -150,6 +153,7 @@ local extract_burst_roll_images = {} extract_burst_roll_images.preferred_tag_reader = nil extract_burst_roll_images.on_import = true +extract_burst_roll_images.select_button = true extract_burst_roll_images.imported_images = {} extract_burst_roll_images.log_level = DEFAULT_LOG_LEVEL @@ -172,9 +176,18 @@ dt.preferences.register(MODULE, -- script: This is a string used to avoid "bool", -- type _("extract burst roll images on import"), -- label _("extract burst roll images on import"), -- tooltip + false) -- default + +-- display selection button +dt.preferences.register(MODULE, -- script: This is a string used to avoid name collision in preferences (i.e namespace). Set it to something unique, usually the name of the script handling the preference. + "select_button", -- name + "bool", -- type + _("display select burst roll images button"), -- label + _("add a button to the select module to select burst roll images"), -- tooltip true) -- default + -- - - - - - - - - - - - - - - - - - - - - - - - -- A L I A S E S -- - - - - - - - - - - - - - - - - - - - - - - - @@ -256,27 +269,40 @@ end local function is_burst_roll_image(image) local old_log_level = set_log_level() local result = false - local cmd = EXIFTOOL .. EXIFTOOL_ARGS - if ebri.preferred_tag_reader == "exiv2" then - cmd = EXIV2 .. EXIV2_ARGS - end - - local p = io.popen(cmd .. ds.sanitize(image.path) .. PS .. ds.sanitize(image.filename)) - if p then - for line in p:lines() do - if ebri.preferred_tag_reader == "exiv2" then - line = get_last_value(line) - end - line = tonumber(line) - if type(line) == "number" then - if tonumber(line) > 0 then - log.msg(log.info, image.filename .. " is a burst roll container image") + if HAS_INTERNAL_METADATA then + if image.RawBurstModeRoll then + local val = get_last_value(image.RawBurstModeRoll) + val = tonumber(val) + if type(val) == "number" then + if val > 0 then result = true end end end - p:close() + else + local cmd = EXIFTOOL .. EXIFTOOL_ARGS + + if ebri.preferred_tag_reader == "exiv2" then + cmd = EXIV2 .. EXIV2_ARGS + end + + local p = io.popen(cmd .. ds.sanitize(image.path) .. PS .. ds.sanitize(image.filename)) + if p then + for line in p:lines() do + if ebri.preferred_tag_reader == "exiv2" then + line = get_last_value(line) + end + line = tonumber(line) + if type(line) == "number" then + if line > 0 then + log.msg(log.info, image.filename .. " is a burst roll container image") + result = true + end + end + end + p:close() + end end restore_log_level(old_log_level) @@ -321,6 +347,14 @@ local function extract_burst(image) restore_log_level(old_log_level) end +local function extract_burst_images(images) + for _, image in ipairs(images) do + if is_burst_roll_image(image) then + extract_burst(image) + end + end +end + local function process_image(image) local old_log_level = set_log_level() if is_burst_roll_image(image) then @@ -384,36 +418,86 @@ local function process_import(images) restore_log_level(old_log_level) end +local function select_burst_roll_images(event, images) + local old_log_level = set_log_level() + local selected = {} + + for _, image in ipairs(images) do + if is_burst_roll_image(image) then + table.insert(selected, image) + end + end + + restore_log_level(old_log_level) + return selected +end + +local function set_select_sensitive() + if #dt.collection > 0 then + dt.gui.libs.select.set_sensitive(MODULE, true) + else + dt.gui.libs.select.set_sensitive(MODULE, false) + end +end + +local function set_action_sensitive() + local sensitive = false + if #dt.gui.action_images > 0 then + if is_burst_roll_image(dt.gui.action_images[1]) then + sensitive = true + end + end + dt.gui.libs.image.set_sensitive(MODULE, sensitive) +end -- - - - - - - - - - - - - - - - - - - - - - - - -- M A I N P R O G R A M -- - - - - - - - - - - - - - - - - - - - - - - - -if not DNGLAB then - log.msg(log.error, MODULE .. _("dnglab executable not found")) - log.msg(log.screen, script_data.metadata.name .. _(": dnglab executable not found, exiting...")) - return + if not DNGLAB then + log.msg(log.error, MODULE .. _("dnglab executable not found")) + log.msg(log.screen, script_data.metadata.name .. _(": dnglab executable not found, exiting...")) + return + end + +if HAS_INTERNAL_METADATA then + log.msg(log.info, "Exif.Canon.RawBurstModeRoll detected, using internal metadata") +else + if not EXIFTOOL and not EXIV2 then + log.msg(log.error, MODULE .. _("no exif tag reader available")) + log.msg(log.screen, script_data.metadata.name .. _(" no exif tag reader available, exiting...")) + return + end + + ebri.preferred_tag_reader = pref_read("tag_reader", "enum") + + if not string.match(EXIFTOOL, ebri.preferred_tag_reader) and not string.match(EXIV2, ebri.preferred_tag_reader) then + log.msg(log.error, MODULE .. " selected tag " .. ebri.preferred_tag_reader .." not available") + log.msg(log.screen, script_data.metadata.name .. string.format(_(" selected tag reader %s not available"), ebri.preferred_tag_reader)) + return + end end -if not EXIFTOOL and not EXIV2 then - log.msg(log.error, MODULE .. _("no exif tag reader available")) - log.msg(log.screen, script_data.metadata.name .. _(" no exif tag reader available, exiting...")) - return -end - -ebri.preferred_tag_reader = pref_read("tag_reader", "enum") - -if not string.match(EXIFTOOL, ebri.preferred_tag_reader) and not string.match(EXIV2, ebri.preferred_tag_reader) then - log.msg(log.error, MODULE .. " selected tag " .. ebri.preferred_tag_reader .." not available") - log.msg(log.screen, script_data.metadata.name .. string.format(_(" selected tag reader %s not available"), ebri.preferred_tag_reader)) - return -end - -ebri.on_import = dt.preferences.read(MODULE, "on_import", "bool") +ebri.on_import = pref_read("on_import", "bool") +ebri.select_button = pref_read("select_button", "bool") -- - - - - - - - - - - - - - - - - - - - - - - - -- U S E R I N T E R F A C E -- - - - - - - - - - - - - - - - - - - - - - - - +if ebri.select_button then + dt.gui.libs.select.register_selection(MODULE, _("select burst roll images"), select_burst_roll_images, + _("select burst roll image containers")) +end + +dt.gui.libs.image.register_action( + MODULE, + _("extract burst roll images"), + function(event, images) + extract_burst_images(images) + end, + _("extract and group burst roll images with container") +) + -- - - - - - - - - - - - - - - - - - - - - - - - -- D A R K T A B L E I N T E G R A T I O N -- - - - - - - - - - - - - - - - - - - - - - - - @@ -421,6 +505,10 @@ ebri.on_import = dt.preferences.read(MODULE, "on_import", "bool") local function destroy() dt.destroy_event(MODULE, "post-import-film") dt.destroy_event(MODULE, "post-import-image") + if ebri.select_button then + dt.gui.libs.select.destroy_selection(MODULE) + end + dt.gui.libs.image.action.destroy_action(MODULE) end script_data.destroy = destroy @@ -444,6 +532,27 @@ dt.register_event(MODULE, "post-import-image", end ) +dt.register_event(MODULE, "selection-changed", + function(event) + set_action_sensitive() + end +) + +if dt.preferences.read("darktable", "plugins/lighttable/act_on", "bool") then + dt.print_log("adding mouse-over-image-changed event") + dt.register_event(MODULE, "mouse-over-image-changed", + function(event, image) + set_action_sensitive() + end + ) +end + +dt.register_event(MODULE, "collection-changed", + function(event) + set_select_sensitive() + end +) + if not dt.query_event(MODULE, "shortcut") then dt.register_event(MODULE, "shortcut", function(event, shortcut)