Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
961220f222 |
@@ -79,7 +79,6 @@ passport_guide_germany|Yes|LMW|Add passport cropping guide for German passports
|
||||
[slideshowMusic](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/slideshowMusic)|No|L|Play music during a slideshow
|
||||
[transfer_hierarchy](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/transfer_hierarchy)|Yes|LMW|Image move/copy preserving directory hierarchy
|
||||
[video_ffmpeg](https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/video_ffmpeg)|No|LMW|Export video from darktable
|
||||
website_gallery_export|No|LMW|Export a website gallery for selected images
|
||||
|
||||
### Example Scripts
|
||||
|
||||
|
||||
@@ -288,13 +288,7 @@ local function GetArgsFromPreference(prog_table, prefix) --for each arg in a pro
|
||||
local temp = dt.preferences.read(mod, prefix..argument, arg_data.style)
|
||||
if arg_data.style == 'bool' and temp then
|
||||
prog_table.arg_string = prog_table.arg_string..arg_data.text..' '
|
||||
elseif arg_data.style == 'integer' then
|
||||
prog_table.arg_string = prog_table.arg_string..arg_data.text..temp..' '
|
||||
elseif arg_data.style == 'string' then
|
||||
if dt.configuration.running_os == "windows" then
|
||||
--escape % with %% as % means env var on windows and call will fail otherwise
|
||||
temp = string.gsub(temp, "%%", "%%%%")
|
||||
end
|
||||
elseif arg_data.style == 'integer' or arg_data.style == 'string' then
|
||||
prog_table.arg_string = prog_table.arg_string..arg_data.text..temp..' '
|
||||
elseif arg_data.style == 'float' then
|
||||
temp = string.sub(tostring(temp),1,3)
|
||||
@@ -351,7 +345,7 @@ local function UpdateENFargs(image_table, prefix) --updates the Enfuse arguments
|
||||
end
|
||||
ENF.arg_string = ENF.arg_string..' --depth='..GUI.Target.depth.value..' '
|
||||
if GUI.Target.format.value == 'tif' then ENF.arg_string = ENF.arg_string..'--compression='..GUI.Target.compression_level_tif.value..' '
|
||||
elseif GUI.Target.format.value == 'jpg' then ENF.arg_string = ENF.arg_string..'--compression='..tostring(math.floor(GUI.Target.compression_level_jpg.value))..' '
|
||||
elseif GUI.Target.format.value == 'jpg' then ENF.arg_string = ENF.arg_string..'--compression='..GUI.Target.compression_level_jpg.value..' '
|
||||
end
|
||||
if not GUI.Target.source_location.value then out_path = GUI.Target.output_directory.value end
|
||||
out_name = smallest_name..'-'..largest_id
|
||||
@@ -498,7 +492,6 @@ local function main(storage, image_table, extra_data)
|
||||
remove_temp_files(images_to_remove)
|
||||
dt.print_error(ENF.name..' failed')
|
||||
dt.print(string.format(_("%s failed"), ENF.name))
|
||||
job.valid = false
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
@@ -138,9 +138,8 @@ script_data.show = nil -- only required for libs since the destroy_method only h
|
||||
|
||||
|
||||
|
||||
local PS <const> = dt.configuration.runnin_os == "windows" and "\\" or "/"
|
||||
local ERROR <const> = -1
|
||||
local HAS_INTERNAL_METADATA = dt.metadata.exists("Exif.Image.DataTime")
|
||||
local PS = dt.configuration.runnin_os == "windows" and "\\" or "/"
|
||||
local ERROR = -1
|
||||
|
||||
-- function to convert from exif time to system time
|
||||
local function exiftime2systime(exiftime)
|
||||
@@ -212,6 +211,7 @@ local function year_month2months(year, month)
|
||||
end
|
||||
|
||||
local function months2year_month(months)
|
||||
dt.print_log("months is " .. months)
|
||||
local year = math.floor(months / 12)
|
||||
local month = months - (year * 12)
|
||||
|
||||
@@ -224,7 +224,7 @@ local function get_image_taken_time(image)
|
||||
|
||||
local exiv2 = df.check_if_bin_exists("exiv2")
|
||||
if exiv2 then
|
||||
p = io.popen(exiv2 .. " -K Exif.Image.DateTime " .. df.sanitize_filename(image.path .. PS .. image.filename))
|
||||
p = io.popen(exiv2 .. " -K Exif.Image.DateTime " .. image.path .. PS .. image.filename)
|
||||
if p then
|
||||
for line in p:lines() do
|
||||
if string.match(line, "Exif.Image.DateTime") then
|
||||
@@ -242,16 +242,15 @@ end
|
||||
|
||||
local function _get_windows_image_file_creation_time(image)
|
||||
local datetime = nil
|
||||
local datestring = nil
|
||||
|
||||
if HAS_INTERNAL_METADATA then
|
||||
datestring = image.DateString
|
||||
else
|
||||
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
|
||||
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()
|
||||
@@ -259,20 +258,12 @@ local function _get_windows_image_file_creation_time(image)
|
||||
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
|
||||
|
||||
local function _get_nix_image_file_creation_time(image)
|
||||
local datetime = nil
|
||||
local p = io.popen("ls -lL --time-style=full-iso " .. df.sanitize_filename(image.path .. PS .. image.filename))
|
||||
local p = io.popen("ls -lL --time-style=full-iso " .. image.path .. PS .. image.filename)
|
||||
if p then
|
||||
for line in p:lines() do
|
||||
if string.match(line, ds.sanitize_lua(image.filename)) then
|
||||
|
||||
@@ -264,6 +264,10 @@ function PHOTILS.get_tags(image, with_export)
|
||||
local out_arg = df.sanitize_filename(tmp_file)
|
||||
local executable = photils_installed
|
||||
|
||||
if dt.configuration.running_os == "macos" then
|
||||
executable = executable .. "/Contents/MacOS/photils-cli"
|
||||
end
|
||||
|
||||
if with_export then
|
||||
dt.print_log("use export to for prediction")
|
||||
local export_file = df.create_tmp_file()
|
||||
|
||||
@@ -185,6 +185,9 @@ local function do_rename(images)
|
||||
end
|
||||
end
|
||||
|
||||
local function reset_callback()
|
||||
rename.widgets.pattern.text = ""
|
||||
end
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- W I D G E T S
|
||||
@@ -193,11 +196,7 @@ end
|
||||
rename.widgets.pattern = dt.new_widget("entry"){
|
||||
tooltip = ds.get_substitution_tooltip(),
|
||||
placeholder = _("enter pattern") .. "$(FILE_FOLDER)/$(FILE_NAME)",
|
||||
text = "",
|
||||
reset_callback = function(self)
|
||||
self.text = ""
|
||||
dt.preferences.write(MODULE_NAME, "pattern", "string", self.text)
|
||||
end
|
||||
text = ""
|
||||
}
|
||||
|
||||
local pattern_pref = dt.preferences.read(MODULE_NAME, "pattern", "string")
|
||||
|
||||
@@ -20,19 +20,14 @@ Enable selection of non-existing images in the the currently worked on images, e
|
||||
local dt = require "darktable"
|
||||
local du = require "lib/dtutils"
|
||||
local df = require "lib/dtutils.file"
|
||||
local ds = require "lib/dtutils.string"
|
||||
local log = require "lib/dtutils.log"
|
||||
|
||||
-- module name
|
||||
local MODULE <const> = "select_non_existing"
|
||||
local MODULE = "select_non_existing"
|
||||
|
||||
du.check_min_api_version("9.1.0", MODULE)
|
||||
|
||||
-- figure out the path separator
|
||||
local PS <const> = dt.configuration.running_os == "windows" and "\\" or "/"
|
||||
local DEFAULT_LOG_LEVEL <const> = log.info
|
||||
|
||||
log.log_level(DEFAULT_LOG_LEVEL)
|
||||
local PS = dt.configuration.running_os == "windows" and "\\" or "/"
|
||||
|
||||
local gettext = dt.gettext.gettext
|
||||
|
||||
@@ -44,51 +39,16 @@ local function stop_job(job)
|
||||
job.valid = false
|
||||
end
|
||||
|
||||
local function get_file_list(dir)
|
||||
log.log_level(DEFAULT_LOG_LEVEL)
|
||||
local file_list = {}
|
||||
local cmd = nil
|
||||
log.msg(log.debug, "called get_file_list with " .. dir)
|
||||
|
||||
if dt.configuration.running_os == "windows" then
|
||||
cmd = "forfiles /P " .. ds.sanitize(dir) .. " /M * /C \"cmd /c echo @file\""
|
||||
else
|
||||
cmd = "cd " .. ds.sanitize(dir) ..";ls -1"
|
||||
end
|
||||
log.msg(log.info, "cmd is " .. cmd)
|
||||
|
||||
local p = io.popen(cmd)
|
||||
if p then
|
||||
for line in p:lines() do
|
||||
if line:len() > 4 then
|
||||
if not string.match(line, "xmp$") and not string.match(line, "XMP$") then
|
||||
line = line:gsub("\"", "")
|
||||
file_list[line] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
p:close()
|
||||
end
|
||||
|
||||
return file_list
|
||||
end
|
||||
|
||||
local function select_nonexisting_images(event, images)
|
||||
log.log_level(DEFAULT_LOG_LEVEL)
|
||||
local selection = {}
|
||||
local known_files = {}
|
||||
|
||||
local job = dt.gui.create_job(_("select non existing images"), true, stop_job)
|
||||
for key,image in ipairs(images) do
|
||||
if(job.valid) then
|
||||
if not known_files[image.path] then
|
||||
known_files[image.path] = get_file_list(image.path)
|
||||
end
|
||||
if key % 10 == 0 then
|
||||
job.percent = key/#images
|
||||
end
|
||||
local file_exists = known_files[image.path][image.filename]
|
||||
log.msg(log.debug, image.path .. PS .. image.filename .." exists? => "..tostring(file_exists))
|
||||
job.percent = (key - 1)/#images
|
||||
local filepath = image.path..PS..image.filename
|
||||
local file_exists = df.test_file(filepath, "e")
|
||||
dt.print_log(filepath.." exists? => "..tostring(file_exists))
|
||||
if (not file_exists) then
|
||||
table.insert(selection, image)
|
||||
end
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
--[[
|
||||
|
||||
selected_image_visible.lua - make the selected image visible in lighttable
|
||||
|
||||
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/>.
|
||||
]]
|
||||
--[[
|
||||
selected_image_visible - make the selected image visible in lighttable
|
||||
|
||||
selected_image_visible adds a bshortcut to make the currently
|
||||
selected image or the first image of a selection visible in lighttable
|
||||
|
||||
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
|
||||
None
|
||||
|
||||
USAGE
|
||||
* enable the script in script_manager
|
||||
|
||||
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 = _("selected image visible"), -- visible name of script
|
||||
purpose = _("make the selected image visible in lighttable"), -- 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/contrib/selected_image_visible/" -- URL to help/documentation
|
||||
}
|
||||
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- C O N S T A N T S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local MODULE <const> = "selected_image_visible"
|
||||
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 ";"
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- L O G L E V E L
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
log.log_level(DEFAULT_LOG_LEVEL)
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- N A M E S P A C E
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local selected_image_visible = {}
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- G L O B A L V A R I A B L E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
selected_image_visible.log_level = DEFAULT_LOG_LEVEL
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- P R E F E R E N C E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- A L I A S E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local namespace = selected_image_visible
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- F U N C T I O N S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local function set_image_visible()
|
||||
if dt.gui.current_view().name == "lighttable" and #dt.gui.action_images > 0 then
|
||||
dt.gui.views.lighttable.set_image_visible(dt.gui.action_images[1])
|
||||
end
|
||||
end
|
||||
|
||||
local function set_button_sensitive()
|
||||
if dt.gui.current_view().name == "lighttable"
|
||||
and #dt.gui.selection() > 0
|
||||
and not dt.gui.views.lighttable.is_image_visible(dt.gui.selection()[1]) then
|
||||
dt.gui.libs.image.set_sensitive(MODULE, true)
|
||||
else
|
||||
dt.gui.libs.image.set_sensitive(MODULE, false)
|
||||
end
|
||||
end
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- M A I N P R O G R A M
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- U S E R I N T E R F A C E
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
dt.gui.libs.image.register_action(
|
||||
MODULE, _("selected image visible"),
|
||||
function(event, images) set_image_visible() end,
|
||||
_("make selected image visible in lighttable")
|
||||
)
|
||||
|
||||
set_button_sensitive()
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- D A R K T A B L E I N T E G R A T I O N
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local function destroy()
|
||||
-- put things to destroy (events, storages, etc) here
|
||||
dt.destroy_event(MODULE, "selection-changed")
|
||||
dt.destroy_event(MODULE, "mouse-over-image-changed")
|
||||
dt.gui.libs.image.destroy_action(MODULE)
|
||||
end
|
||||
|
||||
script_data.destroy = destroy
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- E V E N T S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
if not dt.query_event(MODULE, "shortcut") then
|
||||
dt.register_event(MODULE, "shortcut",
|
||||
function(event, shortcut)
|
||||
set_image_visible()
|
||||
end, _("make selected image visible in lighttable")
|
||||
)
|
||||
end
|
||||
|
||||
dt.register_event(MODULE, "selection-changed",
|
||||
function(event)
|
||||
set_button_sensitive()
|
||||
end
|
||||
)
|
||||
|
||||
dt.register_event(MODULE, "mouse-over-image-changed",
|
||||
function(event, image)
|
||||
set_button_sensitive()
|
||||
end
|
||||
)
|
||||
|
||||
return script_data
|
||||
@@ -1,313 +0,0 @@
|
||||
--[[
|
||||
|
||||
toggle_group_view.lua - toggle only group visible in lighttable
|
||||
|
||||
Copyright (C) 2024 Bill Ferguson <wpferguson.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/>.
|
||||
]]
|
||||
--[[
|
||||
toggle_group_view - toggle only group visible in lighttable
|
||||
|
||||
toggle_group_view provides a way to "separate" a group from
|
||||
the lighttable display and limit the display to just the group.
|
||||
Pressing the shortcut again returns to the full lighttable view.
|
||||
|
||||
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
|
||||
None
|
||||
|
||||
USAGE
|
||||
* place the script in your lua scripts folder
|
||||
* start the script with script manager
|
||||
* assign a key sequence to the shortcut
|
||||
|
||||
BUGS, COMMENTS, SUGGESTIONS
|
||||
Bill Ferguson <wpferguson.com>
|
||||
|
||||
CHANGES
|
||||
]]
|
||||
|
||||
local dt = require "darktable"
|
||||
local du = require "lib/dtutils"
|
||||
local log = require "lib/dtutils.log"
|
||||
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- C O N S T A N T S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local MODULE <const> = "toggle_group_view"
|
||||
local DEFAULT_LOG_LEVEL <const> = log.error
|
||||
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 ";"
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- A P I C H E C K
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
du.check_min_api_version("7.0.0", MODULE) -- choose the minimum version that contains the features you need
|
||||
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- I 1 8 N
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local gettext = dt.gettext.gettext
|
||||
|
||||
dt.gettext.bindtextdomain(MODULE , dt.configuration.config_dir .. "/lua/locale/")
|
||||
|
||||
local function _(msgid)
|
||||
return gettext(MODULE, 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 = "toggle_group_view", -- name of script
|
||||
purpose = _("toggle only group visible in lighttable"), -- purpose of script
|
||||
author = "Bill Ferguson <wpferguson.com>", -- your name and optionally e-mail address
|
||||
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/toggle_group_view/" -- URL to help/documentation
|
||||
}
|
||||
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- L O G L E V E L
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
log.log_level(DEFAULT_LOG_LEVEL)
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- N A M E S P A C E
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local toggle_group_view = {}
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- G L O B A L V A R I A B L E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local group_active = false
|
||||
local filmroll = ""
|
||||
local collection_rules = {}
|
||||
local hover_active = true
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- F U N C T I O N S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local function is_group_member(image)
|
||||
if image then
|
||||
if #image:get_group_members() > 1 then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function is_group_displayed()
|
||||
local rules = dt.gui.libs.collect.filter()
|
||||
if rules[1].item == "DT_COLLECTION_PROP_GROUP_ID" then
|
||||
local rule = dt.gui.libs.collect.new_rule()
|
||||
|
||||
rule.mode = "DT_LIB_COLLECT_MODE_AND"
|
||||
rule.data = dt.collection[1].path
|
||||
rule.item = "DT_COLLECTION_PROP_FILMROLL"
|
||||
|
||||
table.insert(collection_rules, rule)
|
||||
group_active = true
|
||||
end
|
||||
end
|
||||
|
||||
local function set_button_sensitive(image)
|
||||
local selected_images = dt.gui.selection()
|
||||
local sensitive
|
||||
|
||||
|
||||
if group_active then
|
||||
dt.gui.libs.image.set_sensitive(MODULE, true)
|
||||
return
|
||||
end
|
||||
|
||||
if #selected_images > 0 then
|
||||
if is_group_member(selected_images[1]) then
|
||||
sensitive = true
|
||||
end
|
||||
else
|
||||
if image then
|
||||
if is_group_member(image) then
|
||||
sensitive = true
|
||||
else
|
||||
sensitive = false
|
||||
end
|
||||
else
|
||||
sensitive = false
|
||||
end
|
||||
end
|
||||
|
||||
dt.gui.libs.image.set_sensitive(MODULE, sensitive)
|
||||
end
|
||||
|
||||
local function add_collection_rule(rule_string)
|
||||
|
||||
local rule = dt.gui.libs.collect.new_rule()
|
||||
|
||||
rule.mode = "DT_LIB_COLLECT_MODE_AND"
|
||||
rule.data = rule_string
|
||||
rule.item = "DT_COLLECTION_PROP_FILENAME"
|
||||
|
||||
table.insert(collection_rules, rule)
|
||||
end
|
||||
|
||||
local function show_group(image)
|
||||
|
||||
local collection_rule_string = ""
|
||||
|
||||
local first = true
|
||||
|
||||
collection_rules = {}
|
||||
|
||||
local group_id = image.group_leader
|
||||
|
||||
local rule = dt.gui.libs.collect.new_rule()
|
||||
|
||||
rule.mode = "DT_LIB_COLLECT_MODE_AND"
|
||||
rule.data = tostring(group_id.id)
|
||||
rule.item = "DT_COLLECTION_PROP_GROUP_ID"
|
||||
|
||||
table.insert(collection_rules, rule)
|
||||
|
||||
-- dt.gui.libs.collect.filter returns the previous rule set when you install another
|
||||
collection_rules = dt.gui.libs.collect.filter(collection_rules)
|
||||
end
|
||||
|
||||
local function reset(image)
|
||||
dt.gui.libs.collect.filter(collection_rules)
|
||||
if image then
|
||||
dt.gui.views.lighttable.set_image_visible(image)
|
||||
else
|
||||
log.msg(log.screen, _("no image supplied"))
|
||||
end
|
||||
set_button_sensitive(image)
|
||||
end
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- M A I N
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
-- check if we are starting with a group already displayed and
|
||||
-- configure appropriately
|
||||
|
||||
if not dt.preferences.read("darktable", "plugins/lighttable/act_on", "bool") then
|
||||
hover_active = false
|
||||
end
|
||||
|
||||
is_group_displayed()
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- U S E R I N T E R F A C E
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
dt.gui.libs.image.register_action(
|
||||
MODULE, "toggle group view",
|
||||
function(event, images)
|
||||
if group_active then
|
||||
local image
|
||||
if dt.gui.action_images[1] then
|
||||
image = dt.gui.action_images[1]
|
||||
reset(dt.gui.action_images[1])
|
||||
else
|
||||
image = dt.collection[1]
|
||||
reset(dt.collection[1])
|
||||
end
|
||||
group_active = false
|
||||
set_button_sensitive(image)
|
||||
else
|
||||
show_group(dt.gui.action_images[1])
|
||||
group_active = true
|
||||
end
|
||||
end,
|
||||
_("toggle group view for selected image")
|
||||
)
|
||||
|
||||
set_button_sensitive(nil)
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- D A R K T A B L E I N T E G R A T I O N
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local function destroy()
|
||||
if hover_active then
|
||||
dt.destroy_event(MODULE, "mouse-over-image-changed")
|
||||
end
|
||||
dt.destroy_event(MODULE, "selection_changed")
|
||||
dt.gui.libs.image.destroy_action(MODULE, "toggle group view")
|
||||
-- leave the shortcut so it doesn't need to be reassigned
|
||||
-- the next time this module starts
|
||||
end
|
||||
|
||||
script_data.destroy = destroy
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- E V E N T S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
dt.register_event(
|
||||
MODULE, "shortcut",
|
||||
function(event, shortcut)
|
||||
if #dt.gui.action_images > 0 then
|
||||
if group_active then
|
||||
reset(dt.gui.action_images[1])
|
||||
group_active = false
|
||||
else
|
||||
show_group(dt.gui.action_images[1])
|
||||
group_active = true
|
||||
end
|
||||
end
|
||||
end,
|
||||
_("toggle group view for selected image")
|
||||
)
|
||||
|
||||
---[[
|
||||
if hover_active then
|
||||
dt.register_event(MODULE, "mouse-over-image-changed",
|
||||
function(event, image)
|
||||
set_button_sensitive(image)
|
||||
end
|
||||
)
|
||||
end
|
||||
--]]
|
||||
|
||||
dt.register_event(MODULE, "selection-changed",
|
||||
function(event)
|
||||
set_button_sensitive(dt.gui.action_images[1])
|
||||
end
|
||||
)
|
||||
|
||||
return script_data
|
||||
@@ -270,7 +270,7 @@ local function doTransfer(transferFunc)
|
||||
local sourceBase = sourceTextBox.text
|
||||
local destBase = destinationTextBox.text
|
||||
local destFilms = {}
|
||||
for film, i in pairs(films) do
|
||||
for film, _ in pairs(films) do
|
||||
films[film] = destBase .. string.sub(film.path, #sourceBase+1)
|
||||
if not pathExists(films[film]) then
|
||||
if createDirectory(films[film]) == nil then
|
||||
|
||||
@@ -1,294 +0,0 @@
|
||||
--[[Export module to create a web gallery from selected images
|
||||
|
||||
copyright (c) 2025 Tino Mettler
|
||||
|
||||
darktable 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.
|
||||
|
||||
darktable 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 software. If not, see <http://www.gnu.org/licenses/>.
|
||||
]]
|
||||
|
||||
--[[
|
||||
TODO:
|
||||
- Lua: remove images dir if already existent
|
||||
- Lua: translations
|
||||
]]
|
||||
|
||||
local dt = require "darktable"
|
||||
local df = require "lib/dtutils.file"
|
||||
|
||||
local PS <const> = dt.configuration.running_os == "windows" and "\\" or "/"
|
||||
|
||||
local temp = dt.preferences.read('web_gallery', 'title', 'string')
|
||||
if temp == nil then temp = 'Darktable gallery' end
|
||||
|
||||
local function _(msgid)
|
||||
return dt.gettext.gettext(msgid)
|
||||
end
|
||||
|
||||
local title_widget = dt.new_widget("entry")
|
||||
{
|
||||
text = temp
|
||||
}
|
||||
|
||||
local temp = dt.preferences.read('web_gallery', 'destination_dir', 'string')
|
||||
if temp == nil then temp = '' end
|
||||
|
||||
local dest_dir_widget = dt.new_widget("file_chooser_button")
|
||||
{
|
||||
title = _("select output folder"),
|
||||
tooltip = _("select output folder"),
|
||||
value = temp,
|
||||
is_directory = true,
|
||||
changed_callback = function(this) dt.preferences.write('web_gallery', 'destination_dir', 'string', this.value) end
|
||||
}
|
||||
|
||||
local gallery_widget = dt.new_widget("box")
|
||||
{
|
||||
orientation=vertical,
|
||||
dt.new_widget("label"){label = _("gallery title")},
|
||||
title_widget,
|
||||
dt.new_widget("label"){label = _("destination directory")},
|
||||
dest_dir_widget
|
||||
}
|
||||
|
||||
local function get_file_name(file)
|
||||
return file:match("[^" .. PS .. "]*.$")
|
||||
end
|
||||
|
||||
function escape_js_string(str)
|
||||
local replacements = {
|
||||
['\\'] = '\\\\',
|
||||
['"'] = '\\"',
|
||||
["'"] = "\\'",
|
||||
['\n'] = '\\n',
|
||||
['\r'] = '\\r',
|
||||
['\t'] = '\\t',
|
||||
['\b'] = '\\b',
|
||||
['\f'] = '\\f',
|
||||
['\v'] = '\\v'
|
||||
}
|
||||
return (str:gsub('[\\\"\n\r\t\b\f\v\']', replacements))
|
||||
end
|
||||
|
||||
local function export_thumbnail(image, filename)
|
||||
exporter = dt.new_format("jpeg")
|
||||
exporter.quality = 90
|
||||
exporter.max_height = 512
|
||||
exporter.max_width = 512
|
||||
exporter:write_image(image, filename, true)
|
||||
end
|
||||
|
||||
local function write_image(image, dest_dir, filename)
|
||||
df.file_move(filename, dest_dir.. PS .. "images" .. PS .. get_file_name(filename))
|
||||
export_thumbnail(image, dest_dir .. PS .. "thumbnails" .. PS .. "thumb_" .. get_file_name(filename))
|
||||
end
|
||||
|
||||
function exiftool_get_image_dimensions(filename)
|
||||
local handle = io.popen("exiftool " .. filename)
|
||||
local result = handle:read("*a")
|
||||
handle:close()
|
||||
for line in result:gmatch("[^\r\n]+") do
|
||||
local w = line:match("^Image Width%s*:%s*(%d+)")
|
||||
if w then
|
||||
width = tonumber(w)
|
||||
end
|
||||
local h = line:match("^Image Height%s*:%s*(%d+)")
|
||||
if h then
|
||||
height = tonumber(h)
|
||||
end
|
||||
end
|
||||
if width and height then
|
||||
return width, height
|
||||
else
|
||||
return nil, nil
|
||||
end
|
||||
end
|
||||
|
||||
local function stop_job(job)
|
||||
job.valid = false
|
||||
end
|
||||
|
||||
local function fill_gallery_table(images_ordered, images_table, title, dest_dir, sizes, exiftool)
|
||||
local gallery_data = { name = escape_js_string(title) }
|
||||
|
||||
local images = {}
|
||||
local index = 1
|
||||
local job = dt.gui.create_job(_("exporting thumbnail images"), true, stop_job)
|
||||
|
||||
for i, image in pairs(images_ordered) do
|
||||
local filename = df.sanitize_filename(images_table[image])
|
||||
dt.print(_("export thumbnail image ") .. index .. "/" .. #images_ordered)
|
||||
write_image(image, dest_dir, filename)
|
||||
|
||||
if exiftool then
|
||||
width, height = exiftool_get_image_dimensions(df.sanitize_filename(dest_dir .. PS .. "images" .. PS .. get_file_name(filename)))
|
||||
else
|
||||
width = sizes[index].width
|
||||
height = sizes[index].height
|
||||
end
|
||||
|
||||
local entry = { filename = "images" .. PS .. get_file_name(filename),
|
||||
width = width, height = height }
|
||||
|
||||
images[index] = entry
|
||||
job.percent = index / #images_ordered
|
||||
index = index + 1
|
||||
end
|
||||
|
||||
stop_job(job)
|
||||
gallery_data.images = images
|
||||
return gallery_data
|
||||
end
|
||||
|
||||
local function generate_javascript_gallery_object(gallery)
|
||||
local js = 'const gallery_data = {\n'
|
||||
js = js .. ' name: "' .. gallery.name .. '",\n'
|
||||
js = js .. ' images: [\n'
|
||||
|
||||
for i, img in ipairs(gallery.images) do
|
||||
js = js .. string.format(' { filename: "%s",\n height: %d,\n width: %d }', img.filename, img.height, img.width)
|
||||
if i < #gallery.images then
|
||||
js = js .. ',\n'
|
||||
else
|
||||
js = js .. '\n'
|
||||
end
|
||||
end
|
||||
|
||||
js = js .. ' ]\n};\n'
|
||||
|
||||
return(js)
|
||||
end
|
||||
|
||||
local function write_javascript_file(gallery_table, dest_dir)
|
||||
dt.print(_("write JavaScript file"))
|
||||
javascript_object = generate_javascript_gallery_object(gallery_table)
|
||||
|
||||
local fileOut, errr = io.open(dest_dir .. PS .. "js" .. PS .. "images.js", 'w+')
|
||||
if fileOut then
|
||||
fileOut:write(javascript_object)
|
||||
else
|
||||
log.msg(log.error, errr)
|
||||
end
|
||||
fileOut:close()
|
||||
end
|
||||
|
||||
local function copy_static_files(dest_dir)
|
||||
|
||||
gfsrc = dt.configuration.config_dir .. PS .. "lua" .. PS .. "data" .. PS .. "website_gallery"
|
||||
local gfiles = {
|
||||
"index.html",
|
||||
"css" .. PS .. "gallery.css",
|
||||
"css" .. PS .. "modal.css",
|
||||
"css" .. PS .. "style.css",
|
||||
"js" .. PS .. "gallery.js",
|
||||
"js" .. PS .. "modal.js",
|
||||
"js" .. PS .. "fullscreen.js"
|
||||
}
|
||||
|
||||
dt.print(_("copy static gallery files"))
|
||||
for _, file in ipairs(gfiles) do
|
||||
df.file_copy(gfsrc .. PS .. file, dest_dir .. PS .. file)
|
||||
end
|
||||
end
|
||||
|
||||
local function build_gallery(storage, images_table, extra_data)
|
||||
local dest_dir = dest_dir_widget.value
|
||||
df.mkdir(df.sanitize_filename(dest_dir))
|
||||
df.mkdir(df.sanitize_filename(dest_dir .. PS .. "images"))
|
||||
df.mkdir(df.sanitize_filename(dest_dir .. PS .. "thumbnails"))
|
||||
df.mkdir(df.sanitize_filename(dest_dir .. PS .. "css"))
|
||||
df.mkdir(df.sanitize_filename(dest_dir .. PS .. "js"))
|
||||
|
||||
local images_ordered = extra_data["images"] -- process images in the correct order
|
||||
local sizes = extra_data["sizes"]
|
||||
local title = _("Darktable export")
|
||||
if title_widget.text ~= "" then
|
||||
title = title_widget.text
|
||||
end
|
||||
local exiftool = df.check_if_bin_exists("exiftool");
|
||||
gallerydata = fill_gallery_table(images_ordered, images_table, title, dest_dir, sizes, exiftool)
|
||||
write_javascript_file(gallerydata, dest_dir)
|
||||
copy_static_files(dest_dir)
|
||||
end
|
||||
|
||||
local script_data = {}
|
||||
|
||||
script_data.metadata = {
|
||||
name = _("website gallery (new)"),
|
||||
purpose = _("create a web gallery from exported images"),
|
||||
author = "Tino Mettler <tino+darktable@tikei.de>",
|
||||
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/contrib/TODO"
|
||||
}
|
||||
|
||||
script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet, otherwise leave as nil
|
||||
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
|
||||
|
||||
local function destroy()
|
||||
dt.preferences.write('web_gallery', 'title', 'string', title_widget.text)
|
||||
dt.destroy_storage("module_webgallery")
|
||||
end
|
||||
script_data.destroy = destroy
|
||||
|
||||
local function show_status(storage, image, format, filename,
|
||||
number, total, high_quality, extra_data)
|
||||
dt.print(string.format(_("export image ").."%i/%i", number, total))
|
||||
aspect = image.aspect_ratio
|
||||
-- calculate the size of the exported image and store it in extra_data
|
||||
-- to make it available in the finalize function
|
||||
if image.final_height == 0 then
|
||||
if aspect < 1 then
|
||||
dimensions = { width = image.height, height = image.width }
|
||||
else
|
||||
dimensions = { width = image.width, height = image.height }
|
||||
end
|
||||
else
|
||||
dimensions = { width = image.final_width, height = image.final_height }
|
||||
end
|
||||
if format.max_height > 0 and dimensions.height > format.max_height then
|
||||
scale = format.max_height / dimensions.height
|
||||
dimensions.height = math.floor(dimensions.height * scale + 0.5)
|
||||
dimensions.width = math.floor(dimensions.width * scale + 0.5)
|
||||
end
|
||||
if format.max_width > 0 and dimensions.width > format.max_width then
|
||||
scale = format.max_width / dimensions.width
|
||||
dimensions.height = math.floor(dimensions.height * scale + 0.5)
|
||||
dimensions.width = math.floor(dimensions.width * scale + 0.5)
|
||||
end
|
||||
extra_data["sizes"][number] = dimensions
|
||||
end
|
||||
|
||||
local function initialize(storage, img_format, images, high_quality, extra_data)
|
||||
dt.preferences.write('web_gallery', 'title', 'string', title_widget.text)
|
||||
extra_data["images"] = images -- needed, to preserve images order
|
||||
extra_data["sizes"] = {};
|
||||
end
|
||||
|
||||
local supported_formats = { "jpg", "tif", "png", "webp" }
|
||||
|
||||
local formats_lut = {}
|
||||
for key,format in pairs(supported_formats) do
|
||||
formats_lut[format] = true
|
||||
end
|
||||
|
||||
function check_supported(storage, format)
|
||||
extension = format.extension
|
||||
if formats_lut[extension] == true then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
dt.register_storage("module_webgallery", "website gallery (new)", show_status, build_gallery, check_supported, initialize, gallery_widget)
|
||||
|
||||
return script_data
|
||||
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
copyright (c) 2025 Tino Mettler
|
||||
|
||||
darktable 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.
|
||||
|
||||
darktable 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 software. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
body {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.heading h1 {
|
||||
text-align: center;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.gallery {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gallery img {
|
||||
cursor: pointer;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.thumb {
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.thumb-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
object-fit: contain;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
/*
|
||||
copyright (c) 2025 Tino Mettler
|
||||
|
||||
darktable 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.
|
||||
|
||||
darktable 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 software. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.viewer {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
touch-action: none;
|
||||
overflow: hidden;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navgrid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.nav {
|
||||
height: var(--navigation-bar-height);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.slider {
|
||||
height: calc(100vh - var(--navigation-bar-height));
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
touch-action: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.slide-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 300%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
transform: translateX(-33.333%);
|
||||
transition: transform 0.3s ease-out;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.slide {
|
||||
width: 33.333%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #000;
|
||||
will-change: contents;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
touch-action: none;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.slide img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
display: block;
|
||||
transform-origin: 0 0;
|
||||
cursor: zoom-in;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.slide.zoomed img {
|
||||
cursor: zoom-out;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.slide img.zooming {
|
||||
transition: transform 0.2s ease-out;
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
body {
|
||||
font-family: system-ui, Arial, Helvetica, sans-serif;
|
||||
background-color: rgb(238, 240, 242);
|
||||
}
|
||||
|
||||
.heading h1 {
|
||||
background-color: #cdd;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.heading h1:after {
|
||||
content: " ";
|
||||
border-bottom: 1px solid #888888;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.gallery {
|
||||
gap: 3vw;
|
||||
margin: 3vw auto;
|
||||
padding: 1.5vw;
|
||||
}
|
||||
|
||||
.gallery img {
|
||||
background: none;
|
||||
border: 1px outset #737780;
|
||||
|
||||
}
|
||||
|
||||
.gallery img:hover {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
.thumb-box {
|
||||
background: none;
|
||||
box-shadow: 0 1vw 4vw 1vw rgba(0,0,0,0.6);
|
||||
}
|
||||
|
||||
.viewer {
|
||||
--navigation-bar-height: 5vh;
|
||||
}
|
||||
|
||||
.nav {
|
||||
background-color: rgba(60,60,60, 0.4);
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.navgrid {
|
||||
grid-template-columns: 4em 1fr 1.5em 1.5em 1fr 1.5em 1.5em;
|
||||
font-size: 3.5vh;
|
||||
grid-column-gap: 1vw;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
text-shadow: 0px 0px 10px #ccc;
|
||||
}
|
||||
|
||||
.counter {
|
||||
font-size: 2.5vh;
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="css/modal.css">
|
||||
<link rel="stylesheet" href="css/gallery.css">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<title id="page-title">Image Gallery</title>
|
||||
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="heading1" class="heading">
|
||||
<h1 id="gallery-title">Image Gallery</h1>
|
||||
</div>
|
||||
<div id="gallery" class="gallery"></div>
|
||||
<div class="viewer" id="viewer">
|
||||
<div class="navgrid">
|
||||
<div id="counter" class="nav"></div>
|
||||
<div class="spacer"></div>
|
||||
<div class="button nav nav-arrow" id="prevArrow">◀</div>
|
||||
<div class="button nav nav-arrow" id="nextArrow">▶</div>
|
||||
<div class="spacer"></div>
|
||||
<div id="fullscreen" class="button nav">⛶</div>
|
||||
<div id="close" class="nav button">×</div>
|
||||
</div>
|
||||
<div class="slider" id="slider1">
|
||||
<div class="slide-container">
|
||||
<div class="slide" id="prev"></div>
|
||||
<div class="slide" id="current"></div>
|
||||
<div class="slide" id="next"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type='text/javascript' >
|
||||
var currentIndex = 1;
|
||||
</script>
|
||||
<script src="js/images.js"></script>
|
||||
<script src="js/fullscreen.js"></script>
|
||||
<script src="js/modal.js"></script>
|
||||
<script src="js/gallery.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,40 +0,0 @@
|
||||
var isFullscreen = false;
|
||||
|
||||
var toggleFullscreen = function (ele) {
|
||||
return isFullscreen ? exitFullscreen(ele) : requestFullscreen(ele);
|
||||
};
|
||||
|
||||
var requestFullscreen = function (ele) {
|
||||
if(isFullscreen == true) return 0 ;
|
||||
|
||||
isFullscreen = true;
|
||||
if (ele.requestFullscreen) {
|
||||
ele.requestFullscreen();
|
||||
} else if (ele.webkitRequestFullscreen) {
|
||||
ele.webkitRequestFullscreen();
|
||||
} else if (ele.mozRequestFullScreen) {
|
||||
ele.mozRequestFullScreen();
|
||||
} else if (ele.msRequestFullscreen) {
|
||||
ele.msRequestFullscreen();
|
||||
} else {
|
||||
console.log('Fullscreen API is not supported.');
|
||||
}
|
||||
};
|
||||
|
||||
var exitFullscreen = function () {
|
||||
if(isFullscreen == false) return 0;
|
||||
|
||||
isFullscreen = false;
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
} else if (document.webkitExitFullscreen) {
|
||||
document.webkitExitFullscreen();
|
||||
} else if (document.mozCancelFullScreen) {
|
||||
document.mozCancelFullScreen();
|
||||
} else if (document.msExitFullscreen) {
|
||||
document.msExitFullscreen();
|
||||
} else {
|
||||
console.log('Fullscreen API is not supported.');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
/*
|
||||
copyright (c) 2025 Tino Mettler
|
||||
|
||||
darktable 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.
|
||||
|
||||
darktable 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 software. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
var scrollPosX = 0;
|
||||
var scrollPosY = 0;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var imageCount = 0;
|
||||
const gallery = document.getElementById('gallery');
|
||||
const viewer = document.getElementById('viewer');
|
||||
|
||||
function showModal(e) {
|
||||
const thumbbox = e.target.parentElement;
|
||||
const index = [...gallery.children].indexOf(thumbbox);
|
||||
currentIndex = index;
|
||||
scrollPosX = document.documentElement.scrollLeft;
|
||||
scrollPosY = document.documentElement.scrollTop;
|
||||
|
||||
gallery.style.display = 'none';
|
||||
document.getElementById('heading1').style.display = 'none';
|
||||
viewer.style.display = 'grid';
|
||||
loadSlides();
|
||||
updateCounter(currentIndex);
|
||||
updateNavigationState();
|
||||
}
|
||||
|
||||
|
||||
function closeModal() {
|
||||
exitFullscreen(document.documentElement);
|
||||
viewer.style.display = 'none';
|
||||
document.getElementById('heading1').style.display = 'grid';
|
||||
gallery.style.display = 'flex';
|
||||
document.documentElement.scrollTo({
|
||||
left: scrollPosX,
|
||||
top: scrollPosY,
|
||||
behavior: "instant",
|
||||
});
|
||||
};
|
||||
|
||||
function createThumbnailElement(imageObj) {
|
||||
const frame = document.createElement('div');
|
||||
frame.className = 'thumb-box';
|
||||
const framesize = 18;
|
||||
|
||||
const width = parseInt(imageObj.width);
|
||||
const height = parseInt(imageObj.height);
|
||||
const sum = width + height;
|
||||
const scalefactor = sum / (framesize * 2.0);
|
||||
frame.style.width = (width / scalefactor) + 'vw';
|
||||
frame.style.height = (height / scalefactor) + 'vw';
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.className = 'thumb';
|
||||
img.src = imageObj.filename.replace(/images\/(.*)$/i, 'thumbnails/thumb_$1');
|
||||
img.alt = imageObj.filename;
|
||||
img.addEventListener('click', function (e) { e.stopPropagation(); showModal(e); });
|
||||
|
||||
frame.appendChild(img);
|
||||
gallery.appendChild(frame);
|
||||
}
|
||||
|
||||
const images = gallery_data.images;
|
||||
|
||||
const title = document.getElementById('gallery-title');
|
||||
const pageTitle = document.getElementById('page-title');
|
||||
if (gallery_data.name) {
|
||||
title.textContent = gallery_data.name;
|
||||
pageTitle.textContent = gallery_data.name;
|
||||
}
|
||||
|
||||
|
||||
document.getElementById('close').onclick = function (e) {
|
||||
e.stopPropagation();
|
||||
closeModal();
|
||||
};
|
||||
|
||||
// Keyboard navigation using left/right arrow keys
|
||||
document.onkeyup = function (e) {
|
||||
e.stopPropagation();
|
||||
switch(e.key) {
|
||||
case "Escape":
|
||||
closeModal();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('fullscreen').onclick = function (e) {
|
||||
e.stopPropagation();
|
||||
toggleFullscreen(document.documentElement);
|
||||
};
|
||||
|
||||
// Populate thumbnail gallery
|
||||
images.forEach(function (imageObj) {
|
||||
createThumbnailElement(imageObj);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
@@ -1,355 +0,0 @@
|
||||
/*
|
||||
copyright (c) 2025 Tino Mettler
|
||||
|
||||
darktable 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.
|
||||
|
||||
darktable 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 software. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const container = document.querySelector('.slide-container');
|
||||
const slides = {
|
||||
prev: document.getElementById('prev'),
|
||||
current: document.getElementById('current'),
|
||||
next: document.getElementById('next')
|
||||
};
|
||||
const prevArrow = document.getElementById('prevArrow');
|
||||
const nextArrow = document.getElementById('nextArrow');
|
||||
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
let isDragging = false; // For swipe navigation
|
||||
let isZoomed = false;
|
||||
let isPanning = false; // Panning in zoomed state
|
||||
let hasPanned = false;
|
||||
let translateX = 0;
|
||||
let translateY = 0;
|
||||
let currentScale = 1;
|
||||
let img;
|
||||
|
||||
let baseWidth, baseHeight; // Image dimensions before zoom
|
||||
|
||||
let scaledWidth, scaledHeight;
|
||||
|
||||
const images = gallery_data.images;
|
||||
|
||||
function updateCounter(index) {
|
||||
const counter = document.getElementById('counter');
|
||||
counter.textContent = (index + 1) + ' / ' + images.length;
|
||||
}
|
||||
|
||||
function updateBoundaries() {
|
||||
containerRect = container.getBoundingClientRect();
|
||||
// Calculate the actual scaled dimensions based on the base (pre-zoom) size
|
||||
scaledWidth = baseWidth * currentScale;
|
||||
scaledHeight = baseHeight * currentScale;
|
||||
}
|
||||
|
||||
function limitPanning(proposedX, proposedY) {
|
||||
const maxX = 0;
|
||||
const minX = baseWidth - scaledWidth;
|
||||
|
||||
const maxY = 0;
|
||||
const minY = baseHeight - scaledHeight;
|
||||
|
||||
let constrainedX = proposedX;
|
||||
let constrainedY = proposedY;
|
||||
|
||||
// Apply constraints
|
||||
if (scaledWidth > baseWidth) {
|
||||
// Image wider than original - constrain panning
|
||||
constrainedX = Math.max(minX, Math.min(maxX, proposedX));
|
||||
} else {
|
||||
// Image narrower than original - center it
|
||||
constrainedX = (baseWidth - scaledWidth) / 2;
|
||||
}
|
||||
|
||||
if (scaledHeight > baseHeight) {
|
||||
// Image taller than original - constrain panning
|
||||
constrainedY = Math.max(minY, Math.min(maxY, proposedY));
|
||||
} else {
|
||||
// Image shorter than original - center it
|
||||
constrainedY = (baseHeight - scaledHeight) / 2;
|
||||
}
|
||||
|
||||
return {
|
||||
x: constrainedX,
|
||||
y: constrainedY
|
||||
};
|
||||
}
|
||||
|
||||
function handleZoom(e) {
|
||||
// If we were actually panning, don't zoom
|
||||
if (hasPanned) {
|
||||
isPanning = false;
|
||||
hasPanned = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're zoomed and haven't panned, zoom out
|
||||
if (isZoomed) {
|
||||
img.classList.add('zooming');
|
||||
translateX = 0;
|
||||
translateY = 0;
|
||||
currentScale = 1;
|
||||
img.style.transform = 'none';
|
||||
container.classList.remove('zoomed');
|
||||
isZoomed = false;
|
||||
updateNavigationState();
|
||||
|
||||
setTimeout(() => {
|
||||
img.classList.remove('zooming');
|
||||
}, 200);
|
||||
return;
|
||||
}
|
||||
|
||||
// Zoom in at clicked/tapped point
|
||||
if (!isZoomed) {
|
||||
const rect = img.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
// Store the base (pre-zoom) dimensions and offset
|
||||
baseWidth = rect.width;
|
||||
baseHeight = rect.height;
|
||||
|
||||
// Calculate the scale for 1:1 pixel zoom
|
||||
currentScale = img.naturalWidth / rect.width;
|
||||
|
||||
// Calculate the translation needed to keep clicked point under cursor
|
||||
// After scaling, the point at (x, y) will be at (x * currentScale, y * currentScale)
|
||||
// We want it to remain at (e.clientX - rect.left, e.clientY - rect.top)
|
||||
translateX = e.clientX - rect.left - (x * currentScale);
|
||||
translateY = e.clientY - rect.top - (y * currentScale);
|
||||
|
||||
updateBoundaries();
|
||||
const limited = limitPanning(translateX, translateY);
|
||||
translateX = limited.x;
|
||||
translateY = limited.y;
|
||||
|
||||
img.classList.add('zooming');
|
||||
img.style.transform = `translate(${translateX}px, ${translateY}px) scale(${currentScale})`;
|
||||
container.classList.add('zoomed');
|
||||
|
||||
setTimeout(() => {
|
||||
img.classList.remove('zooming');
|
||||
}, 200);
|
||||
}
|
||||
isZoomed = true;
|
||||
updateNavigationState();
|
||||
}
|
||||
|
||||
function createImageElement(imageData) {
|
||||
if (!imageData) return null;
|
||||
const img = new Image();
|
||||
img.src = imageData.filename;
|
||||
img.width = imageData.width;
|
||||
img.height = imageData.height;
|
||||
img.addEventListener('dragstart', (e) => e.preventDefault());
|
||||
return img;
|
||||
}
|
||||
|
||||
function loadSlides() {
|
||||
slides.prev.innerHTML = '';
|
||||
slides.current.innerHTML = '';
|
||||
slides.next.innerHTML = '';
|
||||
|
||||
if (currentIndex > 0) {
|
||||
const prevImg = createImageElement(images[currentIndex - 1]);
|
||||
if (prevImg) slides.prev.appendChild(prevImg);
|
||||
}
|
||||
|
||||
const currentImg = createImageElement(images[currentIndex]);
|
||||
if (currentImg) {
|
||||
img = currentImg;
|
||||
slides.current.appendChild(currentImg);
|
||||
}
|
||||
|
||||
if (currentIndex < images.length - 1) {
|
||||
const nextImg = createImageElement(images[currentIndex + 1]);
|
||||
if (nextImg) slides.next.appendChild(nextImg);
|
||||
}
|
||||
|
||||
updateCounter(currentIndex)
|
||||
updateNavigationState();
|
||||
}
|
||||
|
||||
function updateNavigationState() {
|
||||
prevArrow.style.visibility = (currentIndex === 0 || isZoomed) ? 'hidden' : 'visible';
|
||||
nextArrow.style.visibility = (currentIndex === images.length - 1 || isZoomed) ? 'hidden' : 'visible';
|
||||
}
|
||||
|
||||
async function showPreviousImage() {
|
||||
if (currentIndex > 0 && !isZoomed) {
|
||||
currentIndex--;
|
||||
container.style.transition = 'transform 0.3s ease-out';
|
||||
container.style.transform = 'translateX(0%)';
|
||||
await waitForTransition();
|
||||
container.style.transition = 'none';
|
||||
container.style.transform = 'translateX(-33.333%)';
|
||||
loadSlides();
|
||||
}
|
||||
}
|
||||
|
||||
async function showNextImage() {
|
||||
if (currentIndex < images.length - 1 && !isZoomed) {
|
||||
currentIndex++;
|
||||
container.style.transition = 'transform 0.3s ease-out';
|
||||
container.style.transform = 'translateX(-66.666%)';
|
||||
await waitForTransition();
|
||||
container.style.transition = 'none';
|
||||
container.style.transform = 'translateX(-33.333%)';
|
||||
loadSlides();
|
||||
}
|
||||
}
|
||||
|
||||
function handleTouchStart(e) {
|
||||
if (isZoomed) return;
|
||||
startX = e.touches[0].clientX;
|
||||
isDragging = true;
|
||||
container.style.transition = 'none';
|
||||
}
|
||||
|
||||
function handleTouchMove(e) {
|
||||
if (!isDragging || isZoomed) return;
|
||||
|
||||
const currentX = e.touches[0].clientX;
|
||||
const diff = currentX - startX;
|
||||
const baseOffset = -33.333;
|
||||
const percentMoved = (diff / window.innerWidth) * 33.333;
|
||||
|
||||
container.style.transform = `translateX(${baseOffset + percentMoved}%)`;
|
||||
}
|
||||
|
||||
async function handleTouchEnd(e) {
|
||||
if (!isDragging)
|
||||
if(isZoomed) {
|
||||
handleZoom(e);
|
||||
return;
|
||||
}
|
||||
isDragging = false;
|
||||
|
||||
const endX = e.changedTouches[0].clientX;
|
||||
const diff = endX - startX;
|
||||
const threshold = window.innerWidth * 0.2;
|
||||
|
||||
container.style.transition = 'transform 0.3s ease-out';
|
||||
|
||||
if (diff > threshold && currentIndex > 0) {
|
||||
await showPreviousImage();
|
||||
} else if (diff < -threshold && currentIndex < images.length - 1) {
|
||||
await showNextImage();
|
||||
} else {
|
||||
container.style.transform = 'translateX(-33.333%)';
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(e) {
|
||||
if (isZoomed) return;
|
||||
|
||||
if (e.code === 'Space' || e.code === 'ArrowRight') {
|
||||
e.preventDefault();
|
||||
showNextImage();
|
||||
} else if (e.code === 'Backspace' || e.code === 'ArrowLeft') {
|
||||
e.preventDefault();
|
||||
showPreviousImage();
|
||||
}
|
||||
}
|
||||
|
||||
function waitForTransition() {
|
||||
return new Promise(resolve => {
|
||||
container.addEventListener('transitionend', resolve, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize
|
||||
loadSlides();
|
||||
|
||||
// Navigation event listeners
|
||||
container.addEventListener('touchstart', handleTouchStart);
|
||||
container.addEventListener('touchmove', handleTouchMove);
|
||||
container.addEventListener('touchend', handleTouchEnd);
|
||||
container.addEventListener('click', handleZoom);
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
prevArrow.addEventListener('click', showPreviousImage);
|
||||
nextArrow.addEventListener('click', showNextImage);
|
||||
|
||||
// Mouse panning event listeners
|
||||
container.addEventListener('mousedown', function(e) {
|
||||
if (isZoomed && e.target.tagName === 'IMG') {
|
||||
isPanning = true;
|
||||
hasPanned = false;
|
||||
updateBoundaries();
|
||||
startX = e.clientX - translateX;
|
||||
startY = e.clientY - translateY;
|
||||
e.preventDefault();
|
||||
container.style.cursor = 'grabbing';
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('mousemove', function(e) {
|
||||
if (isPanning && isZoomed) {
|
||||
const proposedX = e.clientX - startX;
|
||||
const proposedY = e.clientY - startY;
|
||||
|
||||
const limited = limitPanning(proposedX, proposedY);
|
||||
translateX = limited.x;
|
||||
translateY = limited.y;
|
||||
|
||||
const img = slides.current.querySelector('img');
|
||||
img.style.transform = `translate(${translateX}px, ${translateY}px) scale(${currentScale})`;
|
||||
hasPanned = true;
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('mouseup', function() {
|
||||
if (isPanning) {
|
||||
isPanning = false;
|
||||
container.style.cursor = isZoomed ? 'zoom-out' : 'zoom-in';
|
||||
}
|
||||
});
|
||||
|
||||
// Touch panning event listeners
|
||||
container.addEventListener('touchstart', function(e) {
|
||||
if (isZoomed) {
|
||||
isPanning = true;
|
||||
hasPanned = false;
|
||||
updateBoundaries();
|
||||
const touch = e.touches[0];
|
||||
startX = touch.clientX - translateX;
|
||||
startY = touch.clientY - translateY;
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
container.addEventListener('touchmove', function(e) {
|
||||
if (isPanning && isZoomed) {
|
||||
const touch = e.touches[0];
|
||||
const proposedX = touch.clientX - startX;
|
||||
const proposedY = touch.clientY - startY;
|
||||
|
||||
const limited = limitPanning(proposedX, proposedY);
|
||||
translateX = limited.x;
|
||||
translateY = limited.y;
|
||||
|
||||
const img = slides.current.querySelector('img');
|
||||
img.style.transform = `translate(${translateX}px, ${translateY}px) scale(${currentScale})`;
|
||||
hasPanned = true;
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
container.addEventListener('touchend', function(e) {
|
||||
if (isPanning) {
|
||||
isPanning = false;
|
||||
}
|
||||
});
|
||||
@@ -1,323 +0,0 @@
|
||||
--[[
|
||||
This file is part of darktable,
|
||||
copyright (c) 2026 darktable developers.
|
||||
|
||||
darktable 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.
|
||||
|
||||
darktable 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 darktable. If not, see <http://www.gnu.org/licenses/>.
|
||||
]]
|
||||
--[[
|
||||
ai_denoise - example script demonstrating the darktable.ai Lua
|
||||
API by denoising the selected images with tile-based inference.
|
||||
|
||||
Adds an "AI denoise" panel to the lighttable right column with a
|
||||
"denoise selected" button. Each selected image is loaded through
|
||||
the develop pipeline (so denoise sees the output of all enabled
|
||||
IOPs, not the original raw), run tile-by-tile through the active
|
||||
denoise model, and saved as a 16-bit TIFF grouped with the source.
|
||||
|
||||
Demonstrates:
|
||||
* model lookup via dt.ai.model_for_task
|
||||
* tensor crop/paste for tile-based inference
|
||||
* linear <-> sRGB conversion (NR models train on gamma input)
|
||||
* edge-replicated padding to suppress border artifacts
|
||||
* background job with progress bar and cancellation
|
||||
|
||||
LIMITATIONS
|
||||
* Image must be at least TILE_SIZE x TILE_SIZE (768 x 768 for
|
||||
denoise-nind / denoise-nafnet); smaller inputs return an error.
|
||||
* If a save fails mid-batch the Lua error propagates and aborts
|
||||
the whole job. Wrap denoise_one in pcall if you need
|
||||
batch-robust behaviour.
|
||||
|
||||
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
|
||||
* a denoise model enabled in preferences -> AI (denoise-nind,
|
||||
denoise-nafnet, ...)
|
||||
|
||||
USAGE
|
||||
* require this script from your luarc
|
||||
* select one or more images in lighttable
|
||||
* click the "denoise selected" button in the AI denoise panel
|
||||
|
||||
BUGS, COMMENTS, SUGGESTIONS
|
||||
* file an issue on https://github.com/darktable-org/darktable
|
||||
|
||||
CHANGES
|
||||
]]
|
||||
|
||||
local dt = require "darktable"
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- C O N S T A N T S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local MODULE_NAME = "ai_denoise"
|
||||
|
||||
-- darktable's NR models use static-shape ONNX: each model has a fixed
|
||||
-- input dimension baked in at training time. The Lua API doesn't
|
||||
-- expose this, so keep TILE_SIZE in sync with the model's
|
||||
-- `attributes.input_sizes` in its config.json.
|
||||
local TILE_SIZE = 768 -- denoise-nind, denoise-nafnet
|
||||
|
||||
-- overlap between adjacent tiles. larger values produce smoother
|
||||
-- seams at the cost of more compute per pixel
|
||||
local OVERLAP = 64
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- F U N C T I O N S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
-- Surround the input with `pad` pixels on every side, replicating the
|
||||
-- outermost rows/columns ("edge clamp" padding). Boundary tiles then
|
||||
-- see plausible context instead of a literal image edge, so the
|
||||
-- network's internal zero-padding no longer leaks into the visible
|
||||
-- output as colour artifacts along the image rim.
|
||||
local function pad_replicate(input, pad)
|
||||
local C = input:shape()[2]
|
||||
local H = input:shape()[3]
|
||||
local W = input:shape()[4]
|
||||
local padded = dt.ai.create_tensor({1, C, H + 2 * pad, W + 2 * pad})
|
||||
|
||||
-- center: original image
|
||||
padded:paste(input, pad, pad)
|
||||
|
||||
-- left and right strips: replicate first / last column of input
|
||||
local left_col = input:crop(0, 0, H, 1)
|
||||
local right_col = input:crop(0, W - 1, H, 1)
|
||||
for i = 0, pad - 1 do
|
||||
padded:paste(left_col, pad, i)
|
||||
padded:paste(right_col, pad, pad + W + i)
|
||||
end
|
||||
|
||||
-- top and bottom strips: replicate first / last row of the already
|
||||
-- horizontally-padded buffer so the four corners get filled correctly
|
||||
local top_row = padded:crop(pad, 0, 1, W + 2 * pad)
|
||||
local bottom_row = padded:crop(pad + H - 1, 0, 1, W + 2 * pad)
|
||||
for i = 0, pad - 1 do
|
||||
padded:paste(top_row, i, 0)
|
||||
padded:paste(bottom_row, pad + H + i, 0)
|
||||
end
|
||||
|
||||
return padded
|
||||
end
|
||||
|
||||
-- Tile a [1,C,H,W] tensor through the model with overlap-blended seams.
|
||||
-- Each tile is tile_size x tile_size, successive tiles step by
|
||||
-- (tile_size - 2*overlap), and only the central (non-overlap) region
|
||||
-- of each tile is pasted into the output. Image-boundary edges keep
|
||||
-- their full extent; only image-interior seams drop the overlap margin.
|
||||
--
|
||||
-- The trailing tile in each axis is pinned to the image boundary so
|
||||
-- the residual right/bottom strip is never smaller than 2*overlap --
|
||||
-- otherwise it would have to be skipped (the model has a fixed input
|
||||
-- size) and those pixels would never be written.
|
||||
local function run_tiled(ctx, input, tile_size, overlap,
|
||||
job, base_pct, span_pct)
|
||||
local C = input:shape()[2]
|
||||
local H = input:shape()[3]
|
||||
local W = input:shape()[4]
|
||||
if H < tile_size or W < tile_size then
|
||||
return nil, string.format("image too small (%dx%d) for tile %d",
|
||||
W, H, tile_size)
|
||||
end
|
||||
|
||||
local step = tile_size - 2 * overlap
|
||||
|
||||
-- enumerate tile origins along one axis, ending exactly at the boundary
|
||||
local function origins(extent)
|
||||
local out, p = {}, 0
|
||||
while p + tile_size < extent do
|
||||
out[#out + 1] = p
|
||||
p = p + step
|
||||
end
|
||||
out[#out + 1] = math.max(0, extent - tile_size)
|
||||
return out
|
||||
end
|
||||
local ys, xs = origins(H), origins(W)
|
||||
local total = #xs * #ys
|
||||
|
||||
local output = dt.ai.create_tensor({1, C, H, W})
|
||||
local done = 0
|
||||
|
||||
for _, ty in ipairs(ys) do
|
||||
for _, tx in ipairs(xs) do
|
||||
if job and not job.valid then return nil, "cancelled" end
|
||||
|
||||
local tile_in = input:crop(ty, tx, tile_size, tile_size)
|
||||
local tile_out = ctx:run(tile_in)
|
||||
|
||||
-- central region to keep: drop overlap on every inner edge;
|
||||
-- full extent at image-boundary edges
|
||||
local at_top = (ty == 0)
|
||||
local at_left = (tx == 0)
|
||||
local at_bottom = (ty + tile_size >= H)
|
||||
local at_right = (tx + tile_size >= W)
|
||||
|
||||
local cy = at_top and 0 or overlap
|
||||
local cx = at_left and 0 or overlap
|
||||
local ch = tile_size - cy - (at_bottom and 0 or overlap)
|
||||
local cw = tile_size - cx - (at_right and 0 or overlap)
|
||||
|
||||
output:paste(tile_out:crop(cy, cx, ch, cw), ty + cy, tx + cx)
|
||||
|
||||
done = done + 1
|
||||
if job then
|
||||
job.percent = base_pct + span_pct * (done / total)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return output
|
||||
end
|
||||
|
||||
-- Denoise one image: load through the develop pipeline, run tiled
|
||||
-- inference, save as 16-bit TIFF, group with the source.
|
||||
local function denoise_one(ctx, img, tile_size, job, base_pct, span_pct)
|
||||
-- darktable returns the develop-pipeline output as scene-linear RGB;
|
||||
-- the NR models were trained on sRGB-gamma input, so we re-encode
|
||||
-- before inference and decode back to linear before saving
|
||||
local input = dt.ai.load_image(img)
|
||||
if not input then return false, "load_image failed" end
|
||||
input:linear_to_srgb()
|
||||
|
||||
-- pad so the image's outer rim is no longer at a tile boundary;
|
||||
-- the padded strip absorbs the network's edge artifacts and gets
|
||||
-- cropped away after inference
|
||||
local H = input:shape()[3]
|
||||
local W = input:shape()[4]
|
||||
local padded_in = pad_replicate(input, OVERLAP)
|
||||
|
||||
local padded_out, err = run_tiled(ctx, padded_in, tile_size, OVERLAP,
|
||||
job, base_pct, span_pct)
|
||||
if not padded_out then return false, err end
|
||||
|
||||
local output = padded_out:crop(OVERLAP, OVERLAP, H, W)
|
||||
output:srgb_to_linear()
|
||||
|
||||
local base = img.filename:match("(.+)%..+$") or img.filename
|
||||
local out_path = img.path .. "/" .. base .. "_denoised.tif"
|
||||
output:save_tiff(out_path, 16, img)
|
||||
|
||||
-- import the result and group with the source so they live together
|
||||
-- in the lighttable
|
||||
local imported = dt.database.import(out_path)
|
||||
if imported then imported:group_with(img) end
|
||||
return true
|
||||
end
|
||||
|
||||
-- Button handler: looks up the active denoise model, fans out the
|
||||
-- selected images, and reports progress via a cancellable job.
|
||||
local function process_denoise()
|
||||
local model_id = dt.ai.model_for_task("denoise")
|
||||
if not model_id then
|
||||
dt.print("no denoise model enabled -- pick one in preferences -> AI")
|
||||
return
|
||||
end
|
||||
|
||||
local images = dt.gui.selection()
|
||||
if #images == 0 then
|
||||
dt.print("select at least one image")
|
||||
return
|
||||
end
|
||||
|
||||
-- progress + cancel job. the cancel callback flips job.valid; the
|
||||
-- main loop polls it between tiles to stop cleanly
|
||||
local job = dt.gui.create_job(
|
||||
string.format("AI denoise (%d image%s)",
|
||||
#images, #images == 1 and "" or "s"),
|
||||
true,
|
||||
function(j) j.valid = false end)
|
||||
|
||||
dt.print_log(string.format(
|
||||
"[ai_denoise] starting: %d image%s with model %s",
|
||||
#images, #images == 1 and "" or "s", model_id))
|
||||
local t_start = os.time()
|
||||
|
||||
local ctx = dt.ai.load_model(model_id)
|
||||
if not ctx then
|
||||
dt.print("failed to load model: " .. model_id)
|
||||
job.valid = false
|
||||
return
|
||||
end
|
||||
|
||||
local span = 1.0 / #images
|
||||
local done = 0
|
||||
for i, img in ipairs(images) do
|
||||
if not job.valid then break end
|
||||
dt.print_log(string.format("[ai_denoise] [%d/%d] %s",
|
||||
i, #images, img.filename))
|
||||
local ok, err = denoise_one(ctx, img, TILE_SIZE, job,
|
||||
(i - 1) * span, span)
|
||||
if not ok then
|
||||
dt.print(string.format("[%d/%d] %s: %s",
|
||||
i, #images, img.filename, tostring(err)))
|
||||
dt.print_log(string.format("[ai_denoise] [%d/%d] %s FAILED: %s",
|
||||
i, #images, img.filename, tostring(err)))
|
||||
else
|
||||
done = done + 1
|
||||
end
|
||||
end
|
||||
|
||||
ctx:close()
|
||||
job.valid = false
|
||||
local elapsed = os.time() - t_start
|
||||
dt.print(string.format("denoise complete: %d image%s",
|
||||
#images, #images == 1 and "" or "s"))
|
||||
dt.print_log(string.format(
|
||||
"[ai_denoise] finished: %d/%d in %ds",
|
||||
done, #images, elapsed))
|
||||
end
|
||||
|
||||
-- script_manager integration: called when the user disables the script.
|
||||
-- darktable can't remove a registered lib from the UI at runtime, so we
|
||||
-- only stop any in-flight work; the panel itself remains until restart.
|
||||
local function destroy()
|
||||
-- nothing persistent to clean up: each click runs its own job and
|
||||
-- closes its own model context
|
||||
end
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- M A I N
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
dt.register_lib(
|
||||
MODULE_NAME, -- plugin id
|
||||
"AI denoise", -- displayed name
|
||||
true, -- expandable
|
||||
false, -- no reset button
|
||||
{[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}},
|
||||
dt.new_widget("box") {
|
||||
orientation = "vertical",
|
||||
dt.new_widget("button") {
|
||||
label = "denoise selected",
|
||||
clicked_callback = process_denoise,
|
||||
}
|
||||
},
|
||||
nil, nil
|
||||
)
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- 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.metadata = {
|
||||
name = "AI denoise",
|
||||
purpose = "tile-based AI denoise using the darktable.ai Lua API",
|
||||
author = "Andrii Ryzhkov",
|
||||
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/ai_denoise"
|
||||
}
|
||||
|
||||
script_data.destroy = destroy
|
||||
|
||||
return script_data
|
||||
@@ -1,481 +0,0 @@
|
||||
--[[
|
||||
This file is part of darktable,
|
||||
copyright (c) 2026 darktable developers.
|
||||
|
||||
darktable 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.
|
||||
|
||||
darktable 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 darktable. If not, see <http://www.gnu.org/licenses/>.
|
||||
]]
|
||||
--[[
|
||||
ai_raw_denoise - example script demonstrating the darktable.ai Lua
|
||||
API by denoising the selected raw images with tile-based inference
|
||||
on the raw CFA.
|
||||
|
||||
Adds an "AI raw denoise" panel to the lighttable right column with
|
||||
a "denoise selected" button. Each selected image's raw CFA is
|
||||
loaded, Bayer-packed to 4 channels, run tile-by-tile through the
|
||||
currently enabled rawdenoise model (which denoises AND demosaicks
|
||||
in one pass), and saved as a LinearRaw DNG grouped with the
|
||||
source image.
|
||||
|
||||
Demonstrates:
|
||||
* model lookup via dt.ai.model_for_task
|
||||
* loading model variant files via dt.ai.load_model file arg
|
||||
* raw CFA loading via dt.ai.load_raw
|
||||
* tensor bayer_pack for 4-channel CFA tiling
|
||||
* tile loop with non-trivial input->output scale (here 1 -> 2)
|
||||
* edge-replicated padding to suppress border artifacts
|
||||
* LinearRaw DNG output via dt.ai.save_dng_linear
|
||||
* background job with progress bar and cancellation
|
||||
|
||||
LIMITATIONS
|
||||
* Bayer sensors only. The script loads model_bayer.onnx and uses
|
||||
bayer_pack, which assumes a 2x2 Bayer layout. X-Trans and
|
||||
Foveon sensors need model_linear.onnx with a different
|
||||
preprocessing pipeline (no bayer_pack, demosaicked input).
|
||||
* Image must be at least TILE_SIZE x TILE_SIZE in packed pixels
|
||||
(1024 x 1024 of CFA for the default 512 packed tile size).
|
||||
* If a save fails mid-batch the Lua error propagates and aborts
|
||||
the whole job. Wrap denoise_one in pcall if you need
|
||||
batch-robust behaviour.
|
||||
|
||||
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
|
||||
* a rawdenoise model enabled in preferences -> AI
|
||||
(e.g. rawdenoise-nind)
|
||||
|
||||
USAGE
|
||||
* require this script from your luarc
|
||||
* select one or more raw images in lighttable
|
||||
* click the "denoise selected" button in the AI raw denoise panel
|
||||
|
||||
BUGS, COMMENTS, SUGGESTIONS
|
||||
* file an issue on https://github.com/darktable-org/darktable
|
||||
|
||||
CHANGES
|
||||
]]
|
||||
|
||||
local dt = require "darktable"
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- C O N S T A N T S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local MODULE_NAME = "ai_raw_denoise"
|
||||
|
||||
-- The rawdenoise-nind bayer model is a denoise+demosaic combo:
|
||||
-- input : [1, 4, 512, 512] packed CFA (4 phase planes)
|
||||
-- output : [1, 3, 1024, 1024] linear RGB at 2x the input resolution
|
||||
-- So each tile-size pixel of the input covers a 2x2 CFA block, and the
|
||||
-- model emits two output pixels per input pixel in each axis.
|
||||
-- TILE_SIZE is in packed pixels (model input); SCALE turns those into
|
||||
-- output pixels. Keep in sync with `attributes.input_sizes` in the
|
||||
-- model's config.json.
|
||||
local TILE_SIZE = 512 -- rawdenoise-nind
|
||||
local SCALE = 2 -- model output dim / input dim
|
||||
local OUT_CH = 3 -- model emits linear RGB
|
||||
|
||||
-- overlap between adjacent tiles, in packed pixels (input space).
|
||||
-- larger values produce smoother seams at the cost of more compute
|
||||
local OVERLAP = 32
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- F U N C T I O N S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
-- Bayer-only CFA channel lookup. Returns 0/1/2 (R/G/B) for the cell
|
||||
-- at sensor position (r, c) given the 32-bit `filters` mask from
|
||||
-- load_raw's meta table. Used to find the R-cell origin so bayer_pack's
|
||||
-- plane 0 always holds R. X-Trans (filters == 9) uses meta.xtrans
|
||||
-- instead and is not handled here.
|
||||
local function fc(r, c, filters)
|
||||
local shift = ((r % 2) * 4) + ((c % 2) * 2)
|
||||
return math.floor(filters / (2 ^ shift)) % 4
|
||||
end
|
||||
|
||||
-- daylight WB multipliers (R, G=1, B) derived from the camera's
|
||||
-- XYZ->camRGB color matrix and the D65 white point. mirrors the C
|
||||
-- path's _bayer_wb_daylight in restore_raw_bayer.c — the rawdenoise
|
||||
-- bayer model is trained on daylight-WB'd input, so feeding it
|
||||
-- un-WB'd camRGB produces wrong colors. Falls back to {1, 1, 1} if
|
||||
-- the matrix is missing or degenerate.
|
||||
local function daylight_wb(color_matrix)
|
||||
local d65 = {0.95047, 1.0, 1.08883}
|
||||
if not color_matrix then return 1.0, 1.0, 1.0 end
|
||||
local resp = {0, 0, 0}
|
||||
for c = 1, 3 do
|
||||
resp[c] = color_matrix[c][1] * d65[1]
|
||||
+ color_matrix[c][2] * d65[2]
|
||||
+ color_matrix[c][3] * d65[3]
|
||||
end
|
||||
if resp[1] <= 0 or resp[2] <= 0 or resp[3] <= 0 then
|
||||
return 1.0, 1.0, 1.0
|
||||
end
|
||||
return resp[2] / resp[1], 1.0, resp[2] / resp[3]
|
||||
end
|
||||
|
||||
-- Surround the input with `pad` pixels on every side, replicating the
|
||||
-- outermost rows/columns ("edge clamp" padding). Boundary tiles then
|
||||
-- see plausible context instead of a literal image edge, so the
|
||||
-- network's internal zero-padding no longer leaks into the visible
|
||||
-- output as colour artifacts along the image rim.
|
||||
local function pad_replicate(input, pad)
|
||||
local C = input:shape()[2]
|
||||
local H = input:shape()[3]
|
||||
local W = input:shape()[4]
|
||||
local padded = dt.ai.create_tensor({1, C, H + 2 * pad, W + 2 * pad})
|
||||
|
||||
-- center: original image
|
||||
padded:paste(input, pad, pad)
|
||||
|
||||
-- left and right strips: replicate first / last column of input
|
||||
local left_col = input:crop(0, 0, H, 1)
|
||||
local right_col = input:crop(0, W - 1, H, 1)
|
||||
for i = 0, pad - 1 do
|
||||
padded:paste(left_col, pad, i)
|
||||
padded:paste(right_col, pad, pad + W + i)
|
||||
end
|
||||
|
||||
-- top and bottom strips: replicate first / last row of the already
|
||||
-- horizontally-padded buffer so the four corners get filled correctly
|
||||
local top_row = padded:crop(pad, 0, 1, W + 2 * pad)
|
||||
local bottom_row = padded:crop(pad + H - 1, 0, 1, W + 2 * pad)
|
||||
for i = 0, pad - 1 do
|
||||
padded:paste(top_row, i, 0)
|
||||
padded:paste(bottom_row, pad + H + i, 0)
|
||||
end
|
||||
|
||||
return padded
|
||||
end
|
||||
|
||||
-- Tile a [1,C_in,H,W] tensor through a model that emits [1,C_out,H*scale,
|
||||
-- W*scale]. Each tile is tile_size x tile_size in input space; successive
|
||||
-- tiles step by (tile_size - 2*overlap); only the central (non-overlap)
|
||||
-- region of each tile's output is pasted into the result, with all
|
||||
-- offsets multiplied by `scale`. Image-boundary edges keep their full
|
||||
-- extent; only image-interior seams drop the overlap margin.
|
||||
--
|
||||
-- The trailing tile in each axis is pinned to the image boundary so
|
||||
-- the residual right/bottom strip is never smaller than 2*overlap --
|
||||
-- otherwise it would have to be skipped (the model has a fixed input
|
||||
-- size) and those pixels would never be written.
|
||||
local function run_tiled(ctx, input, tile_size, overlap, scale, c_out,
|
||||
job, base_pct, span_pct)
|
||||
local H = input:shape()[3]
|
||||
local W = input:shape()[4]
|
||||
if H < tile_size or W < tile_size then
|
||||
return nil, string.format("image too small (%dx%d) for tile %d",
|
||||
W, H, tile_size)
|
||||
end
|
||||
|
||||
local step = tile_size - 2 * overlap
|
||||
|
||||
-- enumerate tile origins along one axis, ending exactly at the boundary
|
||||
local function origins(extent)
|
||||
local out, p = {}, 0
|
||||
while p + tile_size < extent do
|
||||
out[#out + 1] = p
|
||||
p = p + step
|
||||
end
|
||||
out[#out + 1] = math.max(0, extent - tile_size)
|
||||
return out
|
||||
end
|
||||
local ys, xs = origins(H), origins(W)
|
||||
local total = #xs * #ys
|
||||
|
||||
-- output is scale-times larger spatially and may have a different
|
||||
-- channel count than the input
|
||||
local output = dt.ai.create_tensor({1, c_out, H * scale, W * scale})
|
||||
local done = 0
|
||||
|
||||
for _, ty in ipairs(ys) do
|
||||
for _, tx in ipairs(xs) do
|
||||
if job and not job.valid then return nil, "cancelled" end
|
||||
|
||||
local tile_in = input:crop(ty, tx, tile_size, tile_size)
|
||||
local tile_out = ctx:run(tile_in)
|
||||
|
||||
-- central region to keep: drop overlap on every inner edge;
|
||||
-- full extent at image-boundary edges. all dimensions in OUTPUT
|
||||
-- (post-scale) space, since that's where we paste
|
||||
local at_top = (ty == 0)
|
||||
local at_left = (tx == 0)
|
||||
local at_bottom = (ty + tile_size >= H)
|
||||
local at_right = (tx + tile_size >= W)
|
||||
|
||||
local ov = overlap * scale
|
||||
local tos = tile_size * scale
|
||||
local cy = at_top and 0 or ov
|
||||
local cx = at_left and 0 or ov
|
||||
local ch = tos - cy - (at_bottom and 0 or ov)
|
||||
local cw = tos - cx - (at_right and 0 or ov)
|
||||
|
||||
output:paste(tile_out:crop(cy, cx, ch, cw),
|
||||
ty * scale + cy, tx * scale + cx)
|
||||
|
||||
done = done + 1
|
||||
if job then
|
||||
job.percent = base_pct + span_pct * (done / total)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return output
|
||||
end
|
||||
|
||||
-- Denoise one raw image. The bayer rawdenoise model denoises AND
|
||||
-- demosaicks in one pass, so its output is linear RGB at the
|
||||
-- original CFA resolution (not a CFA mosaic). Saved as a LinearRaw
|
||||
-- DNG, which darktable can re-import as an already-debayered raw.
|
||||
--
|
||||
-- Pipeline:
|
||||
-- load CFA [1,1,H,W] + image metadata
|
||||
-- -> bayer_pack [1,4,H/2,W/2]
|
||||
-- -> normalize to [0, 1] using black/white levels (the model is
|
||||
-- trained on (val - black) / (white - black), not raw ADC)
|
||||
-- -> pad to [1,4,H/2+2P,W/2+2P]
|
||||
-- -> tile through model (scale=2, c_out=3) -> [1,3,(H+4P),(W+4P)]
|
||||
-- -> crop padding back off -> [1,3,H,W]
|
||||
-- -> save linear DNG -> import + group with source
|
||||
local function denoise_one(ctx, img, tile_size, job, base_pct, span_pct)
|
||||
-- load_raw returns (tensor, meta). tensor is [1,1,H,W] in raw ADC
|
||||
-- units; meta carries black/white/wb/CFA info we need to normalise
|
||||
-- the input into the [0, 1] range the model was trained on
|
||||
local cfa, meta = dt.ai.load_raw(img)
|
||||
if not cfa then return false, "load_raw failed" end
|
||||
|
||||
-- this script is Bayer-only: filters == 9 is X-Trans (handled by a
|
||||
-- different preprocessing pipeline + model_linear.onnx); filters == 0
|
||||
-- means monochrome / non-CFA, which bayer_pack can't handle either
|
||||
if not meta.filters or meta.filters == 0 or meta.filters == 9 then
|
||||
return false, "unsupported CFA pattern (Bayer-only script)"
|
||||
end
|
||||
|
||||
-- Crop to the visible region and shift the origin onto the R cell
|
||||
-- of the CFA pattern. bayer_pack splits by position only, so to make
|
||||
-- plane 0 always hold R (which the model expects) the cropped CFA's
|
||||
-- (0, 0) must sit at an R site. Mirrors the C path's _bayer_origin
|
||||
-- + RGGB shift in restore_raw_bayer.c.
|
||||
local cy = 0
|
||||
local cx = 0
|
||||
local cw = cfa:shape()[4]
|
||||
local ch = cfa:shape()[3]
|
||||
if meta.crop_x and meta.crop_y then
|
||||
cy = meta.crop_y - (meta.crop_y % 2)
|
||||
cx = meta.crop_x - (meta.crop_x % 2)
|
||||
cw = (meta.visible_width or cw) + (meta.crop_x - cx)
|
||||
ch = (meta.visible_height or ch) + (meta.crop_y - cy)
|
||||
end
|
||||
-- find R cell within the 2x2 starting at (cy, cx)
|
||||
local sy, sx = 0, 0
|
||||
for ty = 0, 1 do
|
||||
for tx = 0, 1 do
|
||||
if fc(cy + ty, cx + tx, meta.filters) == 0 then
|
||||
sy, sx = ty, tx
|
||||
end
|
||||
end
|
||||
end
|
||||
cy = cy + sy
|
||||
cx = cx + sx
|
||||
ch = ch - sy
|
||||
cw = cw - sx
|
||||
-- ensure even dims for bayer_pack
|
||||
ch = ch - (ch % 2)
|
||||
cw = cw - (cw % 2)
|
||||
if cy > 0 or cx > 0
|
||||
or cw < cfa:shape()[4] or ch < cfa:shape()[3]
|
||||
then
|
||||
cfa = cfa:crop(cy, cx, ch, cw)
|
||||
end
|
||||
|
||||
-- uniform-black approximation: most sensors report identical black
|
||||
-- levels across the 4 CFA sites, in which case a single scalar
|
||||
-- (val - black) / (white - black) normalisation is exact. cameras
|
||||
-- with non-uniform per-site black levels (e.g. some PDAF sensors)
|
||||
-- would need per-plane scaling via scale_add_planes -- left as an
|
||||
-- exercise
|
||||
local black = meta.black_level
|
||||
local range = meta.white_level - black
|
||||
if range <= 0 then range = 65535 end
|
||||
|
||||
-- pack the 1-channel CFA into 4 phase planes so each channel holds
|
||||
-- one Bayer site type. After the RGGB shift above:
|
||||
-- plane 0 = R, planes 1,2 = G, plane 3 = B
|
||||
local packed = cfa:bayer_pack()
|
||||
packed:scale_add(1.0 / range, -black / range)
|
||||
|
||||
-- apply daylight WB so the four planes sit in the same range the
|
||||
-- model was trained on (R, G, G, B → R*wb_R, G, G, B*wb_B). without
|
||||
-- this, the model sees unbalanced channels and produces wrong colors
|
||||
local wb_R, wb_G, wb_B = daylight_wb(meta.color_matrix)
|
||||
packed:scale_add_planes({wb_R, wb_G, wb_G, wb_B})
|
||||
local pH = packed:shape()[3]
|
||||
local pW = packed:shape()[4]
|
||||
|
||||
-- save the post-normalisation input mean for gain-matching the
|
||||
-- model output: the bayer rawdenoise model emits values on its own
|
||||
-- internal scale, not [0, 1], so we rescale so output mean matches
|
||||
-- input mean (the per-tile equivalent of dt's C reference path)
|
||||
local in_mean = packed:mean()
|
||||
|
||||
-- pad so the image rim is no longer at a tile boundary; the padded
|
||||
-- strip absorbs the network's edge artifacts and is cropped away
|
||||
-- after inference (in output space, where the pad covers 2*OVERLAP
|
||||
-- pixels per side)
|
||||
local padded_in = pad_replicate(packed, OVERLAP)
|
||||
|
||||
local padded_out, err = run_tiled(ctx, padded_in, tile_size, OVERLAP,
|
||||
SCALE, OUT_CH,
|
||||
job, base_pct, span_pct)
|
||||
if not padded_out then return false, err end
|
||||
|
||||
-- crop the padded region (in output coords: pad * scale per side)
|
||||
-- back to the original-CFA resolution
|
||||
local out_rgb = padded_out:crop(OVERLAP * SCALE, OVERLAP * SCALE,
|
||||
pH * SCALE, pW * SCALE)
|
||||
|
||||
-- gain-match: rescale the output so its mean matches the input
|
||||
-- mean, bringing the model's internal-scale output back into the
|
||||
-- [0, 1] range save_dng_linear expects
|
||||
local out_mean = out_rgb:mean()
|
||||
if out_mean > 1e-8 then
|
||||
out_rgb:scale_add(in_mean / out_mean)
|
||||
end
|
||||
|
||||
-- invert the daylight WB so the saved tensor is un-WB'd camRGB
|
||||
-- (save_dng_linear's contract — the consumer applies AsShotNeutral
|
||||
-- on import)
|
||||
out_rgb:scale_add_planes({1.0 / wb_R, 1.0 / wb_G, 1.0 / wb_B})
|
||||
|
||||
local base = img.filename:match("(.+)%..+$") or img.filename
|
||||
local out_path = img.path .. "/" .. base .. "_rawdenoised.dng"
|
||||
-- LinearRaw DNG: tensor values must be normalised to [0, 1] camRGB;
|
||||
-- the model handles black-subtract + scale internally, so we just
|
||||
-- pass the output through
|
||||
dt.ai.save_dng_linear(out_rgb, img, out_path)
|
||||
|
||||
-- import the result and group with the source so they live together
|
||||
-- in the lighttable
|
||||
local imported = dt.database.import(out_path)
|
||||
if imported then imported:group_with(img) end
|
||||
return true
|
||||
end
|
||||
|
||||
-- Button handler: looks up the active rawdenoise model, fans out the
|
||||
-- selected images, and reports progress via a cancellable job.
|
||||
local function process_raw_denoise()
|
||||
local model_id = dt.ai.model_for_task("rawdenoise")
|
||||
if not model_id then
|
||||
dt.print("no rawdenoise model enabled -- pick one in preferences -> AI")
|
||||
return
|
||||
end
|
||||
|
||||
local images = dt.gui.selection()
|
||||
if #images == 0 then
|
||||
dt.print("select at least one image")
|
||||
return
|
||||
end
|
||||
|
||||
-- progress + cancel job. the cancel callback flips job.valid; the
|
||||
-- main loop polls it between tiles to stop cleanly
|
||||
local job = dt.gui.create_job(
|
||||
string.format("AI raw denoise (%d image%s)",
|
||||
#images, #images == 1 and "" or "s"),
|
||||
true,
|
||||
function(j) j.valid = false end)
|
||||
|
||||
dt.print_log(string.format(
|
||||
"[ai_raw_denoise] starting: %d image%s with model %s",
|
||||
#images, #images == 1 and "" or "s", model_id))
|
||||
local t_start = os.time()
|
||||
|
||||
-- rawdenoise packages ship multiple files (one per CFA type); pick
|
||||
-- the Bayer variant. X-Trans / Foveon would use model_linear.onnx
|
||||
-- with a different preprocessing pipeline (no bayer_pack)
|
||||
local ctx = dt.ai.load_model(model_id, nil, "model_bayer.onnx")
|
||||
if not ctx then
|
||||
dt.print("failed to load model: " .. model_id)
|
||||
job.valid = false
|
||||
return
|
||||
end
|
||||
|
||||
local span = 1.0 / #images
|
||||
local done = 0
|
||||
for i, img in ipairs(images) do
|
||||
if not job.valid then break end
|
||||
dt.print_log(string.format("[ai_raw_denoise] [%d/%d] %s",
|
||||
i, #images, img.filename))
|
||||
local ok, err = denoise_one(ctx, img, TILE_SIZE, job,
|
||||
(i - 1) * span, span)
|
||||
if not ok then
|
||||
dt.print(string.format("[%d/%d] %s: %s",
|
||||
i, #images, img.filename, tostring(err)))
|
||||
dt.print_log(string.format("[ai_raw_denoise] [%d/%d] %s FAILED: %s",
|
||||
i, #images, img.filename, tostring(err)))
|
||||
else
|
||||
done = done + 1
|
||||
end
|
||||
end
|
||||
|
||||
ctx:close()
|
||||
job.valid = false
|
||||
local elapsed = os.time() - t_start
|
||||
dt.print(string.format("raw denoise complete: %d image%s",
|
||||
#images, #images == 1 and "" or "s"))
|
||||
dt.print_log(string.format(
|
||||
"[ai_raw_denoise] finished: %d/%d in %ds",
|
||||
done, #images, elapsed))
|
||||
end
|
||||
|
||||
-- script_manager integration: called when the user disables the script.
|
||||
-- darktable can't remove a registered lib from the UI at runtime, so we
|
||||
-- only stop any in-flight work; the panel itself remains until restart.
|
||||
local function destroy()
|
||||
-- nothing persistent to clean up: each click runs its own job and
|
||||
-- closes its own model context
|
||||
end
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- M A I N
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
dt.register_lib(
|
||||
MODULE_NAME, -- plugin id
|
||||
"AI raw denoise", -- displayed name
|
||||
true, -- expandable
|
||||
false, -- no reset button
|
||||
{[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}},
|
||||
dt.new_widget("box") {
|
||||
orientation = "vertical",
|
||||
dt.new_widget("button") {
|
||||
label = "denoise selected",
|
||||
clicked_callback = process_raw_denoise,
|
||||
}
|
||||
},
|
||||
nil, nil
|
||||
)
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- 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.metadata = {
|
||||
name = "AI raw denoise",
|
||||
purpose = "tile-based AI raw denoise using the darktable.ai Lua API",
|
||||
author = "Andrii Ryzhkov",
|
||||
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/ai_raw_denoise"
|
||||
}
|
||||
|
||||
script_data.destroy = destroy
|
||||
|
||||
return script_data
|
||||
@@ -29,7 +29,8 @@
|
||||
by hovering over the image and pressing the key combination.
|
||||
|
||||
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
|
||||
* exiftool - https://exiftool.org
|
||||
* ufraw-batch - https://ufraw.sourceforge.net
|
||||
MacOS - install with homebrew
|
||||
|
||||
USAGE
|
||||
* require this script from your main lua file
|
||||
@@ -43,7 +44,6 @@
|
||||
* Send to Bill Ferguson, wpferguson@gmail.com
|
||||
|
||||
CHANGES
|
||||
20251205 - replaced ufraw-batch with exiftool
|
||||
]]
|
||||
|
||||
--[[
|
||||
@@ -139,36 +139,6 @@ local function copy_image_attributes(from, to, ...)
|
||||
end
|
||||
end
|
||||
|
||||
local function check_file_size(img_name)
|
||||
local result = 0
|
||||
|
||||
if dt.configuration.running_os == "windows" then
|
||||
local path = df.get_path(img_name)
|
||||
local fname = df.get_filename(img_name)
|
||||
|
||||
local p = io.open("forfiles /P " .. path .. " /M " .. fname .. " /C \"cmd /c echo $fsize\"")
|
||||
|
||||
if p then
|
||||
for line in p:lines() do
|
||||
if line:length() > 5 then
|
||||
result = tonumber(line)
|
||||
end
|
||||
end
|
||||
p:close()
|
||||
end
|
||||
else
|
||||
local p = io.popen("ls -s " .. img_name)
|
||||
|
||||
if p then
|
||||
local line = p:read("*a")
|
||||
result = tonumber(du.split(line, " ")[1])
|
||||
p:close()
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
--[[
|
||||
The main function called from the button or the shortcut. It takes one or more raw files, passed
|
||||
in a table and extracts the embedded jpeg images
|
||||
@@ -182,12 +152,11 @@ local function extract_embedded_jpeg(images)
|
||||
in the user's path. When it finds the executable, it returns the command to run it.
|
||||
]]
|
||||
|
||||
local exiftool_executable = df.check_if_bin_exists("exiftool")
|
||||
if exiftool_executable then
|
||||
local ufraw_executable = df.check_if_bin_exists("ufraw-batch")
|
||||
if ufraw_executable then
|
||||
for _, image in ipairs(images) do
|
||||
if image.is_raw then
|
||||
local img_file = tostring(image)
|
||||
local jpg_img_file = string.gsub(img_file, "%....$", ".jpg")
|
||||
local img_file = du.join({image.path, image.filename}, "/")
|
||||
|
||||
--[[
|
||||
dtsys.external_command() is operating system aware and formats the command as necessary.
|
||||
@@ -195,12 +164,8 @@ local function extract_embedded_jpeg(images)
|
||||
operating system aware and uses the corresponding quotes.
|
||||
]]
|
||||
|
||||
if dtsys.external_command(exiftool_executable .. " -b -JpgFromRaw " .. df.sanitize_filename(img_file) .. " > " .. df.sanitize_filename(jpg_img_file)) then
|
||||
dt.print_log("created jpg from raw")
|
||||
if check_file_size(df.sanitize_filename(jpg_img_file)) == 0 then
|
||||
dt.print_log("using preview")
|
||||
dtsys.external_command(exiftool_executable .. " -b -PreviewImage " .. df.sanitize_filename(img_file) .. " > " .. df.sanitize_filename(jpg_img_file))
|
||||
end
|
||||
if dtsys.external_command(ufraw_executable .. " --silent --embedded-image " .. df.sanitize_filename(img_file)) then
|
||||
local jpg_img_file = df.chop_filetype(img_file) .. ".embedded.jpg"
|
||||
dt.print_log("jpg_img_file set to ", jpg_img_file) -- print a debugging message
|
||||
local myimage = dt.database.import(jpg_img_file)
|
||||
myimage:group_with(image.group_leader)
|
||||
@@ -223,8 +188,8 @@ local function extract_embedded_jpeg(images)
|
||||
end
|
||||
end
|
||||
else
|
||||
dt.print_error("exiftool not found. Exiting...") -- print debugging error message
|
||||
dt.print("exiftool not found, exiting...") -- print the error to the screen
|
||||
dt.print_error("ufraw-batch not found. Exiting...") -- print debugging error message
|
||||
dt.print("ufraw-batch not found, exiting...") -- print the error to the screen
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -51,10 +51,6 @@ midi:C0=iop/channelmixerrgb;focus
|
||||
midi:C#0=iop/colorequal;focus
|
||||
midi:D0=iop/colorequal/page;previous
|
||||
midi:D#0=iop/colorequal/page;next
|
||||
|
||||
USAGE:
|
||||
To add a new module, simply add an entry to MODULE_PROFILES below.
|
||||
Get action paths from darktable's shortcut interface.
|
||||
]]
|
||||
|
||||
local dt = require "darktable"
|
||||
@@ -63,283 +59,150 @@ local du = require "lib/dtutils"
|
||||
du.check_min_api_version("9.2.0", "x-touch")
|
||||
|
||||
local gettext = dt.gettext.gettext
|
||||
|
||||
local function _(msgid)
|
||||
return gettext(msgid)
|
||||
end
|
||||
|
||||
-- Constants
|
||||
local NUM_KNOBS = 8
|
||||
local NAN = 0/0
|
||||
local darkroom_view = dt.gui.views.darkroom
|
||||
-- return data structure for script_manager
|
||||
|
||||
-- ============================================================================
|
||||
-- USER CONFIGURATION
|
||||
-- ============================================================================
|
||||
local script_data = {}
|
||||
|
||||
-- Module Profiles: Define behavior when each module has focus
|
||||
-- Structure: [module_name] = { knob1_action, knob2_action, ... }
|
||||
local MODULE_PROFILES = {
|
||||
|
||||
-- Color Zones: Control individual color channels
|
||||
colorzones = {
|
||||
{"iop/colorzones/graph", "red"},
|
||||
{"iop/colorzones/graph", "orange"},
|
||||
{"iop/colorzones/graph", "yellow"},
|
||||
{"iop/colorzones/graph", "green"},
|
||||
{"iop/colorzones/graph", "aqua"},
|
||||
{"iop/colorzones/graph", "blue"},
|
||||
{"iop/colorzones/graph", "purple"},
|
||||
{"iop/colorzones/graph", "magenta"}
|
||||
},
|
||||
|
||||
-- Color Equalizer: Control color channels
|
||||
colorequal = {
|
||||
{"iop/colorequal/graph", "red"},
|
||||
{"iop/colorequal/graph", "orange"},
|
||||
{"iop/colorequal/graph", "yellow"},
|
||||
{"iop/colorequal/graph", "green"},
|
||||
{"iop/colorequal/graph", "cyan"},
|
||||
{"iop/colorequal/graph", "blue"},
|
||||
{"iop/colorequal/graph", "lavender"},
|
||||
{"iop/colorequal/graph", "magenta"}
|
||||
},
|
||||
|
||||
-- Sigmoid: Control primary color adjustments
|
||||
sigmoid = {
|
||||
{"iop/sigmoid/primaries/red attenuation"},
|
||||
{"iop/sigmoid/primaries/red rotation"},
|
||||
{"iop/sigmoid/primaries/green attenuation"},
|
||||
{"iop/sigmoid/primaries/green rotation"},
|
||||
{"iop/sigmoid/primaries/blue attenuation"},
|
||||
{"iop/sigmoid/primaries/blue rotation"},
|
||||
{"iop/sigmoid/primaries/recover purity"}
|
||||
},
|
||||
|
||||
-- Color Calibration (Primaries)
|
||||
primaries = {
|
||||
{"iop/primaries/red hue"},
|
||||
{"iop/primaries/red purity"},
|
||||
{"iop/primaries/green hue"},
|
||||
{"iop/primaries/green purity"},
|
||||
{"iop/primaries/blue hue"},
|
||||
{"iop/primaries/blue purity"},
|
||||
{"iop/primaries/tint hue"},
|
||||
{"iop/primaries/tint purity"}
|
||||
},
|
||||
|
||||
-- AgX
|
||||
agx = {
|
||||
{"iop/agx/white relative exposure"},
|
||||
{"iop/agx/black relative exposure"},
|
||||
{"iop/agx/curve/pivot relative exposure"},
|
||||
{"iop/agx/curve/pivot target output"},
|
||||
{"iop/agx/curve/contrast"},
|
||||
{"iop/agx/curve/shoulder power"},
|
||||
{"iop/agx/curve/toe power"},
|
||||
{"iop/agx/curve/curve y gamma"}
|
||||
},
|
||||
|
||||
-- Tone Equalizer: Control EV bands
|
||||
-- For 8 knobs, using offsets -8 to get bands
|
||||
toneequal = {
|
||||
{"iop/toneequal/simple/-8 EV"},
|
||||
{"iop/toneequal/simple/-7 EV"},
|
||||
{"iop/toneequal/simple/-6 EV"},
|
||||
{"iop/toneequal/simple/-5 EV"},
|
||||
{"iop/toneequal/simple/-4 EV"},
|
||||
{"iop/toneequal/simple/-3 EV"},
|
||||
{"iop/toneequal/simple/-2 EV"},
|
||||
{"iop/toneequal/simple/-1 EV"}
|
||||
},
|
||||
|
||||
-- Color Balance RGB: Selected controls
|
||||
colorbalancergb = {
|
||||
nil, -- Knob 1: unused --> default
|
||||
{"iop/colorbalancergb/global vibrance"},
|
||||
{"iop/colorbalancergb/contrast"},
|
||||
{"iop/colorbalancergb/global chroma"},
|
||||
{"iop/colorbalancergb/global saturation"},
|
||||
{"iop/colorbalancergb/global brilliance"},
|
||||
nil, -- Knob 7: --> default
|
||||
nil -- Knob 8: --> default
|
||||
},
|
||||
|
||||
-- Channel Mixer RGB (Color Calibration): Tab control and sliders
|
||||
channelmixerrgb = {
|
||||
nil, -- Knob 1-4: --> default
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
{"iop/channelmixerrgb/page", "CAT", "tab"}, -- Knob 5: Tab selector
|
||||
{"iop/focus/sliders", "1st"}, -- Knob 6: First slider
|
||||
{"iop/focus/sliders", "2nd"}, -- Knob 7: Second slider
|
||||
{"iop/focus/sliders", "3rd"} -- Knob 8: Third slider
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
local DEFAULT_MAPPINGS = {
|
||||
{"iop/exposure/exposure"},
|
||||
{"iop/filmicrgb/white relative exposure"},
|
||||
{"iop/filmicrgb/black relative exposure"},
|
||||
{"iop/filmicrgb/contrast"},
|
||||
{"iop/crop/left"},
|
||||
{"iop/crop/right"},
|
||||
{"iop/crop/top"},
|
||||
{"iop/crop/bottom"}
|
||||
}
|
||||
-- Mask properties: Always checked first, regardless of module focus
|
||||
local MASK_PROPERTIES = {
|
||||
{"lib/masks/properties/opacity"},
|
||||
{"lib/masks/properties/size"},
|
||||
{"lib/masks/properties/feather"},
|
||||
{"lib/masks/properties/hardness"},
|
||||
{"lib/masks/properties/rotation"},
|
||||
{"lib/masks/properties/curvature"},
|
||||
{"lib/masks/properties/compression"}
|
||||
}
|
||||
|
||||
-- Priority order for checking module focus (most commonly used first)
|
||||
local MODULE_CHECK_ORDER = {
|
||||
"toneequal",
|
||||
"colorequal",
|
||||
"colorbalancergb",
|
||||
"agx",
|
||||
"sigmoid",
|
||||
"primaries",
|
||||
"channelmixerrgb",
|
||||
"colorzones"
|
||||
}
|
||||
|
||||
-- ============================================================================
|
||||
-- CORE LOGIC - No need to edit below unless changing architecture
|
||||
-- ============================================================================
|
||||
|
||||
-- Helper functions
|
||||
local function is_in_darkroom()
|
||||
return dt.gui.current_view() == darkroom_view
|
||||
end
|
||||
|
||||
local function is_valid(value)
|
||||
return value == value -- NaN check
|
||||
end
|
||||
|
||||
local function translate_tab_effect(effect)
|
||||
if effect == "up" then
|
||||
return "next"
|
||||
elseif effect == "down" then
|
||||
return "previous"
|
||||
else
|
||||
return "activate"
|
||||
end
|
||||
end
|
||||
|
||||
-- Execute an action configuration
|
||||
local function execute_action(action_config, element, effect, size)
|
||||
if not action_config then
|
||||
return nil
|
||||
end
|
||||
|
||||
local path = action_config[1]
|
||||
local elem = action_config[2] or element
|
||||
local action_type = action_config[3]
|
||||
|
||||
-- Handle special action types
|
||||
if action_type == "tab" then
|
||||
effect = translate_tab_effect(effect)
|
||||
end
|
||||
|
||||
return dt.gui.action(path, elem, effect, size)
|
||||
end
|
||||
|
||||
-- Try mask properties first
|
||||
local function try_mask_properties(knob_idx, element, effect, size)
|
||||
if knob_idx >= NUM_KNOBS then
|
||||
return NAN
|
||||
end
|
||||
|
||||
local mask_action = MASK_PROPERTIES[knob_idx]
|
||||
if not mask_action then
|
||||
return NAN
|
||||
end
|
||||
|
||||
local value = execute_action(mask_action, element, effect, size)
|
||||
return value
|
||||
end
|
||||
|
||||
-- Main knob handler
|
||||
local function handle_knob_action(knob_idx, element, effect, size)
|
||||
-- Early exit for non-darkroom view
|
||||
if not is_in_darkroom() then
|
||||
return NAN
|
||||
end
|
||||
|
||||
-- Try mask properties first (no focus check needed)
|
||||
local mask_value = try_mask_properties(knob_idx, element, effect, size)
|
||||
if is_valid(mask_value) then
|
||||
return mask_value
|
||||
end
|
||||
|
||||
-- Check focused modules in priority order
|
||||
for _, module_name in ipairs(MODULE_CHECK_ORDER) do
|
||||
local focus_status = dt.gui.action("iop/" .. module_name, "focus")
|
||||
|
||||
if focus_status ~= 0 then
|
||||
-- Module has focus, get its profile
|
||||
local profile = MODULE_PROFILES[module_name]
|
||||
|
||||
if profile then
|
||||
local action_config = profile[knob_idx]
|
||||
|
||||
-- If profile has explicit mapping for this knob, use it
|
||||
if action_config then
|
||||
local result = execute_action(action_config, element, effect, size)
|
||||
if result then
|
||||
return result
|
||||
end
|
||||
else
|
||||
-- Profile exists but this knob is nil -> use default
|
||||
-- This avoids checking other modules when we know the focused one
|
||||
local default_action = DEFAULT_MAPPINGS[knob_idx]
|
||||
return execute_action(default_action, element, effect, size) or NAN
|
||||
end
|
||||
end
|
||||
|
||||
-- If no profile exists for this module, continue checking others
|
||||
end
|
||||
end
|
||||
|
||||
-- No module has focus or no profile matched, fall back to default
|
||||
local default_action = DEFAULT_MAPPINGS[knob_idx]
|
||||
return execute_action(default_action, element, effect, size) or NAN
|
||||
end
|
||||
|
||||
-- Setup mimic sliders
|
||||
local function setup_knobs()
|
||||
for k = 1, NUM_KNOBS do
|
||||
dt.gui.mimic("slider", "knob " .. k,
|
||||
function(action, element, effect, size)
|
||||
local knob_idx = tonumber(action:sub(-1))
|
||||
return handle_knob_action(knob_idx, element, effect, size)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
-- Script metadata
|
||||
local script_data = {
|
||||
metadata = {
|
||||
script_data.metadata = {
|
||||
name = _("x-touch"),
|
||||
purpose = _("data-driven x-touch midi controller configuration"),
|
||||
author = "Martin Straeten",
|
||||
purpose = _("example of how to control an x-touch midi device"),
|
||||
author = "Diederik ter Rahe",
|
||||
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/x-touch"
|
||||
},
|
||||
destroy = function() end,
|
||||
destroy_method = nil,
|
||||
restart = nil,
|
||||
show = nil
|
||||
}
|
||||
|
||||
-- Initialize
|
||||
setup_knobs()
|
||||
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
|
||||
|
||||
-- set up 8 mimic sliders with the same function
|
||||
for k = 1,8 do
|
||||
dt.gui.mimic("slider", "knob ".. k,
|
||||
function(action, element, effect, size)
|
||||
-- take the number from the mimic name
|
||||
local k = tonumber(action:sub(-1))
|
||||
|
||||
-- only operate in darkroom; return NAN otherwise
|
||||
if dt.gui.current_view() ~= dt.gui.views.darkroom then
|
||||
return 0/0
|
||||
end
|
||||
|
||||
local maskval = 0/0
|
||||
if k < 8 then
|
||||
-- first try if the mask slider at that position is active
|
||||
local s = { "opacity",
|
||||
"size",
|
||||
"feather",
|
||||
"hardness",
|
||||
"rotation",
|
||||
"curvature",
|
||||
"compression" }
|
||||
maskval = dt.gui.action("lib/masks/properties/" .. s[k],
|
||||
element, effect, size)
|
||||
end
|
||||
-- if a value different from NAN is returned, the slider was active
|
||||
if maskval == maskval then
|
||||
return maskval
|
||||
|
||||
-- try if colorzones module is focused; if so select element of graph
|
||||
elseif dt.gui.action("iop/colorzones", "focus") ~= 0 then
|
||||
which = "iop/colorzones/graph"
|
||||
local e = { "red",
|
||||
"orange",
|
||||
"yellow",
|
||||
"green",
|
||||
"aqua",
|
||||
"blue",
|
||||
"purple",
|
||||
"magenta" }
|
||||
element = e[k]
|
||||
|
||||
-- try if colorequalizer module is focused; if so select element of graph
|
||||
elseif dt.gui.action("iop/colorequal", "focus") ~= 0 then
|
||||
local e = { "red",
|
||||
"orange",
|
||||
"yellow",
|
||||
"green",
|
||||
"cyan",
|
||||
"blue",
|
||||
"lavender",
|
||||
"magenta" }
|
||||
which = "iop/colorequal/graph"
|
||||
element = e[k]
|
||||
|
||||
-- if the sigmoid rgb primaries is focused,
|
||||
-- check sliders
|
||||
elseif dt.gui.action("iop/sigmoid", "focus") ~= 0 and k <8 then
|
||||
local e = { "red attenuation", "red rotation", "green attenuation", "green rotation", "blue attenuation", "blue rotation", "recover purity" }
|
||||
which = "iop/sigmoid/primaries/"..e[k]
|
||||
|
||||
-- if the rgb primaries is focused,
|
||||
-- check sliders
|
||||
elseif dt.gui.action("iop/primaries", "focus") ~= 0 and k >=1 then
|
||||
local e = { "red hue", "red purity", "green hue", "green purity", "blue hue", "blue purity", "tint hue", "tint purity" }
|
||||
which = "iop/primaries/" ..e[k]
|
||||
|
||||
-- if the tone equalizer is focused,
|
||||
-- select one of the sliders in the "simple" tab
|
||||
elseif dt.gui.action("iop/toneequal", "focus") ~= 0 then
|
||||
which ="iop/toneequal/simple/"..(k-9).." EV"
|
||||
|
||||
-- if color calibration is focused,
|
||||
-- the last 4 knobs are sent there
|
||||
elseif dt.gui.action("iop/channelmixerrgb", "focus") ~= 0
|
||||
and k >= 5 then
|
||||
-- knob 5 selects the active tab; pressing it resets to CAT
|
||||
if k == 5 then
|
||||
which = "iop/channelmixerrgb/page"
|
||||
element = "CAT"
|
||||
-- since the tab action is not a slider,
|
||||
-- the effects need to be translated
|
||||
if effect == "up" then effect = "next"
|
||||
elseif effect == "down" then effect = "previous"
|
||||
else effect = "activate"
|
||||
end
|
||||
else
|
||||
-- knobs 6, 7 and 8 are for the three color sliders on each tab
|
||||
which = "iop/focus/sliders"
|
||||
local e = { "1st",
|
||||
"2nd",
|
||||
"3rd" }
|
||||
element = e[k - 5]
|
||||
end
|
||||
|
||||
-- the 4th knob is contrast;
|
||||
-- either colorbalance if it is focused, or filmic
|
||||
elseif dt.gui.action("iop/colorbalancergb", "focus") ~= 0
|
||||
and k == 4 then
|
||||
which = "iop/colorbalancergb/contrast"
|
||||
|
||||
-- in all other cases use a default selection
|
||||
else
|
||||
local s = { "iop/exposure/exposure",
|
||||
"iop/filmicrgb/white relative exposure",
|
||||
"iop/filmicrgb/black relative exposure",
|
||||
"iop/filmicrgb/contrast",
|
||||
"iop/crop/left",
|
||||
"iop/crop/right",
|
||||
"iop/crop/top",
|
||||
"iop/crop/bottom" }
|
||||
which = s[k]
|
||||
end
|
||||
|
||||
-- now pass the element/effect/size to the selected slider
|
||||
return dt.gui.action(which, element, effect, size)
|
||||
end)
|
||||
end
|
||||
|
||||
local function destroy()
|
||||
-- nothing to destroy
|
||||
end
|
||||
|
||||
script_data.destroy = destroy
|
||||
|
||||
return script_data
|
||||
|
||||
@@ -543,13 +543,10 @@ local PLACEHOLDERS = {"ROLL.NAME",
|
||||
"FILE.NAME",
|
||||
"FILE.EXTENSION",
|
||||
"ID",
|
||||
"IMAGE.ID",
|
||||
"IMAGE.ID.NEXT",
|
||||
"VERSION",
|
||||
"VERSION.IF.MULTI",
|
||||
"VERSION.NAME",
|
||||
"DARKTABLE.VERSION",
|
||||
"Xmp.darktable.version",
|
||||
"DARKTABLE.NAME", -- Not Implemented
|
||||
"SEQUENCE",
|
||||
"WIDTH.SENSOR",
|
||||
@@ -615,15 +612,10 @@ local PLACEHOLDERS = {"ROLL.NAME",
|
||||
"LABELS",
|
||||
"LABELS.ICONS", -- Not Implemented
|
||||
"TITLE",
|
||||
"Xmp.dc.title",
|
||||
"DESCRIPTION",
|
||||
"Xmp.dc.description",
|
||||
"CREATOR",
|
||||
"Xmp.dc.creator",
|
||||
"PUBLISHER",
|
||||
"Xmp.dc.publisher",
|
||||
"RIGHTS",
|
||||
"Xmp.dc.rights",
|
||||
"TAGS", -- Not Implemented
|
||||
"SIDECAR.TXT", -- Not Implemented
|
||||
"FOLDER.PICTURES",
|
||||
@@ -660,37 +652,6 @@ local function get_colorlabels(image)
|
||||
return labels
|
||||
end
|
||||
|
||||
-- handle the ROLL.NAME[n] case
|
||||
|
||||
local function build_rollname_substitution_list(image, variable_string)
|
||||
local old_log_level = log.log_level()
|
||||
log.log_level(dtutils_string.log_level)
|
||||
|
||||
for match in string.gmatch(variable_string, "%$%(.-%)?%)") do -- grab each complete variable
|
||||
log.msg(log.info, "match is " .. match)
|
||||
|
||||
local var = string.match(match, "%$%((.-%)?)%)") -- strip of the leading $( and trailing )
|
||||
log.msg(log.info, "var is " .. var)
|
||||
|
||||
local element = nil
|
||||
|
||||
if string.match(var, "ROLL.NAME%[") then
|
||||
element = string.match(var, "%[(%d)%]")
|
||||
|
||||
local path = du.split(image.film.path, dt.configuration.running_os ~= "windows" and "/" or "\\")
|
||||
|
||||
element = tonumber(element) + 1 -- add one because c arrays are 0 based and lua are 1 based
|
||||
|
||||
if element > #path then
|
||||
log.msg(log.warn, "ROLL.NAME element requested was more than number of elements in film roll")
|
||||
substitutes[var] = ""
|
||||
else
|
||||
substitutes[var] = path[(#path + 1) - element]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- find the $CATEGORYn and $CATEGORY[n,m] requests and add them to the substitute list
|
||||
|
||||
local function build_category_substitution_list(image, variable_string)
|
||||
@@ -773,29 +734,33 @@ function dtutils_string.build_substitute_list(image, sequence, variable_string,
|
||||
|
||||
local labels = get_colorlabels(image)
|
||||
|
||||
local emon_long, emon_short
|
||||
local eyear, emon, eday, ehour, emin, esec, emsec
|
||||
local datetime_taken = ""
|
||||
local use_millisecs = false
|
||||
if dt.preferences.read("darktable", "lighttable/ui/milliseconds", "bool") and is_api_9_1 then
|
||||
use_millisecs = true
|
||||
end
|
||||
|
||||
if image.exif_datetime_taken and image.exif_datetime_taken ~= "" then
|
||||
emon_long = os.date("%B", exiftime2systime(image.exif_datetime_taken))
|
||||
emon_short = os.date("%b", exiftime2systime(image.exif_datetime_taken))
|
||||
datetime_taken = image.exif_datetime_taken
|
||||
else
|
||||
if use_millisecs then
|
||||
datetime_taken = "0000:00:00 00:00:00.0"
|
||||
else
|
||||
datetime_taken = "0000:00:00 00:00:00"
|
||||
end
|
||||
end
|
||||
|
||||
local eyear, emon, eday, ehour, emin, esec, emsec
|
||||
if use_millisecs then
|
||||
eyear, emon, eday, ehour, emin, esec, emsec =
|
||||
string.match(image.exif_datetime_taken, "(%d+):(%d+):(%d+) (%d+):(%d+):(%d+)%.(%d+)$")
|
||||
string.match(datetime_taken, "(%d+):(%d+):(%d+) (%d+):(%d+):(%d+)%.(%d+)$")
|
||||
else
|
||||
emsec = "0"
|
||||
eyear, emon, eday, ehour, emin, esec =
|
||||
string.match(image.exif_datetime_taken, "(%d+):(%d+):(%d+) (%d+):(%d+):(%d+)$")
|
||||
end
|
||||
else
|
||||
log.msg(log.warn, "no capture date and time found in EXIF data")
|
||||
emon_long, emon_short, eyear, emon, eday, ehour, emin, esec, emsec = "", "", "", "", "", "", "", "", ""
|
||||
string.match(datetime_taken, "(%d+):(%d+):(%d+) (%d+):(%d+):(%d+)$")
|
||||
end
|
||||
|
||||
|
||||
local version_multi = #image:get_group_members() > 1 and image.duplicate_index or ""
|
||||
|
||||
local replacements = {dtutils_string.get_basename(image.film.path),-- ROLL.NAME
|
||||
@@ -803,13 +768,10 @@ function dtutils_string.build_substitute_list(image, sequence, variable_string,
|
||||
dtutils_string.get_basename(image.filename),-- FILE.NAME
|
||||
dtutils_string.get_filetype(image.filename),-- FILE.EXTENSION
|
||||
image.id, -- ID
|
||||
image.id, -- IMAGE.ID
|
||||
dt.database[#dt.database].id + 1, -- IMAGE.ID.NEXT
|
||||
image.duplicate_index, -- VERSION
|
||||
version_multi, -- VERSION.IF_MULTI
|
||||
image.version_name and image.version_name or "", -- VERSION.NAME
|
||||
dt.configuration.version, -- DARKTABLE.VERSION
|
||||
dt.configuration.version, -- Xmp.darktable.version
|
||||
"", -- DARKTABLE.NAME
|
||||
string.format("%04d", sequence), -- SEQUENCE
|
||||
image.width, -- WIDTH.SENSOR
|
||||
@@ -834,10 +796,10 @@ function dtutils_string.build_substitute_list(image, sequence, variable_string,
|
||||
string.format("%02d", datetime.sec), -- SECOND
|
||||
0, -- MSEC
|
||||
eyear, -- EXIF.YEAR
|
||||
(eyear ~= nil) and string.sub(eyear, 3) or "", -- EXIF.YEAR.SHORT
|
||||
string.sub(eyear, 3), -- EXIF.YEAR.SHORT
|
||||
emon, -- EXIF.MONTH
|
||||
emon_long, -- EXIF.MONTH.LONG
|
||||
emon_short, -- EXIF.MONTH.SHORT
|
||||
os.date("%B", exiftime2systime(datetime_taken)), -- EXIF.MONTH.LONG
|
||||
os.date("%b", exiftime2systime(datetime_taken)), -- EXIF.MONTH.SHORT
|
||||
eday, -- EXIF.DAY
|
||||
ehour, -- EXIF.HOUR
|
||||
"", -- EXIF.HOUR.AMPM
|
||||
@@ -875,22 +837,17 @@ function dtutils_string.build_substitute_list(image, sequence, variable_string,
|
||||
labels, -- LABELS
|
||||
"", -- LABELS.ICONS - wont be implemented
|
||||
image.title and image.title or "", -- TITLE
|
||||
image.title and image.title or "", -- Xmp.dc.title
|
||||
image.description and image.description or "", -- DESCRIPTION
|
||||
image.description and image.description or "", -- Xmp.dc.description
|
||||
image.creator and image.creator or "", -- CREATOR
|
||||
image.creator and image.creator or "", -- Xmp.dc.creator
|
||||
image.publisher and image.publisher or "", -- PUBLISHER
|
||||
image.publisher and image.publisher or "", -- Xmp.dc.publisher
|
||||
image.rights and image.rights or "", -- RIGHTS
|
||||
image.rights and image.rights or "", -- Xmp.dc.rights
|
||||
"", -- TAGS - wont be implemented
|
||||
"", -- SIDECAR.TXT - wont be implemented
|
||||
pictures_folder, -- FOLDER.PICTURES
|
||||
home_folder, -- FOLDER.HOME
|
||||
desktop_folder, -- FOLDER.DESKTOP
|
||||
"", -- OPENCL.ACTIVATED - wont be implemented
|
||||
(user_name ~= nil) and user_name or "",-- USERNAME
|
||||
user_name, -- USERNAME
|
||||
"", -- NL - wont be implemented
|
||||
"" -- JOBCODE - wont be implemented
|
||||
}
|
||||
@@ -905,7 +862,6 @@ function dtutils_string.build_substitute_list(image, sequence, variable_string,
|
||||
-- do category substitutions separately
|
||||
|
||||
build_category_substitution_list(image, variable_string)
|
||||
build_rollname_substitution_list(image, variable_string)
|
||||
|
||||
log.log_level(old_log_level)
|
||||
end
|
||||
@@ -933,13 +889,10 @@ function dtutils_string.get_substitution_tooltip()
|
||||
_("$(FILE.NAME) - basename of the input image"),
|
||||
_("$(FILE.EXTENSION) - extension of the input image"),
|
||||
_("$(ID) - image ID"),
|
||||
_("$(IMAGE.ID) - image ID"),
|
||||
_("$(IMAGE.ID.NEXT) - next image ID to be assigned on import"),
|
||||
_("$(VERSION) - duplicate version"),
|
||||
_("$(VERSION.IF_MULTI) - same as $(VERSION) but null string if only one version exists"),
|
||||
_("$(VERSION.NAME) - version name from metadata"),
|
||||
_("$(DARKTABLE.VERSION) - current darktable version"),
|
||||
_("$(Xmp.darktable.version) - current darktable version"),
|
||||
-- _("$(DARKTABLE.NAME) - darktable name"), -- not implemented
|
||||
_("$(SEQUENCE[n,m]) - sequence number, n: number of digits, m: start number"),
|
||||
_("$(WIDTH.SENSOR) - image sensor width"),
|
||||
@@ -1004,15 +957,10 @@ function dtutils_string.get_substitution_tooltip()
|
||||
_("$(LABELS) - color labels as text"),
|
||||
-- _("$(LABELS.ICONS) - color labels as icons"),-- not implemented
|
||||
_("$(TITLE) - title from metadata"),
|
||||
_("$(Xmp.dc.title) - title from metadata"),
|
||||
_("$(DESCRIPTION) - description from metadata"),
|
||||
_("$(Xmp.dc.description) - description from metadata"),
|
||||
_("$(CREATOR) - creator from metadata"),
|
||||
_("$(Xmp.dc.creator) - creator from metadata"),
|
||||
_("$(PUBLISHER) - publisher from metadata"),
|
||||
_("$(Xmp.dc.publisher) - publisher from metadata"),
|
||||
_("$(RIGHTS) - rights from metadata"),
|
||||
_("$(Xmp.dc.rights) - rights from metadata"),
|
||||
--_("$(TAGS) - tags as set in metadata settings"),
|
||||
_("$(CATEGORY[n,category]) - subtag of level n in hierarchical tags"),
|
||||
_("$(SIDECAR_TXT) - contents of .txt sidecar file, if present"),
|
||||
@@ -1062,11 +1010,6 @@ local function treat(var_string)
|
||||
if not ret_val then ret_val = "" end
|
||||
log.msg(log.info, "ret_val is " .. ret_val)
|
||||
|
||||
elseif string.match(var_string, "ROLL.NAME%[") then
|
||||
ret_val = substitutes[var_string]
|
||||
if not ret_val then ret_val = "" end
|
||||
log.msg(log.info, "ret_val is " .. ret_val)
|
||||
|
||||
elseif string.match(var_string, "SEQUENCE%[") then
|
||||
local width, start = string.match(var_string, "(%d+),(%d+)")
|
||||
local seq_val = tonumber(substitutes[var])
|
||||
@@ -1300,49 +1243,6 @@ function dtutils_string.substitute(image, sequence, variable_string, username, p
|
||||
return str
|
||||
end
|
||||
|
||||
dtutils_string.libdoc.functions["format_exposure"] = {
|
||||
Name = [[format_exposure]],
|
||||
Synopsis = [[format exif_exposure from dt_lua_image_t to an exposure time]],
|
||||
Usage = [[local ds = require "lib/dtutils.string"
|
||||
ds.format_exposure(image.exif_exposure)
|
||||
image.exif_exposure - dt_lua_image_t exif exposure field
|
||||
Description = [[format_exposure takes the EXIF exposure field and formats it into an exposure string]],
|
||||
Return_Value = [[result - string - the EXIF exposure formatted as an exposure string]],
|
||||
Limitations = [[]],
|
||||
Example = [[]],
|
||||
See_Also = [[https://docs.darktable.org/usermanual/4.6/en/special-topics/variables/]],
|
||||
Reference = [[]],
|
||||
License = [[]],
|
||||
Copyright = [[]],
|
||||
}
|
||||
|
||||
local function _nearbyintf(v)
|
||||
return math.floor(v + .5)
|
||||
end
|
||||
|
||||
function dtutils_string.format_exposure(exif_exposure)
|
||||
result = nil
|
||||
|
||||
if exif_exposure > 1.0 then
|
||||
if _nearbyintf(exif_exposure) == exif_exposure then
|
||||
result = string.format("%.0f″", exif_exposure)
|
||||
else
|
||||
result = string.format("%.1f″", exif_exposure)
|
||||
end
|
||||
-- catch everything below 0.3 seconds
|
||||
elseif exif_exposure < 0.2 then
|
||||
result = string.format("1/%.0f", 1.0 / exif_exposure)
|
||||
-- catch 1/2, 1/3, ...
|
||||
elseif _nearbyintf(1.0 / exif_exposure) == (1.0 / exif_exposure) then
|
||||
result = string.format("1/%.0f", 1.0 / exif_exposure)
|
||||
-- catch 1/1.3, 1/1.6, ...
|
||||
elseif (10 * _nearbyintf(10.0 / exif_exposure)) == _nearbyintf(100.0 / exif_exposure) then
|
||||
result = string.format("1/%.1f", 1.0 / exif_exposure)
|
||||
else
|
||||
result = string.format("%.1f″", exif_exposure)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
return dtutils_string
|
||||
|
||||
@@ -91,10 +91,8 @@ function dtutils_system.windows_command(command)
|
||||
file:write("chcp 65001>nul\n") -- change the encoding of the terminal to handle non-english characters in path
|
||||
file:write("\n")
|
||||
file:write(command .. "\n")
|
||||
file:write("set COMMAND_RC=%ERRORLEVEL%")
|
||||
file:write("\n")
|
||||
file:write("chcp %cp%>nul\n")
|
||||
file:write("exit /b %COMMAND_RC%") -- otherwise, when the real command fails, script will still return "good" error code of chcp
|
||||
file:close()
|
||||
|
||||
result = dt.control.execute(fname)
|
||||
|
||||
@@ -1,288 +0,0 @@
|
||||
--[[
|
||||
|
||||
auto_straighten.lua - straighten an image in darkroom using image metadata
|
||||
|
||||
Copyright (C) 2025 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/>.
|
||||
]]
|
||||
--[[
|
||||
auto_straighten - straighten an image in darkroom using image metadata
|
||||
|
||||
auto_straighten reads the roll and pitch angle imformation captured when
|
||||
an image is captured. A correction is computed and applied to correct the
|
||||
roll to the nearest vertical or horizontal axis using the rotate and
|
||||
perspective module.
|
||||
|
||||
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
|
||||
exiftool - https://exiftool.org
|
||||
|
||||
USAGE
|
||||
* add it sowewhere in your scripts directory
|
||||
* ensure exiftool is installed and accessible
|
||||
* enable it using script manager
|
||||
|
||||
BUGS, COMMENTS, SUGGESTIONS
|
||||
Bill Ferguson <wpferguson@gmail.com>
|
||||
|
||||
CHANGES
|
||||
|
||||
LIMITATIONS
|
||||
* Roll and pitch angles are taken when an image is captured. If you shoot
|
||||
in burst mode, the roll and pitch angles of the first image are used in
|
||||
the rest of the burst (at least for Canon).
|
||||
* If you are running multiple scripts that trigger on an image being opened
|
||||
in darkroom then the auto crop may not work. There is a shortcut included
|
||||
that can have a key sequence assigned so that the auto crop will be reapplied.
|
||||
* Currently oply roll angle is corrected
|
||||
|
||||
LIEENSE
|
||||
GPL V2
|
||||
]]
|
||||
|
||||
local dt = require "darktable"
|
||||
local du = require "lib/dtutils"
|
||||
-- local da = require "lib/dtutils.action"
|
||||
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"
|
||||
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- C O N S T A N T S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local MODULE <const> = "auto_straighten"
|
||||
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 ";"
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- A P I C H E C K
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
du.check_min_api_version("7.0.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 = _("auto_straighten"), -- visible name of script
|
||||
purpose = _("straighten an image in darkroom using image metadata"), -- 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/auto_straighten/" -- URL to help/documentation
|
||||
}
|
||||
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- L O G L E V E L
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
log.log_level(DEFAULT_LOG_LEVEL)
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- N A M E S P A C E
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local auto_straighten = {}
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- G L O B A L V A R I A B L E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
auto_straighten.pixelpipe_busy = false
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- P R E F E R E N C E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
dt.preferences.register(MODULE, "crop_area", "enum",
|
||||
_("automatic_cropping"),
|
||||
_("what automatic cropping to perform"),
|
||||
_("original format"),
|
||||
_("off"), _("largest area"), _("original format"))
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- A L I A S E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local namespace = auto_straighten
|
||||
local as = auto_straighten
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- F U N C T I O N S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local function determine_correction(roll_angle)
|
||||
|
||||
--[[
|
||||
Canon camera roll angle is as foillows
|
||||
* landscape orientation camera upright = 0
|
||||
* camera rolled to the left (shutter button up) portrait = -90
|
||||
* camera inverted landscape = +/- 180
|
||||
* camera rolled to the right (shutter button down) portrait = 90
|
||||
|
||||
rotate and perspective corrections
|
||||
* rotate the image to the right is a negative correction
|
||||
* rotate the image left is a positive correction
|
||||
]]
|
||||
|
||||
if roll_angle == 0 or -- landscape
|
||||
roll_angle == -90 or -- roll left portrait
|
||||
math.abs(roll_angle) == 180 or -- inverted landscape
|
||||
roll_angle == 90 then -- roll right portrait
|
||||
dt.print_log("on axis, no correction required")
|
||||
return 0
|
||||
elseif roll_angle > 0 and roll_angle <= 45 then
|
||||
return (roll_angle * -1.0)
|
||||
elseif roll_angle > 45 and roll_angle < 90 then
|
||||
return (90 - roll_angle)
|
||||
elseif roll_angle > 90 and roll_angle <= 135 then
|
||||
return (roll_angle - 90) * -1.0
|
||||
elseif roll_angle > 135 and roll_angle < 180 then
|
||||
return (180 - roll_angle)
|
||||
elseif roll_angle > -180 and roll_angle <= -135 then
|
||||
return (-180 - roll_angle)
|
||||
elseif roll_angle > -135 and roll_angle <= -90 then
|
||||
return (math.abs(roll_angle) - 90)
|
||||
elseif roll_angle > -90 and roll_angle <= -45 then
|
||||
return (-90 - roll_angle)
|
||||
else
|
||||
return roll_angle * -1.0
|
||||
end
|
||||
end
|
||||
|
||||
local function toggle_auto_crop()
|
||||
local crop = dt.preferences.read(MODULE, "crop_area", "enum")
|
||||
dt.gui.action("iop/ashift/automatic cropping", "selection", "item:off", 1.000)
|
||||
dt.control.sleep(100)
|
||||
dt.gui.action("iop/ashift/automatic cropping", "selection", "item:" .. crop, 1.000)
|
||||
end
|
||||
|
||||
local function straighten_image(image)
|
||||
|
||||
local roll = nil
|
||||
local pitch = nil
|
||||
|
||||
-- Roll Angle 0 is landscape, -90 is rolled to the left in portrait, +- 180 is landscape inverted, 90 is portrait rolled to the right
|
||||
|
||||
local crop = dt.preferences.read(MODULE, "crop_area", "enum")
|
||||
|
||||
local pipe = io.popen(as.exiftool .. " -RollAngle -PitchAngle " .. image.path .. PS .. image.filename)
|
||||
if pipe then
|
||||
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)
|
||||
-- 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
|
||||
pitch = tonumber(val)
|
||||
end
|
||||
end
|
||||
pipe:close()
|
||||
end
|
||||
|
||||
if roll and dt.gui.action("iop/ashift", "enable", "", "") == 0.0 then
|
||||
dt.print_log("straightening image")
|
||||
dt.gui.action("iop/ashift/rotation", "value", "set", roll)
|
||||
-- dt.gui.action("iop/ashift/automatic cropping", "selection", "item:" .. crop, 1.000)
|
||||
-- da.wait_until_pixelpipe_complete()
|
||||
toggle_auto_crop()
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- M A I N P R O G R A M
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
as.exiftool = df.check_if_bin_exists("exiftool")
|
||||
|
||||
if not as.exiftool then
|
||||
log.msg(log.error, "exiftool not found, exiting...")
|
||||
log.msg(log.screen, _("exiftool not found, exiting..."))
|
||||
return
|
||||
end
|
||||
|
||||
-- da.register_events(MODULE, straighten_image)
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- U S E R I N T E R F A C E
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- 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, "darkroom-image-loaded")
|
||||
end
|
||||
|
||||
script_data.destroy = destroy
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- E V E N T S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
---[[
|
||||
dt.register_event(MODULE, "darkroom-image-loaded",
|
||||
function(event, clean, image)
|
||||
if clean then
|
||||
straighten_image(image)
|
||||
end
|
||||
end
|
||||
)
|
||||
--]]
|
||||
|
||||
dt.register_event(MODULE, "shortcut",
|
||||
function(event, shortcut)
|
||||
toggle_auto_crop()
|
||||
end, "toggle auto crop off and on"
|
||||
)
|
||||
|
||||
return script_data
|
||||
@@ -1,568 +0,0 @@
|
||||
--[[
|
||||
|
||||
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
|
||||
@@ -1,694 +0,0 @@
|
||||
--[[
|
||||
|
||||
group_persistence.lua - preserve groups between darktable instances
|
||||
|
||||
Copyright (C) 2024-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/>.
|
||||
]]
|
||||
--[[
|
||||
group_persistence - preserve groups between darktable instances
|
||||
|
||||
group_persistence uses "functional" tagging to maintain image groups
|
||||
across database instances, allowing image grouping to be restored when
|
||||
the database is rebuilt from the XMP sidecar files.
|
||||
|
||||
Once the script is started it "listens" for changes in image grouping and
|
||||
applies or removes the necessary tags automatically.
|
||||
|
||||
A shortcut can be assigned to read a collection and apply the necessary
|
||||
grouping tags so that existing collections can be maintained.
|
||||
|
||||
If the database is lost and needs to be rebuilt then start the script before
|
||||
importing any images. As images are imported the tags will be read and the
|
||||
grouping applied in the database.
|
||||
|
||||
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
|
||||
|
||||
None
|
||||
|
||||
USAGE
|
||||
|
||||
* Enable the script using script manager
|
||||
* Optionally, create a keystroke combination for the shortcut
|
||||
|
||||
LIMITATIONS
|
||||
|
||||
In normal use you wont notice that the script is running.
|
||||
|
||||
However if you do something like import 4000+ images, then
|
||||
run the autogrouper script to create groups whenever there is
|
||||
a 5 second difference between 2 images, your system will be busy
|
||||
for awhile. If for some reason you need to do something like this
|
||||
turn off group_persistence, run the autogrouper, turn on group_persistence
|
||||
and use the shortcut to read all the grouping information and apply
|
||||
the necessary tags. It will still be an impact, but less than trying
|
||||
to do both at once. Learned from experience :-)
|
||||
|
||||
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"
|
||||
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- C O N S T A N T S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local MODULE <const> = "group_persistence"
|
||||
local DEFAULT_LOG_LEVEL <const> = log.error
|
||||
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 ";"
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- A P I C H E C K
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
du.check_min_api_version("9.4.0", MODULE) -- choose the minimum version that contains the features you need
|
||||
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- I 1 8 N
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local gettext = dt.gettext.gettext
|
||||
|
||||
dt.gettext.bindtextdomain(MODULE , dt.configuration.config_dir .. "/lua/locale/")
|
||||
|
||||
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 = _("group persistence"), -- name of script
|
||||
purpose = _("preserve groups between darktable instances"), -- 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/group_persistence" -- URL to help/documentation
|
||||
}
|
||||
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- L O G L E V E L
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
log.log_level(DEFAULT_LOG_LEVEL)
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- N A M E S P A C E
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local group_persistence = {}
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- A L I A S E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local namespace = group_persistence
|
||||
|
||||
local gp = group_persistence
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- G L O B A L V A R I A B L E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
-- table of imported images that need grouping rebuilt
|
||||
gp.imported_images = {}
|
||||
|
||||
-- uuid to group leader mapping
|
||||
gp.uuids = {}
|
||||
|
||||
-- group leader to uuid mapping
|
||||
gp.leaders = {}
|
||||
|
||||
-- uuid to group tag mapping
|
||||
gp.group_tags = {}
|
||||
|
||||
-- uuid to group leader tag mapping
|
||||
gp.leader_tags = {}
|
||||
|
||||
-- when the groups are rebuilt after import they
|
||||
-- cause the group change events to fire but the
|
||||
-- events are queued until after rebuilding completes
|
||||
-- so we need to build a table of images to ignore when
|
||||
-- the queued events catch up. As the event fires for an
|
||||
-- image, the image is removed.
|
||||
gp.rebuilt_images = {}
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- P R E F E R E N C E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- F U N C T I O N S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
-- utilities for building/debugging
|
||||
|
||||
local function dump_image_tags(image)
|
||||
log.msg(log.debug, "")
|
||||
|
||||
for _, tag in ipairs(image:get_tags()) do
|
||||
log.msg(log.debug, "tag for image " .. image.id .. " is " .. tag.name)
|
||||
end
|
||||
|
||||
log.msg(log.debug, "group_leader is " .. image.group_leader.id .. " and group has " .. #image:get_group_members() .. " members")
|
||||
|
||||
log.msg(log.debug, "")
|
||||
end
|
||||
|
||||
local function dump_group_tags(image)
|
||||
local leader = image.group_leader
|
||||
|
||||
for _, tag in ipairs(image:get_tags()) do
|
||||
if string.match(tag.name, "darktable|group") then
|
||||
log.msg(log.debug, "group leader image " .. image.id .. " has tag " .. tag.name)
|
||||
end
|
||||
end
|
||||
|
||||
for _, img in ipairs(image:get_group_members()) do
|
||||
for _, tag in ipairs(img:get_tags()) do
|
||||
if string.match(tag.name, "darktable|group") then
|
||||
log.msg(log.debug, "group member image " .. img.id .. " has tag " .. tag.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- rebuild grouping on import
|
||||
|
||||
local function save_group_leader(image, uuid)
|
||||
|
||||
log.log_level(DEFAULT_LOG_LEVEL)
|
||||
|
||||
log.msg(log.debug, "input image is " .. image.id .. " and uuid is " .. uuid)
|
||||
|
||||
gp.uuids[image] = uuid
|
||||
gp.leaders[uuid] = image
|
||||
|
||||
log.msg(log.debug, "after saving group leader uuid is " .. gp.leaders[uuid].id .. " and image is " .. gp.uuids[image])
|
||||
end
|
||||
|
||||
local function get_group_and_uuid(tags)
|
||||
|
||||
log.log_level(DEFAULT_LOG_LEVEL)
|
||||
|
||||
local group_type = nil
|
||||
local uuid = nil
|
||||
|
||||
for _, tag in ipairs(tags) do
|
||||
|
||||
if string.match(tag.name, "darktable|group") then
|
||||
|
||||
log.msg(log.debug, "found tag darktable|group")
|
||||
|
||||
local parts = du.split(tag.name, "|")
|
||||
|
||||
group_type = parts[2]
|
||||
uuid = parts[3]
|
||||
|
||||
log.msg(log.info, "found type " .. group_type .. " with uuid " .. uuid)
|
||||
end
|
||||
end
|
||||
|
||||
return group_type, uuid
|
||||
end
|
||||
|
||||
local function get_uuid_from_tag(tag)
|
||||
local parts = du.split(tag.name, "|")
|
||||
return parts[#parts]
|
||||
end
|
||||
|
||||
local function find_group_leader(uuid)
|
||||
|
||||
local group_leader = nil
|
||||
|
||||
local tag_name = "darktable|group_leader|" .. uuid
|
||||
|
||||
local found_tag = dt.tags.find(tag_name)
|
||||
|
||||
if found_tag then
|
||||
|
||||
group_leader = dt.tags.get_tagged_images(found_tag)[1]
|
||||
|
||||
end
|
||||
|
||||
return group_leader
|
||||
end
|
||||
|
||||
local function rebuild_groups()
|
||||
|
||||
log.log_level(DEFAULT_LOG_LEVEL)
|
||||
|
||||
for _, image in ipairs(gp.imported_images) do
|
||||
|
||||
log.msg(log.info, "processing image " .. image.id)
|
||||
|
||||
local tags = image:get_tags()
|
||||
|
||||
local group_type, uuid = get_group_and_uuid(tags)
|
||||
|
||||
if group_type =="group_leader" then
|
||||
|
||||
table.insert(gp.rebuilt_images, image)
|
||||
|
||||
log.msg(log.debug, "took group_leader branch")
|
||||
|
||||
save_group_leader(image, uuid)
|
||||
|
||||
elseif group_type == "group" then
|
||||
|
||||
table.insert(gp.rebuilt_images, image)
|
||||
|
||||
log.msg(log.debug, "took group branch")
|
||||
|
||||
local leader = gp.uuids[uuid]
|
||||
|
||||
if leader then
|
||||
|
||||
image:group_with(leader)
|
||||
|
||||
else
|
||||
|
||||
local group_leader = find_group_leader(uuid)
|
||||
|
||||
if group_leader then
|
||||
|
||||
save_group_leader(group_leader, uuid)
|
||||
image:group_with(group_leader)
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
gp.imported_images = {}
|
||||
end
|
||||
|
||||
-- rebuild grouping on command (shortcut)
|
||||
|
||||
local function is_group_leader(image)
|
||||
result = false
|
||||
|
||||
if image.group_leader == image and #image:get_group_members() > 1 then
|
||||
result = true
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local function has_group_tag(image)
|
||||
local result = false
|
||||
|
||||
for _, tag in ipairs(image:get_tags()) do
|
||||
if string.match(tag.name, "darktable|group|") then
|
||||
result = tag
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local function has_group_leader_tag(image)
|
||||
local result = false
|
||||
|
||||
for _, tag in ipairs(image:get_tags()) do
|
||||
if string.match(tag.name, "darktable|group_leader") then
|
||||
result = tag
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
-- handle grouping changes in the UI
|
||||
|
||||
local function group_exists(group_leader)
|
||||
local exists = false
|
||||
|
||||
local tags = group_leader:get_tags()
|
||||
|
||||
for _, tag in ipairs(tags) do
|
||||
if string.match(tag.name, "group_leader") then
|
||||
exists = true
|
||||
break
|
||||
end
|
||||
end
|
||||
return exists
|
||||
end
|
||||
|
||||
local function add_group_leader(image, group)
|
||||
-- tag image as group leader
|
||||
-- create the uuid and leader/group tags if necessary
|
||||
|
||||
log.log_level(DEFAULT_LOG_LEVEL)
|
||||
|
||||
local uuid = du.gen_uuid()
|
||||
|
||||
if uuid then
|
||||
log.msg(log.debug, "in add_group_leader and uuid is " .. uuid)
|
||||
else
|
||||
log.msg(log.debug, "uuid not created")
|
||||
end
|
||||
|
||||
local tag = dt.tags.create("darktable|group_leader|" .. uuid)
|
||||
|
||||
local group_tag = dt.tags.create("darktable|group|" .. uuid)
|
||||
|
||||
log.msg(log.info, "created group leader tag " .. tag.name)
|
||||
log.msg(log.info, "created group tag " .. group_tag.name)
|
||||
|
||||
gp.group_tags[uuid] = group_tag
|
||||
gp.leader_tags[uuid] = tag
|
||||
|
||||
gp.uuids[uuid] = image
|
||||
gp.leaders[image] = uuid
|
||||
|
||||
image:attach_tag(tag)
|
||||
log.msg(log.info, "attached tag " .. tag.name .. " to image " .. image.id)
|
||||
end
|
||||
|
||||
local function add_to_group(image, group_leader)
|
||||
-- add an image to a group
|
||||
-- create the group if necessary
|
||||
|
||||
log.log_level(DEFAULT_LOG_LEVEL)
|
||||
|
||||
if image == group_leader then
|
||||
|
||||
add_group_leader(image, group_leader)
|
||||
|
||||
else
|
||||
|
||||
if not group_exists(group_leader) then
|
||||
add_group_leader(group_leader, group_leader)
|
||||
end
|
||||
|
||||
local group_tag = nil
|
||||
|
||||
log.msg(log.debug, "image is " .. image.id .. " and group_leader is " .. image.group_leader.id)
|
||||
|
||||
local group, uuid = get_group_and_uuid(image.group_leader:get_tags())
|
||||
|
||||
group_tag = gp.group_tags[uuid]
|
||||
|
||||
if not group_tag then
|
||||
log.msg(log.info, "creating group tag darktable|group|" .. uuid)
|
||||
group_tag = dt.tags.create("darktable|group|" .. uuid)
|
||||
gp.group_tags[uuid] = group_tag
|
||||
end
|
||||
|
||||
image:attach_tag(group_tag)
|
||||
log.msg(log.info, "group tag attached to image " .. image.id)
|
||||
end
|
||||
end
|
||||
|
||||
local function build_group(leader)
|
||||
|
||||
if not has_group_leader_tag(leader) then
|
||||
add_group_leader(leader)
|
||||
end
|
||||
|
||||
for _, image in ipairs(leader:get_group_members()) do
|
||||
if not has_group_tag(image) then
|
||||
add_to_group(image, leader)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function process(image_set)
|
||||
local images
|
||||
|
||||
if image_set == "collection" then
|
||||
images = dt.collection
|
||||
else
|
||||
images = dt.gui.selection()
|
||||
if #images == 0 then
|
||||
images = dt.gui.action_images
|
||||
end
|
||||
end
|
||||
|
||||
for _, image in ipairs(images) do
|
||||
if is_group_leader(image) then
|
||||
build_group(image)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function check_and_remove_empty_group_data(group_tag)
|
||||
if not group_tag then
|
||||
return -- handle duplicate getting deleted
|
||||
end
|
||||
|
||||
local group_leader_tag = nil
|
||||
|
||||
group_leader_tag = dt.tags.find(string.gsub(group_tag.name, "|group|", "|group_leader|"))
|
||||
|
||||
local group_leader_count = 0
|
||||
local group_count = #dt.tags.get_tagged_images(group_tag)
|
||||
|
||||
if group_leader_tag then
|
||||
group_leader_count = #dt.tags.get_tagged_images(group_leader_tag)
|
||||
end
|
||||
|
||||
if group_leader_count + group_count < 2 then -- there's either 1 or 0, so no group
|
||||
local uuid = get_uuid_from_tag(group_tag)
|
||||
|
||||
if gp.group_tags[uuid] then
|
||||
gp.group_tags[uuid] = nil
|
||||
end
|
||||
|
||||
if gp.uuids[image] then
|
||||
gp.uuids[image] = nil
|
||||
end
|
||||
|
||||
if gp.leaders[uuid] then
|
||||
gp.leaders[uuid] = nil
|
||||
end
|
||||
|
||||
group_tag:delete()
|
||||
if group_leader_tag then
|
||||
group_leader_tag:delete()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function remove_group_leader(image, new_group_leader)
|
||||
-- remove image group leader tag
|
||||
-- if it's the last image, then remove the leader/group tags
|
||||
|
||||
log.log_level(DEFAULT_LOG_LEVEL)
|
||||
|
||||
dump_image_tags(image)
|
||||
|
||||
local group, uuid = get_group_and_uuid(image:get_tags())
|
||||
|
||||
local leader_tag = dt.tags.find("darktable|group_leader|" .. uuid)
|
||||
local group_tag = dt.tags.find("darktable|group|" .. uuid)
|
||||
|
||||
image:detach_tag(leader_tag)
|
||||
gp.leaders[image] = nil
|
||||
|
||||
log.msg(log.info, "removed image " .. image.id .. " as group leader for uuid " .. uuid)
|
||||
|
||||
new_group_leader:detach_tag(group_tag)
|
||||
new_group_leader:attach_tag(leader_tag)
|
||||
|
||||
log.msg(log.info, "added image " .. new_group_leader.id .. " as group leader for uuid " .. uuid)
|
||||
|
||||
gp.leaders[new_group_leader] = uuid
|
||||
gp.uuids[uuid] = new_group_leader
|
||||
|
||||
check_and_remove_empty_group_data(group_tag)
|
||||
end
|
||||
|
||||
local function remove_from_group(image, group_leader)
|
||||
-- remove an image from a group
|
||||
-- if it's the last image then remove the tags
|
||||
|
||||
-- image.id > group_leder.id - simple remove
|
||||
-- image.id < group_leader.id - deleting old group_leader (image) with new group_leader (group_leader)
|
||||
|
||||
log.log_level(DEFAULT_LOG_LEVEL)
|
||||
|
||||
if image.id < group_leader.id then
|
||||
|
||||
remove_group_leader(image, group_leader)
|
||||
|
||||
else
|
||||
|
||||
local group_tag = has_group_tag(image)
|
||||
|
||||
if group_tag then
|
||||
|
||||
image:detach_tag(group_tag)
|
||||
|
||||
log.msg(log.info, "image " .. image.filename .. " with id " .. image.id .. " removed from group " .. group_tag.name)
|
||||
end
|
||||
|
||||
check_and_remove_empty_group_data(group_tag)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function change_group_leader(image)
|
||||
-- remove the group leader tag from the old leader
|
||||
-- replace the old group leader tag with a group tag
|
||||
-- add the group leader tag to the new image
|
||||
|
||||
log.log_level(DEFAULT_LOG_LEVEL)
|
||||
|
||||
dump_image_tags(image)
|
||||
|
||||
local group, uuid = get_group_and_uuid(image:get_tags())
|
||||
|
||||
local leader_tag = dt.tags.find("darktable|group_leader|" .. uuid)
|
||||
local group_tag = dt.tags.find("darktable|group|" .. uuid)
|
||||
|
||||
old_leader = dt.tags.get_tagged_images(leader_tag)[1]
|
||||
|
||||
old_leader:detach_tag(leader_tag)
|
||||
log.msg(log.info, "remove leader tag from image " .. old_leader.id)
|
||||
old_leader:attach_tag(group_tag)
|
||||
log.msg(log.info, "attached group tag to image " .. old_leader.id)
|
||||
image:detach_tag(group_tag)
|
||||
log.msg(log.info, "removed group tag from new leader image " .. image.id)
|
||||
image:attach_tag(leader_tag)
|
||||
log.msg(log.info, "attached leader tag to new leader image " .. image.id)
|
||||
gp.leaders[uuid] = image
|
||||
gp.uuids[image] = uuid
|
||||
end
|
||||
|
||||
-- event handling
|
||||
|
||||
local function add_group_event()
|
||||
dt.register_event(MODULE, "image-group-information-changed",
|
||||
function(event, reason, image, other_image)
|
||||
log.msg(log.debug, "")
|
||||
log.msg(log.debug, "caught event with reason " .. reason .. " and image " .. image.id .. " and other image " .. other_image.id)
|
||||
log.msg(log.debug, "")
|
||||
for pos, rimage in ipairs(gp.rebuilt_images) do
|
||||
if rimage == image then
|
||||
table.remove(gp.rebuilt_images, pos)
|
||||
return
|
||||
end
|
||||
end
|
||||
if reason == "add" then
|
||||
log.msg(log.debug, "in add, image is " .. image.id .. " and group is " .. other_image.id)
|
||||
if image == other_image then
|
||||
add_group_leader(image, other_image)
|
||||
else
|
||||
add_to_group(image, other_image)
|
||||
end
|
||||
elseif reason == "remove" then
|
||||
log.msg(log.debug, "in remove with image " .. image.id .. " and group_id " .. other_image.id)
|
||||
remove_from_group(image, other_image)
|
||||
elseif reason == "remove-leader" then
|
||||
remove_group_leader(image, other_image)
|
||||
log.msg(log.debug, "in remove-leader with image " .. image.id .. " and new group id " .. other_image.id)
|
||||
else
|
||||
if image ~= other_image then
|
||||
log.msg(log.debug, "in change-leader with image " .. image.id .. " and other_id " .. other_image.id)
|
||||
change_group_leader(image)
|
||||
log.msg(log.debug, "in change-leader with image " .. image.id .. " and other_id " .. other_image.id)
|
||||
else
|
||||
log.msg(log.debug, "in change-leader with image " .. image.id .. " and other_id " .. other_image.id)
|
||||
log.msg(log.debug, "calling add_group_leader")
|
||||
add_group_leader(image, other_image)
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- M A I N P R O G R A M
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
dt.util.message(MODULE, "autogrouper", "running")
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- 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-image")
|
||||
dt.destroy_event(MODULE, "post-import-film")
|
||||
dt.destroy_event(MODULE, "image-group-information-changed")
|
||||
end
|
||||
|
||||
script_data.destroy = destroy
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- E V E N T S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
dt.register_event(MODULE, "post-import-image",
|
||||
function(event, image)
|
||||
table.insert(gp.imported_images, image)
|
||||
end
|
||||
)
|
||||
|
||||
dt.register_event(MODULE, "post-import-film",
|
||||
function(event, filmroll)
|
||||
rebuild_groups()
|
||||
end
|
||||
)
|
||||
|
||||
add_group_event()
|
||||
|
||||
dt.register_event(MODULE, "shortcut",
|
||||
function(event, string)
|
||||
dump_image_tags(dt.gui.action_images[1])
|
||||
end, "dump image tags"
|
||||
)
|
||||
|
||||
dt.register_event(MODULE, "shortcut",
|
||||
function(event, string)
|
||||
dump_group_tags(dt.gui.action_images[1])
|
||||
end, "dump group_tags"
|
||||
)
|
||||
|
||||
dt.register_event(MODULE, "shortcut",
|
||||
function(event, string)
|
||||
process("collection")
|
||||
end, "group collection"
|
||||
)
|
||||
|
||||
|
||||
return script_data
|
||||
@@ -1,343 +0,0 @@
|
||||
--[[
|
||||
|
||||
recent_bookmarks.lua - add recently edited and exported images to system recent bookmarks
|
||||
|
||||
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/>.
|
||||
]]
|
||||
--[[
|
||||
recent_bookmarks - add recently edited and exported images to system recently used files
|
||||
|
||||
recent_bookmarks adds images edited in darktable and the directory of images exported from
|
||||
darktable to the reccently-used.xbel file so that they show up in the recently used system
|
||||
shortcuts.
|
||||
|
||||
Currently recent_bookmarks only works on Linux
|
||||
|
||||
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
|
||||
|
||||
file - linux system file utility to get mime-type
|
||||
|
||||
USAGE
|
||||
|
||||
eanble from script manager
|
||||
|
||||
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("7.0.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 = _("recent_bookmarks"), -- visible name of script
|
||||
purpose = _("add recently edited and exported images to system recent bookmarks"), -- purpose of script
|
||||
author = "Bill Ferguson <wpferguson@gmail.com>", -- your name and optionally e-mail address
|
||||
help = "" -- URL to help/documentation
|
||||
}
|
||||
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- C O N S T A N T S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local MODULE <const> = "recent_bookmarks"
|
||||
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 ";"
|
||||
|
||||
-- format strings for writing the bookmarks
|
||||
|
||||
local BOOKMARK_FORMAT <const> = " <bookmark href=\"file://%s\" added=\"%s\" modified=\"%s\" visited=\"%s\">\n"
|
||||
local MIME_FORMAT <const> = " <mime:mime-type type=\"%s\"/>\n"
|
||||
local EXPORT_FORMAT <const> = " <bookmark:application name=\"xdg-open\" exec=\"'xdg-open %%u'\" modified=\"%s\" count=\"1\"/>\n"
|
||||
local EDIT_FORMAT <const> = " <bookmark:application name=\"darktable\" exec=\"'darktable %%u'\" modified=\"%s\" count=\"1\"/>\n"
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- L O G L E V E L
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
log.log_level(DEFAULT_LOG_LEVEL)
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- N A M E S P A C E
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local recent_bookmarks = {}
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- G L O B A L V A R I A B L E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
recent_bookmarks.log_level = DEFAULT_LOG_LEVEL
|
||||
recent_bookmarks.bookmark_file = nil
|
||||
recent_bookmarks.current_view = nil
|
||||
recent_bookmarks.in_darkroom = false
|
||||
recent_bookmarks.current_image = nil
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- P R E F E R E N C E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- A L I A S E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local namespace = recent_bookmarks
|
||||
local rb = recent_bookmarks
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- 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(namespace.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(namespace.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
|
||||
|
||||
--------------------
|
||||
-- program functions
|
||||
--------------------
|
||||
|
||||
local function zulu_timestamp()
|
||||
return os.date("!%Y-%m-%dT%H:%M:%SZ")
|
||||
end
|
||||
|
||||
local function get_mimetype(filepath)
|
||||
local mimetype = "application/octet-stream"
|
||||
local p = io.popen("file -L --mime-type " .. df.sanitize_filename(filepath))
|
||||
if p then
|
||||
for line in p:lines() do
|
||||
local result = string.match(line, ".-: (.+)$")
|
||||
if result then
|
||||
mimetype = result
|
||||
end
|
||||
end
|
||||
end
|
||||
return mimetype
|
||||
end
|
||||
|
||||
local function find_bookmark_file()
|
||||
local file = nil
|
||||
local home = os.getenv("HOME")
|
||||
if home then
|
||||
if df.test_file(home .. PS ..".local/share/recently-used.xbel", "e") then
|
||||
file = home .. PS ..".local/share/recently-used.xbel"
|
||||
end
|
||||
end
|
||||
return file
|
||||
end
|
||||
|
||||
local function write_bookmark_file(bookmark, mimetype, application)
|
||||
local file = io.open(rb.bookmark_file, "r+")
|
||||
if file then
|
||||
local pos = file:seek("end", -7)
|
||||
if pos > 0 then
|
||||
file:write(bookmark)
|
||||
file:write(" <info>\n <metadata owner=\"http://freedesktop.org\">\n")
|
||||
file:write(mimetype)
|
||||
file:write(" <bookmark:applications>\n")
|
||||
file:write(application)
|
||||
file:write(" </bookmark:applications>\n </metadata>\n </info>\n </bookmark>\n</xbel>")
|
||||
file:close()
|
||||
else
|
||||
log.msg(log.screen, _("failed to open bookmark file"))
|
||||
end
|
||||
else
|
||||
log.msg(log.screen, _("failed to open bookmark file"))
|
||||
end
|
||||
end
|
||||
|
||||
local function create_bookmark_string(filename, zulu)
|
||||
return string.format(BOOKMARK_FORMAT, filename, zulu, zulu, zulu)
|
||||
end
|
||||
|
||||
local function create_mimetype_string(mimetype)
|
||||
return string.format(MIME_FORMAT, mimetype)
|
||||
end
|
||||
|
||||
local function process_bookmark(image, filename)
|
||||
|
||||
local zulu = zulu_timestamp()
|
||||
local filepath = filename and filename or (image.path .. PS .. image.filename)
|
||||
local format = filename and EXPORT_FORMAT or EDIT_FORMAT
|
||||
local mimetype = get_mimetype(filepath)
|
||||
local bookmark_string = create_bookmark_string(filepath, zulu)
|
||||
local mime_string = create_mimetype_string(mimetype)
|
||||
local app_string = string.format(format, zulu)
|
||||
write_bookmark_file(bookmark_string, mime_string, app_string)
|
||||
end
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- M A I N P R O G R A M
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
if dt.configuration.running_os ~= "linux" then
|
||||
log.msg(log.screen, script_data.metadata.name .. _(" only runs on Linux"))
|
||||
return
|
||||
end
|
||||
|
||||
rb.bookmark_file = find_bookmark_file()
|
||||
if not rb.bookmark_file then
|
||||
log.msg(log.screen, _("recent bookmarks file not found"))
|
||||
return
|
||||
end
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- U S E R I N T E R F A C E
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- 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, "intermediate-export-image")
|
||||
dt.destroy_event(MODULE, "darkroom-image-loaded")
|
||||
dt.destroy_event(MODULE, "darkroom-image-history-changed")
|
||||
dt.destroy_event(MODULE, "view-changed")
|
||||
end
|
||||
|
||||
script_data.destroy = destroy
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- E V E N T S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
dt.register_event(MODULE, "intermediate-export-image",
|
||||
function(event, image, filename, format, storage)
|
||||
process_bookmark(image, filename)
|
||||
end
|
||||
)
|
||||
|
||||
dt.register_event(MODULE, "darkroom-image-loaded",
|
||||
function(event, clean, image)
|
||||
if clean then
|
||||
rb.in_darkroom = true
|
||||
rb.current_view = "darkroom"
|
||||
rb.current_image = image
|
||||
end
|
||||
end
|
||||
)
|
||||
|
||||
dt.register_event(MODULE, "darkroom-image-history-changed",
|
||||
function(event, image)
|
||||
if image == rb.current_image then
|
||||
process_bookmark(image)
|
||||
end
|
||||
end
|
||||
)
|
||||
|
||||
dt.register_event(MODULE, "view-changed",
|
||||
function(event, old_view, new_view)
|
||||
rb.current_view = new_view.id
|
||||
if rb.current_view == "darkroom" then
|
||||
rb.in_darkroom = true
|
||||
else
|
||||
if rb.in_darkroom then
|
||||
rb.in_darkroom = false
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
|
||||
return script_data
|
||||
@@ -76,10 +76,10 @@ script_data.restart = nil -- how to restart the (lib) script after it'
|
||||
script_data.show = nil -- only required for libs since the destroy_method only hides them
|
||||
|
||||
script_data.metadata = {
|
||||
name = _("regenerate thumbnails"), -- visible name of script
|
||||
name = _("regenerate_thumbnails"), -- visible name of script
|
||||
purpose = _("regenerate mipmap cache for selected images"), -- 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/regenerate_thumbnails" -- URL to help/documentation
|
||||
help = "https://docs.darktable.org/lua/development/lua.scripts.manual/scripts/official/regenerate_thumbnails" -- URL to help/documentation
|
||||
}
|
||||
|
||||
|
||||
@@ -173,7 +173,7 @@ local function generate_thumbnails(images)
|
||||
local has_job = false
|
||||
|
||||
if #images > 50 then
|
||||
rt.job = dt.gui.create_job(_("regenerating thumbnails"), true, stop_job)
|
||||
rt.job = dt.gui.create_job("regenerating thumbnails", true, stop_job)
|
||||
has_job = true
|
||||
end
|
||||
|
||||
@@ -243,7 +243,7 @@ dt.register_event(MODULE, "shortcut",
|
||||
if images then
|
||||
generate_thumbnails(images)
|
||||
end
|
||||
end, _("regenerate thumbnails")
|
||||
end, "regenerate thumbnails"
|
||||
)
|
||||
|
||||
dt.register_event(MODULE, "selection-changed",
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
--[[
|
||||
|
||||
scheduler.lua - provide scheduler services for multitasking
|
||||
|
||||
Copyright (C) 2024 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/>.
|
||||
]]
|
||||
--[[
|
||||
scheduler - provide scheduler services for multitasking
|
||||
|
||||
scheduler implements a sinple first in first out (FIFO) scheduler
|
||||
necessary for lua script multitasking.
|
||||
|
||||
Scripts voluntarily yield to the scheduler and they are placed at
|
||||
the end of the run queue. The first item in the run queue is told
|
||||
to resume.
|
||||
|
||||
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
|
||||
None
|
||||
|
||||
USAGE
|
||||
start script from script_manager
|
||||
|
||||
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"
|
||||
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- C O N S T A N T S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local MODULE <const> = "scheduler"
|
||||
local DEFAULT_LOG_LEVEL <const> = log.info
|
||||
local TMP_DIR <const> = dt.configuration.tmp_dir
|
||||
local BROADCAST_MSG <const> = "broadcast"
|
||||
|
||||
-- path separator
|
||||
local PS <const> = dt.configuration.running_os == "windows" and "\\" or "/"
|
||||
|
||||
-- command separator
|
||||
local CS <const> = dt.configuration.running_os == "windows" and "&" or ";"
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- A P I C H E C K
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
du.check_min_api_version("9.4.0", MODULE) -- choose the minimum version that contains the features you need
|
||||
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- I 1 8 N
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local gettext = dt.gettext.gettext
|
||||
|
||||
dt.gettext.bindtextdomain(MODULE , dt.configuration.config_dir .. "/lua/locale/")
|
||||
|
||||
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 = _("scheduler"), -- name of script
|
||||
purpose = _("provide scheduler services for multitasking"), -- 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/scheduler/" -- URL to help/documentation
|
||||
}
|
||||
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- L O G L E V E L
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
log.log_level(DEFAULT_LOG_LEVEL)
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- N A M E S P A C E
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local scheduler = {}
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- G L O B A L V A R I A B L E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
scheduler.run_queue = {}
|
||||
scheduler.message_queue = {}
|
||||
scheduler.log_level = log.debug
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- P R E F E R E N C E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
dt.preferences.register(MODULE, "target_time_slice_seconds", "float",
|
||||
_("scheduler preferred time slice"), _("target time slice before interrupt"), 1.0, 0.1, 1.0, 0.1)
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- A L I A S E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local sch = scheduler
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- F U N C T I O N S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local function remove_from_run_queue()
|
||||
|
||||
log.log_level(sch.log_level)
|
||||
|
||||
if #sch.run_queue > 0 then
|
||||
local module = sch.run_queue[1]
|
||||
local message = sch.message_queue[1]
|
||||
table.remove(sch.run_queue, 1)
|
||||
table.remove(sch.message_queue, 1)
|
||||
log.msg(log.info, "run queue length is " .. #sch.run_queue)
|
||||
log.msg(log.debug, "sending " .. message .. " to " .. module)
|
||||
dt.util.message(MODULE, module, message)
|
||||
else
|
||||
log.msg(log.warn, "run queue empty, nothing to resume")
|
||||
end
|
||||
end
|
||||
|
||||
local function add_to_run_queue(module, message)
|
||||
|
||||
log.log_level(sch.log_level)
|
||||
|
||||
table.insert(sch.run_queue, module)
|
||||
table.insert(sch.message_queue, message)
|
||||
|
||||
log.msg(log.info, "run queue length is " .. #sch.run_queue)
|
||||
|
||||
dt.control.sleep(500) -- allow time for waiting scripts to start
|
||||
|
||||
remove_from_run_queue()
|
||||
end
|
||||
|
||||
local function send_status(sender)
|
||||
dt.util.message(MODULE, sender, "running, queue length is " .. #sch.run_queue)
|
||||
end
|
||||
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- M A I N P R O G R A M
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
dt.util.message(MODULE, BROADCAST_MSG, "starting")
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- U S E R I N T E R F A C E
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- D A R K T A B L E I N T E G R A T I O N
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local function destroy()
|
||||
-- put things to destroy (events, storages, etc) here
|
||||
dt.util.message(MODULE, BROADCAST_MSG, "stopping")
|
||||
dt.destroy_event(MODULE, "inter-script-communication")
|
||||
end
|
||||
|
||||
script_data.destroy = destroy
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- E V E N T S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
dt.register_event(MODULE, "inter-script-communication",
|
||||
function(event, sender,receiver, message)
|
||||
|
||||
log.log_level(sch.log_level)
|
||||
|
||||
if receiver == "scheduler" then
|
||||
log.msg(log.debug, "got message from " .. sender .. " to " .. receiver .. " with message " .. message)
|
||||
if string.match(message, "^yield") then
|
||||
log.msg(log.debug, "got message " .. message .. " from " .. sender)
|
||||
add_to_run_queue(sender, string.gsub(message, "yield", "resume"))
|
||||
elseif message == "status" then
|
||||
send_status(sender)
|
||||
elseif messages == "done" then
|
||||
-- do nothing
|
||||
else
|
||||
log.msg(log.warn, "unrecognized message " .. message)
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
|
||||
return script_data
|
||||
@@ -1,266 +0,0 @@
|
||||
--[[
|
||||
|
||||
select_duplicates.lua - select duplicate images from a collection
|
||||
|
||||
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/>.
|
||||
]]
|
||||
--[[
|
||||
select_duplicates - select duplicate images from a collection
|
||||
|
||||
select_duplicates adds a button to the select module to select
|
||||
duplicate images from a collection. If the selection2collection
|
||||
script is also active an option is available to have the selected
|
||||
duplicates appear as a temporary collection.
|
||||
|
||||
A shortcut is available to select the duplicates from the collection
|
||||
but it is somewhat slower because it doesn't have access to the current
|
||||
image cache and therfore has to open each image separately.
|
||||
|
||||
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
|
||||
|
||||
None
|
||||
|
||||
USAGE
|
||||
|
||||
* enable in script manager
|
||||
* Press the button
|
||||
|
||||
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("7.0.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 = _("select duplicates"), -- visible name of script
|
||||
purpose = _("select duplicate images from a collection"), -- 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/select_duplicates/" -- URL to help/documentation
|
||||
}
|
||||
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- C O N S T A N T S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local MODULE <const> = "select_duplicates"
|
||||
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 ";"
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- L O G L E V E L
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
log.log_level(DEFAULT_LOG_LEVEL)
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- N A M E S P A C E
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local select_duplicates = {}
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- G L O B A L V A R I A B L E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
select_duplicates.log_level = DEFAULT_LOG_LEVEL
|
||||
select_duplicates.create_collection = false
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- P R E F E R E N C E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
dt.preferences.register(MODULE, "create_collection", "bool",
|
||||
script_data.metadata.name .. _(" create collection from selection"),
|
||||
_("create collection of selected duplicates"), false)
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- A L I A S E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local namespace = select_duplicates
|
||||
local sd = select_duplicates
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- 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(namespace.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(namespace.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 select_duplicate_files(event, images)
|
||||
local duplicates = {}
|
||||
local sorted_files = {}
|
||||
sd.create_collection = pref_read("create_collection", "bool")
|
||||
|
||||
for _, img in ipairs(images) do
|
||||
local rawname = img.path .. PS .. img.filename
|
||||
if not sorted_files[rawname] then
|
||||
sorted_files[rawname] = {}
|
||||
end
|
||||
table.insert(sorted_files[rawname], img)
|
||||
end
|
||||
for k,v in pairs(sorted_files) do
|
||||
if #v > 1 then
|
||||
local old_image = nil
|
||||
for _,i in ipairs(v) do
|
||||
if not old_image then
|
||||
old_image = i
|
||||
else
|
||||
if i.duplicate_index > old_image.duplicate_index then
|
||||
table.insert(duplicates, i)
|
||||
else
|
||||
table.insert(duplicates, old_image)
|
||||
old_image = i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if sd.create_collection then
|
||||
dt.util.message(MODULE, "selection2collection", "create")
|
||||
end
|
||||
return duplicates
|
||||
end
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- M A I N P R O G R A M
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- U S E R I N T E R F A C E
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
dt.gui.libs.select.register_selection(MODULE, _("select duplicates"), select_duplicate_files,
|
||||
_("select duplicated images"))
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- D A R K T A B L E I N T E G R A T I O N
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local function destroy()
|
||||
dt.gui.libs.select.destroy_selection(MODULE)
|
||||
end
|
||||
|
||||
script_data.destroy = destroy
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- E V E N T S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
if not dt.query_event(MODULE, "shortcut") then -- skip if shortcut already registered
|
||||
dt.register_event(MODULE, "shortcut",
|
||||
function(event, shortcut)
|
||||
local duplicates = select_duplicate_files(event, dt.collection)
|
||||
if duplicates then
|
||||
dt.gui.selection(duplicates)
|
||||
end
|
||||
end, _("select duplicates")
|
||||
)
|
||||
end
|
||||
|
||||
return script_data
|
||||
@@ -1,180 +0,0 @@
|
||||
--[[
|
||||
|
||||
select_raw_non_raw.lua - select raw or non raw files from a collection
|
||||
|
||||
Copyright (C) 2025 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/>.
|
||||
]]
|
||||
--[[
|
||||
select_raw_non_raw - select raw or non raw files from a collection
|
||||
|
||||
select_raw_non_raw adds two selection buttons, one to select raw
|
||||
images and one to select non raw images.
|
||||
|
||||
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
|
||||
None
|
||||
|
||||
USAGE
|
||||
* enable the script with script_manager
|
||||
|
||||
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"
|
||||
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- C O N S T A N T S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local MODULE <const> = "select_raw_non_raw"
|
||||
local DEFAULT_LOG_LEVEL <const> = log.warn
|
||||
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 ";"
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- A P I C H E C K
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
du.check_min_api_version("7.0.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 = _("select raw/non-raw"), -- name of script
|
||||
purpose = _("select raw or non raw files from collection"), -- 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/select_raw_non_raw" -- URL to help/documentation
|
||||
}
|
||||
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- L O G L E V E L
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
log.log_level(DEFAULT_LOG_LEVEL)
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- N A M E S P A C E
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local select_group_followers = {}
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- G L O B A L V A R I A B L E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- P R E F E R E N C E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- A L I A S E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local namespace = select_group_followers
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- F U N C T I O N S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local function select_raw_files(event, images)
|
||||
local raws = {}
|
||||
|
||||
for _, img in ipairs(images) do
|
||||
if img.is_raw then
|
||||
table.insert(raws, img)
|
||||
end
|
||||
end
|
||||
|
||||
return raws
|
||||
end
|
||||
|
||||
local function select_non_raw_files(event, images)
|
||||
local non_raws = {}
|
||||
|
||||
for _, img in ipairs(images) do
|
||||
if not img.is_raw then
|
||||
table.insert(non_raws, img)
|
||||
end
|
||||
end
|
||||
|
||||
return non_raws
|
||||
end
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- M A I N P R O G R A M
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- U S E R I N T E R F A C E
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
dt.gui.libs.select.register_selection(MODULE .. "_raws", _("select raw"), select_raw_files,
|
||||
_("select raw files"))
|
||||
dt.gui.libs.select.register_selection(MODULE .. "_non_raws", _("select non-raw"), select_non_raw_files,
|
||||
_("select non-raw files"))
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- D A R K T A B L E I N T E G R A T I O N
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local function destroy()
|
||||
-- put things to destroy (events, storages, etc) here
|
||||
dt.gui.libs.select.destroy_selection(MODULE .. "_raws")
|
||||
dt.gui.libs.select.destroy_selection(MODULE .. "_non_raws")
|
||||
end
|
||||
|
||||
script_data.destroy = destroy
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- E V E N T S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
return script_data
|
||||
@@ -1,168 +0,0 @@
|
||||
--[[
|
||||
|
||||
select_unaltered.lua - select images that have not been altered in any way
|
||||
|
||||
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/>.
|
||||
]]
|
||||
--[[
|
||||
select_unaltered - select images that have not been altered in any way
|
||||
|
||||
select_unaltered selects images from the current collection that have not
|
||||
been altered in any way. In other words just imported into darktable, not
|
||||
loaded into darkroom nor having any style applied.
|
||||
|
||||
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
|
||||
None
|
||||
|
||||
USAGE
|
||||
* enable with script_manager
|
||||
|
||||
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("7.0.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 = _("select unaltered"), -- visible name of script
|
||||
purpose = _("select images that have not been altered in any way"), -- 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/select_unaltered/" -- URL to help/documentation
|
||||
}
|
||||
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- C O N S T A N T S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local MODULE <const> = "select_unaltered"
|
||||
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 ";"
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- L O G L E V E L
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
log.log_level(DEFAULT_LOG_LEVEL)
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- N A M E S P A C E
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local select_unaltered = {}
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- G L O B A L V A R I A B L E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
select_unaltered.log_level = DEFAULT_LOG_LEVEL
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- P R E F E R E N C E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- A L I A S E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local namespace = select_unaltered
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- F U N C T I O N S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local function select_unaltered_files(event, images)
|
||||
local unaltered = {}
|
||||
|
||||
for _, img in ipairs(images) do
|
||||
if not img.is_altered then
|
||||
table.insert(unaltered, img)
|
||||
end
|
||||
end
|
||||
|
||||
return unaltered
|
||||
end
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- M A I N P R O G R A M
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- U S E R I N T E R F A C E
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
dt.gui.libs.select.register_selection(MODULE, _("select unaltered"), select_unaltered_files,
|
||||
_("select unaltered files"))
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- D A R K T A B L E I N T E G R A T I O N
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local function destroy()
|
||||
dt.gui.libs.select.destroy_selection(MODULE)
|
||||
end
|
||||
|
||||
script_data.destroy = destroy
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- E V E N T S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
return script_data
|
||||
@@ -1,354 +0,0 @@
|
||||
--[[
|
||||
|
||||
selection2collection.lua - create a temporary collection from a selection
|
||||
|
||||
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/>.
|
||||
]]
|
||||
--[[
|
||||
selection2collection - create a temporary collection from a selection
|
||||
|
||||
selection2collection creates a temporary collection, using functional
|
||||
tagging, from a selection. The collection lasts until darktable exits
|
||||
and then the collections are removed, but not the images.
|
||||
|
||||
To create the temporary collection, a function tag (darktable|functional|s2c)
|
||||
tag with a date and a time is applied to the selected images. The collection
|
||||
module filters on the tag to display the connection. More than one temporary
|
||||
collection can be created and collections can be changed by selecting the
|
||||
appropriate tag.
|
||||
|
||||
The tags are autmatically destroyed when darktable exits, thus removing the
|
||||
collections. There is a preference in the Lua options to keep the collections
|
||||
across darktable runs.
|
||||
|
||||
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
|
||||
|
||||
None
|
||||
|
||||
USAGE
|
||||
|
||||
* start from script_manager
|
||||
* make a selection
|
||||
* click the create temporary collection button the the actions on selected images
|
||||
module
|
||||
|
||||
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 = _("selection to collection"), -- visible name of script
|
||||
purpose = _("create a temporary collection from a selection"), -- 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/selection2collection/" -- URL to help/documentation
|
||||
}
|
||||
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- C O N S T A N T S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local MODULE <const> = "selection2collection"
|
||||
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 ";"
|
||||
|
||||
-- script specific
|
||||
|
||||
local TAG_HEADER <const> = "darktable|functional|s2c|"
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- L O G L E V E L
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
log.log_level(DEFAULT_LOG_LEVEL)
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- N A M E S P A C E
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local selection2collection = {}
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- G L O B A L V A R I A B L E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
selection2collection.log_level = DEFAULT_LOG_LEVEL
|
||||
selection2collection.keep_collections = false
|
||||
selection2collection.collection_rules = {}
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- P R E F E R E N C E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
dt.preferences.register(MODULE, "keep_collections", "bool",
|
||||
script_data.metadata.name .. _(" keep collections created from selection"),
|
||||
_("keep collections created from selections"), false)
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- A L I A S E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local namespace = selection2collection
|
||||
local s2c = selection2collection
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- 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(namespace.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(namespace.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
|
||||
|
||||
-------------------
|
||||
-- program functions
|
||||
-------------------
|
||||
|
||||
local function clear_collections()
|
||||
s2c.keep_collections = pref_read("keep_collections", "bool")
|
||||
|
||||
if not s2c.keep_collections then
|
||||
for _, tag in ipairs(dt.tags) do
|
||||
if string.match(tag.name, TAG_HEADER) then
|
||||
tag:delete()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function create_collection_tag()
|
||||
|
||||
local t = os.date("*t")
|
||||
|
||||
local ymd = string.format("%4d:%02d:%02d", t.year, t.month, t.day)
|
||||
local hms = string.format("%02d:%02d:%02d", t.hour, t.min, t.sec)
|
||||
|
||||
local tag_string = TAG_HEADER .. ymd .. "|" .. hms
|
||||
dt.print_log("tag_string is " .. tag_string)
|
||||
|
||||
local tag = dt.tags.create(tag_string)
|
||||
|
||||
return tag
|
||||
end
|
||||
|
||||
local function display_collection(tag_name)
|
||||
local collection_rules = dt.gui.libs.collect.filter()
|
||||
|
||||
local rule = dt.gui.libs.collect.new_rule()
|
||||
|
||||
rule.mode = "DT_LIB_COLLECT_MODE_AND"
|
||||
rule.data = tostring(tag_name)
|
||||
rule.item = "DT_COLLECTION_PROP_TAG"
|
||||
|
||||
table.insert(collection_rules, rule)
|
||||
|
||||
-- dt.gui.libs.collect.filter returns the previous rule set when you install another
|
||||
s2c.collection_rules = dt.gui.libs.collect.filter(collection_rules)
|
||||
end
|
||||
|
||||
local function create_collection()
|
||||
local images = dt.gui.selection()
|
||||
|
||||
if #images > 1 then
|
||||
|
||||
local tag = create_collection_tag()
|
||||
|
||||
if tag then
|
||||
for _, image in ipairs(images) do
|
||||
image:attach_tag(tag)
|
||||
end
|
||||
end
|
||||
|
||||
display_collection(tag.name)
|
||||
else
|
||||
log.msg(log.screen, _("not enough images to create a collection"))
|
||||
end
|
||||
end
|
||||
|
||||
local function communications_handler(sender, receiver, message)
|
||||
if receiver == MODULE then
|
||||
if message == "create" then
|
||||
create_collection()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function set_button_sensitive(images)
|
||||
if #images > 1 then
|
||||
dt.gui.libs.image.set_sensitive(MODULE, true)
|
||||
else
|
||||
dt.gui.libs.image.set_sensitive(MODULE, false)
|
||||
end
|
||||
end
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- M A I N P R O G R A M
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
s2c.keep_collections = pref_read("keep_collections", "bool")
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- U S E R I N T E R F A C E
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
dt.gui.libs.image.register_action(
|
||||
MODULE, _("create temporary collection"),
|
||||
function(event, images)
|
||||
create_collection()
|
||||
end,
|
||||
_("convert selection to collection")
|
||||
)
|
||||
|
||||
set_button_sensitive(dt.gui.selection())
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- 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, "inter-script-communication")
|
||||
dt.destroy_event(MODULE, "exit")
|
||||
dt.destroy_event(MODULE, "selection-changed")
|
||||
dt.gui.libs.image.destroy_action(MODULE, "create_collection")
|
||||
end
|
||||
|
||||
script_data.destroy = destroy
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- E V E N T S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
dt.register_event(MODULE, "inter-script-communication",
|
||||
function(event, sender, receiver, message)
|
||||
communications_handler(sender, receiver, message)
|
||||
end
|
||||
)
|
||||
|
||||
if not dt.query_event(MODULE, "shortcut") then -- skip if shortcut already registered
|
||||
dt.register_event(MODULE, "shortcut",
|
||||
function(event, shortcut)
|
||||
create_collection()
|
||||
end, _("convert selection to collection")
|
||||
)
|
||||
end
|
||||
|
||||
dt.register_event(MODULE, "exit",
|
||||
function(event)
|
||||
if not s2c.keep_collections then
|
||||
dt.gui.libs.collect.filter(s2c.collection_rules)
|
||||
clear_collections()
|
||||
end
|
||||
end
|
||||
)
|
||||
|
||||
dt.register_event(MODULE, "selection-changed",
|
||||
function(event)
|
||||
set_button_sensitive(dt.gui.selection())
|
||||
end
|
||||
)
|
||||
|
||||
return script_data
|
||||
@@ -54,7 +54,7 @@ local script_data = {}
|
||||
|
||||
script_data.metadata = {
|
||||
name = _("selection to PDF"),
|
||||
purpose = _("generate a PDF file of selected images"),
|
||||
purpose = _("generate a pdf file of selected images"),
|
||||
author = "Jérémy Rosen & Pascal Obry",
|
||||
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/selection_to_pdf"
|
||||
}
|
||||
@@ -66,8 +66,8 @@ script_data.show = nil -- only required for libs since the destroy_method only h
|
||||
|
||||
dt.preferences.register
|
||||
("selection_to_pdf","Open with","string",
|
||||
_("a PDF viewer"),
|
||||
_("can be an absolute pathname or the tool may be in the PATH\nif not specified the system default will be used to open the PDF"),
|
||||
_("a pdf viewer"),
|
||||
_("can be an absolute pathname or the tool may be in the PATH\nif not specified the system default will be used to open the pdf"),
|
||||
"")
|
||||
|
||||
local title_widget = dt.new_widget("entry") {
|
||||
@@ -134,7 +134,7 @@ local function destroy()
|
||||
dt.print_log("done destroying")
|
||||
end
|
||||
|
||||
dt.register_storage("export_pdf", _("export thumbnails to PDF"),
|
||||
dt.register_storage("export_pdf", _("export thumbnails to pdf"),
|
||||
nil,
|
||||
function(storage,image_table)
|
||||
local my_title = title_widget.text
|
||||
@@ -200,7 +200,7 @@ dt.register_storage("export_pdf", _("export thumbnails to PDF"),
|
||||
command = command .. " " .. pdffile
|
||||
local result = dt.control.execute(command)
|
||||
if result ~= 0 then
|
||||
dt.print(_("problem running PDF viewer")) -- this one is probably usefull to the user
|
||||
dt.print(_("problem running pdf viewer")) -- this one is probably usefull to the user
|
||||
error("Problem running "..command)
|
||||
end
|
||||
|
||||
|
||||
@@ -1,337 +0,0 @@
|
||||
--[[
|
||||
|
||||
use_paired_jpg_as_mipmap.lua - Use the JPG from the RAW JPG pair as the full size mipmap
|
||||
|
||||
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/>.
|
||||
]]
|
||||
--[[
|
||||
use_paired_jpg_as_mipmap - Use the JPG from the RAW JPG pair as the full size mipmap
|
||||
|
||||
use_paired_jpg_as_mipmap looks for RAW+JPEG image pairs as images are imported. After
|
||||
import the JPEG image is copied to the mipmap cache as the full resolution mipmap. Requests
|
||||
for smaller mipmap sizes can be satisfied by down sampling the full resolution mipmap. User's can
|
||||
decide whether or not to keep the paired JPEG files. The default is to keep them. If the
|
||||
user doesn't want them, they are deleted after being copied to the mipmap cache.
|
||||
|
||||
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
|
||||
|
||||
None
|
||||
|
||||
USAGE
|
||||
|
||||
- Add the script to the lua-scripts
|
||||
- Enable the script
|
||||
- Turn off the keep_jpgs preference if the JPEGS are not wanted
|
||||
|
||||
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("7.0.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 = _("use paired jpg as mipmap"), -- visible name of script
|
||||
purpose = _("Use the JPG from the RAW JPG pair as the full size mipmap"), -- 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/use_paired_jpg_as_mipmap/" -- URL to help/documentation
|
||||
}
|
||||
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- C O N S T A N T S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local MODULE <const> = "use_paired_jpg_as_mipmap"
|
||||
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 ";"
|
||||
|
||||
-- max mipmap size - 5.4.x = 8, after is 10
|
||||
local MAX_MIPMAP_SIZE <const> = (dt.configuration.version > "5.4.1") and "10" or "8"
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- L O G L E V E L
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
log.log_level(DEFAULT_LOG_LEVEL)
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- N A M E S P A C E
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local use_paired_jpg_as_mipmap = {}
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- G L O B A L V A R I A B L E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
use_paired_jpg_as_mipmap.log_level = DEFAULT_LOG_LEVEL
|
||||
use_paired_jpg_as_mipmap.imported_images = {}
|
||||
use_paired_jpg_as_mipmap.image_count = 0
|
||||
use_paired_jpg_as_mipmap.mipmap_dir = nil
|
||||
use_paired_jpg_as_mipmap.keep_jpgs = true
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- P R E F E R E N C E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
dt.preferences.register(MODULE, "keep_jpgs", "bool", script_data.metadata.name .. " - " .. _("keep JPEG files"),
|
||||
_("don't delete JPEGs after copying them to mipmap folder"), true)
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- A L I A S E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
local namespace = use_paired_jpg_as_mipmap
|
||||
local pmj = use_paired_jpg_as_mipmap
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- 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(namespace.log_level)
|
||||
|
||||
log.msg(log.debug, MODULE .. ": name is " .. name .. " and type is " .. pref_type)
|
||||
|
||||
local val = dt.preferences.read(MODULE, name, pref_type)
|
||||
|
||||
log.msg(log.debug, MODULE .. ": 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(namespace.log_level)
|
||||
|
||||
log.msg(log.debug, MODULE .. ": writing value " .. tostring(value) .. " for name " .. name)
|
||||
|
||||
dt.preferences.write(MODULE, name, pref_type, value)
|
||||
|
||||
restore_log_level(old_log_level)
|
||||
end
|
||||
|
||||
local function refresh_collection()
|
||||
local rules = dt.gui.libs.collect.filter()
|
||||
dt.gui.libs.collect.filter(rules)
|
||||
end
|
||||
|
||||
local function get_mipmap_dir()
|
||||
local old_log_level = set_log_level(pmj.log_level)
|
||||
local mipmap_dir = nil
|
||||
local cachedir = dt.configuration.cache_dir
|
||||
|
||||
local cmd = "cd " .. cachedir .. "; ls -d mip*"
|
||||
|
||||
if dt.configuration.running_os == "windows" then
|
||||
cmd = "forfiles /P \"" .. cachedir .. "\" /M mip* /C \"cmd /c echo @file\""
|
||||
end
|
||||
|
||||
local p = io.popen(cmd)
|
||||
if p then
|
||||
for line in p:lines() do
|
||||
if line:len() > 4 then
|
||||
mipmap_dir = cachedir .. PS .. line
|
||||
log.msg(log.info, MODULE .. ": mipmap_dir is " .. mipmap_dir)
|
||||
end
|
||||
end
|
||||
p:close()
|
||||
end
|
||||
|
||||
restore_log_level(old_log_level)
|
||||
return mipmap_dir
|
||||
end
|
||||
|
||||
local function stop_job(job)
|
||||
job.valid = false
|
||||
end
|
||||
|
||||
local function process_images(images, count)
|
||||
local old_log_level = set_log_level(pmj.log_level)
|
||||
|
||||
-- if dt.collection is equal to count there are no
|
||||
-- RAW+JPEG pairs
|
||||
|
||||
if count == #dt.collection then
|
||||
log.msg(log.screen, _("No RAW+JPEG pairs to process"))
|
||||
return
|
||||
end
|
||||
|
||||
local job = dt.gui.create_job(_("Generating cache from jpgs"), true, stop_job)
|
||||
local processed_count = 0
|
||||
local mipmap_dir = get_mipmap_dir() .. PS .. MAX_MIPMAP_SIZE .. PS
|
||||
|
||||
local rc = df.mkdir(mipmap_dir)
|
||||
log.msg(log.debug, MODULE .. ": rc from mkdir is " .. rc)
|
||||
|
||||
for k,v in pairs(images) do
|
||||
if job.valid then
|
||||
if v["raw"] and v["jpg"] then
|
||||
local raw_image_id = v["raw"].id
|
||||
local fname = v["jpg"].path .. PS .. v["jpg"].filename
|
||||
df.file_copy(fname, mipmap_dir .. raw_image_id .. ".jpg")
|
||||
if not pmj.keep_jpgs then
|
||||
log.msg(log.debug, MODULE .. ": removing jpg")
|
||||
v["jpg"]:delete()
|
||||
local success, msg = os.remove(v["jpg"].path .. PS .. v["jpg"].filename)
|
||||
if not success then
|
||||
log.msg(log.warn, MODULE .. ": unable to remove jpg - reason: " .. msg)
|
||||
end
|
||||
success, msg = os.remove(v["jpg"].path .. PS .. v["jpg"].filename .. ".xmp")
|
||||
if not success then
|
||||
log.msg(log.warn, MODULE .. ": unable to remove jpg xmp - reason: " .. msg)
|
||||
end
|
||||
end
|
||||
-- v["raw"]:generate_cache(true, 6, 6)
|
||||
processed_count = processed_count + 1
|
||||
end
|
||||
job.percent = processed_count / count
|
||||
end
|
||||
dt.control.sleep(5)
|
||||
end
|
||||
stop_job(job)
|
||||
if not pmj.keep_jpgs then
|
||||
refresh_collection()
|
||||
end
|
||||
dt.util.message(MODULE, "broadcast", "finished")
|
||||
end
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- M A I N P R O G R A M
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
use_paired_jpg_as_mipmap.keep_jpgs = pref_read("keep_jpgs", "bool")
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- U S E R I N T E R F A C E
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- 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")
|
||||
end
|
||||
|
||||
script_data.destroy = destroy
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- E V E N T S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
dt.register_event(MODULE, "post-import-film",
|
||||
function(event, film)
|
||||
process_images(pmj.imported_images, pmj.image_count)
|
||||
pmj.imported_images = {}
|
||||
pmj.image_count = 0
|
||||
end
|
||||
)
|
||||
|
||||
dt.register_event(MODULE, "post-import-image",
|
||||
function(event, image)
|
||||
local basename = df.get_basename(image.filename)
|
||||
local extension = string.lower(df.get_filetype(image.filename))
|
||||
|
||||
if not string.match(extension, "jpg") then
|
||||
extension = "raw"
|
||||
end
|
||||
|
||||
if not pmj.imported_images[basename] then
|
||||
pmj.imported_images[basename] = {}
|
||||
end
|
||||
|
||||
pmj.imported_images[basename][extension] = image
|
||||
|
||||
if not string.match(extension, "jpg") then
|
||||
pmj.image_count = pmj.image_count + 1
|
||||
end
|
||||
end
|
||||
)
|
||||
|
||||
return script_data
|
||||
@@ -54,7 +54,7 @@ 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 dtdebug = require "darktable.debug"
|
||||
local debug = require "darktable.debug"
|
||||
|
||||
local gettext = dt.gettext.gettext
|
||||
|
||||
@@ -62,19 +62,6 @@ local function _(msgid)
|
||||
return gettext(msgid)
|
||||
end
|
||||
|
||||
-- figure out where we are running from
|
||||
|
||||
local info = debug.getinfo(1, "S")
|
||||
local scripts_path = info.source:sub(2)
|
||||
|
||||
-- assume user based
|
||||
local system_based = false
|
||||
|
||||
if string.match(scripts_path, ds.sanitize_lua(dt.configuration.data_dir)) then
|
||||
log.msg(log.info, "script_manager using system based scripts")
|
||||
system_based = true
|
||||
end
|
||||
|
||||
-- api check
|
||||
|
||||
-- du.check_min_api_version("9.3.0", "script_manager")
|
||||
@@ -84,7 +71,7 @@ end
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
-- script_manager required API version
|
||||
local SM_API_VER_REQD <const> = "9.7.0"
|
||||
local SM_API_VER_REQD <const> = "9.3.0"
|
||||
|
||||
-- path separator
|
||||
local PS <const> = dt.configuration.running_os == "windows" and "\\" or "/"
|
||||
@@ -98,44 +85,21 @@ local MIN_BUTTONS_PER_PAGE <const> = 5
|
||||
local MAX_BUTTONS_PER_PAGE <const> = 20
|
||||
local DEFAULT_BUTTONS_PER_PAGE <const> = 10
|
||||
|
||||
local DEFAULT_LOG_LEVEL <const> = log.info
|
||||
|
||||
local USER_LUA_DIR <const> = dt.configuration.config_dir .. PS .. "lua"
|
||||
local SYSTEM_LUA_DIR <const> = dt.configuration.data_dir .. PS .. "lua-scripts"
|
||||
local DEFAULT_LOG_LEVEL <const> = log.warn
|
||||
|
||||
local LUA_DIR <const> = dt.configuration.config_dir .. PS .. "lua"
|
||||
local LUA_SCRIPT_REPO <const> = "https://github.com/darktable-org/lua-scripts.git"
|
||||
|
||||
local LUA_API_VER <const> = "API-" .. dt.configuration.api_version_string
|
||||
|
||||
-- local POWER_ICON = dt.configuration.config_dir .. "/lua/data/data/icons/power.png"
|
||||
local USER_POWER_ICON <const> = dt.configuration.config_dir .. "/lua/data/icons/path20.png"
|
||||
local USER_BLANK_ICON <const> = dt.configuration.config_dir .. "/lua/data/icons/blank20.png"
|
||||
local POWER_ICON <const> = dt.configuration.config_dir .. "/lua/data/icons/path20.png"
|
||||
local BLANK_ICON <const> = dt.configuration.config_dir .. "/lua/data/icons/blank20.png"
|
||||
|
||||
local SYSTEM_POWER_ICON <const> = dt.configuration.data_dir .. "/lua-scripts/data/icons/path20.png"
|
||||
local SYSTEM_BLANK_ICON <const> = dt.configuration.data_dir .. "/lua-scripts/data/icons/blank20.png"
|
||||
|
||||
local LUA_DIR = USER_LUA_DIR
|
||||
local POWER_ICON = USER_POWER_ICON
|
||||
local BLANK_ICON = USER_BLANK_ICON
|
||||
|
||||
if system_based then
|
||||
LUA_DIR = SYSTEM_LUA_DIR
|
||||
POWER_ICON = SYSTEM_POWER_ICON
|
||||
BLANK_ICON = SYSTEM_BLANK_ICON
|
||||
end
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- P R E F E R E N C E S
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
dt.preferences.register(MODULE, "disable_scripts", "bool",
|
||||
_("disable Lua scripts"),
|
||||
_("do not run the Lua scripts"),
|
||||
false)
|
||||
|
||||
if dt.preferences.read(MODULE, "disable_scripts", "bool") then
|
||||
return
|
||||
end
|
||||
|
||||
dt.preferences.register(MODULE, "check_update", "bool",
|
||||
_("check for updated scripts on start up"),
|
||||
_("automatically update scripts to correct version"),
|
||||
@@ -748,7 +712,7 @@ local function scan_scripts(script_dir)
|
||||
local find_cmd = "find -L " .. script_dir .. " -name \\*.lua -print | sort"
|
||||
|
||||
if dt.configuration.running_os == "windows" then
|
||||
find_cmd = "dir /b/s \"" .. script_dir .. "\\*.lua\" 2>nul | sort"
|
||||
find_cmd = "dir /b/s \"" .. script_dir .. "\\*.lua\" | sort"
|
||||
end
|
||||
|
||||
log.msg(log.debug, "find command is " .. find_cmd)
|
||||
@@ -757,12 +721,7 @@ local function scan_scripts(script_dir)
|
||||
local output = io.popen(find_cmd)
|
||||
for line in output:lines() do
|
||||
log.msg(log.debug, "line is " .. line)
|
||||
local script_dir = SYSTEM_LUA_DIR
|
||||
if string.match(line, ds.sanitize_lua(USER_LUA_DIR)) then
|
||||
script_dir = USER_LUA_DIR
|
||||
end
|
||||
-- add a string.match to figure out which lua dir to use
|
||||
local l = string.gsub(line, ds.sanitize_lua(script_dir) .. PS, "") -- strip the lua dir off
|
||||
local l = string.gsub(line, ds.sanitize_lua(LUA_DIR) .. PS, "") -- strip the lua dir off
|
||||
local script_file = l:sub(1,-5) -- strip off .lua\n
|
||||
if not string.match(script_file, "script_manager") then -- let's not include ourself
|
||||
if not string.match(script_file, "plugins") then -- skip plugins
|
||||
@@ -822,10 +781,8 @@ local function update_script_update_choices()
|
||||
|
||||
for i, repo in ipairs(sm.installed_repositories) do
|
||||
table.insert(installs, repo.name)
|
||||
if not string.match(pref_string, ds.sanitize_lua(repo.name)) then
|
||||
pref_string = pref_string .. i .. "," .. repo.name .. "," .. repo.directory .. ","
|
||||
end
|
||||
end
|
||||
|
||||
update_combobox_choices(sm.widgets.update_script_choices, installs, 1)
|
||||
|
||||
@@ -842,7 +799,7 @@ local function scan_repositories()
|
||||
local find_cmd = "find -L " .. LUA_DIR .. " -name \\*.git -print | sort"
|
||||
|
||||
if dt.configuration.running_os == "windows" then
|
||||
find_cmd = "dir /b/s /a:d " .. LUA_DIR .. PS .. "*.git 2>nul | sort"
|
||||
find_cmd = "dir /b/s /a:d " .. LUA_DIR .. PS .. "*.git | sort"
|
||||
end
|
||||
|
||||
log.msg(log.debug, "find command is " .. find_cmd)
|
||||
@@ -894,17 +851,8 @@ local function install_scripts()
|
||||
local url = sm.widgets.script_url.text
|
||||
local folder = sm.widgets.new_folder.text
|
||||
|
||||
if not df.test_file(USER_LUA_DIR, "d") then
|
||||
df.mkdir(USER_LUA_DIR)
|
||||
if not df.test_file(USER_LUA_DIR, "d") then
|
||||
log.msg(log.error, "unable to create user lua directory")
|
||||
log.msg(log.screen, _("unable to create user lua directory, installing additional scripts failed"))
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
if string.match(du.join(sm.folders, " "), ds.sanitize_lua(folder)) then
|
||||
log.msg(log.screen, string.format(_("folder %s is already in use. Please specify a different folder name."), folder))
|
||||
log.msg(log.screen, _(string.format("folder %s is already in use. Please specify a different folder name.", folder)))
|
||||
log.msg(log.error, "folder " .. folder .. " already exists, returning...")
|
||||
restore_log_level(old_log_level)
|
||||
return
|
||||
@@ -920,7 +868,7 @@ local function install_scripts()
|
||||
return
|
||||
end
|
||||
|
||||
local git_command = "cd " .. USER_LUA_DIR .. " " .. CS .. " " .. git .. " clone " .. url .. " " .. folder
|
||||
local git_command = "cd " .. LUA_DIR .. " " .. CS .. " " .. git .. " clone " .. url .. " " .. folder
|
||||
log.msg(log.debug, "update git command is " .. git_command)
|
||||
|
||||
if dt.configuration.running_os == "windows" then
|
||||
@@ -932,12 +880,12 @@ local function install_scripts()
|
||||
log.msg(log.info, "result from import is " .. result)
|
||||
|
||||
if result == 0 then
|
||||
local count = scan_scripts(USER_LUA_DIR)
|
||||
local count = scan_scripts(LUA_DIR .. PS .. folder)
|
||||
|
||||
if count > 0 then
|
||||
update_combobox_choices(sm.widgets.folder_selector, sm.folders, sm.widgets.folder_selector.selected)
|
||||
dt.print(string.format(_("scripts successfully installed into folder %s"), folder))
|
||||
table.insert(sm.installed_repositories, {name = folder, directory = USER_LUA_DIR .. PS .. folder})
|
||||
dt.print(_(string.format("scripts successfully installed into folder %s"), folder))
|
||||
table.insert(sm.installed_repositories, {name = folder, directory = LUA_DIR .. PS .. folder})
|
||||
update_script_update_choices()
|
||||
|
||||
for i = 1, #sm.widgets.folder_selector do
|
||||
@@ -1043,7 +991,6 @@ local function populate_buttons(folder, first, last)
|
||||
button_num = button_num + 1
|
||||
end
|
||||
|
||||
log.msg(log.debug, "button num is " .. button_num .. " and num_buttons is " .. sm.page_status.num_buttons)
|
||||
if button_num <= sm.page_status.num_buttons then
|
||||
for i = button_num, sm.page_status.num_buttons do
|
||||
clear_button(i)
|
||||
@@ -1296,7 +1243,7 @@ end
|
||||
|
||||
script_manager_running_script = "script_manager"
|
||||
|
||||
if (check_for_updates or SM_API_VER_REQD > dt.configuration.api_version_string) and not system_based then
|
||||
if check_for_updates or SM_API_VER_REQD > dt.configuration.api_version_string then
|
||||
local repo_data = get_repo_status(LUA_DIR)
|
||||
local current_branch = get_current_repo_branch(LUA_DIR)
|
||||
local clean = is_repo_clean(repo_data)
|
||||
@@ -1376,26 +1323,7 @@ if (check_for_updates or SM_API_VER_REQD > dt.configuration.api_version_string)
|
||||
end
|
||||
|
||||
scan_scripts(LUA_DIR)
|
||||
log.msg(log.info, "finished processing scripts")
|
||||
|
||||
if system_based then
|
||||
-- exec user luarc file if it exists
|
||||
local luarc = loadfile(USER_LUA_DIR .. "rc")
|
||||
if luarc then
|
||||
luarc()
|
||||
end
|
||||
|
||||
if df.test_file(USER_LUA_DIR, "d") then
|
||||
LUA_DIR = USER_LUA_DIR
|
||||
scan_scripts(LUA_DIR)
|
||||
LUA_DIR = SYSTEM_LUA_DIR
|
||||
end
|
||||
|
||||
dt.configuration["lua_dir"] = LUA_DIR
|
||||
|
||||
dt.print_log("lua dir is " .. dt.configuration.lua_dir)
|
||||
-- scan_script(USER_LUA_DIR)
|
||||
end
|
||||
log.msg(log.debug, "finished processing scripts")
|
||||
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
@@ -1636,7 +1564,7 @@ script_manager_running_script = nil
|
||||
-- D A R K T A B L E I N T E G R A T I O N
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
if (dt.gui.current_view().id == "lighttable") and darktable_gui_safe then
|
||||
if dt.gui.current_view().id == "lighttable" then
|
||||
install_module()
|
||||
else
|
||||
if not sm.event_registered then
|
||||
|
||||
Reference in New Issue
Block a user