Skip to content

New script submission: export_edit_reload — external edit workflow panel #708

Description

@Tomomoto10

Not sure if an issue is the right place for this, but submitting here as I'm not familiar with the PR process.

Im welcome to any input on changes / additions needed.

Currently the included external editors script does not work for my workflow as 1. I use Affinity on Linux and the wine wrapper doesn't open the image, and 2. It requires the editor to be closed to update back in Darktable.

With this you can open the image in any program, and can update live in Darktable while editing externally.

Its made my life easier so far, hopefully it helps someone else.


export_edit_reload.lua is a panel that streamlines the workflow for editing images in an external application and reloading the changes back into darktable.

Four actions are provided:

  • Copy and Bake — exports a 16-bit TIFF with all Darktable edits baked into the pixels, imports it into the library grouped with the original, and auto-selects it
  • Copy Original — byte-copies the original file and its XMP sidecar, imports it grouped with the original, and auto-selects it. DT edits are not baked in — they remain on top of any external edits
  • Open In Browser — opens the folder of the selected image in the system file manager, highlighting the file for drag and drop into an external editor. Requires XMP sidecar (prompt shown if missing)
  • Reload Image — reimports the selected image(s), preserving ratings, colour labels and Darktable edits, regenerates thumbnails, and generates an XMP sidecar if one does not exist

All four actions are also bindable as keyboard shortcuts via Preferences > Shortcuts > Lua.

Typical workflow:
Copy and Bake (or Copy Original) → Open In Browser → drag into external editor → save → select image → Reload Image

Notes:

  • Available in both lighttable (right panel) and darkroom (left panel)
  • File manager opened via D-Bus org.freedesktop.FileManager1.ShowItems where available (highlights the file), with per-app fallbacks for Nautilus and Dolphin, and xdg-open as a last resort
  • Copies and reimports are tagged with action type and timestamp for audit trail
