From bc4c04f795d0562bfc5d9f9aaae1de41f603f370 Mon Sep 17 00:00:00 2001 From: "U-DESKTOP-HQME86J\\marco" Date: Sat, 18 Jan 2020 12:45:58 +0100 Subject: [PATCH 1/9] error fix --- contrib/ext_editor.lua | 590 ++++++++++++++++++++--------------------- 1 file changed, 295 insertions(+), 295 deletions(-) diff --git a/contrib/ext_editor.lua b/contrib/ext_editor.lua index b5e8eb2..f9a96dd 100644 --- a/contrib/ext_editor.lua +++ b/contrib/ext_editor.lua @@ -1,64 +1,64 @@ --[[ - DESCRIPTION + DESCRIPTION ext_editor.lua - edit images with external editors This script provides helpers to edit image files with programs external to darktable. - It adds: - - a new target storage "collection". Image exported will be reimported to collection for - further edit with external programs - - a new lighttable module "external editors", to select a program from a list of up to - - 9 external editors and run it on a selected image - - a set of lua preferences in order to configure name and path of up to 9 external editors - - a set of lua shortcuts in order to quick launch the external editors - - USAGE + It adds: + - a new target storage "collection". Image exported will be reimported to collection for + further edit with external programs + - a new lighttable module "external editors", to select a program from a list of up to + - 9 external editors and run it on a selected image (adjust this limit by changing MAX_EDITORS) + - a set of lua preferences in order to configure name and path of up to 9 external editors + - a set of lua shortcuts in order to quick launch the external editors + + USAGE * require this script from main lua file - - -- setup -- - * in "preferences/lua options" configure name and path/command of external programs - * note that if a program name is left empty, that and all following entries will be ignored - * in "preferences/shortcuts/lua" configure shortcuts for external programs (optional) - * whenever programs preferences are changed, in lighttable/external editors, press "update list" + + -- setup -- + * in "preferences/lua options" configure name and path/command of external programs + * note that if a program name is left empty, that and all following entries will be ignored + * in "preferences/shortcuts/lua" configure shortcuts for external programs (optional) + * whenever programs preferences are changed, in lighttable/external editors, press "update list" - -- use -- + -- use -- * in the export dialog choose "collection" and select the format and bit depth for the exported image * press "export" - * the exported image will be imported into collection and grouped with the original image + * the exported image will be imported into collection and grouped with the original image - * select an image for editing with en external program, and: - * in lighttable/external editors, select program and press "edit" - * edit the image with the external editor, overwite the file, quit the external program - * the selected image will be updated - or - * in lighttable/external editors, select program and press "edit a copy" - * edit the image with the external editor, overwite the file, quit the external program - * a copy of the selected image will be created and updated - or - * in lighttable select target storage "collection" - * enter in darkroom - * to create an export or a copy press CRTL+E - * use the shortcut to edit the current image with the corresponding external editor - * overwite the file, quit the external program - * the darkroom view will be updated - - * warning: mouseover on lighttable/filmstrip will prevail on current image - * this is the default DT behavior, not a bug of this script + * select an image for editing with en external program, and: + * in lighttable/external editors, select program and press "edit" + * edit the image with the external editor, overwite the file, quit the external program + * the selected image will be updated + or + * in lighttable/external editors, select program and press "edit a copy" + * edit the image with the external editor, overwite the file, quit the external program + * a copy of the selected image will be created and updated + or + * in lighttable select target storage "collection" + * enter in darkroom + * to create an export or a copy press CRTL+E + * use the shortcut to edit the current image with the corresponding external editor + * overwite the file, quit the external program + * the darkroom view will be updated + + * warning: mouseover on lighttable/filmstrip will prevail on current image + * this is the default DT behavior, not a bug of this script CAVEATS - * MAC compatibility not tested - - TODO - * send multiple images to the same program, maybe - + * MAC compatibility not tested + + TODO + * send multiple images to the same program, maybe + BUGS, COMMENTS, SUGGESTIONS * send to Marco Carrarini, marco.carrarini@gmail.com CHANGES * 20191224 - initial version - * 20191227 - added button "update list", better error handling, fixed bug with groups/tags in "edit" - + * 20191227 - added button "update list", better error handling, fixed bug with groups/tags in "edit" + ]] @@ -87,341 +87,341 @@ local function _(msgid) return gettext.dgettext(MODULE_NAME, msgid) end +-- maximum number of external programs +local MAX_EDITORS = 9 -- number of valid entries in the list of external programs local n_entries -- allowed file extensions, to exclude RAW, which cannot be edited externally -local allowed_file_types = {"JPG", "jpg", "JPEG", "jpeg", "TIF", "tif", "TIFF", "tiff", "EXR", "exr"} +local allowed_file_types = {"JPG", "jpg", "JPEG", "jpeg", "TIF", "tif", "TIFF", "tiff", "EXR", "exr", "PNG", "png"} -- last used editor initialization if not dt.preferences.read(MODULE_NAME,"initialized", "bool") then - dt.preferences.write(MODULE_NAME,"lastchoice", "integer", 0) - dt.preferences.write(MODULE_NAME,"initialized", "bool", true) - end + dt.preferences.write(MODULE_NAME,"lastchoice", "integer", 0) + dt.preferences.write(MODULE_NAME,"initialized", "bool", true) + end local lastchoice = 0 -- update lists of program names and paths, as well as combobox --------------- local function UpdateProgramList(combobox, button_edit, button_edit_copy, update_button_pressed) - -- initialize lists - program_names = {} - program_paths = {} + -- initialize lists + program_names = {} + program_paths = {} - -- build lists from preferences - local name - local last = false - n_entries = 0 - for i = 1, 9 do - name = dt.preferences.read(MODULE_NAME,"program_name_"..i, "string") - if (name == "" or name == nil) then last = true end - if last then - if combobox[n_entries + 1] then combobox[n_entries + 1] = nil end -- remove extra combobox entries - else - combobox[i] = i..": "..name - program_names[i] = name - program_paths[i] = df.sanitize_filename(dt.preferences.read(MODULE_NAME, "program_path_"..i, "string")) - n_entries = i - end - end + -- build lists from preferences + local name + local last = false + n_entries = 0 + for i = 1, MAX_EDITORS do + name = dt.preferences.read(MODULE_NAME,"program_name_"..i, "string") + if (name == "" or name == nil) then last = true end + if last then + if combobox[n_entries + 1] then combobox[n_entries + 1] = nil end -- remove extra combobox entries + else + combobox[i] = i..": "..name + program_names[i] = name + program_paths[i] = df.sanitize_filename(dt.preferences.read(MODULE_NAME, "program_path_"..i, "string")) + n_entries = i + end + end - lastchoice = dt.preferences.read(MODULE_NAME, "lastchoice", "integer") - if lastchoice == 0 and n_entries > 0 then lastchoice = 1 end - if lastchoice > n_entries then lastchoice = n_entries end - dt.preferences.write(MODULE_NAME, "lastchoice", "integer", lastchoice) + lastchoice = dt.preferences.read(MODULE_NAME, "lastchoice", "integer") + if lastchoice == 0 and n_entries > 0 then lastchoice = 1 end + if lastchoice > n_entries then lastchoice = n_entries end + dt.preferences.write(MODULE_NAME, "lastchoice", "integer", lastchoice) - -- widgets enabled if there is at least one program configured - combobox.selected = lastchoice - local active = n_entries > 0 + -- widgets enabled if there is at least one program configured + combobox.selected = lastchoice + local active = n_entries > 0 combobox.sensitive = active button_edit.sensitive = active button_edit_copy.sensitive = active - if update_button_pressed then dt.print(n_entries.._(" editors configured")) end - end - - --- shows export progress ------------------------------------------------------ -local function show_status(storage, image, format, filename, number, total, high_quality, extra_data) - - dt.print(_("exporting image ").. number.." / "..total.." ...") - end + if update_button_pressed then dt.print(n_entries.._(" editors configured")) end + end -- callback for buttons "edit" and "edit a copy" ------------------------------ local function OpenWith(images, choice, copy) - - -- check choice is valid, return if not - if choice > n_entries then - dt.print(_("not a valid choice")) - return - end + + -- check choice is valid, return if not + if choice > n_entries then + dt.print(_("not a valid choice")) + return + end - -- check if one image is selected, return if not - if #images ~= 1 then - dt.print(_("please select one image")) - return - end - - local bin = program_paths[choice] - local friendly_name = program_names[choice] + -- check if one image is selected, return if not + if #images ~= 1 then + dt.print(_("please select one image")) + return + end + + local bin = program_paths[choice] + local friendly_name = program_names[choice] - -- check if external program executable exists, return if not - if not df.check_if_bin_exists(bin) then - dt.print(friendly_name.._(" not found")) - return - end + -- check if external program executable exists, return if not + if not df.check_if_bin_exists(bin) then + dt.print(friendly_name.._(" not found")) + return + end - -- image to be edited - local image - i, image = next(images) - local name = image.path..PS..image.filename + -- image to be edited + local image + i, image = next(images) + local name = image.path..PS..image.filename - -- check if image is raw, return if it is - -- please note that the image property image.is_raw fails when filepath contains spaces - -- so as a workaround we allow only TIF, JPG and EXR - local file_ext = df.get_filetype (image.filename) - local allowed = false - for i,v in pairs(allowed_file_types) do - if v == file_ext then - allowed = true - break - end - end - if not allowed then - dt.print(_("file type not allowed")) - return - end + -- check if image is raw, return if it is + -- please note that the image property image.is_raw fails when filepath contains spaces + -- so as a workaround we allow only TIF, JPG and EXR + local file_ext = df.get_filetype (image.filename) + local allowed = false + for i,v in pairs(allowed_file_types) do + if v == file_ext then + allowed = true + break + end + end + if not allowed then + dt.print(_("file type not allowed")) + return + end - -- save image tags, rating and color - local tags = {} + -- save image tags, rating and color + local tags = {} for i, tag in ipairs(dt.tags.get_tags(image)) do - if not (string.sub(tag.name, 1, 9) == "darktable") then table.insert(tags, tag) end - end - local rating = image.rating - local red = image.red - local blue = image.blue - local green = image.green - local yellow = image.yellow - local purple = image.purple + if not (string.sub(tag.name, 1, 9) == "darktable") then table.insert(tags, tag) end + end + local rating = image.rating + local red = image.red + local blue = image.blue + local green = image.green + local yellow = image.yellow + local purple = image.purple -- new image local new_name = name - local new_image = image + local new_image = image if copy then - -- create unique filename - while true do -- dirty solution to workaround issue in lib function check_if_file_exists() - if dt.configuration.running_os == "windows" then - if not df.check_if_file_exists(df.sanitize_filename(new_name)) then break end - else - if not df.check_if_file_exists(new_name) then break end - end - new_name = df.filename_increment(new_name) - -- limit to 50 more exports of the original export - if string.match(df.get_basename(new_name), "_%d%d$") == "_50" then break end - end - - -- physical copy, check result, return if error - local copy_success = df.file_copy(name, new_name) - if not copy_success then - dt.print(_("error copying file ")..name) - return - end + -- create unique filename + while true do -- dirty solution to workaround issue in lib function check_if_file_exists() + if dt.configuration.running_os == "windows" then + if not df.check_if_file_exists(df.sanitize_filename(new_name)) then break end + else + if not df.check_if_file_exists(new_name) then break end + end + new_name = df.filename_increment(new_name) + -- limit to 50 more exports of the original export + if string.match(df.get_basename(new_name), "_%d%d$") == "_50" then break end + end + + -- physical copy, check result, return if error + local copy_success = df.file_copy(name, new_name) + if not copy_success then + dt.print(_("error copying file ")..name) + return + end end - -- launch the external editor, check result, return if error - local run_cmd = bin.." "..df.sanitize_filename(new_name) - dt.print(_("launching ")..friendly_name.."...") - local result = dtsys.external_command(run_cmd) - if result ~= 0 then - dt.print(_("error launching ")..friendly_name) - return - end + -- launch the external editor, check result, return if error + local run_cmd = bin.." "..df.sanitize_filename(new_name) + dt.print(_("launching ")..friendly_name.."...") + local result = dtsys.external_command(run_cmd) + if result ~= 0 then + dt.print(_("error launching ")..friendly_name) + return + end if copy then - -- import in database and group - new_image = dt.database.import(new_name) - new_image:group_with(image) + -- import in database and group + new_image = dt.database.import(new_name) + new_image:group_with(image) else -- refresh the image view - -- note that only image:drop_cache() is not enough to refresh view in darkroom mode - -- therefore image must be deleted and reimported to force refresh + -- note that only image:drop_cache() is not enough to refresh view in darkroom mode + -- therefore image must be deleted and reimported to force refresh -- find the grouping status - local image_leader = image.group_leader - local group_members = image:get_group_members() - local new_leader - local index = nil - local found = false - - -- membership status, three different cases - if image_leader == image then - if #group_members > 1 then - -- case 1: image is leader in a group with more members - while not found do - index, new_leader = next(group_members, index) - if new_leader ~= image_leader then found = true end - end - new_leader:make_group_leader() - image:delete() - new_image = dt.database.import(name) - new_image:group_with(new_leader) - new_image:make_group_leader() - else - -- case 2: image is the only member in group - image:delete() - new_image = dt.database.import(name) - new_image:group_with() - end - else - -- case 3: image is in a group but is not leader - image:delete() - new_image = dt.database.import(name) - new_image:group_with(image_leader) - end - -- refresh darkroom view - if dt.gui.current_view() == dt.gui.views.darkroom then - dt.gui.views.darkroom.display_image(new_image) - end - end + local image_leader = image.group_leader + local group_members = image:get_group_members() + local new_leader + local index = nil + local found = false + + -- membership status, three different cases + if image_leader == image then + if #group_members > 1 then + -- case 1: image is leader in a group with more members + while not found do + index, new_leader = next(group_members, index) + if new_leader ~= image_leader then found = true end + end + new_leader:make_group_leader() + image:delete() + if image.local_copy then image:drop_cache() end -- to fix fail to allocate cache error + new_image = dt.database.import(name) + new_image:group_with(new_leader) + new_image:make_group_leader() + else + -- case 2: image is the only member in group + image:delete() + if image.local_copy then image:drop_cache() end -- to fix fail to allocate cache error + new_image = dt.database.import(name) + new_image:group_with() + end + else + -- case 3: image is in a group but is not leader + image:delete() + if image.local_copy then image:drop_cache() end -- to fix fail to allocate cache error + new_image = dt.database.import(name) + new_image:group_with(image_leader) + end + -- refresh darkroom view + if dt.gui.current_view() == dt.gui.views.darkroom then + dt.gui.views.darkroom.display_image(new_image) + end + end - -- restore image tags, rating and color, must be put after refresh darkroom view - for i, tag in ipairs(tags) do dt.tags.attach(tag, new_image) end - new_image.rating = rating - new_image.red = red - new_image.blue = blue - new_image.green = green - new_image.yellow = yellow - new_image.purple = purple + -- restore image tags, rating and color, must be put after refresh darkroom view + for i, tag in ipairs(tags) do dt.tags.attach(tag, new_image) end + new_image.rating = rating + new_image.red = red + new_image.blue = blue + new_image.green = green + new_image.yellow = yellow + new_image.purple = purple -- select the new image - local selection = {} - table.insert(selection, new_image) - dt.gui.selection (selection) + local selection = {} + table.insert(selection, new_image) + dt.gui.selection (selection) - end + end -- callback function for shortcuts -------------------------------------------- local function program_shortcut(event, shortcut) - OpenWith(dt.gui.action_images, tonumber(string.sub(shortcut, -1)), false) - end + OpenWith(dt.gui.action_images, tonumber(string.sub(shortcut, -1)), false) + end -- export images and reimport in collection ----------------------------------- local function export2collection(storage, image_table, extra_data) - local new_name, new_image, result + local new_name, new_image, result - for image, temp_name in pairs(image_table) do + for image, temp_name in pairs(image_table) do - -- images are first exported in temp folder then moved to collection folder + -- images are first exported in temp folder then moved to collection folder - -- create unique filename - new_name = image.path..PS..df.get_filename(temp_name) - while true do -- dirty solution to workaround issue in lib function check_if_file_exists() - if dt.configuration.running_os == "windows" then - if not df.check_if_file_exists(df.sanitize_filename(new_name)) then break end - else - if not df.check_if_file_exists(new_name) then break end - end - new_name = df.filename_increment(new_name) - -- limit to 50 more exports of the original export - if string.match(df.get_basename(new_name), "_%d%d$") == "_50" then break end - end + -- create unique filename + new_name = image.path..PS..df.get_filename(temp_name) + while true do -- dirty solution to workaround issue in lib function check_if_file_exists() + if dt.configuration.running_os == "windows" then + if not df.check_if_file_exists(df.sanitize_filename(new_name)) then break end + else + if not df.check_if_file_exists(new_name) then break end + end + new_name = df.filename_increment(new_name) + -- limit to 50 more exports of the original export + if string.match(df.get_basename(new_name), "_%d%d$") == "_50" then break end + end - -- move image to collection folder, check result, return if error - move_success = df.file_move(temp_name, new_name) - if not move_success then - dt.print(_("error moving file ")..temp_name) - return - end + -- move image to collection folder, check result, return if error + move_success = df.file_move(temp_name, new_name) + if not move_success then + dt.print(_("error moving file ")..temp_name) + return + end - -- import in database and group - new_image = dt.database.import(new_name) - new_image:group_with(image.group_leader) - end - end + -- import in database and group + new_image = dt.database.import(new_name) + new_image:group_with(image.group_leader) + end + + dt.print (_("finished exporting")) + end -- register new storage ------------------------------------------------------- -- note that placing this declaration later makes the export selected module -- not to remember the choice "collection" when restarting DT, don't know why -dt.register_storage("exp2coll", _("collection"), show_status, export2collection) +dt.register_storage("exp2coll", _("collection"), null, export2collection) -- combobox, with variable number of entries ---------------------------------- local combobox = dt.new_widget("combobox") { - label = _("choose program"), - tooltip = _("select the external editor from the list"), - changed_callback = function(self) - dt.preferences.write(MODULE_NAME, "lastchoice", "integer", self.selected) - end, - "" - } + label = _("choose program"), + tooltip = _("select the external editor from the list"), + changed_callback = function(self) + dt.preferences.write(MODULE_NAME, "lastchoice", "integer", self.selected) + end, + "" + } -- button edit ---------------------------------------------------------------- local button_edit = dt.new_widget("button") { - label = _("edit"), - tooltip = _("open the selected image in external editor"), - --sensitive = false, - clicked_callback = function() - OpenWith(dt.gui.action_images, combobox.selected, false) - end - } + label = _("edit"), + tooltip = _("open the selected image in external editor"), + --sensitive = false, + clicked_callback = function() + OpenWith(dt.gui.action_images, combobox.selected, false) + end + } -- button edit a copy --------------------------------------------------------- local button_edit_copy = dt.new_widget("button") { - label = _("edit a copy"), - tooltip = _("create a copy of the selected image and open it in external editor"), - clicked_callback = function() - OpenWith(dt.gui.action_images, combobox.selected, true) - end - } + label = _("edit a copy"), + tooltip = _("create a copy of the selected image and open it in external editor"), + clicked_callback = function() + OpenWith(dt.gui.action_images, combobox.selected, true) + end + } -- button update list --------------------------------------------------------- local button_update_list = dt.new_widget("button") { - label = _("update list"), - tooltip = _("update list of programs if lua preferences are changed"), - clicked_callback = function() - UpdateProgramList(combobox, button_edit, button_edit_copy, true) - end - } + label = _("update list"), + tooltip = _("update list of programs if lua preferences are changed"), + clicked_callback = function() + UpdateProgramList(combobox, button_edit, button_edit_copy, true) + end + } -- box for the buttons -------------------------------------------------------- -- it doesn't seem there is a way to make the buttons equal in size local box1 = dt.new_widget("box") { orientation = "horizontal", - button_edit, - button_edit_copy, - button_update_list - } + button_edit, + button_edit_copy, + button_update_list + } -- register new module "external editors" in lighttable ------------------------ dt.register_lib( - MODULE_NAME, - _("external editors"), - true, -- expandable - false, -- resetable - {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}}, - dt.new_widget("box") { - orientation = "vertical", - combobox, - box1 - }, - nil, -- view_enter - nil -- view_leave - ) + MODULE_NAME, + _("external editors"), + true, -- expandable + false, -- resetable + {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}}, + dt.new_widget("box") { + orientation = "vertical", + combobox, + box1 + }, + nil, -- view_enter + nil -- view_leave + ) -- initialize list of programs and widgets ------------------------------------ @@ -429,23 +429,23 @@ UpdateProgramList(combobox, button_edit, button_edit_copy, false) -- register the new preferences ----------------------------------------------- -for i = 9, 1, -1 do - dt.preferences.register(MODULE_NAME, "program_path_"..i, "file", - _("executable for external editor ")..i, - _("select executable for external editor") , _("(None)")) - dt.preferences.register(MODULE_NAME, "program_name_"..i, "string", - _("name of external editor ")..i, - _("friendly name of external editor"), "") - end +for i = MAX_EDITORS, 1, -1 do + dt.preferences.register(MODULE_NAME, "program_path_"..i, "file", + _("executable for external editor ")..i, + _("select executable for external editor") , _("(None)")) + dt.preferences.register(MODULE_NAME, "program_name_"..i, "string", + _("name of external editor ")..i, + _("friendly name of external editor"), "") + end --- register the new shortcuts ------------------------------------------------- -for i = 1, 9 do - dt.register_event("shortcut", program_shortcut, _("edit with program ")..i) - end +-- register the new shortcuts ------------------------------------------------- +for i = 1, MAX_EDITORS do + dt.register_event("shortcut", program_shortcut, _("edit with program ")..i) + end -- end of script -------------------------------------------------------------- --- vim: shiftwidth=4 expandtab tabstop=4 cindent syntax=lua +-- vim: shiftwidth=2 expandtab tabstop=2 cindent syntax=lua -- kate: hl Lua; From 5b977733b304ab890fc2a203a1360f7ad310f209 Mon Sep 17 00:00:00 2001 From: Bill Ferguson Date: Sun, 19 Jan 2020 23:11:41 -0500 Subject: [PATCH 2/9] Rewrote sanitize and is_not_sanitized to handle strings with embedded operatings system quotes. Fixes #216. --- lib/dtutils/string.lua | 47 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/lib/dtutils/string.lua b/lib/dtutils/string.lua index 89af3a7..ba4cd67 100644 --- a/lib/dtutils/string.lua +++ b/lib/dtutils/string.lua @@ -193,11 +193,29 @@ dtutils_string.libdoc.functions["sanitize"] = { } function dtutils_string.sanitize(str) - local result = "" + local result = str local os_quote = dt.configuration.running_os == "windows" and '"' or "'" + local escaped_os_quote = "\\" .. os_quote - if dtutils_string.is_not_sanitized(str) then - result = os_quote .. str .. os_quote + if dtutils_string.is_not_sanitized(result) then + local pos = string.find(result, os_quote) + if not pos or pos > 1 then + result = os_quote .. result + end + pos = string.find(result, os_quote, string.len(result)) + if not pos then + result = result .. os_quote + end + + if dtutils_string.is_not_sanitized(result) then --check for embedded os_quotes + pos = string.find(result, os_quote, 2) + while pos < string.len(result) do + if not string.find(result, escaped_os_quote, pos - 1) then + result = string.format("%s\\%s", string.sub(result, 1, pos -1), string.sub(result, pos)) + end + pos = string.find(result, os_quote, pos+2) + end + end end return result @@ -222,12 +240,29 @@ dtutils_string.libdoc.functions["is_not_sanitized"] = { function dtutils_string.is_not_sanitized(str) local os_quote = dt.configuration.running_os == "windows" and '"' or "'" + local escaped_os_quote = "\\" .. os_quote + local length = string.len(str) + local not_sanitized = false - if string.match(str, os_quote .. ".*" .. os_quote) then - return false + local pos = string.find(str, os_quote) + if pos == 1 then + if string.find(str, os_quote, length) then + pos = string.find(str, os_quote, 2) + while pos ~= length do + if not string.find(str, escaped_os_quote, pos - 1) then + not_sanitized = true + end + pos = string.find(str, os_quote, pos + 1) + end + else + not_sanitized = true + end else - return true + not_sanitized = true end + +return not_sanitized + end From 847c39749daa4130549b518c1650642b6c9d8b3a Mon Sep 17 00:00:00 2001 From: August Schwerdfeger Date: Tue, 21 Jan 2020 00:29:18 -0600 Subject: [PATCH 3/9] Corrected escaping of command-line arguments in the 'sanitize' and 'is_not_sanitized' functions. --- lib/dtutils/string.lua | 100 +++++++++++++++++++++-------------------- 1 file changed, 52 insertions(+), 48 deletions(-) diff --git a/lib/dtutils/string.lua b/lib/dtutils/string.lua index ba4cd67..2da4c90 100644 --- a/lib/dtutils/string.lua +++ b/lib/dtutils/string.lua @@ -192,33 +192,28 @@ dtutils_string.libdoc.functions["sanitize"] = { Copyright = [[]], } -function dtutils_string.sanitize(str) - local result = str - local os_quote = dt.configuration.running_os == "windows" and '"' or "'" - local escaped_os_quote = "\\" .. os_quote - - if dtutils_string.is_not_sanitized(result) then - local pos = string.find(result, os_quote) - if not pos or pos > 1 then - result = os_quote .. result - end - pos = string.find(result, os_quote, string.len(result)) - if not pos then - result = result .. os_quote - end - - if dtutils_string.is_not_sanitized(result) then --check for embedded os_quotes - pos = string.find(result, os_quote, 2) - while pos < string.len(result) do - if not string.find(result, escaped_os_quote, pos - 1) then - result = string.format("%s\\%s", string.sub(result, 1, pos -1), string.sub(result, pos)) - end - pos = string.find(result, os_quote, pos+2) - end - end +function dtutils_string.sanitize_posix(str) + if dtutils_string.is_not_sanitized(str) then + return "'" .. string.gsub(str, "'", "'\\''") .. "'" + else + return str + end +end + +function dtutils_string.sanitize_windows(str) + if dtutils_string.is_not_sanitized(str) then + return "\"" .. string.gsub(str, "\"", "\"^\"\"") .. "\"" + else + return str + end +end + +function dtutils_string.sanitize(str) + if dt.configuration.running_os == "windows" then + return dtutils_string.sanitize_windows(str) + else + return dtutils_string.sanitize_posix(str) end - - return result end dtutils_string.libdoc.functions["is_not_sanitized"] = { @@ -238,31 +233,40 @@ dtutils_string.libdoc.functions["is_not_sanitized"] = { Copyright = [[]], } +function dtutils_string.is_not_sanitized_posix(str) + -- A sanitized string must be quoted. + if not string.match(str, "^'.*'$") then + return true + -- A quoted string containing no quote characters within is sanitized. + elseif string.match(str, "^'[^']*'$") then + return false + end + + -- Any quote characters within a sanitized string must be properly + -- escaped. + local quotesStripped = string.sub(str, 2, -2) + local escapedQuotesRemoved = string.gsub(quotesStripped, "'\\''", "") + if string.find(escapedQuotesRemoved, "'") then + return true + else + return false + end +end + +function dtutils_string.is_not_sanitized_windows(str) + if not string.match(str, "^\".*\"$") then + return true + else + return false + end +end + function dtutils_string.is_not_sanitized(str) - local os_quote = dt.configuration.running_os == "windows" and '"' or "'" - local escaped_os_quote = "\\" .. os_quote - local length = string.len(str) - local not_sanitized = false - - local pos = string.find(str, os_quote) - if pos == 1 then - if string.find(str, os_quote, length) then - pos = string.find(str, os_quote, 2) - while pos ~= length do - if not string.find(str, escaped_os_quote, pos - 1) then - not_sanitized = true - end - pos = string.find(str, os_quote, pos + 1) - end - else - not_sanitized = true - end + if dt.configuration.running_os == "windows" then + return dtutils_string.is_not_sanitized_windows(str) else - not_sanitized = true + return dtutils_string.is_not_sanitized_posix(str) end - -return not_sanitized - end From 51eda4e755f34c38886c1f04d447a6a9937a72a9 Mon Sep 17 00:00:00 2001 From: August Schwerdfeger Date: Tue, 21 Jan 2020 22:17:12 -0600 Subject: [PATCH 4/9] Made Windows- and POSIX-specific sanitization functions local; moved the 'sanitize' functions after the 'is_not_sanitized' functions that they call. --- lib/dtutils/string.lua | 92 +++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/lib/dtutils/string.lua b/lib/dtutils/string.lua index 2da4c90..f58f38c 100644 --- a/lib/dtutils/string.lua +++ b/lib/dtutils/string.lua @@ -174,48 +174,6 @@ function dtutils_string.urlencode(str) end -dtutils_string.libdoc.functions["sanitize"] = { - Name = [[sanitize]], - Synopsis = [[surround a string in quotes making it safe to pass as an argument]], - Usage = [[local ds = require "lib/dtutils.string" - - local result = ds.sanitize(str) - str - string - the string that needs to be made safe]], - Description = [[sanitize converts a string into a version suitable for - use passing as an argument in a system command.]], - Return_Value = [[result - string - a websafe string]], - Limitations = [[]], - Example = [[]], - See_Also = [[]], - Reference = [[]], - License = [[]], - Copyright = [[]], -} - -function dtutils_string.sanitize_posix(str) - if dtutils_string.is_not_sanitized(str) then - return "'" .. string.gsub(str, "'", "'\\''") .. "'" - else - return str - end -end - -function dtutils_string.sanitize_windows(str) - if dtutils_string.is_not_sanitized(str) then - return "\"" .. string.gsub(str, "\"", "\"^\"\"") .. "\"" - else - return str - end -end - -function dtutils_string.sanitize(str) - if dt.configuration.running_os == "windows" then - return dtutils_string.sanitize_windows(str) - else - return dtutils_string.sanitize_posix(str) - end -end - dtutils_string.libdoc.functions["is_not_sanitized"] = { Name = [[is_not_sanitized]], Synopsis = [[Check if a string has been sanitized]], @@ -233,7 +191,7 @@ dtutils_string.libdoc.functions["is_not_sanitized"] = { Copyright = [[]], } -function dtutils_string.is_not_sanitized_posix(str) +local function _is_not_sanitized_posix(str) -- A sanitized string must be quoted. if not string.match(str, "^'.*'$") then return true @@ -253,7 +211,7 @@ function dtutils_string.is_not_sanitized_posix(str) end end -function dtutils_string.is_not_sanitized_windows(str) +local function _is_not_sanitized_windows(str) if not string.match(str, "^\".*\"$") then return true else @@ -263,9 +221,51 @@ end function dtutils_string.is_not_sanitized(str) if dt.configuration.running_os == "windows" then - return dtutils_string.is_not_sanitized_windows(str) + return _is_not_sanitized_windows(str) else - return dtutils_string.is_not_sanitized_posix(str) + return _is_not_sanitized_posix(str) + end +end + +dtutils_string.libdoc.functions["sanitize"] = { + Name = [[sanitize]], + Synopsis = [[surround a string in quotes making it safe to pass as an argument]], + Usage = [[local ds = require "lib/dtutils.string" + + local result = ds.sanitize(str) + str - string - the string that needs to be made safe]], + Description = [[sanitize converts a string into a version suitable for + use passing as an argument in a system command.]], + Return_Value = [[result - string - a websafe string]], + Limitations = [[]], + Example = [[]], + See_Also = [[]], + Reference = [[]], + License = [[]], + Copyright = [[]], +} + +local function _sanitize_posix(str) + if _is_not_sanitized_posix(str) then + return "'" .. string.gsub(str, "'", "'\\''") .. "'" + else + return str + end +end + +local function _sanitize_windows(str) + if _is_not_sanitized_windows(str) then + return "\"" .. string.gsub(str, "\"", "\"^\"\"") .. "\"" + else + return str + end +end + +function dtutils_string.sanitize(str) + if dt.configuration.running_os == "windows" then + return _sanitize_windows(str) + else + return _sanitize_posix(str) end end From 1eafae6c6f2b38be49b2e709d7a26a436046cff1 Mon Sep 17 00:00:00 2001 From: Bill Ferguson Date: Wed, 22 Jan 2020 13:15:14 -0500 Subject: [PATCH 5/9] Add check for WARNING lines complaining about files with no faces in the faces directory --- contrib/face_recognition.lua | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/contrib/face_recognition.lua b/contrib/face_recognition.lua index 45229b6..8379f1b 100644 --- a/contrib/face_recognition.lua +++ b/contrib/face_recognition.lua @@ -259,13 +259,15 @@ local function face_recognition () dt.print(_("processing results...")) local result = {} for line in io.lines(OUTPUT) do - local file, tag = string.match (line, "(.*),(.*)$") - tag = string.gsub (tag, "%d*$", "") - dt.print_log ("File:"..file .." Tag:".. tag) - if result[file] ~= nil then - table.insert (result[file], tag) - else - result[file] = {tag} + if not string.match(line, "^WARNING:") then + local file, tag = string.match (line, "(.*),(.*)$") + tag = string.gsub (tag, "%d*$", "") + dt.print_log ("File:"..file .." Tag:".. tag) + if result[file] ~= nil then + table.insert (result[file], tag) + else + result[file] = {tag} + end end end From 470811080443b2107d2034fa15790f65da909f5d Mon Sep 17 00:00:00 2001 From: Piter Dias Date: Thu, 23 Jan 2020 21:12:49 -0300 Subject: [PATCH 6/9] 1. Add "no persons found" entry for the situations that no persons are detected in the photos 2. Check if the tags to applied are non empty and skip otherwise. It applies to empty entries in the GUI ("unknown person tag" and "no persons found"), when the user types something and then deletes it 3. Change the string "togs of images to ignore" to "tags of images to ignore" --- contrib/face_recognition.lua | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/contrib/face_recognition.lua b/contrib/face_recognition.lua index 8379f1b..526c0f9 100644 --- a/contrib/face_recognition.lua +++ b/contrib/face_recognition.lua @@ -78,6 +78,7 @@ if not dt.preferences.read(MODULE, "initialized", "bool") then dt.preferences.write(MODULE, "max_width", "integer", 1000) dt.preferences.write(MODULE, "max_height", "integer", 1000) dt.preferences.write(MODULE, "initialized", "bool", true) + dt.preferences.write(MODULE, "no_persons_found_tag", "string", "no_persons_found") end local function build_image_table(images) @@ -158,6 +159,7 @@ local function save_preferences() dt.preferences.write(MODULE, "max_width", "integer", tonumber(fc.width.text)) dt.preferences.write(MODULE, "max_height", "integer", tonumber(fc.height.text)) dt.preferences.write(MODULE, "num_cores", "integer", fc.num_cores.value) + dt.preferences.write(MODULE, "no_persons_found_tag", "string", fc.no_persons_found_tag.text) local val = fc.tolerance.value val = string.gsub(tostring(val), ",", ".") dt.preferences.write(MODULE, "tolerance", "float", tonumber(val)) @@ -205,6 +207,7 @@ local function face_recognition () local nrCores = dt.preferences.read(MODULE, "num_cores", "integer") local ignoreTagString = dt.preferences.read(MODULE, "ignore_tags", "string") local unknownTag = dt.preferences.read(MODULE, "unknown_tag", "string") + local nonpersonsfoundTag = dt.preferences.read(MODULE, "no_persons_found_tag", "string") -- face_recognition uses -1 for all cores, we use 0 in preferences if nrCores < 1 then @@ -285,10 +288,16 @@ local function face_recognition () if t == "unknown_person" then t = unknownTag end - dt.print_log ("ImgId:" .. img.id .. " Tag:".. t) - -- Create tag if it does not exists - local tag = dt.tags.create (t) - img:attach_tag (tag) + -- Check of unrecognized no_persons_found + if t == "no_persons_found" then + t = nonpersonsfoundTag + end + if t ~= "" and t ~= nil then + dt.print_log ("ImgId:" .. img.id .. " Tag:".. t) + -- Create tag if it does not exists + local tag = dt.tags.create (t) + img:attach_tag (tag) + end end end end @@ -316,6 +325,12 @@ fc.unknown_tag = dt.new_widget("entry"){ editable = true, } +fc.no_persons_found_tag = dt.new_widget("entry"){ + text = dt.preferences.read(MODULE, "no_persons_found_tag", "string"), + tooltip = _("tag to be used when no persons are found"), + editable = true, +} + fc.ignore_tags = dt.new_widget("entry"){ text = dt.preferences.read(MODULE, "ignore_tags", "string"), tooltip = _("tags of images to ignore"), @@ -388,7 +403,9 @@ fc.execute = dt.new_widget("button"){ local widgets = { dt.new_widget("label"){ label = _("unknown person tag")}, fc.unknown_tag, - dt.new_widget("label"){ label = _("togs of images to ignore")}, + dt.new_widget("label"){ label = _("no persons found tag")}, + fc.no_persons_found_tag, + dt.new_widget("label"){ label = _("tags of images to ignore")}, fc.ignore_tags, dt.new_widget("label"){ label = _("face data directory")}, fc.known_image_path, From e96b78b175a744d856802c4583f799655b7e0416 Mon Sep 17 00:00:00 2001 From: Piter Dias Date: Mon, 27 Jan 2020 17:44:49 -0300 Subject: [PATCH 7/9] Add reset callback to face recognition widget and show the default values in the GUI. --- contrib/face_recognition.lua | 41 +++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/contrib/face_recognition.lua b/contrib/face_recognition.lua index 526c0f9..fe1e9f6 100644 --- a/contrib/face_recognition.lua +++ b/contrib/face_recognition.lua @@ -69,16 +69,9 @@ end -- preferences if not dt.preferences.read(MODULE, "initialized", "bool") then - dt.preferences.write(MODULE, "unknown_tag", "string", "unknown_person") - dt.preferences.write(MODULE, "ignore_tags", "string", "") - dt.preferences.write(MODULE, "tolerance", "float", 0.6) - dt.preferences.write(MODULE, "num_cores", "integer", 0) - dt.preferences.write(MODULE, "known_image_path", "directory", dt.configuration.config_dir .. "/face_recognition") - dt.preferences.write(MODULE, "export_format", "integer", 1) - dt.preferences.write(MODULE, "max_width", "integer", 1000) - dt.preferences.write(MODULE, "max_height", "integer", 1000) + reset_preferences() + save_preferences() dt.preferences.write(MODULE, "initialized", "bool", true) - dt.preferences.write(MODULE, "no_persons_found_tag", "string", "no_persons_found") end local function build_image_table(images) @@ -155,14 +148,29 @@ end local function save_preferences() dt.preferences.write(MODULE, "unknown_tag", "string", fc.unknown_tag.text) - dt.preferences.write(MODULE, "ignore_tags", "string", fc.ignore_tags.text) - dt.preferences.write(MODULE, "max_width", "integer", tonumber(fc.width.text)) - dt.preferences.write(MODULE, "max_height", "integer", tonumber(fc.height.text)) - dt.preferences.write(MODULE, "num_cores", "integer", fc.num_cores.value) dt.preferences.write(MODULE, "no_persons_found_tag", "string", fc.no_persons_found_tag.text) + dt.preferences.write(MODULE, "ignore_tags", "string", fc.ignore_tags.text) + dt.preferences.write(MODULE, "known_image_path", "directory", fc.known_image_path.value) local val = fc.tolerance.value val = string.gsub(tostring(val), ",", ".") dt.preferences.write(MODULE, "tolerance", "float", tonumber(val)) + dt.preferences.write(MODULE, "num_cores", "integer", fc.num_cores.value) + dt.preferences.write(MODULE, "export_format", "integer", fc.export_format.selected) + dt.preferences.write(MODULE, "max_width", "integer", tonumber(fc.width.text)) + dt.preferences.write(MODULE, "max_height", "integer", tonumber(fc.height.text)) +end + +local function reset_preferences() + fc.unknown_tag.text = "unknown_person" + fc.no_persons_found_tag.text = "no_persons_found" + fc.ignore_tags.text = "" + fc.known_image_path.value = dt.configuration.config_dir .. "/face_recognition" + fc.tolerance.value = 0.6 + fc.num_cores.value = -1 + fc.export_format.selected = 1 + fc.width.text = 1000 + fc.height.text = 1000 + save_preferences() end -- Check if image has ignored tag attached @@ -432,7 +440,10 @@ table.insert(widgets, fc.execute) fc.widget = dt.new_widget("box"){ orientation = vertical, - table.unpack(widgets) + reset_callback = function(this) + reset_preferences() + end, + table.unpack(widgets), } --fc.tolerance.value = dt.preferences.read(MODULE, "tolerance", "float") @@ -444,7 +455,7 @@ dt.register_lib( "face_recognition", -- Module name _("face recognition"), -- Visible name true, -- expandable - false, -- resetable + true, -- resetable {[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 300}}, -- containers fc.widget, nil,-- view_enter From d32e0eb5ab7711570285ad3f00ddff87c97b4dac Mon Sep 17 00:00:00 2001 From: Bill Ferguson Date: Tue, 28 Jan 2020 12:11:33 -0500 Subject: [PATCH 8/9] Removed include_all.lua since we now run cross platform and not all scripts run on all operating systems. --- include_all.lua | 61 ------------------------------------------------- 1 file changed, 61 deletions(-) delete mode 100644 include_all.lua diff --git a/include_all.lua b/include_all.lua deleted file mode 100644 index c25dd54..0000000 --- a/include_all.lua +++ /dev/null @@ -1,61 +0,0 @@ ---[[ - This file is part of darktable, - copyright (c) 2014 Jérémy Rosen - copyright (c) 2018 Bill Ferguson - - darktable is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - darktable is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with darktable. If not, see . -]] ---[[ -INCLUDE ALL -Automatically include all scripts in the script repository - -This is intended for debugging purpose - - -USAGE -* require this file from your main lua config file: -* go to configuration => preferences -* Enable the scripts you want to use -* restart darktable - -Note that you need to restart DT for your changes to enabled scripts to take effect - -]] -local dt = require "darktable" -local io = require "io" - --- must be loaded for scripts using darktable.control_execute to work -require "official/yield" - -dt.configuration.check_version(...,{3,0,0},{4,0,0},{5,0,0}) - --- find all scripts, but skip the lib and tools directories -local output = io.popen("cd "..dt.configuration.config_dir.."/lua ;find . -name lib -prune -o -name tools -prune -o -name \\*.lua -print") - -local my_name={...} -my_name = my_name[1] -for line in output:lines() do - local req_name = line:sub(3,-5) - if req_name ~= my_name and not string.match(req_name, "yield") then - dt.preferences.register(my_name,req_name,"bool","enable "..req_name, - "Should the script "..req_name.." be enabled at next startup",false) - - if dt.preferences.read(my_name,req_name,"bool") then - require(req_name) - end - end -end - --- --- vim: shiftwidth=2 expandtab tabstop=2 cindent syntax=lua From 430f44391641c814104b7f2c75b29ec5c2ae1691 Mon Sep 17 00:00:00 2001 From: Bill Ferguson Date: Tue, 28 Jan 2020 14:14:47 -0500 Subject: [PATCH 9/9] Added Stefan Klinger's darktable lua scripts repository --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7577026..8acac4b 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ The following third-party projects are listed for information only. Think of thi * [johnnyrun/darktable_lua_gimp](https://github.com/johnnyrun/darktable_lua_gimp) – GIMP export * [arru/darktable-scripts](https://github.com/arru/darktable-scripts) * [nbremond77/darktable](https://github.com/nbremond77/darktable/tree/master/scripts) +* [s5k6/dtscripts](https://github.com/s5k6/dtscripts) ## Download and Install