fix: use ImageMagick for RAW decoding instead of dcraw_emu

dcraw_emu is not included in libraw-dev (only libraries). ImageMagick
already delegates to LibRaw for all major RAW formats (DNG, CR2, NEF,
ARW, ORF, RW2). Moved ImageMagick helpers above RAW decoder to fix
declaration order.
This commit is contained in:
ashim-hq
2026-04-21 10:03:18 +08:00
parent 2aadb66031
commit 2df0fe026e
+25 -59
View File
@@ -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<string> {
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<Buffer> {
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<string> {
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<string> {
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<Buffer> {
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.
*/