diff --git a/apps/api/src/lib/format-decoders.ts b/apps/api/src/lib/format-decoders.ts index efac8564..c1fab88f 100644 --- a/apps/api/src/lib/format-decoders.ts +++ b/apps/api/src/lib/format-decoders.ts @@ -4,7 +4,6 @@ import { readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { promisify } from "node:util"; -import sharp from "sharp"; const execFileAsync = promisify(execFile); @@ -36,65 +35,12 @@ export async function decodeToSharpCompat(buffer: Buffer, format: string): Promi } } -// ── RAW decoder (dcraw_emu / dcraw) ───────────────────────────── - -let cachedRawCmd: string | null = null; - -async function findRawCmd(): Promise { - if (cachedRawCmd) return cachedRawCmd; - for (const cmd of ["dcraw_emu", "dcraw"]) { - try { - await execFileAsync(cmd, [], { timeout: 5_000 }); - cachedRawCmd = cmd; - return cmd; - } catch { - // dcraw_emu / dcraw exit non-zero with no args but that's fine - - // if the binary exists the exec won't throw ENOENT - if (cachedRawCmd === null) { - // Check if the error was ENOENT (not found) vs normal exit code - try { - await execFileAsync("which", [cmd], { timeout: 5_000 }); - cachedRawCmd = cmd; - return cmd; - } catch { - // not found, try next - } - } - } - } - throw new Error("No RAW decoder found. Install libraw-dev (provides dcraw_emu) or dcraw."); -} - -/** - * Decode Camera RAW buffer to PNG via dcraw_emu. - * dcraw_emu -T produces a TIFF file alongside the input (same name, .tiff extension). - * We then convert TIFF to PNG via Sharp for consistent downstream handling. - */ -async function decodeRaw(buffer: Buffer): Promise { - const cmd = await findRawCmd(); - const id = randomUUID(); - const inputPath = join(tmpdir(), `raw-in-${id}.dng`); - const tiffPath = join(tmpdir(), `raw-in-${id}.tiff`); - - try { - await writeFile(inputPath, buffer); - // -T = output TIFF, -w = use camera white balance, -W = disable auto-brightness - await execFileAsync(cmd, ["-T", "-w", "-W", inputPath], { timeout: 120_000 }); - const tiffBuffer = await readFile(tiffPath); - return await sharp(tiffBuffer).png().toBuffer(); - } finally { - await rm(inputPath, { force: true }).catch(() => {}); - await rm(tiffPath, { force: true }).catch(() => {}); - } -} - -// ── ImageMagick decoders (PSD, TGA, EXR, HDR) ────────────────── +// ── ImageMagick helpers ──────────────────────────────────────── let cachedMagickCmd: string | null = null; async function findMagickCmd(): Promise { if (cachedMagickCmd) return cachedMagickCmd; - // ImageMagick 7 uses `magick`, v6 uses `convert` for (const cmd of ["magick", "convert"]) { try { await execFileAsync(cmd, ["--version"], { timeout: 5_000 }); @@ -107,14 +53,34 @@ async function findMagickCmd(): Promise { throw new Error("No ImageMagick found. Install imagemagick (provides convert/magick)."); } -/** - * Build the ImageMagick command args. For ImageMagick 7 (`magick`), - * the subcommand `convert` must be prepended. - */ function magickArgs(cmd: string, args: string[]): string[] { return cmd === "magick" ? ["convert", ...args] : args; } +// ── RAW decoder (ImageMagick with LibRaw delegate) ───────────── + +async function decodeRaw(buffer: Buffer): Promise { + const cmd = await findMagickCmd(); + const id = randomUUID(); + const inputPath = join(tmpdir(), `raw-in-${id}.dng`); + const outputPath = join(tmpdir(), `raw-out-${id}.png`); + + try { + await writeFile(inputPath, buffer); + await execFileAsync( + cmd, + magickArgs(cmd, [inputPath, "-colorspace", "sRGB", "-auto-orient", `png:${outputPath}`]), + { timeout: 120_000 }, + ); + return await readFile(outputPath); + } finally { + await rm(inputPath, { force: true }).catch(() => {}); + await rm(outputPath, { force: true }).catch(() => {}); + } +} + +// ── ImageMagick decoders (PSD, TGA, EXR, HDR) ────────────────── + /** * Decode PSD to PNG. Uses [0] to read only the flattened composite layer. */