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
569 lines
18 KiB
Lua
569 lines
18 KiB
Lua
--[[
|
|
|
|
extract_burst_roll_images.lua - extract burst images from a burst roll file
|
|
|
|
Copyright (C) 2026 Bill Ferguson <wpferguson@gmail.com>.
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation; either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
]]
|
|
--[[
|
|
extract_burst_roll_images - extract burst images from a burst roll file
|
|
|
|
Canon cameras are capable of shooting in burst mode. When the shutter
|
|
button is half pressed the camera starts "recording". When the shutter is
|
|
fully pressed the pervious 1/2 second of imformation is recorded as individual
|
|
images stored in one file as well as images captured at 30 frames/sec until
|
|
the buffer fills or the shutter button is released. So a Canon R7 burst file
|
|
could contain ~90 images.
|
|
|
|
Extracting the images from this file required use of Canon proprietary software
|
|
until recently when dnglab got the capability to extract the embedded images as
|
|
DNG raws.
|
|
|
|
This script can be set to run on import, scanning for burst roll containers,
|
|
extracting the embedded images and grouping them with the burst roll container.
|
|
The script can alsu be utilized via a short cut and applied to selected images.
|
|
|
|
When set to on import the script scans each image after import using exiv2 or exiftool
|
|
to check the image EXIF information to see if the file is a busrt roll container.
|
|
Once the import images are scanned, the detected burst roll containers are processed,
|
|
the embedded images extracted as DNSs, and grouped with the container image.
|
|
|
|
In shortcut mode the user selects the burst roll containers manually and uses the shortcut
|
|
to extract the embedded images as DNGs
|
|
|
|
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
|
|
- exiv2 - https://exiv2.org
|
|
- exiftool - https://exiftool.org
|
|
- dnglab - https://github.com/dnglab/dnglab
|
|
|
|
USAGE
|
|
- add the script to your lua scripts
|
|
- enable in script manager
|
|
- set the preferences in perferences->Lua options
|
|
|
|
LIMITATIONS
|
|
extract_burst_roll_images makes heavy use of external programs to identify burst roll
|
|
containters and to extract the images. Windows users may want to use it in shortcut mode
|
|
to lessen the number of windows popping up.
|
|
|
|
BUGS, COMMENTS, SUGGESTIONS
|
|
Bill Ferguson <wpferguson@gmail.com>
|
|
|
|
CHANGES
|
|
]]
|
|
|
|
local dt = require "darktable"
|
|
local du = require "lib/dtutils"
|
|
local df = require "lib/dtutils.file"
|
|
local ds = require "lib/dtutils.string"
|
|
local dtsys = require "lib/dtutils.system"
|
|
local log = require "lib/dtutils.log"
|
|
-- local debug = require "darktable.debug"
|
|
|
|
|
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
|
-- A P I C H E C K
|
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
du.check_min_api_version("9.6.0", MODULE) -- choose the minimum version that contains the features you need
|
|
|
|
|
|
-- - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
-- I 1 8 N
|
|
-- - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
local gettext = dt.gettext.gettext
|
|
|
|
local function _(msgid)
|
|
return gettext(msgid)
|
|
end
|
|
|
|
|
|
-- - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
-- S C R I P T M A N A G E R I N T E G R A T I O N
|
|
-- - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
local script_data = {}
|
|
|
|
script_data.destroy = nil -- function to destory the script
|
|
script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet
|
|
script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again
|
|
script_data.show = nil -- only required for libs since the destroy_method only hides them
|
|
|
|
script_data.metadata = {
|
|
name = _("extract burst roll images"), -- visible name of script
|
|
purpose = _("extract burst images from a burst roll file"), -- purpose of script
|
|
author = "Bill Ferguson <wpferguson@gmail.com>", -- your name and optionally e-mail address
|
|
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/extract_burst_roll_images/" -- URL to help/documentation
|
|
}
|
|
|
|
|
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
|
-- C O N S T A N T S
|
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
local MODULE <const> = "extract_burst_roll_images"
|
|
local DEFAULT_LOG_LEVEL <const> = log.info
|
|
local TMP_DIR <const> = dt.configuration.tmp_dir
|
|
|
|
-- path separator
|
|
local PS <const> = dt.configuration.running_os == "windows" and "\\" or "/"
|
|
|
|
-- command separator
|
|
local CS <const> = dt.configuration.running_os == "windows" and "&" or ";"
|
|
|
|
local EXIFTOOL <const> = df.check_if_bin_exists("exiftool")
|
|
local EXIV2 <const> = df.check_if_bin_exists("exiv2")
|
|
local DNGLAB <const> = df.check_if_bin_exists("dnglab")
|
|
local EXIV2TAG <const> = "Exif.Canon.RawBurstModeRoll"
|
|
|
|
local HAS_INTERNAL_METADATA <const> = dt.metadata.exists(EXIV2TAG)
|
|
|
|
local DNGLAB_ARGS <const> = " convert --image-index all --embed-raw false "
|
|
local EXIV2_ARGS <const> = " -K Exif.Canon.RawBurstModeRoll -pt "
|
|
local EXIFTOOL_ARGS <const> = " -T -RawBurstImageCount "
|
|
|
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
|
-- L O G L E V E L
|
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
log.log_level(DEFAULT_LOG_LEVEL)
|
|
|
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
|
-- N A M E S P A C E
|
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
local extract_burst_roll_images = {}
|
|
|
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
|
-- G L O B A L V A R I A B L E S
|
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
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
|
|
|
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
|
-- P R E F E R E N C E S
|
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
-- exiv2 or exiftool
|
|
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.
|
|
"tag_reader", -- name
|
|
"enum", -- type
|
|
_("preferred exif tag reader"), -- label
|
|
_("use exiftool or exiv2 to read exif tags"), -- tooltip
|
|
"exiftool", -- default
|
|
"exiftool", "exiv2") -- values
|
|
|
|
-- run on import
|
|
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.
|
|
"on_import", -- name
|
|
"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
|
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
local namespace = extract_burst_roll_images
|
|
local ebri = extract_burst_roll_images
|
|
|
|
local sleep = dt.control.sleep
|
|
|
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
|
-- F U N C T I O N S
|
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
-------------------
|
|
-- helper functions
|
|
-------------------
|
|
|
|
local function set_log_level(level)
|
|
if not level then
|
|
level = namespace.log_level
|
|
end
|
|
|
|
local old_log_level = log.log_level()
|
|
log.log_level(level)
|
|
|
|
return old_log_level
|
|
end
|
|
|
|
local function restore_log_level(level)
|
|
if not level then
|
|
level = namespace.log_level
|
|
end
|
|
|
|
log.log_level(level)
|
|
end
|
|
|
|
local function reset_log_level()
|
|
log.log_level(DEFAULT_LOG_LEVEL)
|
|
namespace.log_level = DEFAULT_LOG_LEVEL
|
|
end
|
|
|
|
local function pref_read(name, pref_type)
|
|
local old_log_level = set_log_level(ebri.log_level)
|
|
|
|
log.msg(log.debug, "name is " .. name .. " and type is " .. pref_type)
|
|
|
|
local val = dt.preferences.read(MODULE, name, pref_type)
|
|
|
|
log.msg(log.debug, "read value " .. tostring(val))
|
|
|
|
restore_log_level(old_log_level)
|
|
return val
|
|
end
|
|
|
|
local function pref_write(name, pref_type, value)
|
|
local old_log_level = set_log_level(ebri.log_level)
|
|
|
|
log.msg(log.debug, "writing value " .. tostring(value) .. " for name " .. name)
|
|
|
|
dt.preferences.write(MODULE, name, pref_type, value)
|
|
|
|
restore_log_level(old_log_level)
|
|
end
|
|
|
|
local function get_last_value(valuestr)
|
|
local old_log_level = set_log_level()
|
|
local parts = du.split(valuestr, " +")
|
|
|
|
if parts then
|
|
log.msg(log.debug, "last value of parts is " .. tostring(parts[#parts]))
|
|
return parts[#parts]
|
|
else
|
|
log.msg(log.debug, "last part not detected, returning nil")
|
|
return nil
|
|
end
|
|
restore_log_level(old_log_level)
|
|
end
|
|
|
|
local function is_burst_roll_image(image)
|
|
local old_log_level = set_log_level()
|
|
local result = false
|
|
|
|
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
|
|
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)
|
|
return result
|
|
end
|
|
|
|
local function group_extracted_dngs(image)
|
|
local old_log_level = set_log_level()
|
|
local count = 0
|
|
|
|
local cmd = "ls -1 " .. ds.sanitize(image.path) .. PS .. ds.sanitize(ds.get_basename(image.filename)) .. "*.dng"
|
|
|
|
if dt.configuration.running_os == "windows" then
|
|
cmd = "forfiles /P " .. ds.sanitize(image.path) .. " /M " .. ds.sanitize(ds.get_basename(image.filename)) .. "*.dng" .. " /C \"cmd /c echo @path\\@fname\""
|
|
end
|
|
|
|
local p = io.popen(cmd)
|
|
if p then
|
|
for line in p:lines() do
|
|
if line:len() > 5 then -- skip blank lines created by forfiles
|
|
local dng_image = dt.database.import(line)
|
|
dng_image:group_with(image)
|
|
count = count + 1
|
|
end
|
|
end
|
|
p:close()
|
|
log.msg(log.screen, string.format(_("extracted %d dng files from %s"), count, image.filename))
|
|
end
|
|
restore_log_level(old_log_level)
|
|
end
|
|
|
|
local function extract_burst(image)
|
|
local old_log_level = set_log_level()
|
|
|
|
local cmd = DNGLAB .. DNGLAB_ARGS .. ds.sanitize(image.path) .. PS .. ds.sanitize(image.filename) .. " " .. ds.sanitize(image.path)
|
|
|
|
local result = dtsys.external_command(cmd)
|
|
|
|
if result == 0 then
|
|
group_extracted_dngs(image)
|
|
end
|
|
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
|
|
extract_burst(image)
|
|
end
|
|
restore_log_level(old_log_level)
|
|
end
|
|
|
|
local function stop_job(job)
|
|
job.valid = false
|
|
end
|
|
|
|
local function process_images(images)
|
|
local old_log_level = set_log_level()
|
|
local job = dt.gui.create_job(_("scanning for burst roll images"), true, stop_job)
|
|
|
|
for i, image in ipairs(images) do
|
|
if ebri.job.valid then
|
|
extract_burst(image)
|
|
ebri.job.percent = i / #images
|
|
sleep(10)
|
|
end
|
|
end
|
|
|
|
stop_job(job)
|
|
|
|
restore_log_level(old_log_level)
|
|
end
|
|
|
|
local function process_import(images)
|
|
local old_log_level = set_log_level()
|
|
local burst_roll_images = {}
|
|
local job = dt.gui.create_job(_("scanning for burst roll images"), true, stop_job)
|
|
|
|
for i, image in ipairs(images) do
|
|
if job.valid then
|
|
if is_burst_roll_image(image) then
|
|
table.insert(burst_roll_images, image)
|
|
end
|
|
if i % 10 == 0 then
|
|
job.percent = i / #images
|
|
end
|
|
sleep(10)
|
|
end
|
|
end
|
|
|
|
stop_job(job)
|
|
|
|
if #burst_roll_images > 0 then
|
|
job = dt.gui.create_job(_("extracting burst roll images"), true, stop_job)
|
|
for j, image in ipairs(burst_roll_images) do
|
|
if job.valid then
|
|
extract_burst(image)
|
|
job.percent = j / #burst_roll_images
|
|
sleep(10)
|
|
end
|
|
end
|
|
stop_job(job)
|
|
end
|
|
|
|
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
|
|
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
|
|
|
|
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
|
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
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
|
|
|
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
|
-- E V E N T S
|
|
-- - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
dt.register_event(MODULE, "post-import-film",
|
|
function(event, film)
|
|
process_import(ebri.imported_images)
|
|
ebri.imported_images = {}
|
|
end
|
|
)
|
|
|
|
dt.register_event(MODULE, "post-import-image",
|
|
function(event, image)
|
|
if ebri.on_import then
|
|
table.insert(ebri.imported_images, image)
|
|
end
|
|
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)
|
|
if #dt.gui.action_images > 1 then
|
|
process_images(dt.gui.action_images)
|
|
else
|
|
process_image(dt.gui.action_images[1])
|
|
end
|
|
end, "extract roll burst images"
|
|
)
|
|
end
|
|
|
|
return script_data
|