mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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:
@@ -20,6 +20,7 @@ import { formatZodErrors } from "../lib/errors.js";
|
||||
import { isToolInstalled } from "../lib/feature-status.js";
|
||||
import { validateImageBuffer } from "../lib/file-validation.js";
|
||||
import { sanitizeFilename } from "../lib/filename.js";
|
||||
import { decodeToSharpCompat, needsCliDecode } from "../lib/format-decoders.js";
|
||||
import { decodeHeic } from "../lib/heic-converter.js";
|
||||
import { type JobProgress, updateJobProgress } from "./progress.js";
|
||||
import { getToolConfig } from "./tool-factory.js";
|
||||
@@ -144,7 +145,7 @@ export async function registerBatchRoutes(app: FastifyInstance): Promise<void> {
|
||||
updateJobProgress({ ...progress });
|
||||
|
||||
// Validate the image
|
||||
const validation = await validateImageBuffer(file.buffer);
|
||||
const validation = await validateImageBuffer(file.buffer, file.filename);
|
||||
if (!validation.valid) {
|
||||
progress.failedFiles++;
|
||||
progress.errors.push({
|
||||
@@ -167,6 +168,11 @@ export async function registerBatchRoutes(app: FastifyInstance): Promise<void> {
|
||||
const ext = processFilename.match(/\.[^.]+$/)?.[0];
|
||||
if (ext) processFilename = `${processFilename.slice(0, -ext.length)}.png`;
|
||||
}
|
||||
if (!skipPreprocess && needsCliDecode(validation.format)) {
|
||||
processBuffer = await decodeToSharpCompat(processBuffer, validation.format);
|
||||
const ext = processFilename.match(/\.[^.]+$/)?.[0];
|
||||
if (ext) processFilename = `${processFilename.slice(0, -ext.length)}.png`;
|
||||
}
|
||||
if (!skipPreprocess) {
|
||||
processBuffer = await autoOrient(processBuffer);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||
import sharp from "sharp";
|
||||
import { validateImageBuffer } from "../lib/file-validation.js";
|
||||
import { sanitizeFilename } from "../lib/filename.js";
|
||||
import { decodeToSharpCompat, needsCliDecode } from "../lib/format-decoders.js";
|
||||
import { decodeHeic } from "../lib/heic-converter.js";
|
||||
import { createWorkspace, getWorkspacePath } from "../lib/workspace.js";
|
||||
|
||||
@@ -49,8 +50,8 @@ export async function fileRoutes(app: FastifyInstance): Promise<void> {
|
||||
// Skip empty parts (e.g. empty file field)
|
||||
if (buffer.length === 0) continue;
|
||||
|
||||
// Validate the image
|
||||
const validation = await validateImageBuffer(buffer);
|
||||
// Validate the image (pass filename for extension-based format detection)
|
||||
const validation = await validateImageBuffer(buffer, part.filename);
|
||||
if (!validation.valid) {
|
||||
return reply.status(400).send({
|
||||
error: `Invalid file "${part.filename}": ${validation.reason}`,
|
||||
@@ -132,7 +133,7 @@ export async function fileRoutes(app: FastifyInstance): Promise<void> {
|
||||
}
|
||||
let buffer = await data.toBuffer();
|
||||
|
||||
const validation = await validateImageBuffer(buffer);
|
||||
const validation = await validateImageBuffer(buffer, data.filename);
|
||||
if (!validation.valid) {
|
||||
return reply.status(400).send({ error: validation.reason });
|
||||
}
|
||||
@@ -146,6 +147,17 @@ export async function fileRoutes(app: FastifyInstance): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// Decode CLI-decoded formats (RAW, PSD, TGA, EXR, HDR) via external tools
|
||||
if (needsCliDecode(validation.format)) {
|
||||
try {
|
||||
buffer = await decodeToSharpCompat(buffer, validation.format);
|
||||
} catch {
|
||||
return reply.status(422).send({
|
||||
error: `Failed to decode ${validation.format.toUpperCase()} file`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const webp = await sharp(buffer)
|
||||
.resize(1200, 1200, { fit: "inside", withoutEnlargement: true })
|
||||
.webp({ quality: 80 })
|
||||
@@ -170,6 +182,19 @@ function getContentType(ext: string): string {
|
||||
zip: "application/zip",
|
||||
ico: "image/x-icon",
|
||||
json: "application/json",
|
||||
jxl: "image/jxl",
|
||||
dng: "image/x-adobe-dng",
|
||||
cr2: "image/x-canon-cr2",
|
||||
nef: "image/x-nikon-nef",
|
||||
arw: "image/x-sony-arw",
|
||||
orf: "image/x-olympus-orf",
|
||||
rw2: "image/x-panasonic-rw2",
|
||||
tga: "image/x-tga",
|
||||
psd: "image/vnd.adobe.photoshop",
|
||||
exr: "image/x-exr",
|
||||
hdr: "image/vnd.radiance",
|
||||
heic: "image/heic",
|
||||
heif: "image/heif",
|
||||
};
|
||||
return map[ext] ?? "application/octet-stream";
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import { formatZodErrors } from "../lib/errors.js";
|
||||
import { isToolInstalled } from "../lib/feature-status.js";
|
||||
import { validateImageBuffer } from "../lib/file-validation.js";
|
||||
import { sanitizeFilename } from "../lib/filename.js";
|
||||
import { decodeToSharpCompat, needsCliDecode } from "../lib/format-decoders.js";
|
||||
import { decodeHeic } from "../lib/heic-converter.js";
|
||||
import { createWorkspace } from "../lib/workspace.js";
|
||||
import { requireAuth } from "../plugins/auth.js";
|
||||
@@ -101,7 +102,7 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
|
||||
}
|
||||
|
||||
// Validate the initial image
|
||||
const validation = await validateImageBuffer(fileBuffer);
|
||||
const validation = await validateImageBuffer(fileBuffer, filename);
|
||||
if (!validation.valid) {
|
||||
return reply.status(400).send({
|
||||
error: `Invalid image: ${validation.reason}`,
|
||||
@@ -123,6 +124,20 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
|
||||
}
|
||||
}
|
||||
|
||||
// Decode CLI-decoded formats (RAW, TGA, PSD, EXR, HDR)
|
||||
if (needsCliDecode(validation.format)) {
|
||||
try {
|
||||
fileBuffer = await decodeToSharpCompat(fileBuffer, validation.format);
|
||||
const ext = filename.match(/\.[^.]+$/)?.[0];
|
||||
if (ext) filename = `${filename.slice(0, -ext.length)}.png`;
|
||||
} catch (err) {
|
||||
return reply.status(422).send({
|
||||
error: `Failed to decode ${validation.format} file`,
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize EXIF orientation before passing to pipeline steps
|
||||
fileBuffer = await autoOrient(fileBuffer);
|
||||
|
||||
@@ -517,7 +532,7 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
|
||||
updateJobProgress({ ...progress });
|
||||
|
||||
// Validate the image
|
||||
const validation = await validateImageBuffer(file.buffer);
|
||||
const validation = await validateImageBuffer(file.buffer, file.filename);
|
||||
if (!validation.valid) {
|
||||
progress.failedFiles++;
|
||||
progress.errors.push({
|
||||
@@ -540,6 +555,13 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
|
||||
if (ext) currentFilename = `${currentFilename.slice(0, -ext.length)}.png`;
|
||||
}
|
||||
|
||||
// Decode CLI-decoded formats (RAW, TGA, PSD, EXR, HDR)
|
||||
if (needsCliDecode(validation.format)) {
|
||||
currentBuffer = await decodeToSharpCompat(currentBuffer, validation.format);
|
||||
const ext = currentFilename.match(/\.[^.]+$/)?.[0];
|
||||
if (ext) currentFilename = `${currentFilename.slice(0, -ext.length)}.png`;
|
||||
}
|
||||
|
||||
// Normalize EXIF orientation
|
||||
currentBuffer = await autoOrient(currentBuffer);
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { formatZodErrors } from "../lib/errors.js";
|
||||
import { isToolInstalled } from "../lib/feature-status.js";
|
||||
import { validateImageBuffer } from "../lib/file-validation.js";
|
||||
import { sanitizeFilename } from "../lib/filename.js";
|
||||
import { decodeToSharpCompat, needsCliDecode } from "../lib/format-decoders.js";
|
||||
import { decodeHeic } from "../lib/heic-converter.js";
|
||||
import type { WorkerInput, WorkerOutput } from "../lib/image-worker.js";
|
||||
import { sanitizeSvg } from "../lib/svg-sanitize.js";
|
||||
@@ -147,7 +148,7 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
|
||||
}
|
||||
|
||||
// Validate the uploaded image
|
||||
const validation = await validateImageBuffer(fileBuffer);
|
||||
const validation = await validateImageBuffer(fileBuffer, filename);
|
||||
if (!validation.valid) {
|
||||
return reply.status(400).send({ error: `Invalid image: ${validation.reason}` });
|
||||
}
|
||||
@@ -169,6 +170,21 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
|
||||
}
|
||||
}
|
||||
|
||||
// Decode CLI-decoded formats (RAW, PSD, TGA, EXR, HDR) via external tools.
|
||||
// The decoded buffer is PNG, so update the filename extension to match.
|
||||
if (needsCliDecode(validation.format)) {
|
||||
try {
|
||||
fileBuffer = await decodeToSharpCompat(fileBuffer, validation.format);
|
||||
const ext = filename.match(/\.[^.]+$/)?.[0];
|
||||
if (ext) filename = `${filename.slice(0, -ext.length)}.png`;
|
||||
} catch (err) {
|
||||
return reply.status(422).send({
|
||||
error: `Failed to decode ${validation.format.toUpperCase()} file`,
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize SVG input to prevent XXE, SSRF, and script injection
|
||||
const isSvg = validation.format === "svg";
|
||||
if (isSvg) {
|
||||
@@ -279,6 +295,7 @@ export function createToolRoute<T>(app: FastifyInstance, config: ToolRouteConfig
|
||||
"image/svg+xml",
|
||||
"image/bmp",
|
||||
"image/avif",
|
||||
"image/x-icon",
|
||||
]);
|
||||
let previewUrl: string | undefined;
|
||||
if (!BROWSER_PREVIEWABLE.has(result.contentType)) {
|
||||
|
||||
@@ -104,7 +104,7 @@ export function registerBarcodeRead(app: FastifyInstance) {
|
||||
}
|
||||
|
||||
// --- Validate ---
|
||||
const validation = await validateImageBuffer(fileBuffer);
|
||||
const validation = await validateImageBuffer(fileBuffer, filename);
|
||||
if (!validation.valid) {
|
||||
return reply.status(400).send({
|
||||
error: `Invalid image: ${validation.reason}`,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { z } from "zod";
|
||||
import { autoOrient } from "../../lib/auto-orient.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, ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
import { updateSingleFileProgress } from "../progress.js";
|
||||
@@ -60,7 +61,7 @@ export function registerBlurFaces(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}` });
|
||||
}
|
||||
@@ -72,6 +73,11 @@ export function registerBlurFaces(app: FastifyInstance) {
|
||||
fileBuffer = await decodeHeic(fileBuffer);
|
||||
}
|
||||
|
||||
// Decode CLI-decoded formats (RAW, TGA, PSD, EXR, HDR)
|
||||
if (needsCliDecode(validation.format)) {
|
||||
fileBuffer = await decodeToSharpCompat(fileBuffer, validation.format);
|
||||
}
|
||||
|
||||
request.log.info(
|
||||
{
|
||||
toolId: "blur-faces",
|
||||
|
||||
@@ -455,7 +455,7 @@ export function registerCollage(app: FastifyInstance) {
|
||||
|
||||
// Validate all files and decode HEIC/HEIF
|
||||
for (const file of files) {
|
||||
const validation = await validateImageBuffer(file.buffer);
|
||||
const validation = await validateImageBuffer(file.buffer, file.filename);
|
||||
if (!validation.valid) {
|
||||
return reply
|
||||
.status(400)
|
||||
|
||||
@@ -9,6 +9,7 @@ import { z } from "zod";
|
||||
import { autoOrient } from "../../lib/auto-orient.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 { resolveOutputFormat } from "../../lib/output-format.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
@@ -66,7 +67,7 @@ export function registerColorize(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}` });
|
||||
}
|
||||
@@ -86,6 +87,11 @@ export function registerColorize(app: FastifyInstance) {
|
||||
fileBuffer = await decodeHeic(fileBuffer);
|
||||
}
|
||||
|
||||
// Decode CLI-decoded formats (RAW, TGA, PSD, EXR, HDR)
|
||||
if (needsCliDecode(validation.format)) {
|
||||
fileBuffer = await decodeToSharpCompat(fileBuffer, validation.format);
|
||||
}
|
||||
|
||||
// Auto-orient to fix EXIF rotation
|
||||
fileBuffer = await autoOrient(fileBuffer);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { z } from "zod";
|
||||
import { autoOrient } from "../../lib/auto-orient.js";
|
||||
import { formatZodErrors } from "../../lib/errors.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 } from "../../lib/workspace.js";
|
||||
import { registerToolProcessFn } from "../tool-factory.js";
|
||||
@@ -56,7 +57,7 @@ export function registerContentAwareResize(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}` });
|
||||
}
|
||||
@@ -75,6 +76,20 @@ export function registerContentAwareResize(app: FastifyInstance) {
|
||||
}
|
||||
}
|
||||
|
||||
// Decode CLI-decoded formats (RAW, TGA, PSD, EXR, HDR)
|
||||
if (needsCliDecode(validation.format)) {
|
||||
try {
|
||||
fileBuffer = await decodeToSharpCompat(fileBuffer, validation.format);
|
||||
const ext = filename.match(/\.[^.]+$/)?.[0];
|
||||
if (ext) filename = `${filename.slice(0, -ext.length)}.png`;
|
||||
} catch (err) {
|
||||
return reply.status(422).send({
|
||||
error: `Failed to decode ${validation.format} file`,
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Validate settings
|
||||
let settings: Settings;
|
||||
try {
|
||||
@@ -162,6 +177,11 @@ export function registerContentAwareResize(app: FastifyInstance) {
|
||||
if (["heic", "heif", "hif"].includes(ext)) {
|
||||
buf = await decodeHeic(buf);
|
||||
}
|
||||
// Decode CLI-decoded formats (RAW, TGA, PSD, EXR, HDR) for pipeline/batch mode
|
||||
const cliCheck = await validateImageBuffer(inputBuffer, filename);
|
||||
if (cliCheck.valid && needsCliDecode(cliCheck.format)) {
|
||||
buf = await decodeToSharpCompat(inputBuffer, cliCheck.format);
|
||||
}
|
||||
const orientedBuffer = await autoOrient(buf);
|
||||
const jobId = randomUUID();
|
||||
const workspacePath = await createWorkspace(jobId);
|
||||
|
||||
@@ -16,10 +16,11 @@ const FORMAT_CONTENT_TYPES: Record<string, string> = {
|
||||
gif: "image/gif",
|
||||
heic: "image/heic",
|
||||
heif: "image/heif",
|
||||
jxl: "image/jxl",
|
||||
};
|
||||
|
||||
const settingsSchema = z.object({
|
||||
format: z.enum(["jpg", "png", "webp", "avif", "tiff", "gif", "heic", "heif"]),
|
||||
format: z.enum(["jpg", "png", "webp", "avif", "tiff", "gif", "heic", "heif", "jxl"]),
|
||||
quality: z.number().min(1).max(100).optional(),
|
||||
});
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ export function registerEditMetadata(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}` });
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { z } from "zod";
|
||||
import { autoOrient } from "../../lib/auto-orient.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 } from "../../lib/workspace.js";
|
||||
import { updateSingleFileProgress } from "../progress.js";
|
||||
@@ -61,7 +62,7 @@ export function registerEnhanceFaces(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}` });
|
||||
}
|
||||
@@ -82,6 +83,11 @@ export function registerEnhanceFaces(app: FastifyInstance) {
|
||||
fileBuffer = await decodeHeic(fileBuffer);
|
||||
}
|
||||
|
||||
// Decode CLI-decoded formats (RAW, TGA, PSD, EXR, HDR)
|
||||
if (needsCliDecode(validation.format)) {
|
||||
fileBuffer = await decodeToSharpCompat(fileBuffer, validation.format);
|
||||
}
|
||||
|
||||
// Auto-orient to fix EXIF rotation before face detection
|
||||
fileBuffer = await autoOrient(fileBuffer);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import sharp from "sharp";
|
||||
import { autoOrient } from "../../lib/auto-orient.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, encodeHeic } from "../../lib/heic-converter.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
import { updateSingleFileProgress } from "../progress.js";
|
||||
@@ -90,11 +91,11 @@ export function registerEraseObject(app: FastifyInstance) {
|
||||
});
|
||||
}
|
||||
|
||||
const imageValidation = await validateImageBuffer(imageBuffer);
|
||||
const imageValidation = await validateImageBuffer(imageBuffer, filename);
|
||||
if (!imageValidation.valid) {
|
||||
return reply.status(400).send({ error: `Invalid image: ${imageValidation.reason}` });
|
||||
}
|
||||
const maskValidation = await validateImageBuffer(maskBuffer);
|
||||
const maskValidation = await validateImageBuffer(maskBuffer, "mask.png");
|
||||
if (!maskValidation.valid) {
|
||||
return reply.status(400).send({ error: `Invalid mask: ${maskValidation.reason}` });
|
||||
}
|
||||
@@ -115,6 +116,11 @@ export function registerEraseObject(app: FastifyInstance) {
|
||||
imageBuffer = await decodeHeic(imageBuffer);
|
||||
}
|
||||
|
||||
// Decode CLI-decoded formats (RAW, TGA, PSD, EXR, HDR)
|
||||
if (needsCliDecode(imageValidation.format)) {
|
||||
imageBuffer = await decodeToSharpCompat(imageBuffer, imageValidation.format);
|
||||
}
|
||||
|
||||
// Auto-orient to fix EXIF rotation
|
||||
imageBuffer = await autoOrient(imageBuffer);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import sharp from "sharp";
|
||||
import { z } from "zod";
|
||||
import { autoOrient } from "../../lib/auto-orient.js";
|
||||
import { validateImageBuffer } from "../../lib/file-validation.js";
|
||||
import { decodeToSharpCompat, needsCliDecode } from "../../lib/format-decoders.js";
|
||||
import { decodeHeic } from "../../lib/heic-converter.js";
|
||||
import { resolveOutputFormat } from "../../lib/output-format.js";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
@@ -60,6 +61,7 @@ export function registerImageEnhancement(app: FastifyInstance) {
|
||||
"/api/v1/tools/image-enhancement/analyze",
|
||||
async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
let fileBuffer: Buffer | null = null;
|
||||
let filename = "image";
|
||||
|
||||
try {
|
||||
const parts = request.parts();
|
||||
@@ -70,6 +72,7 @@ export function registerImageEnhancement(app: FastifyInstance) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
fileBuffer = Buffer.concat(chunks);
|
||||
filename = part.filename ?? "image";
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -84,7 +87,7 @@ export function registerImageEnhancement(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 +103,18 @@ export function registerImageEnhancement(app: FastifyInstance) {
|
||||
}
|
||||
}
|
||||
|
||||
// Decode CLI-decoded formats (RAW, TGA, PSD, EXR, HDR)
|
||||
if (needsCliDecode(validation.format)) {
|
||||
try {
|
||||
fileBuffer = await decodeToSharpCompat(fileBuffer, validation.format);
|
||||
} catch (err) {
|
||||
return reply.status(422).send({
|
||||
error: `Failed to decode ${validation.format} file`,
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
fileBuffer = await autoOrient(fileBuffer);
|
||||
const analysis = await analyzeImage(fileBuffer);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { z } from "zod";
|
||||
import { autoOrient } from "../../lib/auto-orient.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 } from "../../lib/workspace.js";
|
||||
import { updateSingleFileProgress } from "../progress.js";
|
||||
@@ -72,7 +73,7 @@ export function registerNoiseRemoval(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}` });
|
||||
}
|
||||
@@ -89,6 +90,11 @@ export function registerNoiseRemoval(app: FastifyInstance) {
|
||||
fileBuffer = await decodeHeic(fileBuffer);
|
||||
}
|
||||
|
||||
// Decode CLI-decoded formats (RAW, TGA, PSD, EXR, HDR)
|
||||
if (needsCliDecode(validation.format)) {
|
||||
fileBuffer = await decodeToSharpCompat(fileBuffer, validation.format);
|
||||
}
|
||||
|
||||
// Auto-orient to fix EXIF rotation before processing
|
||||
fileBuffer = await autoOrient(fileBuffer);
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ export function registerOcr(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}` });
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { autoOrient } from "../../lib/auto-orient.js";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { validateImageBuffer } from "../../lib/file-validation.js";
|
||||
import { sanitizeFilename } from "../../lib/filename.js";
|
||||
import { decodeToSharpCompat, needsCliDecode } from "../../lib/format-decoders.js";
|
||||
import { decodeHeic } from "../../lib/heic-converter.js";
|
||||
import { sanitizeSvg } from "../../lib/svg-sanitize.js";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
@@ -83,7 +84,7 @@ export function registerOptimizeForWeb(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,18 @@ export function registerOptimizeForWeb(app: FastifyInstance) {
|
||||
}
|
||||
}
|
||||
|
||||
// Decode CLI-decoded formats (RAW, TGA, PSD, EXR, HDR)
|
||||
if (needsCliDecode(validation.format)) {
|
||||
try {
|
||||
fileBuffer = await decodeToSharpCompat(fileBuffer, validation.format);
|
||||
} catch (err) {
|
||||
return reply.status(422).send({
|
||||
error: `Failed to decode ${validation.format} file`,
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize SVG
|
||||
if (validation.format === "svg") {
|
||||
try {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { autoOrient } from "../../lib/auto-orient.js";
|
||||
import { formatZodErrors } from "../../lib/errors.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";
|
||||
@@ -164,7 +165,7 @@ export function registerPassportPhoto(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}` });
|
||||
}
|
||||
@@ -177,6 +178,13 @@ export function registerPassportPhoto(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);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { z } from "zod";
|
||||
import { autoOrient } from "../../lib/auto-orient.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, ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
import { updateSingleFileProgress } from "../progress.js";
|
||||
@@ -62,7 +63,7 @@ export function registerRedEyeRemoval(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}` });
|
||||
}
|
||||
@@ -74,6 +75,11 @@ export function registerRedEyeRemoval(app: FastifyInstance) {
|
||||
fileBuffer = await decodeHeic(fileBuffer);
|
||||
}
|
||||
|
||||
// Decode CLI-decoded formats (RAW, TGA, PSD, EXR, HDR)
|
||||
if (needsCliDecode(validation.format)) {
|
||||
fileBuffer = await decodeToSharpCompat(fileBuffer, validation.format);
|
||||
}
|
||||
|
||||
request.log.info(
|
||||
{
|
||||
toolId: "red-eye-removal",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,6 +9,7 @@ import { z } from "zod";
|
||||
import { autoOrient } from "../../lib/auto-orient.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 { resolveOutputFormat } from "../../lib/output-format.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
@@ -75,7 +76,7 @@ export function registerRestorePhoto(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}` });
|
||||
}
|
||||
@@ -93,6 +94,11 @@ export function registerRestorePhoto(app: FastifyInstance) {
|
||||
fileBuffer = await decodeHeic(fileBuffer);
|
||||
}
|
||||
|
||||
// Decode CLI-decoded formats (RAW, TGA, PSD, EXR, HDR)
|
||||
if (needsCliDecode(validation.format)) {
|
||||
fileBuffer = await decodeToSharpCompat(fileBuffer, validation.format);
|
||||
}
|
||||
|
||||
// Auto-orient to fix EXIF rotation
|
||||
fileBuffer = await autoOrient(fileBuffer);
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ export function registerStitch(app: FastifyInstance) {
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const validation = await validateImageBuffer(file.buffer);
|
||||
const validation = await validateImageBuffer(file.buffer, file.filename);
|
||||
if (!validation.valid) {
|
||||
return reply
|
||||
.status(400)
|
||||
|
||||
@@ -9,6 +9,7 @@ import { z } from "zod";
|
||||
import { autoOrient } from "../../lib/auto-orient.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, encodeHeic } from "../../lib/heic-converter.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
import { updateSingleFileProgress } from "../progress.js";
|
||||
@@ -64,7 +65,7 @@ export function registerUpscale(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}` });
|
||||
}
|
||||
@@ -87,6 +88,11 @@ export function registerUpscale(app: FastifyInstance) {
|
||||
fileBuffer = await decodeHeic(fileBuffer);
|
||||
}
|
||||
|
||||
// Decode CLI-decoded formats (RAW, TGA, PSD, EXR, HDR)
|
||||
if (needsCliDecode(validation.format)) {
|
||||
fileBuffer = await decodeToSharpCompat(fileBuffer, validation.format);
|
||||
}
|
||||
|
||||
// Auto-orient to fix EXIF rotation before upscaling
|
||||
fileBuffer = await autoOrient(fileBuffer);
|
||||
|
||||
|
||||
@@ -176,7 +176,7 @@ export async function userFileRoutes(app: FastifyInstance): Promise<void> {
|
||||
if (buffer.length === 0) continue;
|
||||
|
||||
// Validate image
|
||||
const validation = await validateImageBuffer(buffer);
|
||||
const validation = await validateImageBuffer(buffer, part.filename);
|
||||
if (!validation.valid) {
|
||||
return reply.status(400).send({
|
||||
error: `Invalid file "${part.filename}": ${validation.reason}`,
|
||||
@@ -503,7 +503,7 @@ export async function userFileRoutes(app: FastifyInstance): Promise<void> {
|
||||
}
|
||||
|
||||
// Validate the image
|
||||
const validation = await validateImageBuffer(fileBuffer);
|
||||
const validation = await validateImageBuffer(fileBuffer, filename);
|
||||
if (!validation.valid) {
|
||||
return reply.status(400).send({
|
||||
error: `Invalid file: ${validation.reason}`,
|
||||
|
||||
Reference in New Issue
Block a user