Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ CollectHelper|Yes|LMW|Add buttons to selected images module to manipulate the co
copy_attach_detach_tags|Yes|LMW|Copy and paste tags from/to images
cr2hdr|Yes|L|Process image created with Magic Lantern Dual ISO
enfuseAdvanced|No|LMW|Merge multiple images into Dynamic Range Increase (DRI) or Depth From Focus (DFF) images
ext_editor|No|LW|Export pictures to collection and edit them with up to nine user-defined external editors
face_recognition|No|LM|Identify and tag images using facial recognition
fujifilm_ratings|No|LM|Support importing Fujifilm ratings
geoJSON_export|No|L|Create a geo JSON script with thumbnails for use in ...
Expand Down
371 changes: 371 additions & 0 deletions contrib/ext_editor.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,371 @@
--[[

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
* require this script from main lua file

* in "preferences / lua options" configure name and path/command of external programs
* in "preferences / shortcuts / lua" configure shortcuts for external programs (optional)

* 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

* select an image for editing with en external program
* 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
* tested with DT 3.0 in Windows and Ubuntu
* MAC compatibility not tested

TODO
* localization
* buttons are not equal in size and centered

BUGS, COMMENTS, SUGGESTIONS
* send to Marco Carrarini, marco.carrarini@gmail.com

CHANGES
* 20191220 - initial version, PR done
* 20191221 - reworked, cleaned, added "copy and edit"
* 20191222 - added darkroom mode
* 20191223 - check API version, OS compatibility

]]


local dt = require "darktable"
local du = require "lib/dtutils"
local df = require "lib/dtutils.file"
local dtsys = require "lib/dtutils.system"


-- check API version
du.check_min_api_version("5.0.2", "ext_editor") -- darktable 3.0


-- OS compatibility
local PS = dt.configuration.running_os == "windows" and "\\" or "/"


-- executables of the external editors
local program_paths = {
[1] = dt.preferences.read("ext_editor","program_path_1", "string"),
[2] = dt.preferences.read("ext_editor","program_path_2", "string"),
[3] = dt.preferences.read("ext_editor","program_path_3", "string"),
[4] = dt.preferences.read("ext_editor","program_path_4", "string"),
[5] = dt.preferences.read("ext_editor","program_path_5", "string"),
[6] = dt.preferences.read("ext_editor","program_path_6", "string"),
[7] = dt.preferences.read("ext_editor","program_path_7", "string"),
[8] = dt.preferences.read("ext_editor","program_path_8", "string"),
[9] = dt.preferences.read("ext_editor","program_path_9", "string")
}


-- friendly names of the external editors
local program_names = {
[1] = dt.preferences.read("ext_editor","program_name_1", "string"),
[2] = dt.preferences.read("ext_editor","program_name_2", "string"),
[3] = dt.preferences.read("ext_editor","program_name_3", "string"),
[4] = dt.preferences.read("ext_editor","program_name_4", "string"),
[5] = dt.preferences.read("ext_editor","program_name_5", "string"),
[6] = dt.preferences.read("ext_editor","program_name_6", "string"),
[7] = dt.preferences.read("ext_editor","program_name_7", "string"),
[8] = dt.preferences.read("ext_editor","program_name_8", "string"),
[9] = dt.preferences.read("ext_editor","program_name_9", "string")
}


-- last used editor
if not (dt.preferences.read("ext_editor","initialized", "bool")) then
dt.preferences.write("ext_editor","lastchoice", "integer", 1)
dt.preferences.write("ext_editor","initialized", "bool", true)
end
local lastchoice = dt.preferences.read("ext_editor","lastchoice", "integer")


-- shows export progress
local function show_status(storage, image, format, filename,
number, total, high_quality, extra_data)
dt.print(string.format("Exporting Image %i/%i ...", number, total))
end


-- callback for button "edit"
local function OpenWith(images, choice)

if #images == 1 then
for _, image in pairs(images) do

-- image to be edited
name = image.path..PS..image.filename

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could add a check here to see if the image is raw or not and act accordingly. You can check if image.is_ldr is true (jpg, tif, etc) or not (raw).

