- 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.
503 lines
18 KiB
Lua
503 lines
18 KiB
Lua
-- 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
|