Add darktable REST API implementation
- Introduced a new Lua-based REST API for darktable, allowing external applications to interact with the darktable library. - Implemented core functionalities including image listing, retrieval, and preview serving. - Added support for OAuth 2.0 authentication with token generation. - Created a simple HTTP router to handle API requests. - Included JSON encoding/decoding utilities for API responses. - Developed a script to launch the API server alongside darktable, ensuring seamless integration. - Added necessary configuration and setup for the API server, including CORS support and error handling.
This commit is contained in:
123
contrib/darktable-api/preview.lua
Normal file
123
contrib/darktable-api/preview.lua
Normal file
@@ -0,0 +1,123 @@
|
||||
-- Serve image previews from darktable's mipmap cache.
|
||||
--
|
||||
-- Darktable stores mipmaps as individual JPEG files at:
|
||||
-- <cache_dir>/mipmaps-<sha1_of_library_realpath>.d/<mip_level>/<imgid>.jpg
|
||||
--
|
||||
-- Mip levels (approximate pixel sizes on the long edge):
|
||||
-- 0 → ~67 px 1 → ~135 px 2 → ~270 px 3 → ~540 px
|
||||
-- 4 → ~1080 px (good "medium") 5 → ~2160 px
|
||||
--
|
||||
-- If no cached mipmap is found we fall back to exiftool (embedded EXIF
|
||||
-- thumbnail) and finally to serving the original file path as a redirect.
|
||||
local ffi = require "ffi"
|
||||
local config = require "config"
|
||||
|
||||
ffi.cdef[[
|
||||
unsigned char *SHA1(const unsigned char *d, size_t n, unsigned char *md);
|
||||
]]
|
||||
local crypto = ffi.load("crypto")
|
||||
|
||||
local M = {}
|
||||
|
||||
-- ── Locate the mipmap cache directory (computed once) ─────────────────────────
|
||||
|
||||
local _mipmap_base -- e.g. "/home/user/.cache/darktable/mipmaps-abc123"
|
||||
|
||||
local function mipmap_base()
|
||||
if _mipmap_base then return _mipmap_base end
|
||||
|
||||
-- Resolve the real path of library.db (darktable uses realpath before hashing)
|
||||
local h = io.popen("realpath " .. config.library_db .. " 2>/dev/null")
|
||||
local realpath = h:read("*l"); h:close()
|
||||
realpath = realpath or config.library_db
|
||||
|
||||
-- SHA1 hex of the realpath (same algorithm as mipmap_cache.c)
|
||||
local md = ffi.new("unsigned char[20]")
|
||||
crypto.SHA1(realpath, #realpath, md)
|
||||
local hex = {}
|
||||
for i = 0, 19 do hex[i+1] = string.format("%02x", md[i]) end
|
||||
local sha1hex = table.concat(hex)
|
||||
|
||||
_mipmap_base = config.cache_dir .. "/mipmaps-" .. sha1hex
|
||||
return _mipmap_base
|
||||
end
|
||||
|
||||
-- ── Size → mip level mapping ──────────────────────────────────────────────────
|
||||
|
||||
-- size param: "thumb" (~135 px), "small" (~270 px), "medium" (~540 px),
|
||||
-- "large" (~1080 px), "xlarge" (~2160 px), or an integer level 0-5.
|
||||
local SIZE_TO_MIP = {
|
||||
thumb = 1,
|
||||
small = 2,
|
||||
medium = 3,
|
||||
large = 4,
|
||||
xlarge = 5,
|
||||
}
|
||||
|
||||
local function size_to_mip(size)
|
||||
if not size or size == "" then return 3 end -- default: "medium"
|
||||
local n = tonumber(size)
|
||||
if n then return math.max(0, math.min(5, math.floor(n))) end
|
||||
return SIZE_TO_MIP[size:lower()] or 3
|
||||
end
|
||||
|
||||
-- ── Try to read a file, return nil on failure ─────────────────────────────────
|
||||
|
||||
local function read_file(path)
|
||||
local f = io.open(path, "rb")
|
||||
if not f then return nil end
|
||||
local data = f:read("*a")
|
||||
f:close()
|
||||
return data
|
||||
end
|
||||
|
||||
-- ── Public: get preview data for an image id ──────────────────────────────────
|
||||
--
|
||||
-- Returns: data (string), content_type (string)
|
||||
-- or: nil, nil, redirect_url (caller should issue a 302)
|
||||
-- or: nil, nil, nil (404)
|
||||
function M.get(image_id, image_path, filename, size_param)
|
||||
local mip = size_to_mip(size_param)
|
||||
|
||||
-- 1. Walk mip levels downward until we find a cached JPEG.
|
||||
local base = mipmap_base()
|
||||
for level = mip, 0, -1 do
|
||||
local path = string.format("%s.d/%d/%d.jpg", base, level, image_id)
|
||||
local data = read_file(path)
|
||||
if data then return data, "image/jpeg" end
|
||||
end
|
||||
|
||||
-- 2. Try exiftool to extract the embedded EXIF thumbnail from the source file.
|
||||
if image_path and filename then
|
||||
local src = image_path .. "/" .. filename
|
||||
-- exiftool writes the thumbnail to stdout with -b
|
||||
local cmd = string.format("exiftool -b -ThumbnailImage %q 2>/dev/null", src)
|
||||
local fh = io.popen(cmd)
|
||||
if fh then
|
||||
local data = fh:read("*a"); fh:close()
|
||||
if data and #data > 100 then
|
||||
return data, "image/jpeg"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 3. For JPEG/PNG source files redirect to the original (client caches it).
|
||||
if image_path and filename then
|
||||
local src = image_path .. "/" .. filename
|
||||
local ext = filename:match("%.([^%.]+)$") or ""
|
||||
if ext:lower():match("^jpe?g$") or ext:lower() == "png" then
|
||||
return nil, nil, "file://" .. src -- caller issues redirect or reads directly
|
||||
end
|
||||
end
|
||||
|
||||
return nil, nil, nil -- no preview available
|
||||
end
|
||||
|
||||
-- ── Helper: read a local file and serve it inline ─────────────────────────────
|
||||
-- Used by the API handler when get() returns a file:// redirect.
|
||||
function M.read_local(path)
|
||||
local stripped = path:match("^file://(.+)$") or path
|
||||
return read_file(stripped)
|
||||
end
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user