-- launch the external editor
local run_cmd = df.sanitize_filename(program_paths[choice]).." "..df.sanitize_filename(name)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should check and see if program_paths[choice] is actually and executable or not, just in case someone made a typo when they typed in the path, or a file got moved, etc. df.check_if_bin_exists() returns the sanitized executable command (i.e. prefixes wine when trying to run a .exe file on linux or macos).

dt.print("Launching "..program_names[choice].."...")
dtsys.external_command(run_cmd)

@wpferguson wpferguson Dec 26, 2019

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can check the result of the dtsys.external_command to see if it succeeded or not and then act accordingly.

This is also a destructive operation. If you have a jpg, then invoke an editor on it, then save over the file, the original is destroyed and can't be recovered, except from backup or some external copy. I would recommend creating a duplicate (image:duplicate()) and editing that so that the original image isn't destroyed.

-- 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

local image_leader = image.group_leader
image:delete ()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure the edit was successful before doing the delete/import.

local new_image = dt.database.import(name)
new_image:group_with(image_leader)

-- copy tags from image leader
for _,tag in pairs(dt.tags.get_tags(image_leader)) do
if not (string.sub(tag.name,1,9) == "darktable") then
dt.tags.attach(tag, new_image)
end
end

-- remember the last editor used
dt.preferences.write("ext_editor","lastchoice", "integer", choice)

-- refresh darkroom view
if dt.gui.current_view () == dt.gui.views.darkroom then
dt.gui.views.darkroom.display_image(new_image)
end
end
else
dt.print('please select one image')
end

end


-- callback for button "copy and edit"
local function CopyAndOpenWith(images, choice)

if #images == 1 then
for _,image in pairs(images) do

-- image to be copied and edited
local name = image.path..PS..image.filename
local new_name = name

-- create unique filename
while df.check_if_file_exists(new_name) do
new_name = df.filename_increment(new_name)
end

-- limit to 99 more exports of the original export
if string.match(df.get_basename(new_name), "_(d-)$") == "99" then
break
end

-- physical copy
local result = df.file_copy(name, new_name)

if result then
-- launch the external editor
local run_cmd = df.sanitize_filename(program_paths[choice]).." "..df.sanitize_filename(new_name)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as earlier, make sure the executable exists

dt.print("Launching "..program_names[choice].."...")
dtsys.external_command(run_cmd)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and check the result


-- import in database and group
local new_image = dt.database.import(new_name)
new_image:group_with(image.group_leader)

-- copy image tags
for _,tag in pairs(dt.tags.get_tags(image)) do
if not (string.sub(tag.name,1,9) == "darktable") then
dt.tags.attach(tag, new_image)
end
end

-- remember the last editor used
dt.preferences.write("ext_editor","lastchoice", "integer", choice)
end
end
else
dt.print('please select one image')
end


end


-- callback function for shortcuts
local function program_shortcut(event, shortcut)
local choice = tonumber(string.sub(shortcut, -1))
OpenWith(dt.gui.action_images, choice)
end


-- export images and reimport in collection

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this to create an export from a raw so that the editor can be invoked on the export?

local function export2collection(storage, image_table, extra_data)

for image, temp_name in pairs(image_table) do

-- images are first exported in temp folder
-- create unique filename
local new_name = image.path ..PS..df.get_filename(temp_name)
while df.check_if_file_exists(new_name) do
new_name = df.filename_increment(new_name)

-- limit to 99 more exports of the original export
if string.match(df.get_basename(new_name), "_(d-)$") == "99" then
break
end
end

-- image moved to collection folder
local result = df.file_move(temp_name, new_name)

if result then
-- import in database and group
local new_image = dt.database.import(new_name)
new_image:group_with(image.group_leader)
end
end
end


-- combobox
local combobox = dt.new_widget("combobox")
{
label = "choose program",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can get some bonus points, it you make the strings translatable: ;)
https://github.com/darktable-org/lua-scripts/blob/master/examples/gettextExample.lua

