Compare commits
113 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1629ee719 | ||
|
|
b32216d94e | ||
|
|
a55de740e5 | ||
|
|
dc31b221ef | ||
|
|
34b88490c2 | ||
|
|
d9b77bb3a2 | ||
|
|
cc198d6ca1 | ||
|
|
84582cbf29 | ||
|
|
9e7a18e290 | ||
|
|
8ff81fcfa4 | ||
|
|
b649e7a840 | ||
|
|
efa06ee34c | ||
|
|
77959422e0 | ||
|
|
0bff346b05 | ||
|
|
d05c494147 | ||
|
|
9cbbae9621 | ||
|
|
fb2aade198 | ||
|
|
7ffd8bdc27 | ||
|
|
b689dbc7ee | ||
|
|
027135a257 | ||
|
|
643057deaf | ||
|
|
a00f71f533 | ||
|
|
36d6db24e7 | ||
|
|
f62a5a817f | ||
|
|
44b40b305b | ||
|
|
97dade06cf | ||
|
|
30167631e7 | ||
|
|
8235de1f0a | ||
|
|
d67ada8300 | ||
|
|
c6788e4370 | ||
|
|
d238fcee61 | ||
|
|
28d71c989f | ||
|
|
74f020c809 | ||
|
|
80769a753a | ||
|
|
427359769e | ||
|
|
5e5b073cec | ||
|
|
c771c5fc1b | ||
|
|
7cdf1d4c7f | ||
|
|
b6ea6211db | ||
|
|
4d3924939b | ||
|
|
80ff1ef0aa | ||
|
|
af362315e8 | ||
|
|
28f3e9f6a2 | ||
|
|
4f05672809 | ||
|
|
8d8f295c77 | ||
|
|
a81ab437cf | ||
|
|
a94a0c2c90 | ||
|
|
0eda1b6ac5 | ||
|
|
58bc3a69df | ||
|
|
35398c55e5 | ||
|
|
1762b372b1 | ||
|
|
1907f9a1f5 | ||
|
|
b1dd3eca5a | ||
|
|
732d2788ca | ||
|
|
04908e3f0b | ||
|
|
3ba3facad2 | ||
|
|
cb209d1d56 | ||
|
|
36533b1004 | ||
|
|
cbbf0f413f | ||
|
|
181aea5bc2 | ||
|
|
94c6dff245 | ||
|
|
7a61d77c27 | ||
|
|
2635e261a0 | ||
|
|
d197851478 | ||
|
|
6f519b3ce8 | ||
|
|
03eb79e83a | ||
|
|
fb03b49a93 | ||
|
|
c0865c37bb | ||
|
|
182a2c96c3 | ||
|
|
06522802d8 | ||
|
|
ad263fed11 | ||
|
|
ced60016fb | ||
|
|
1d6d0695fe | ||
|
|
fbe504b47a | ||
|
|
7da38b3665 | ||
|
|
08fe1ba410 | ||
|
|
088c79e140 | ||
|
|
f06eebe316 | ||
|
|
d6a1cec9fa | ||
|
|
e194ad2bc7 | ||
|
|
22e3c59d69 | ||
|
|
eb743e4b3b | ||
|
|
7a8f3afc40 | ||
|
|
2b3910c698 | ||
|
|
c584ee05c9 | ||
|
|
bb3a74feac | ||
|
|
90d2764a5a | ||
|
|
b0373f7166 | ||
|
|
c6002e7eb7 | ||
|
|
a47de82afd | ||
|
|
7f09e55525 | ||
|
|
f83ac1366a | ||
|
|
6cda27172e | ||
|
|
df78522a79 | ||
|
|
6e64d6db6f | ||
|
|
7f00cb8d6c | ||
|
|
dfeff73fcd | ||
|
|
d75f00daa0 | ||
|
|
3e430ee2be | ||
|
|
3856ae3449 | ||
|
|
d54a9f8b7e | ||
|
|
8f6cc850fb | ||
|
|
e9f18a3d36 | ||
|
|
f3a89a89bd | ||
|
|
d075fb7128 | ||
|
|
7f4bae4234 | ||
|
|
071a3527c1 | ||
|
|
22c9a7cd6e | ||
|
|
12ccefcc3b | ||
|
|
fa6014fefa | ||
|
|
3c131c41d1 | ||
|
|
ffa125b134 | ||
|
|
056f616283 |
@@ -79,6 +79,7 @@ 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,7 +288,13 @@ 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' or arg_data.style == 'string' then
|
||||
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
|
||||
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)
|
||||
@@ -345,7 +351,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='..GUI.Target.compression_level_jpg.value..' '
|
||||
elseif GUI.Target.format.value == 'jpg' then ENF.arg_string = ENF.arg_string..'--compression='..tostring(math.floor(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
|
||||
@@ -492,6 +498,7 @@ 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
|
||||
|
||||
@@ -1165,4 +1172,4 @@ end
|
||||
|
||||
script_data.destroy = destroy
|
||||
|
||||
return script_data
|
||||
return script_data
|
||||
|
||||
@@ -211,7 +211,6 @@ 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 +223,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 " .. image.path .. PS .. image.filename)
|
||||
p = io.popen(exiv2 .. " -K Exif.Image.DateTime " .. df.sanitize_filename(image.path .. PS .. image.filename))
|
||||
if p then
|
||||
for line in p:lines() do
|
||||
if string.match(line, "Exif.Image.DateTime") then
|
||||
@@ -263,7 +262,7 @@ end
|
||||
|
||||
local function _get_nix_image_file_creation_time(image)
|
||||
local datetime = nil
|
||||
local p = io.popen("ls -lL --time-style=full-iso " .. image.path .. PS .. image.filename)
|
||||
local p = io.popen("ls -lL --time-style=full-iso " .. df.sanitize_filename(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,10 +264,6 @@ 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,9 +185,6 @@ local function do_rename(images)
|
||||
end
|
||||
end
|
||||
|
||||
local function reset_callback()
|
||||
rename.widgets.pattern.text = ""
|
||||
end
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- W I D G E T S
|
||||
@@ -196,7 +193,11 @@ end
|
||||
rename.widgets.pattern = dt.new_widget("entry"){
|
||||
tooltip = ds.get_substitution_tooltip(),
|
||||
placeholder = _("enter pattern") .. "$(FILE_FOLDER)/$(FILE_NAME)",
|
||||
text = ""
|
||||
text = "",
|
||||
reset_callback = function(self)
|
||||
self.text = ""
|
||||
dt.preferences.write(MODULE_NAME, "pattern", "string", self.text)
|
||||
end
|
||||
}
|
||||
|
||||
local pattern_pref = dt.preferences.read(MODULE_NAME, "pattern", "string")
|
||||
|
||||
@@ -20,14 +20,19 @@ 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 = "select_non_existing"
|
||||
local MODULE <const> = "select_non_existing"
|
||||
|
||||
du.check_min_api_version("9.1.0", MODULE)
|
||||
|
||||
-- figure out the path separator
|
||||
local PS = dt.configuration.running_os == "windows" and "\\" or "/"
|
||||
local PS <const> = dt.configuration.running_os == "windows" and "\\" or "/"
|
||||
local DEFAULT_LOG_LEVEL <const> = log.info
|
||||
|
||||
log.log_level(DEFAULT_LOG_LEVEL)
|
||||
|
||||
local gettext = dt.gettext.gettext
|
||||
|
||||
@@ -39,26 +44,61 @@ local function stop_job(job)
|
||||
job.valid = false
|
||||
end
|
||||
|
||||
local function select_nonexisting_images(event, images)
|
||||
local selection = {}
|
||||
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)
|
||||
|
||||
local job = dt.gui.create_job(_("select non existing images"), true, stop_job)
|
||||
for key,image in ipairs(images) do
|
||||
if(job.valid) then
|
||||
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
|
||||
else
|
||||
break
|
||||
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
|
||||
stop_job(job)
|
||||
|
||||
return selection
|
||||
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))
|
||||
if (not file_exists) then
|
||||
table.insert(selection, image)
|
||||
end
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
stop_job(job)
|
||||
|
||||
return selection
|
||||
end
|
||||
|
||||
local function destroy()
|
||||
|
||||
199
contrib/selected_image_visible.lua
Normal file
199
contrib/selected_image_visible.lua
Normal file
@@ -0,0 +1,199 @@
|
||||
--[[
|
||||
|
||||
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
|
||||
313
contrib/toggle_group_view.lua
Normal file
313
contrib/toggle_group_view.lua
Normal file
@@ -0,0 +1,313 @@
|
||||
--[[
|
||||
|
||||
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, _ in pairs(films) do
|
||||
for film, i 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
|
||||
|
||||
294
contrib/website_gallery_export.lua
Normal file
294
contrib/website_gallery_export.lua
Normal file
@@ -0,0 +1,294 @@
|
||||
--[[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
|
||||
56
data/website_gallery/css/gallery.css
Normal file
56
data/website_gallery/css/gallery.css
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
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;
|
||||
}
|
||||
99
data/website_gallery/css/modal.css
Normal file
99
data/website_gallery/css/modal.css
Normal file
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
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;
|
||||
}
|
||||
|
||||
66
data/website_gallery/css/style.css
Normal file
66
data/website_gallery/css/style.css
Normal file
@@ -0,0 +1,66 @@
|
||||
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;
|
||||
}
|
||||
46
data/website_gallery/index.html
Normal file
46
data/website_gallery/index.html
Normal file
@@ -0,0 +1,46 @@
|
||||
<!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>
|
||||
40
data/website_gallery/js/fullscreen.js
Normal file
40
data/website_gallery/js/fullscreen.js
Normal file
@@ -0,0 +1,40 @@
|
||||
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.');
|
||||
}
|
||||
};
|
||||
|
||||
112
data/website_gallery/js/gallery.js
Normal file
112
data/website_gallery/js/gallery.js
Normal file
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
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);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
355
data/website_gallery/js/modal.js
Normal file
355
data/website_gallery/js/modal.js
Normal file
@@ -0,0 +1,355 @@
|
||||
/*
|
||||
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;
|
||||
}
|
||||
});
|
||||
@@ -29,8 +29,7 @@
|
||||
by hovering over the image and pressing the key combination.
|
||||
|
||||
ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT
|
||||
* ufraw-batch - https://ufraw.sourceforge.net
|
||||
MacOS - install with homebrew
|
||||
* exiftool - https://exiftool.org
|
||||
|
||||
USAGE
|
||||
* require this script from your main lua file
|
||||
@@ -44,6 +43,7 @@
|
||||
* Send to Bill Ferguson, wpferguson@gmail.com
|
||||
|
||||
CHANGES
|
||||
20251205 - replaced ufraw-batch with exiftool
|
||||
]]
|
||||
|
||||
--[[
|
||||
@@ -139,6 +139,36 @@ 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
|
||||
@@ -152,11 +182,12 @@ local function extract_embedded_jpeg(images)
|
||||
in the user's path. When it finds the executable, it returns the command to run it.
|
||||
]]
|
||||
|
||||
local ufraw_executable = df.check_if_bin_exists("ufraw-batch")
|
||||
if ufraw_executable then
|
||||
local exiftool_executable = df.check_if_bin_exists("exiftool")
|
||||
if exiftool_executable then
|
||||
for _, image in ipairs(images) do
|
||||
if image.is_raw then
|
||||
local img_file = du.join({image.path, image.filename}, "/")
|
||||
local img_file = tostring(image)
|
||||
local jpg_img_file = string.gsub(img_file, "%....$", ".jpg")
|
||||
|
||||
--[[
|
||||
dtsys.external_command() is operating system aware and formats the command as necessary.
|
||||
@@ -164,8 +195,12 @@ local function extract_embedded_jpeg(images)
|
||||
operating system aware and uses the corresponding quotes.
|
||||
]]
|
||||
|
||||
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"
|
||||
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
|
||||
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)
|
||||
@@ -188,8 +223,8 @@ local function extract_embedded_jpeg(images)
|
||||
end
|
||||
end
|
||||
else
|
||||
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
|
||||
dt.print_error("exiftool not found. Exiting...") -- print debugging error message
|
||||
dt.print("exiftool not found, exiting...") -- print the error to the screen
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -51,6 +51,10 @@ 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"
|
||||
@@ -59,150 +63,283 @@ 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)
|
||||
local function _(msgid)
|
||||
return gettext(msgid)
|
||||
end
|
||||
|
||||
-- return data structure for script_manager
|
||||
-- Constants
|
||||
local NUM_KNOBS = 8
|
||||
local NAN = 0/0
|
||||
local darkroom_view = dt.gui.views.darkroom
|
||||
|
||||
local script_data = {}
|
||||
-- ============================================================================
|
||||
-- USER CONFIGURATION
|
||||
-- ============================================================================
|
||||
|
||||
script_data.metadata = {
|
||||
name = _("x-touch"),
|
||||
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"
|
||||
-- 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
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
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"}
|
||||
}
|
||||
|
||||
-- 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))
|
||||
-- Priority order for checking module focus (most commonly used first)
|
||||
local MODULE_CHECK_ORDER = {
|
||||
"toneequal",
|
||||
"colorequal",
|
||||
"colorbalancergb",
|
||||
"agx",
|
||||
"sigmoid",
|
||||
"primaries",
|
||||
"channelmixerrgb",
|
||||
"colorzones"
|
||||
}
|
||||
|
||||
-- only operate in darkroom; return NAN otherwise
|
||||
if dt.gui.current_view() ~= dt.gui.views.darkroom then
|
||||
return 0/0
|
||||
end
|
||||
-- ============================================================================
|
||||
-- CORE LOGIC - No need to edit below unless changing architecture
|
||||
-- ============================================================================
|
||||
|
||||
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
|
||||
-- Helper functions
|
||||
local function is_in_darkroom()
|
||||
return dt.gui.current_view() == darkroom_view
|
||||
end
|
||||
|
||||
-- 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]
|
||||
local function is_valid(value)
|
||||
return value == value -- NaN check
|
||||
end
|
||||
|
||||
-- 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]
|
||||
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 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"
|
||||
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
|
||||
-- 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]
|
||||
-- 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
|
||||
|
||||
-- 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)
|
||||
|
||||
-- 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
|
||||
|
||||
local function destroy()
|
||||
-- nothing to destroy
|
||||
-- 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_data.destroy = destroy
|
||||
-- Script metadata
|
||||
local script_data = {
|
||||
metadata = {
|
||||
name = _("x-touch"),
|
||||
purpose = _("data-driven x-touch midi controller configuration"),
|
||||
author = "Martin Straeten",
|
||||
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()
|
||||
|
||||
return script_data
|
||||
|
||||
@@ -542,11 +542,14 @@ local PLACEHOLDERS = {"ROLL.NAME",
|
||||
"FILE.FOLDER",
|
||||
"FILE.NAME",
|
||||
"FILE.EXTENSION",
|
||||
"ID",
|
||||
"ID",
|
||||
"IMAGE.ID",
|
||||
"IMAGE.ID.NEXT",
|
||||
"VERSION",
|
||||
"VERSION.IF.MULTI",
|
||||
"VERSION.NAME",
|
||||
"DARKTABLE.VERSION",
|
||||
"Xmp.darktable.version",
|
||||
"DARKTABLE.NAME", -- Not Implemented
|
||||
"SEQUENCE",
|
||||
"WIDTH.SENSOR",
|
||||
@@ -612,10 +615,15 @@ 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",
|
||||
@@ -652,6 +660,37 @@ 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)
|
||||
@@ -734,33 +773,29 @@ function dtutils_string.build_substitute_list(image, sequence, variable_string,
|
||||
|
||||
local labels = get_colorlabels(image)
|
||||
|
||||
local datetime_taken = ""
|
||||
local emon_long, emon_short
|
||||
local eyear, emon, eday, ehour, emin, esec, emsec
|
||||
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
|
||||
datetime_taken = image.exif_datetime_taken
|
||||
else
|
||||
emon_long = os.date("%B", exiftime2systime(image.exif_datetime_taken))
|
||||
emon_short = os.date("%b", exiftime2systime(image.exif_datetime_taken))
|
||||
if use_millisecs then
|
||||
datetime_taken = "0000:00:00 00:00:00.0"
|
||||
eyear, emon, eday, ehour, emin, esec, emsec =
|
||||
string.match(image.exif_datetime_taken, "(%d+):(%d+):(%d+) (%d+):(%d+):(%d+)%.(%d+)$")
|
||||
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(datetime_taken, "(%d+):(%d+):(%d+) (%d+):(%d+):(%d+)%.(%d+)$")
|
||||
emsec = "0"
|
||||
eyear, emon, eday, ehour, emin, esec =
|
||||
string.match(image.exif_datetime_taken, "(%d+):(%d+):(%d+) (%d+):(%d+):(%d+)$")
|
||||
end
|
||||
else
|
||||
emsec = "0"
|
||||
eyear, emon, eday, ehour, emin, esec =
|
||||
string.match(datetime_taken, "(%d+):(%d+):(%d+) (%d+):(%d+):(%d+)$")
|
||||
log.msg(log.warn, "no capture date and time found in EXIF data")
|
||||
emon_long, emon_short, eyear, emon, eday, ehour, emin, esec, emsec = "", "", "", "", "", "", "", "", ""
|
||||
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
|
||||
@@ -768,10 +803,13 @@ 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
|
||||
@@ -796,10 +834,10 @@ function dtutils_string.build_substitute_list(image, sequence, variable_string,
|
||||
string.format("%02d", datetime.sec), -- SECOND
|
||||
0, -- MSEC
|
||||
eyear, -- EXIF.YEAR
|
||||
string.sub(eyear, 3), -- EXIF.YEAR.SHORT
|
||||
(eyear ~= nil) and string.sub(eyear, 3) or "", -- EXIF.YEAR.SHORT
|
||||
emon, -- EXIF.MONTH
|
||||
os.date("%B", exiftime2systime(datetime_taken)), -- EXIF.MONTH.LONG
|
||||
os.date("%b", exiftime2systime(datetime_taken)), -- EXIF.MONTH.SHORT
|
||||
emon_long, -- EXIF.MONTH.LONG
|
||||
emon_short, -- EXIF.MONTH.SHORT
|
||||
eday, -- EXIF.DAY
|
||||
ehour, -- EXIF.HOUR
|
||||
"", -- EXIF.HOUR.AMPM
|
||||
@@ -837,17 +875,22 @@ 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, -- USERNAME
|
||||
(user_name ~= nil) and user_name or "",-- USERNAME
|
||||
"", -- NL - wont be implemented
|
||||
"" -- JOBCODE - wont be implemented
|
||||
}
|
||||
@@ -862,6 +905,7 @@ 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
|
||||
@@ -889,10 +933,13 @@ 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"),
|
||||
@@ -957,10 +1004,15 @@ 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"),
|
||||
@@ -1010,6 +1062,11 @@ 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])
|
||||
@@ -1243,6 +1300,49 @@ 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,8 +91,10 @@ 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)
|
||||
|
||||
283
official/auto_straighten.lua
Normal file
283
official/auto_straighten.lua
Normal file
@@ -0,0 +1,283 @@
|
||||
--[[
|
||||
|
||||
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
|
||||
dt.print_log("roll angle is " .. val)
|
||||
log.msg(log.debug, "roll angle is " .. val)
|
||||
roll = determine_correction(tonumber(val))
|
||||
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
|
||||
459
official/extract_burst_roll_images.lua
Normal file
459
official/extract_burst_roll_images.lua
Normal file
@@ -0,0 +1,459 @@
|
||||
--[[
|
||||
|
||||
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 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.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
|
||||
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
|
||||
local cmd = EXIFTOOL .. EXIFTOOL_ARGS
|
||||
|
||||
if ebri.preferred_tag_reader == "exiv2" then
|
||||
cmd = EXIV2 .. EXIV2_ARGS
|
||||
end
|
||||
|
||||
local p = io.popen(cmd .. ds.sanitize(image.path) .. PS .. ds.sanitize(image.filename))
|
||||
if p then
|
||||
for line in p:lines() do
|
||||
if ebri.preferred_tag_reader == "exiv2" then
|
||||
line = get_last_value(line)
|
||||
end
|
||||
line = tonumber(line)
|
||||
if type(line) == "number" then
|
||||
if tonumber(line) > 0 then
|
||||
log.msg(log.info, image.filename .. " is a burst roll container image")
|
||||
result = true
|
||||
end
|
||||
end
|
||||
end
|
||||
p:close()
|
||||
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:length() > 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 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
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- 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 not EXIFTOOL and not EXIV2 then
|
||||
log.msg(log.error, MODULE .. _("no exif tag reader available"))
|
||||
log.msg(log.screen, script_data.metadata.name .. _(" no exif tag reader available, exiting..."))
|
||||
return
|
||||
end
|
||||
|
||||
ebri.preferred_tag_reader = pref_read("tag_reader", "enum")
|
||||
|
||||
if not string.match(EXIFTOOL, ebri.preferred_tag_reader) and not string.match(EXIV2, ebri.preferred_tag_reader) then
|
||||
log.msg(log.error, MODULE .. " selected tag " .. ebri.preferred_tag_reader .." not available")
|
||||
log.msg(log.screen, script_data.metadata.name .. string.format(_(" selected tag reader %s not available"), ebri.preferred_tag_reader))
|
||||
return
|
||||
end
|
||||
|
||||
ebri.on_import = dt.preferences.read(MODULE, "on_import", "bool")
|
||||
|
||||
-- - - - - - - - - - - - - - - - - - - - - - - -
|
||||
-- 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_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
|
||||
)
|
||||
|
||||
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
|
||||
694
official/group_persistence.lua
Normal file
694
official/group_persistence.lua
Normal file
@@ -0,0 +1,694 @@
|
||||
--[[
|
||||
|
||||
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 = "" -- 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
|
||||
343
official/recent_bookmarks.lua
Normal file
343
official/recent_bookmarks.lua
Normal file
@@ -0,0 +1,343 @@
|
||||
--[[
|
||||
|
||||
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
|
||||
224
official/scheduler.lua
Normal file
224
official/scheduler.lua
Normal file
@@ -0,0 +1,224 @@
|
||||
--[[
|
||||
|
||||
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
|
||||
266
official/select_duplicates.lua
Normal file
266
official/select_duplicates.lua
Normal file
@@ -0,0 +1,266 @@
|
||||
--[[
|
||||
|
||||
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
|
||||
180
official/select_raw_non_raw.lua
Normal file
180
official/select_raw_non_raw.lua
Normal file
@@ -0,0 +1,180 @@
|
||||
--[[
|
||||
|
||||
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
|
||||
168
official/select_unaltered.lua
Normal file
168
official/select_unaltered.lua
Normal file
@@ -0,0 +1,168 @@
|
||||
--[[
|
||||
|
||||
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
|
||||
354
official/selection2collection.lua
Normal file
354
official/selection2collection.lua
Normal file
@@ -0,0 +1,354 @@
|
||||
--[[
|
||||
|
||||
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 = _("selection2collection"), -- 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
|
||||
337
official/use_paired_jpg_as_mipmap.lua
Normal file
337
official/use_paired_jpg_as_mipmap.lua
Normal file
@@ -0,0 +1,337 @@
|
||||
--[[
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user