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:
502
contrib/darktable-api/modules.lua
Normal file
502
contrib/darktable-api/modules.lua
Normal file
@@ -0,0 +1,502 @@
|
||||
-- Module parameter decoder / encoder for the darktable REST API.
|
||||
-- Runs in the LuaJIT server process. Uses FFI to unpack and repack the
|
||||
-- binary op_params blobs stored in the library.db history table.
|
||||
--
|
||||
-- Each struct definition mirrors the darktable 4.6 iop C struct exactly so
|
||||
-- that sizeof() matches the byte count in the database. The `module` column
|
||||
-- in the history table carries the version number stored for that row.
|
||||
--
|
||||
-- Unknown modules (or unsupported versions) pass through as a base64 string.
|
||||
|
||||
local ffi = require "ffi"
|
||||
local base64 = require "base64"
|
||||
|
||||
-- ── C struct definitions ───────────────────────────────────────────────────────
|
||||
|
||||
ffi.cdef[[
|
||||
/* --- flip (ver=2, 4 bytes) --- */
|
||||
typedef struct { int32_t orientation; } dt_flip_v2_t;
|
||||
|
||||
/* --- gamma / display encoding (ver=1, 8 bytes) --- */
|
||||
typedef struct { float gamma; float linear; } dt_gamma_v1_t;
|
||||
|
||||
/* --- temperature / white balance (ver=3, 16 bytes) --- */
|
||||
typedef struct { float red; float green; float blue; float various; } dt_temperature_v3_t;
|
||||
|
||||
/* --- exposure (ver=7, 28 bytes) --- */
|
||||
typedef struct {
|
||||
int32_t mode;
|
||||
float black;
|
||||
float exposure;
|
||||
float deflicker_percentile;
|
||||
float deflicker_target_level;
|
||||
int32_t compensate_exposure_bias;
|
||||
int32_t compensate_hilite_pres;
|
||||
} dt_exposure_v7_t;
|
||||
|
||||
/* --- rawprepare (ver=1, 28 bytes) --- */
|
||||
typedef struct {
|
||||
int32_t left; int32_t top; int32_t right; int32_t bottom;
|
||||
uint16_t raw_black_level_separate[4];
|
||||
uint16_t raw_white_point;
|
||||
uint16_t _pad1;
|
||||
} dt_rawprepare_v1_t;
|
||||
|
||||
/* --- rawprepare (ver=2, 32 bytes) --- */
|
||||
typedef struct {
|
||||
int32_t left; int32_t top; int32_t right; int32_t bottom;
|
||||
uint16_t raw_black_level_separate[4];
|
||||
uint16_t raw_white_point;
|
||||
uint16_t _pad1;
|
||||
int32_t flat_field;
|
||||
} dt_rawprepare_v2_t;
|
||||
|
||||
/* --- demosaic (ver=3, 20 bytes) --- */
|
||||
typedef struct {
|
||||
int32_t green_eq;
|
||||
float median_thrs;
|
||||
int32_t color_smoothing;
|
||||
int32_t demosaicing_method;
|
||||
int32_t lmmse_refine;
|
||||
} dt_demosaic_v3_t;
|
||||
|
||||
/* --- highlights (ver=4, 48 bytes) --- */
|
||||
typedef struct {
|
||||
int32_t mode;
|
||||
float blendL;
|
||||
float blendC;
|
||||
float strength;
|
||||
float clip;
|
||||
float noise_level;
|
||||
int32_t iterations;
|
||||
int32_t scales;
|
||||
float candidating;
|
||||
float combine;
|
||||
int32_t recovery;
|
||||
float solid_color;
|
||||
} dt_highlights_v4_t;
|
||||
|
||||
/* --- channelmixerrgb (ver=3, 160 bytes) --- */
|
||||
typedef struct {
|
||||
float red[4];
|
||||
float green[4];
|
||||
float blue[4];
|
||||
float saturation[4];
|
||||
float lightness[4];
|
||||
float grey[4];
|
||||
int32_t normalize_R;
|
||||
int32_t normalize_G;
|
||||
int32_t normalize_B;
|
||||
int32_t normalize_sat;
|
||||
int32_t normalize_light;
|
||||
int32_t normalize_grey;
|
||||
int32_t illuminant;
|
||||
int32_t illum_fluo;
|
||||
int32_t illum_led;
|
||||
int32_t adaptation;
|
||||
float x;
|
||||
float y;
|
||||
float temperature;
|
||||
float gamut;
|
||||
int32_t clip;
|
||||
int32_t version;
|
||||
} dt_channelmixerrgb_v3_t;
|
||||
|
||||
/* --- filmicrgb (ver=6, 116 bytes) --- */
|
||||
typedef struct {
|
||||
float grey_point_source;
|
||||
float black_point_source;
|
||||
float white_point_source;
|
||||
float reconstruct_threshold;
|
||||
float reconstruct_feather;
|
||||
float reconstruct_bloom_vs_details;
|
||||
float reconstruct_grey_vs_color;
|
||||
float reconstruct_structure_vs_texture;
|
||||
float security_factor;
|
||||
float grey_point_target;
|
||||
float black_point_target;
|
||||
float white_point_target;
|
||||
float output_power;
|
||||
float latitude;
|
||||
float contrast;
|
||||
float saturation;
|
||||
float balance;
|
||||
float noise_level;
|
||||
int32_t preserve_color;
|
||||
int32_t version;
|
||||
int32_t auto_hardness;
|
||||
int32_t custom_grey;
|
||||
int32_t high_quality_reconstruction;
|
||||
int32_t noise_distribution;
|
||||
int32_t shadows;
|
||||
int32_t highlights;
|
||||
int32_t compensate_icc_black;
|
||||
int32_t spline_version;
|
||||
int32_t enable_highlight_reconstruction;
|
||||
} dt_filmicrgb_v6_t;
|
||||
]]
|
||||
|
||||
-- ── Internal helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
local function bool(v) return v ~= 0 end
|
||||
|
||||
-- Cast a Lua binary string into a pointer to ctype.
|
||||
local function cast_blob(ctype, blob)
|
||||
local buf = ffi.new("uint8_t[?]", #blob)
|
||||
ffi.copy(buf, blob, #blob)
|
||||
return ffi.cast(ctype, buf), buf -- return buf too so GC doesn't collect it
|
||||
end
|
||||
|
||||
-- Read a 4-float array field from a cdata array into a Lua table.
|
||||
local function farr(p, n)
|
||||
n = n or 4
|
||||
local t = {}
|
||||
for i = 0, n-1 do t[i+1] = p[i] end
|
||||
return t
|
||||
end
|
||||
|
||||
-- Write a Lua table back into a cdata 4-float array.
|
||||
local function set_farr(p, t, n)
|
||||
n = n or 4
|
||||
for i = 0, n-1 do
|
||||
if t[i+1] ~= nil then p[i] = t[i+1] end
|
||||
end
|
||||
end
|
||||
|
||||
-- Apply a field-level patch: if `src[k]` exists, set `dst.k = src[k]`.
|
||||
local function patch_fields(dst, src, ...)
|
||||
for _, k in ipairs({...}) do
|
||||
if src[k] ~= nil then dst[k] = src[k] end
|
||||
end
|
||||
end
|
||||
|
||||
-- Build a fresh binary blob by copying the existing one and applying changes.
|
||||
local function make_blob(ctype, size, existing, apply_fn)
|
||||
local buf = ffi.new("uint8_t[?]", size)
|
||||
if existing then ffi.copy(buf, existing, size) end
|
||||
local p = ffi.cast(ctype, buf)
|
||||
apply_fn(p)
|
||||
return ffi.string(buf, size)
|
||||
end
|
||||
|
||||
-- ── Per-module decode / encode ────────────────────────────────────────────────
|
||||
|
||||
local function decode_flip(blob, ver)
|
||||
if ver ~= 2 or #blob ~= 4 then return nil end
|
||||
local p, _ = cast_blob("dt_flip_v2_t *", blob)
|
||||
return { orientation = tonumber(p.orientation) }
|
||||
end
|
||||
|
||||
local function encode_flip(blob, changes)
|
||||
return make_blob("dt_flip_v2_t *", 4, blob, function(p)
|
||||
if changes.orientation ~= nil then p.orientation = changes.orientation end
|
||||
end)
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local function decode_gamma(blob, ver)
|
||||
if ver ~= 1 or #blob ~= 8 then return nil end
|
||||
local p, _ = cast_blob("dt_gamma_v1_t *", blob)
|
||||
return { gamma = tonumber(p.gamma), linear = tonumber(p.linear) }
|
||||
end
|
||||
|
||||
local function encode_gamma(blob, changes)
|
||||
return make_blob("dt_gamma_v1_t *", 8, blob, function(p)
|
||||
patch_fields(p, changes, "gamma", "linear")
|
||||
end)
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local function decode_temperature(blob, ver)
|
||||
if ver ~= 3 or #blob ~= 16 then return nil end
|
||||
local p, _ = cast_blob("dt_temperature_v3_t *", blob)
|
||||
return {
|
||||
red = tonumber(p.red),
|
||||
green = tonumber(p.green),
|
||||
blue = tonumber(p.blue),
|
||||
various = tonumber(p.various),
|
||||
}
|
||||
end
|
||||
|
||||
local function encode_temperature(blob, changes)
|
||||
return make_blob("dt_temperature_v3_t *", 16, blob, function(p)
|
||||
patch_fields(p, changes, "red", "green", "blue", "various")
|
||||
end)
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local function decode_exposure(blob, ver)
|
||||
if ver ~= 7 or #blob ~= 28 then return nil end
|
||||
local p, _ = cast_blob("dt_exposure_v7_t *", blob)
|
||||
return {
|
||||
mode = tonumber(p.mode),
|
||||
black = tonumber(p.black),
|
||||
exposure = tonumber(p.exposure),
|
||||
deflicker_percentile = tonumber(p.deflicker_percentile),
|
||||
deflicker_target_level = tonumber(p.deflicker_target_level),
|
||||
compensate_exposure_bias = bool(p.compensate_exposure_bias),
|
||||
compensate_hilite_pres = bool(p.compensate_hilite_pres),
|
||||
}
|
||||
end
|
||||
|
||||
local function encode_exposure(blob, changes)
|
||||
return make_blob("dt_exposure_v7_t *", 28, blob, function(p)
|
||||
patch_fields(p, changes, "mode", "black", "exposure",
|
||||
"deflicker_percentile", "deflicker_target_level")
|
||||
if changes.compensate_exposure_bias ~= nil then
|
||||
p.compensate_exposure_bias = changes.compensate_exposure_bias and 1 or 0
|
||||
end
|
||||
if changes.compensate_hilite_pres ~= nil then
|
||||
p.compensate_hilite_pres = changes.compensate_hilite_pres and 1 or 0
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local function decode_rawprepare(blob, ver)
|
||||
if ver == 1 and #blob == 28 then
|
||||
local p, _ = cast_blob("dt_rawprepare_v1_t *", blob)
|
||||
return {
|
||||
left = tonumber(p.left), top = tonumber(p.top),
|
||||
right = tonumber(p.right), bottom = tonumber(p.bottom),
|
||||
raw_black_level_separate = farr(p.raw_black_level_separate, 4),
|
||||
raw_white_point = tonumber(p.raw_white_point),
|
||||
}
|
||||
elseif ver == 2 and #blob == 32 then
|
||||
local p, _ = cast_blob("dt_rawprepare_v2_t *", blob)
|
||||
return {
|
||||
left = tonumber(p.left), top = tonumber(p.top),
|
||||
right = tonumber(p.right), bottom = tonumber(p.bottom),
|
||||
raw_black_level_separate = farr(p.raw_black_level_separate, 4),
|
||||
raw_white_point = tonumber(p.raw_white_point),
|
||||
flat_field = tonumber(p.flat_field),
|
||||
}
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function encode_rawprepare(blob, changes, ver)
|
||||
if ver == 1 then
|
||||
return make_blob("dt_rawprepare_v1_t *", 28, blob, function(p)
|
||||
patch_fields(p, changes, "left", "top", "right", "bottom", "raw_white_point")
|
||||
if changes.raw_black_level_separate then
|
||||
set_farr(p.raw_black_level_separate, changes.raw_black_level_separate, 4)
|
||||
end
|
||||
end)
|
||||
else
|
||||
return make_blob("dt_rawprepare_v2_t *", 32, blob, function(p)
|
||||
patch_fields(p, changes, "left", "top", "right", "bottom", "raw_white_point", "flat_field")
|
||||
if changes.raw_black_level_separate then
|
||||
set_farr(p.raw_black_level_separate, changes.raw_black_level_separate, 4)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local function decode_demosaic(blob, ver)
|
||||
if ver ~= 3 or #blob ~= 20 then return nil end
|
||||
local p, _ = cast_blob("dt_demosaic_v3_t *", blob)
|
||||
return {
|
||||
green_eq = tonumber(p.green_eq),
|
||||
median_thrs = tonumber(p.median_thrs),
|
||||
color_smoothing = tonumber(p.color_smoothing),
|
||||
demosaicing_method = tonumber(p.demosaicing_method),
|
||||
lmmse_refine = tonumber(p.lmmse_refine),
|
||||
}
|
||||
end
|
||||
|
||||
local function encode_demosaic(blob, changes)
|
||||
return make_blob("dt_demosaic_v3_t *", 20, blob, function(p)
|
||||
patch_fields(p, changes, "green_eq", "median_thrs", "color_smoothing",
|
||||
"demosaicing_method", "lmmse_refine")
|
||||
end)
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local function decode_highlights(blob, ver)
|
||||
if ver ~= 4 or #blob ~= 48 then return nil end
|
||||
local p, _ = cast_blob("dt_highlights_v4_t *", blob)
|
||||
return {
|
||||
mode = tonumber(p.mode),
|
||||
blendL = tonumber(p.blendL),
|
||||
blendC = tonumber(p.blendC),
|
||||
strength = tonumber(p.strength),
|
||||
clip = tonumber(p.clip),
|
||||
noise_level = tonumber(p.noise_level),
|
||||
iterations = tonumber(p.iterations),
|
||||
scales = tonumber(p.scales),
|
||||
candidating = tonumber(p.candidating),
|
||||
combine = tonumber(p.combine),
|
||||
recovery = tonumber(p.recovery),
|
||||
solid_color = tonumber(p.solid_color),
|
||||
}
|
||||
end
|
||||
|
||||
local function encode_highlights(blob, changes)
|
||||
return make_blob("dt_highlights_v4_t *", 48, blob, function(p)
|
||||
patch_fields(p, changes, "mode", "blendL", "blendC", "strength", "clip",
|
||||
"noise_level", "iterations", "scales", "candidating",
|
||||
"combine", "recovery", "solid_color")
|
||||
end)
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local function decode_channelmixerrgb(blob, ver)
|
||||
if ver ~= 3 or #blob ~= 160 then return nil end
|
||||
local p, _ = cast_blob("dt_channelmixerrgb_v3_t *", blob)
|
||||
return {
|
||||
red = farr(p.red),
|
||||
green = farr(p.green),
|
||||
blue = farr(p.blue),
|
||||
saturation = farr(p.saturation),
|
||||
lightness = farr(p.lightness),
|
||||
grey = farr(p.grey),
|
||||
normalize_R = bool(p.normalize_R),
|
||||
normalize_G = bool(p.normalize_G),
|
||||
normalize_B = bool(p.normalize_B),
|
||||
normalize_sat = bool(p.normalize_sat),
|
||||
normalize_light = bool(p.normalize_light),
|
||||
normalize_grey = bool(p.normalize_grey),
|
||||
illuminant = tonumber(p.illuminant),
|
||||
illum_fluo = tonumber(p.illum_fluo),
|
||||
illum_led = tonumber(p.illum_led),
|
||||
adaptation = tonumber(p.adaptation),
|
||||
x = tonumber(p.x),
|
||||
y = tonumber(p.y),
|
||||
temperature = tonumber(p.temperature),
|
||||
gamut = tonumber(p.gamut),
|
||||
clip = bool(p.clip),
|
||||
version = tonumber(p.version),
|
||||
}
|
||||
end
|
||||
|
||||
local function encode_channelmixerrgb(blob, changes)
|
||||
return make_blob("dt_channelmixerrgb_v3_t *", 160, blob, function(p)
|
||||
for _, k in ipairs({"red","green","blue","saturation","lightness","grey"}) do
|
||||
if changes[k] then set_farr(p[k], changes[k], 4) end
|
||||
end
|
||||
for _, k in ipairs({"normalize_R","normalize_G","normalize_B",
|
||||
"normalize_sat","normalize_light","normalize_grey","clip"}) do
|
||||
if changes[k] ~= nil then p[k] = changes[k] and 1 or 0 end
|
||||
end
|
||||
patch_fields(p, changes, "illuminant","illum_fluo","illum_led","adaptation",
|
||||
"x","y","temperature","gamut","version")
|
||||
end)
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local function decode_filmicrgb(blob, ver)
|
||||
if ver ~= 6 or #blob ~= 116 then return nil end
|
||||
local p, _ = cast_blob("dt_filmicrgb_v6_t *", blob)
|
||||
return {
|
||||
grey_point_source = tonumber(p.grey_point_source),
|
||||
black_point_source = tonumber(p.black_point_source),
|
||||
white_point_source = tonumber(p.white_point_source),
|
||||
reconstruct_threshold = tonumber(p.reconstruct_threshold),
|
||||
reconstruct_feather = tonumber(p.reconstruct_feather),
|
||||
reconstruct_bloom_vs_details = tonumber(p.reconstruct_bloom_vs_details),
|
||||
reconstruct_grey_vs_color = tonumber(p.reconstruct_grey_vs_color),
|
||||
reconstruct_structure_vs_texture = tonumber(p.reconstruct_structure_vs_texture),
|
||||
security_factor = tonumber(p.security_factor),
|
||||
grey_point_target = tonumber(p.grey_point_target),
|
||||
black_point_target = tonumber(p.black_point_target),
|
||||
white_point_target = tonumber(p.white_point_target),
|
||||
output_power = tonumber(p.output_power),
|
||||
latitude = tonumber(p.latitude),
|
||||
contrast = tonumber(p.contrast),
|
||||
saturation = tonumber(p.saturation),
|
||||
balance = tonumber(p.balance),
|
||||
noise_level = tonumber(p.noise_level),
|
||||
preserve_color = tonumber(p.preserve_color),
|
||||
version = tonumber(p.version),
|
||||
auto_hardness = bool(p.auto_hardness),
|
||||
custom_grey = bool(p.custom_grey),
|
||||
high_quality_reconstruction = tonumber(p.high_quality_reconstruction),
|
||||
noise_distribution = tonumber(p.noise_distribution),
|
||||
shadows = tonumber(p.shadows),
|
||||
highlights = tonumber(p.highlights),
|
||||
compensate_icc_black = bool(p.compensate_icc_black),
|
||||
spline_version = tonumber(p.spline_version),
|
||||
enable_highlight_reconstruction = bool(p.enable_highlight_reconstruction),
|
||||
}
|
||||
end
|
||||
|
||||
local function encode_filmicrgb(blob, changes)
|
||||
return make_blob("dt_filmicrgb_v6_t *", 116, blob, function(p)
|
||||
patch_fields(p, changes,
|
||||
"grey_point_source","black_point_source","white_point_source",
|
||||
"reconstruct_threshold","reconstruct_feather",
|
||||
"reconstruct_bloom_vs_details","reconstruct_grey_vs_color",
|
||||
"reconstruct_structure_vs_texture","security_factor",
|
||||
"grey_point_target","black_point_target","white_point_target",
|
||||
"output_power","latitude","contrast","saturation","balance","noise_level",
|
||||
"preserve_color","version","high_quality_reconstruction",
|
||||
"noise_distribution","shadows","highlights","spline_version")
|
||||
for _, k in ipairs({"auto_hardness","custom_grey","compensate_icc_black",
|
||||
"enable_highlight_reconstruction"}) do
|
||||
if changes[k] ~= nil then p[k] = changes[k] and 1 or 0 end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- ── Dispatch tables ───────────────────────────────────────────────────────────
|
||||
|
||||
local DECODERS = {
|
||||
flip = decode_flip,
|
||||
gamma = decode_gamma,
|
||||
temperature = decode_temperature,
|
||||
exposure = decode_exposure,
|
||||
rawprepare = decode_rawprepare,
|
||||
demosaic = decode_demosaic,
|
||||
highlights = decode_highlights,
|
||||
channelmixerrgb = decode_channelmixerrgb,
|
||||
filmicrgb = decode_filmicrgb,
|
||||
}
|
||||
|
||||
local ENCODERS = {
|
||||
flip = encode_flip,
|
||||
gamma = encode_gamma,
|
||||
temperature = encode_temperature,
|
||||
exposure = encode_exposure,
|
||||
rawprepare = function(b, c, v) return encode_rawprepare(b, c, v) end,
|
||||
demosaic = encode_demosaic,
|
||||
highlights = encode_highlights,
|
||||
channelmixerrgb = encode_channelmixerrgb,
|
||||
filmicrgb = encode_filmicrgb,
|
||||
}
|
||||
|
||||
-- ── Public API ────────────────────────────────────────────────────────────────
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Decode a binary op_params blob for `operation` (ver = history.module column).
|
||||
-- Returns a Lua table of named fields, or nil if the operation/version is unknown.
|
||||
function M.decode(operation, ver, blob)
|
||||
local fn = DECODERS[operation]
|
||||
if not fn then return nil end
|
||||
local ok, result = pcall(fn, blob, ver)
|
||||
if ok then return result end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Apply `changes` (a table of field overrides) to `blob` and return the
|
||||
-- modified binary string. Returns nil if the operation is not known.
|
||||
-- For unknown modules, the caller should pass params_b64 directly.
|
||||
function M.encode(operation, ver, blob, changes)
|
||||
local fn = ENCODERS[operation]
|
||||
if not fn then return nil end
|
||||
local ok, result = pcall(fn, blob, changes, ver)
|
||||
if ok then return result end
|
||||
return nil
|
||||
end
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user