feat: add support for JXL, Camera RAW, ICO, TGA, PSD, EXR, HDR image formats

Extends the platform to handle 7 new image format families alongside
the existing AVIF support gap-fill. Uses the established HEIC decoder
pattern (CLI decode → PNG → Sharp) for formats Sharp can't handle
natively: Camera RAW via dcraw_emu/LibRaw, PSD/TGA/EXR/HDR via
ImageMagick. JXL and ICO are Sharp-native. Adds server-side preview
for non-browser-displayable formats and JXL as a new convert output
target. All 27 validateImageBuffer callers updated with filename for
extension-based format detection.
This commit is contained in:
ashim-hq
2026-04-21 09:59:57 +08:00
parent e94ac945bb
commit 2aadb66031
39 changed files with 583 additions and 43 deletions
+15 -2
View File
@@ -9,6 +9,7 @@ import { autoOrient } from "../../lib/auto-orient.js";
import { applyEffects } from "../../lib/bg-effects.js";
import { isToolInstalled } from "../../lib/feature-status.js";
import { validateImageBuffer } from "../../lib/file-validation.js";
import { decodeToSharpCompat, needsCliDecode } from "../../lib/format-decoders.js";
import { decodeHeic } from "../../lib/heic-converter.js";
import { createWorkspace, getWorkspacePath } from "../../lib/workspace.js";
import { updateSingleFileProgress } from "../progress.js";
@@ -85,7 +86,7 @@ export function registerRemoveBackground(app: FastifyInstance) {
return reply.status(400).send({ error: "No image file provided" });
}
const validation = await validateImageBuffer(fileBuffer);
const validation = await validateImageBuffer(fileBuffer, filename);
if (!validation.valid) {
return reply.status(400).send({ error: `Invalid image: ${validation.reason}` });
}
@@ -100,6 +101,13 @@ export function registerRemoveBackground(app: FastifyInstance) {
if (ext) filename = `${filename.slice(0, -ext.length)}.png`;
}
// Decode CLI-decoded formats (RAW, TGA, PSD, EXR, HDR)
if (needsCliDecode(validation.format)) {
fileBuffer = await decodeToSharpCompat(fileBuffer, validation.format);
const ext = filename.match(/\.[^.]+$/)?.[0];
if (ext) filename = `${filename.slice(0, -ext.length)}.png`;
}
// Auto-orient to fix EXIF rotation
fileBuffer = await autoOrient(fileBuffer);
@@ -177,6 +185,7 @@ export function registerRemoveBackground(app: FastifyInstance) {
async (request: FastifyRequest, reply: FastifyReply) => {
let settingsRaw: string | null = null;
let bgImageBuffer: Buffer | null = null;
let bgFilename = "background";
try {
const parts = request.parts();
@@ -185,6 +194,7 @@ export function registerRemoveBackground(app: FastifyInstance) {
const chunks: Buffer[] = [];
for await (const chunk of part.file) chunks.push(chunk);
bgImageBuffer = Buffer.concat(chunks);
bgFilename = part.filename ?? "background";
} else if (part.type === "field" && part.fieldname === "settings") {
settingsRaw = part.value as string;
}
@@ -221,10 +231,13 @@ export function registerRemoveBackground(app: FastifyInstance) {
// Decode HEIC/HEIF background image if needed
if (bgImageBuffer) {
const bgValidation = await validateImageBuffer(bgImageBuffer);
const bgValidation = await validateImageBuffer(bgImageBuffer, bgFilename);
if (bgValidation.valid && bgValidation.format === "heif") {
bgImageBuffer = await decodeHeic(bgImageBuffer);
}
if (bgValidation.valid && needsCliDecode(bgValidation.format)) {
bgImageBuffer = await decodeToSharpCompat(bgImageBuffer, bgValidation.format);
}
}
// Apply effects using cached mask + original