feat(api): enhance image module management and export functionality

- Added read-write access to the darktable SQLite catalog for module parameter edits.
- Implemented new endpoints for exporting images and retrieving full-resolution images.
- Introduced PATCH method for updating module parameters with optional preview.
- Added comprehensive tests for new API functionalities, including module retrieval and error handling.
- Updated CORS headers to support new HTTP methods.
- Created detailed documentation for new API endpoints and module operations.
This commit is contained in:
2026-06-10 19:50:56 -04:00
parent bd40b4f850
commit 0831ee943c
13 changed files with 1362 additions and 52 deletions

View File

@@ -2,21 +2,23 @@
darktable-api-bridge.lua
Companion plugin for the darktable REST API server.
Runs inside a live darktable instance and watches a queue file written by the
standalone LuaJIT server. When image IDs appear in the queue this plugin
calls image:drop_cache() and image:generate_cache() so that darktable's
thumbnail panel reflects the latest on-disk state.
Runs inside a live darktable instance and watches two queue files written by
the standalone LuaJIT server.
Refresh queue (DT_REFRESH_QUEUE, default /tmp/dt-api-refresh.queue)
One image ID per line. The bridge calls image:drop_cache() +
image:generate_cache(true, …) so that darktable re-renders thumbnails
through its full processing pipeline, reflecting tone curves, exposure,
colour grading, and all other development settings.
Export queue (DT_EXPORT_QUEUE, default /tmp/dt-api-export.queue)
One image ID per line. The bridge exports a full-resolution JPEG via
darktable's rendering pipeline to DT_EXPORT_DIR/<id>.jpg.
Install: add the following line to ~/.config/darktable/luarc
require "contrib/darktable-api/darktable-api-bridge"
Or let the startup script (start-with-api.sh) do it automatically.
Queue file format (written by the server's /api/v1/images/:id/refresh):
One image ID (integer) per line, e.g.:
42
7
100
--]]
local dt = require "darktable"
@@ -28,13 +30,16 @@ local MODULE = "darktable-api-bridge"
-- ── Configuration ─────────────────────────────────────────────────────────────
-- Must match config.refresh_queue in config.lua (or the DT_REFRESH_QUEUE env var).
local QUEUE_FILE = os.getenv("DT_REFRESH_QUEUE") or "/tmp/dt-api-refresh.queue"
local POLL_MS = 1000 -- poll interval in milliseconds
-- Mipmap range to regenerate: 0 = smallest thumbnail, 8 = full resolution.
-- Levels 0-4 cover all GUI thumbnail sizes (~67 px … ~1080 px).
local MIP_MIN = 0
local MIP_MAX = 4
-- Must match config.refresh_queue / export_queue / export_dir in config.lua.
local QUEUE_FILE = os.getenv("DT_REFRESH_QUEUE") or "/tmp/dt-api-refresh.queue"
local EXPORT_QUEUE = os.getenv("DT_EXPORT_QUEUE") or "/tmp/dt-api-export.queue"
local EXPORT_DIR = os.getenv("DT_EXPORT_DIR") or "/tmp/dt-api-exports"
local POLL_MS = 1000 -- poll interval in milliseconds
-- Mipmap range to regenerate: 0 = smallest thumbnail, 4 ≈ 1080 px.
local MIP_MIN = 0
local MIP_MAX = 4
-- JPEG quality used for full-resolution exports.
local EXPORT_QUALITY = tonumber(os.getenv("DT_EXPORT_QUALITY")) or 95
-- ── Logging ───────────────────────────────────────────────────────────────────
@@ -93,25 +98,58 @@ local function refresh_image(img)
local label = string.format("id=%d %s/%s", img.id, img.path, img.filename)
log("refreshing " .. label)
-- Drop the current cached thumbnails so darktable knows they're stale.
img:drop_cache()
-- Regenerate thumbnails for mip levels MIP_MIN..MIP_MAX.
-- full=false uses embedded JPEG thumbnails when available (fast);
-- set full=true to force a complete RAW render (accurate but slow).
local full = false
img:generate_cache(full, MIP_MIN, MIP_MAX)
-- full=true forces darktable to re-process through its complete rendering
-- pipeline (tone curves, exposure, colour grading, etc.) instead of using
-- the stale embedded EXIF thumbnail from the source file.
img:generate_cache(true, MIP_MIN, MIP_MAX)
log("done " .. label)
end
-- Process all pending IDs in one pass.
local function export_image(img, dest_path)
local label = string.format("id=%d → %s", img.id, dest_path)
log("exporting " .. label)
local fmt = dt.new_format("jpeg")
fmt.quality = EXPORT_QUALITY
-- max_width/max_height = 0 means no scaling: full resolution.
fmt.max_width = 0
fmt.max_height = 0
local export_err = fmt:write_image(img, dest_path)
if export_err then
error("export failed: " .. tostring(export_err))
end
log("export done " .. label)
end
-- Atomically claim the export queue and return a set of unique image IDs.
local function drain_export_queue()
local processing = EXPORT_QUEUE .. ".processing"
local ok = os.rename(EXPORT_QUEUE, processing)
if not ok then return {} end
local ids = {}
local f = io.open(processing, "r")
if f then
for line in f:lines() do
local id = tonumber(line)
if id then ids[id] = true end
end
f:close()
os.remove(processing)
end
return ids
end
-- Process all pending refresh IDs in one pass.
local function process_queue()
local ids = drain_queue()
local count = 0
for id, _ in pairs(ids) do
count = count + 1
end
for id, _ in pairs(ids) do count = count + 1 end
if count == 0 then return end
log(string.format("processing %d queued refresh request(s)", count))
@@ -129,11 +167,37 @@ local function process_queue()
end
end
-- Nudge the lighttable to redraw so the user sees updated thumbnails
-- without having to scroll away and back.
-- Nudge the lighttable to redraw so updated thumbnails appear immediately.
dt.gui.views.lighttable.is_culling_mode = dt.gui.views.lighttable.is_culling_mode
end
-- Process all pending export IDs in one pass.
local function process_export_queue()
local ids = drain_export_queue()
local count = 0
for id, _ in pairs(ids) do count = count + 1 end
if count == 0 then return end
-- Ensure export directory exists.
os.execute('mkdir -p "' .. EXPORT_DIR .. '"')
log(string.format("processing %d queued export request(s)", count))
local id_map = build_id_map()
for id, _ in pairs(ids) do
local img = id_map[id]
if img then
local dest = EXPORT_DIR .. "/" .. tostring(id) .. ".jpg"
local ok, err = pcall(export_image, img, dest)
if not ok then
log("error exporting id=" .. id .. ": " .. tostring(err))
end
else
log("export: image id=" .. id .. " not found in database")
end
end
end
-- ── Background polling loop ───────────────────────────────────────────────────
-- dt.control.dispatch runs the function asynchronously in darktable's worker
@@ -143,13 +207,16 @@ local running = true
local function poll_loop()
notify(string.format(
"started; polling %s every %d ms", QUEUE_FILE, POLL_MS))
"started; polling queues every %d ms\n refresh: %s\n export: %s → %s",
POLL_MS, QUEUE_FILE, EXPORT_QUEUE, EXPORT_DIR))
while running do
local ok, err = pcall(process_queue)
if not ok then
log("process_queue error: " .. tostring(err))
end
if not ok then log("process_queue error: " .. tostring(err)) end
local ok2, err2 = pcall(process_export_queue)
if not ok2 then log("process_export_queue error: " .. tostring(err2)) end
dt.control.sleep(POLL_MS)
end
@@ -182,6 +249,8 @@ dt.register_event(MODULE, "shortcut",
log("manual poll triggered via shortcut")
local ok, err = pcall(process_queue)
if not ok then notify("error during manual poll: " .. tostring(err)) end
local ok2, err2 = pcall(process_export_queue)
if not ok2 then notify("export error during manual poll: " .. tostring(err2)) end
end,
"poll refresh queue now"
)