- Implemented main entry point for React application in `main.jsx`. - Created `Gallery` component for displaying images with infinite scroll and pull-to-refresh functionality. - Developed `ImageDetail` component for showing detailed information and actions for selected images. - Added `Settings` component for configuring API connection settings. - Integrated Vite for build and development with PWA support. - Implemented caching strategies for API responses and image previews using Workbox.
132 lines
4.8 KiB
Lua
132 lines
4.8 KiB
Lua
-- 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: first downward (smaller = smaller file, faster),
|
|
-- then upward (larger mip levels, let the browser scale down).
|
|
-- This ensures we serve something even when only higher-resolution
|
|
-- mipmaps have been cached (e.g. darktable only wrote levels 2-4).
|
|
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
|
|
for level = mip + 1, 5 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
|