--[[ export_edit_reload.lua - external edit workflow panel for darktable Copyright (c) 2026 tomomoto darktable is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. darktable is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with darktable. If not, see <http://www.gnu.org/licenses/>.]]--[[ export_edit_reload.lua A panel that streamlines the workflow for editing images in an external application and reloading the changes back into darktable. Four actions are provided: Copy and Bake Exports a 16-bit TIFF with all Darktable edits baked into the pixels, saves it alongside the original as <filename>_edit.tif, imports it into the library grouped with the original, and auto-selects it ready for the next step. Copy Original Byte-copies the original file and its XMP sidecar alongside the original, imports the copy into the library grouped with the original, and auto-selects it. Darktable edits are not baked in — they remain on top of any external edits made to the copy. Open In Browser Opens the folder of the selected image in the system file manager, highlighting the file for drag and drop into an external editor. Checks for an XMP sidecar first — if none exists, prompts the user to enable XMP writing in Preferences > Storage or run Reload Image to generate one. XMP is required for all images so that any DT edits made after import survive the reimport step. Reload Image Deletes and reimports the selected image(s), preserving ratings, colour labels and Darktable edits, then regenerates thumbnails. The reimport also generates an XMP sidecar if one does not already exist. TYPICAL WORKFLOW Copy and Bake (or Copy Original) [copy is auto-selected] -> Open In Browser -> drag into external editor -> save -> select image in library -> Reload Image USAGE * enable via Script Manager or add to luarc: require "contrib/export_edit_reload" * available in both lighttable (right panel) and darkroom (left panel) * select an image and use the panel buttons or the keyboard shortcuts * all four actions are bindable via Preferences > Shortcuts > Lua NOTES * XMP sidecar writing must be enabled (Preferences > Storage) or Reload Image must be run at least once before Open In Browser will work. This ensures any Darktable edits made after import are not lost on reimport. * The file manager is opened via the D-Bus org.freedesktop.FileManager1 interface where available (highlights the file), with per-app fallbacks for Nautilus and Dolphin, and xdg-open on the folder as a last resort.]]localdt=require"darktable"localdu=require"lib/dtutils"du.check_min_api_version("7.0.0", "export_edit_reload")
localMODULE="export_edit_reload"localPS=dt.configuration.running_os=="windows" and"\\" or"/"localscript_data= {}
script_data.metadata= {
name="export edit reload",
purpose="open images for external editing and reimport changes",
author="tomomoto",
help=""
}
localmodule_installed=falselocalevent_registered=falselocalfunctionapply_tag(image, tag_name)
dt.tags.attach(dt.tags.create(tag_name.." | " ..os.date("%Y-%m-%d %H:%M")), image)
endlocalfunctionshell_ok(cmd)
localr=os.execute(cmd)
returnr==0orr==trueendlocalfunctionopen_and_select(folder, filepath)
-- D-Bus ShowItems is the universal way to open a file manager and highlight-- a specific file — supported by Thunar, Nautilus, Dolphin, Nemo, Cajaifshell_ok("which dbus-send > /dev/null 2>&1") thenlocalok=shell_ok(
"dbus-send --session --print-reply --dest=org.freedesktop.FileManager1 " .."/org/freedesktop/FileManager1 org.freedesktop.FileManager1.ShowItems " .."array:string:\"file://" ..filepath.."\" string:\"\" > /dev/null 2>&1"
)
ifokthenreturnendend-- per-app fallbacks with --select supportifshell_ok("which nautilus > /dev/null 2>&1") thenos.execute("nautilus --select \"" ..filepath.."\" &")
elseifshell_ok("which dolphin > /dev/null 2>&1") thenos.execute("dolphin --select \"" ..filepath.."\" &")
elseos.execute("xdg-open \"" ..folder.."\" &")
endendlocalfunctionopen_in_browser()
localimages=dt.gui.action_imagesif#images==0thendt.print("No images selected"); returnendlocalimage=images[1]
-- XMP must exist for any image before external editing: without it, any DT-- edits made after import live only in the database and will be lost on reimport.-- Reload Image generates the XMP as a side effect.localxmp_path=image.path..PS..image.filename..".xmp"localf=io.open(xmp_path, "r")
ifnotfthendt.print("No XMP found — enable in Preferences > Storage, or press Reload Image to create one for this image")
returnendio.close(f)
open_and_select(image.path, image.path..PS..image.filename)
endlocalfunctioncopy_for_edit()
-- exports a 16-bit TIFF with DT edits baked inlocalimages=dt.gui.action_imagesif#images==0thendt.print("No images selected"); returnendlocalimage=images[1]
localbasename=image.filename:match("(.+)%..+$") orimage.filenamelocalout_path=image.path..PS..basename.."_edit.tif"localcounter=1whileio.open(out_path, "r") doio.close(io.open(out_path, "r"))
out_path=image.path..PS..basename.."_edit_" ..counter..".tif"counter=counter+1endlocalexporter=dt.new_format("tiff")
exporter.bpp=16localok=exporter:write_image(image, out_path)
ifokthenlocalnew_image=dt.database.import(out_path)
ifnew_imagethennew_image:group_with(image.group_leader)
apply_tag(new_image, "Copy and Baked")
dt.gui.selection({new_image})
enddt.print("Baked copy created and selected — press Open In Browser to locate it")
elsedt.print("Export failed")
endendlocalfunctioncopy_original()
-- byte-copies the original file; DT edits are not baked inlocalimages=dt.gui.action_imagesif#images==0thendt.print("No images selected"); returnendlocalimage=images[1]
localsrc_path=image.path..PS..image.filenamelocalsrc_ext= (image.filename:match("%.([^%.]+)$") or""):lower()
localbasename=image.filename:match("(.+)%..+$") orimage.filenamelocalout_path=image.path..PS..basename.."_copy." ..src_extlocalcounter=1whileio.open(out_path, "r") doio.close(io.open(out_path, "r"))
out_path=image.path..PS..basename.."_copy_" ..counter.."." ..src_extcounter=counter+1endlocalok=os.execute("cp \"" ..src_path.."\"\"" ..out_path.."\"")
ok= (ok==0orok==true)
ifnotokthendt.print("Copy failed"); returnend-- copy XMP sidecar so the new copy carries the same DT edit history;-- DT reads the sidecar on import to restore the history stacklocalsrc_xmp=src_path..".xmp"localout_xmp=out_path..".xmp"localxmp_f=io.open(src_xmp, "r")
ifxmp_fthenxmp_f:close()
os.execute("cp \"" ..src_xmp.."\"\"" ..out_xmp.."\"")
endlocalnew_image=dt.database.import(out_path)
ifnew_imagethennew_image:group_with(image.group_leader)
apply_tag(new_image, "Copy of Original")
dt.gui.selection({new_image})
enddt.print("Original copied and selected — press Open In Browser to locate it")
endlocalfunctionreload_images()
localimages=dt.gui.action_imagesif#images==0thendt.print("No images selected"); returnendlocalrefreshed=0localnew_selection= {}
for_, imageinipairs(images) dolocalfilepath=image.path..PS..image.filenamelocalrating=image.ratinglocalred, blue, green, yellow, purple=image.red, image.blue, image.green, image.yellow, image.purplelocalgroup_leader=image.group_leaderlocalis_leader= (image.group_leader==image)
localmembers=image:get_group_members()
-- hand off group leadership before deleting so the group survivesifis_leaderand#members>1thenfor_, minipairs(members) doifm~=imagethenm:make_group_leader(); breakendendendimage:delete()
-- reimport picks up any external changes on disk and generates an XMP sidecarlocalnew_image=dt.database.import(filepath)
ifnew_imagethenifnotis_leaderthennew_image:group_with(group_leader) endnew_image.rating=ratingnew_image.red=rednew_image.blue=bluenew_image.green=greennew_image.yellow=yellownew_image.purple=purplenew_image:drop_cache()
new_image:generate_cache(true, 1, 3)
table.insert(new_selection, new_image)
refreshed=refreshed+1endendfor_, imginipairs(new_selection) doapply_tag(img, "Externally Edited | Reimported")
endif#new_selection>0thendt.gui.selection(new_selection) enddt.print(string.format("Refreshed %d image(s)", refreshed))
endlocalfunctionsep()
returndt.new_widget("label") { label="─────────────────────────", halign="center" }
endlocallbl_browser=dt.new_widget("label") {
label="Opens the folder of the selected image.\nThe newest copy is auto-selected.",
ellipsize="none",
halign="start"
}
localbtn_open_browser=dt.new_widget("button") {
label="Open In Browser",
tooltip="Opens the folder of the selected image. After copying, the new file is auto-selected so you can press this immediately. Requires XMP — run Reload Image first if none exists.",
clicked_callback=function() open_in_browser() end
}
locallbl_copy_bake=dt.new_widget("label") {
label="Exports a 16-bit TIFF with DT edits baked\ninto pixels. Imports copy next to original.",
ellipsize="none",
halign="start"
}
localbtn_copy_bake=dt.new_widget("button") {
label="Copy and Bake",
tooltip="Exports a 16-bit TIFF with Darktable edits baked in and imports it into the library grouped with the original.",
clicked_callback=function() copy_for_edit() end
}
locallbl_copy_original=dt.new_widget("label") {
label="Copies the file and XMP. DT edits\nremain on top of any external edits.",
ellipsize="none",
halign="start"
}
localbtn_copy_original=dt.new_widget("button") {
label="Copy Original",
tooltip="Byte-copies the original file and its XMP sidecar. DT edits are not baked in — they remain on top.",
clicked_callback=function() copy_original() end
}
locallbl_reload=dt.new_widget("label") {
label="Reimports after external edits.\nDT edits are preserved.",
ellipsize="none",
halign="start"
}
localbtn_reload=dt.new_widget("button") {
label="Reload Image",
tooltip="Reimports the selected image(s) and refreshes thumbnails after external editing.",
clicked_callback=function() reload_images() end
}
localfunctioninstall_module()
ifnotmodule_installedthendt.register_lib(
MODULE,
"export edit reload",
true,
false,
{[dt.gui.views.lighttable] = {"DT_UI_CONTAINER_PANEL_RIGHT_CENTER", 99},
[dt.gui.views.darkroom] = {"DT_UI_CONTAINER_PANEL_LEFT_CENTER", 99}},
dt.new_widget("box") {
orientation="vertical",
lbl_copy_bake,
btn_copy_bake,
sep(),
lbl_copy_original,
btn_copy_original,
sep(),
lbl_browser,
btn_open_browser,
sep(),
lbl_reload,
btn_reload
},
nil, nil
)
module_installed=trueendend-- register keyboard shortcuts for all four actions (bind in Preferences > Shortcuts > Lua)dt.register_event(MODULE, "shortcut", copy_for_edit, "Copy and Bake selected image")
dt.register_event(MODULE, "shortcut", copy_original, "Copy Original selected image")
dt.register_event(MODULE, "shortcut", open_in_browser, "Open image folder in file browser")
dt.register_event(MODULE, "shortcut", reload_images, "Reload externally edited images")
-- install immediately if already in lighttable, otherwise wait for the view switchifdt.gui.current_view().id=="lighttable" theninstall_module()
elseifnotevent_registeredthendt.register_event(MODULE, "view-changed",
function(event, old_view, new_view)
ifnew_view.name=="lighttable" ornew_view.name=="darkroom" theninstall_module()
endend
)
event_registered=trueendendlocalfunctiondestroy()
dt.destroy_event(MODULE, "shortcut")
dt.destroy_event(MODULE, "view-changed")
dt.gui.libs[MODULE].visible=falseendscript_data.destroy=destroyscript_data.destroy_method="hide" -- libs cannot be fully destroyed, only hiddenscript_data.restart=function() dt.gui.libs[MODULE].visible=trueendscript_data.show=function() dt.gui.libs[MODULE].visible=trueendreturnscript_data

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions