diff --git a/apps/api/src/lib/format-decoders.ts b/apps/api/src/lib/format-decoders.ts index 91c05f00..333aa085 100644 --- a/apps/api/src/lib/format-decoders.ts +++ b/apps/api/src/lib/format-decoders.ts @@ -24,6 +24,9 @@ const CLI_DECODED_FORMATS = new Set([ "dds", "cur", "dpx", + "ppm", + "pgm", + "pbm", "fits", ]); @@ -78,6 +81,11 @@ export async function decodeToSharpCompat( return decodeFits(buffer); case "qoi": return decodeQoi(buffer); + case "ppm": + case "pgm": + case "pbm": + return decodeNetpbm(buffer, format); + return decodeQoi(buffer); default: return buffer; } @@ -452,3 +460,27 @@ async function decodeQoi(buffer: Buffer): Promise { .png() .toBuffer(); } + +// ── Netpbm (PPM/PGM/PBM) decoder ── + +async function decodeNetpbm(buffer: Buffer, format: string): Promise { + try { + return await sharp(buffer).png().toBuffer(); + } catch { + const cmd = await findMagickCmd(); + const id = randomUUID(); + const ext = format === "pgm" ? "pgm" : format === "pbm" ? "pbm" : "ppm"; + const inputPath = join(tmpdir(), `netpbm-in-${id}.${ext}`); + const outputPath = join(tmpdir(), `netpbm-out-${id}.png`); + try { + await writeFile(inputPath, buffer); + await execFileAsync(cmd, magickArgs(cmd, [inputPath, `png:${outputPath}`]), { + timeout: 120_000, + }); + return await readFile(outputPath); + } finally { + await rm(inputPath, { force: true }).catch(() => {}); + await rm(outputPath, { force: true }).catch(() => {}); + } + } +} diff --git a/tests/fixtures/formats/sample.qoi b/tests/fixtures/formats/sample.qoi new file mode 100644 index 00000000..6605fb53 Binary files /dev/null and b/tests/fixtures/formats/sample.qoi differ diff --git a/tests/integration/format-matrix.test.ts b/tests/integration/format-matrix.test.ts index 0cc00b30..8ec2f96c 100644 --- a/tests/integration/format-matrix.test.ts +++ b/tests/integration/format-matrix.test.ts @@ -207,7 +207,7 @@ const FORMAT_SAMPLES: FormatSample[] = [ name: "PPM", file: "sample.ppm", mime: "image/x-portable-pixmap", - needsCliDecoder: false, + needsCliDecoder: true, needsHeifDecoder: false, mayFailValidation: false, }, @@ -215,7 +215,7 @@ const FORMAT_SAMPLES: FormatSample[] = [ name: "PGM", file: "sample.pgm", mime: "image/x-portable-graymap", - needsCliDecoder: false, + needsCliDecoder: true, needsHeifDecoder: false, mayFailValidation: false, }, @@ -223,7 +223,7 @@ const FORMAT_SAMPLES: FormatSample[] = [ name: "PBM", file: "sample.pbm", mime: "image/x-portable-bitmap", - needsCliDecoder: false, + needsCliDecoder: true, needsHeifDecoder: false, mayFailValidation: false, }, @@ -267,6 +267,14 @@ const FORMAT_SAMPLES: FormatSample[] = [ needsHeifDecoder: false, mayFailValidation: false, }, + { + name: "QOI", + file: "sample.qoi", + mime: "image/x-qoi", + needsCliDecoder: true, + needsHeifDecoder: false, + mayFailValidation: false, + }, ]; // ---------------------------------------------------------------------------