mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat: expand camera RAW support with exiftool-first decode strategy
- Add CR3 (Canon) brand detection in ftyp magic bytes, returning "raw" to route through the RAW decode pipeline - Add magic bytes for Fujifilm RAF, Sigma X3F, and Minolta MRW formats - Expand RAW_EXTENSIONS set from 6 to 24 formats (CR3, NRW, RAF, PEF, 3FR, IIQ, SRW, X3F, RWL, GPR, FFF, MRW, MEF, KDC, DCR, ERF, PTX) - Rewrite decodeRaw() with two-tier strategy: ExifTool embedded JPEG extraction (fast path) with ImageMagick+LibRaw fallback (full decode) - Add ext parameter to decodeToSharpCompat/decodeRaw so temp files use the original extension for correct format identification - Pass fileExt from tool-factory when calling decodeToSharpCompat - Add JXL, ICO, PSD, EXR, HDR, TGA MIME entries to CONTENT_TYPE_TO_EXT
This commit is contained in:
@@ -17,11 +17,22 @@ export function needsCliDecode(format: string): boolean {
|
||||
/**
|
||||
* Main entry point - routes to the right decoder based on format.
|
||||
* Returns a PNG buffer that Sharp can process downstream.
|
||||
*
|
||||
* @param buffer - The raw file buffer
|
||||
* @param format - The detected format string (e.g. "raw", "psd", "ico")
|
||||
* @param ext - Optional original file extension (e.g. "cr3", "nef").
|
||||
* Passed to decodeRaw so the temp file uses the correct
|
||||
* extension, which helps ExifTool and ImageMagick identify
|
||||
* the RAW variant.
|
||||
*/
|
||||
export async function decodeToSharpCompat(buffer: Buffer, format: string): Promise<Buffer> {
|
||||
export async function decodeToSharpCompat(
|
||||
buffer: Buffer,
|
||||
format: string,
|
||||
ext?: string,
|
||||
): Promise<Buffer> {
|
||||
switch (format) {
|
||||
case "raw":
|
||||
return decodeRaw(buffer);
|
||||
return decodeRaw(buffer, ext);
|
||||
case "ico":
|
||||
return decodeIco(buffer);
|
||||
case "psd":
|
||||
@@ -84,16 +95,47 @@ async function decodeIco(buffer: Buffer): Promise<Buffer> {
|
||||
}
|
||||
}
|
||||
|
||||
// ── RAW decoder (ImageMagick with LibRaw delegate) ─────────────
|
||||
// ── RAW decoder (ExifTool-first, ImageMagick fallback) ──────────
|
||||
//
|
||||
// Strategy: Many camera RAW files (CR2, CR3, NEF, ARW, etc.) embed a
|
||||
// full-size JPEG preview. ExifTool can extract it near-instantly with
|
||||
// `-b -JpgFromRaw`. This is faster and more reliable than ImageMagick's
|
||||
// LibRaw delegate, which may not support newer formats like CR3.
|
||||
//
|
||||
// If ExifTool extraction fails (no embedded JPEG, or exiftool not
|
||||
// installed), we fall back to ImageMagick + LibRaw.
|
||||
|
||||
async function decodeRaw(buffer: Buffer): Promise<Buffer> {
|
||||
const cmd = await findMagickCmd();
|
||||
async function decodeRaw(buffer: Buffer, ext?: string): Promise<Buffer> {
|
||||
const id = randomUUID();
|
||||
const inputPath = join(tmpdir(), `raw-in-${id}.dng`);
|
||||
// Use the original extension so ExifTool / ImageMagick can identify the RAW variant.
|
||||
const suffix = ext ? `.${ext.replace(/^\./, "")}` : ".dng";
|
||||
const inputPath = join(tmpdir(), `raw-in-${id}${suffix}`);
|
||||
const outputPath = join(tmpdir(), `raw-out-${id}.png`);
|
||||
|
||||
try {
|
||||
await writeFile(inputPath, buffer);
|
||||
|
||||
// Attempt 1: ExifTool embedded JPEG extraction (fast path)
|
||||
try {
|
||||
const { stdout } = await execFileAsync("exiftool", ["-b", "-JpgFromRaw", inputPath], {
|
||||
encoding: "buffer",
|
||||
maxBuffer: 50 * 1024 * 1024,
|
||||
timeout: 30_000,
|
||||
} as never);
|
||||
// stdout is a Buffer when encoding is "buffer"
|
||||
const jpegBuf = stdout as unknown as Buffer;
|
||||
if (jpegBuf && jpegBuf.length > 1000) {
|
||||
// Verify it starts with JPEG SOI marker
|
||||
if (jpegBuf[0] === 0xff && jpegBuf[1] === 0xd8) {
|
||||
return jpegBuf;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ExifTool not available or no embedded JPEG -- fall through
|
||||
}
|
||||
|
||||
// Attempt 2: ImageMagick + LibRaw delegate (full decode)
|
||||
const cmd = await findMagickCmd();
|
||||
await execFileAsync(
|
||||
cmd,
|
||||
magickArgs(cmd, [inputPath, "-colorspace", "sRGB", "-auto-orient", `png:${outputPath}`]),
|
||||
|
||||
Reference in New Issue
Block a user