tooltip = "select the external editor from the list",
value = lastchoice, -- remember status,
program_names[1],
program_names[2],
program_names[3],
program_names[4],
program_names[5],
program_names[6],
program_names[7],
program_names[8],
program_names[9]
}


-- button edit
local button1 = dt.new_widget("button")
{
label = "edit",
tooltip = "open the selected image in external editor",
dt_lua_align_t = center,
clicked_callback = function (_)
local choice = combobox.selected
OpenWith(dt.gui.action_images, choice)
end
}


-- button edit a copy
local button2 = 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 (_)
local choice = combobox.selected
CopyAndOpenWith(dt.gui.action_images, choice)
end
}


-- box for the two buttons
local box1 = dt.new_widget ("box") {
orientation = "horizontal",
button1,
button2
}


-- register new lighttable module
dt.register_lib(
"ext_editor", -- module name
"external editors", -- name
true, -- expandable
false, -- resetable
{[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 100}},
dt.new_widget("box") -- widget
{
orientation = "vertical",
combobox,
box1
},
nil,-- view_enter
nil -- view_leave
)


-- register new storage
dt.register_storage("exp2coll", "collection", show_status, export2collection)


-- register the new preferences
dt.preferences.register("ext_editor","program_path_9", "string","command for external editor 9", "command for external editor, including full path if needed","")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of the path entries should be file choosers to prevent typing errors

dt.preferences.register("ext_editor","program_name_9", "string","friendly name of external editor 9", "friendly name of external editor","")

dt.preferences.register("ext_editor","program_path_8", "string","command for external editor 8", "command for external editor, including full path if needed","")
dt.preferences.register("ext_editor","program_name_8", "string","friendly name of external editor 8", "friendly name of external editor","")

dt.preferences.register("ext_editor","program_path_7", "string","command for external editor 7", "command for external editor, including full path if needed","")
dt.preferences.register("ext_editor","program_name_7", "string","friendly name of external editor 7", "friendly name of external editor","")

dt.preferences.register("ext_editor","program_path_6", "string","command for external editor 6", "command for external editor, including full path if needed","")
dt.preferences.register("ext_editor","program_name_6", "string","friendly name of external editor 6", "friendly name of external editor","")

dt.preferences.register("ext_editor","program_path_5", "string","command for external editor 5", "command for external editor, including full path if needed","")
dt.preferences.register("ext_editor","program_name_5", "string","friendly name of external editor 5", "friendly name of external editor","")

dt.preferences.register("ext_editor","program_path_4", "string","command for external editor 4", "command for external editor, including full path if needed","")
dt.preferences.register("ext_editor","program_name_4", "string","friendly name of external editor 4", "friendly name of external editor","")

dt.preferences.register("ext_editor","program_path_3", "string","command for external editor 3", "command for external editor, including full path if needed","")
dt.preferences.register("ext_editor","program_name_3", "string","friendly name of external editor 3", "friendly name of external editor","")

dt.preferences.register("ext_editor","program_path_2", "string","command for external editor 2", "command for external editor, including full path if needed","")
dt.preferences.register("ext_editor","program_name_2", "string","friendly name of external editor 2", "friendly name of external editor","")

dt.preferences.register("ext_editor","program_path_1", "string","command for external editor 1", "command for external editor, including full path if needed","")
dt.preferences.register("ext_editor","program_name_1", "string","friendly name of external editor 1", "friendly name of external editor","")


-- register the new shortcuts
dt.register_event("shortcut", program_shortcut, "edit with program 1")
dt.register_event("shortcut", program_shortcut, "edit with program 2")
dt.register_event("shortcut", program_shortcut, "edit with program 3")
dt.register_event("shortcut", program_shortcut, "edit with program 4")
dt.register_event("shortcut", program_shortcut, "edit with program 5")
dt.register_event("shortcut", program_shortcut, "edit with program 6")
dt.register_event("shortcut", program_shortcut, "edit with program 7")
dt.register_event("shortcut", program_shortcut, "edit with program 8")
dt.register_event("shortcut", program_shortcut, "edit with program 9")

-- end of script

-- vim: shiftwidth=2 expandtab tabstop=2 cindent syntax=lua
-- kate: hl Lua;