mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: add exotic format decoding and SVG sanitization to all custom-route tools
Custom-route tools (split, compare, collage, find-duplicates, etc.) only handled HEIC via ensureSharpCompat, failing on BMP, PSD, RAW, TGA, EXR, HDR, JXL, and other formats Sharp cannot decode natively. Added the full decode pipeline from createToolRoute to all 16 affected routes: format validation via validateImageBuffer, HEIC decoding with actionable error messages, CLI-based exotic format decoding with nested fallback, and SVG sanitization to prevent XXE/SSRF/script injection.
This commit is contained in:
@@ -9,7 +9,9 @@ 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 { ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
import { decodeToSharpCompat, needsCliDecode } from "../../lib/format-decoders.js";
|
||||
import { decodeHeic } from "../../lib/heic-converter.js";
|
||||
import { decompressSvgz, sanitizeSvg } from "../../lib/svg-sanitize.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
@@ -136,8 +138,41 @@ export function registerBarcodeRead(app: FastifyInstance) {
|
||||
try {
|
||||
const tryHarder = settings.tryHarder;
|
||||
|
||||
// Decode HEIC/HEIF if needed, then auto-orient
|
||||
fileBuffer = await ensureSharpCompat(fileBuffer);
|
||||
if (validation.format === "heif") {
|
||||
try {
|
||||
fileBuffer = await decodeHeic(fileBuffer);
|
||||
} catch (err) {
|
||||
return reply.status(422).send({
|
||||
error: "Failed to decode HEIC file. Ensure libheif-examples is installed.",
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (needsCliDecode(validation.format)) {
|
||||
try {
|
||||
const fileExt = filename.split(".").pop()?.toLowerCase();
|
||||
fileBuffer = await decodeToSharpCompat(fileBuffer, validation.format, fileExt);
|
||||
} catch {
|
||||
try {
|
||||
await sharp(fileBuffer).metadata();
|
||||
} catch (err) {
|
||||
return reply.status(422).send({
|
||||
error: `Failed to decode ${validation.format.toUpperCase()} file`,
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (validation.format === "svg") {
|
||||
try {
|
||||
fileBuffer = decompressSvgz(fileBuffer);
|
||||
fileBuffer = sanitizeSvg(fileBuffer);
|
||||
} catch (err) {
|
||||
return reply.status(400).send({
|
||||
error: err instanceof Error ? err.message : "Invalid SVG",
|
||||
});
|
||||
}
|
||||
}
|
||||
fileBuffer = await autoOrient(fileBuffer);
|
||||
|
||||
// Convert to raw RGBA pixel data
|
||||
|
||||
@@ -19,8 +19,11 @@ import {
|
||||
import { renderFrame } from "../../lib/beautify/frames.js";
|
||||
import { applyShadow } from "../../lib/beautify/shadow.js";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { validateImageBuffer } from "../../lib/file-validation.js";
|
||||
import { sanitizeFilename } from "../../lib/filename.js";
|
||||
import { ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
import { decodeToSharpCompat, needsCliDecode } from "../../lib/format-decoders.js";
|
||||
import { decodeHeic } from "../../lib/heic-converter.js";
|
||||
import { decompressSvgz, sanitizeSvg } from "../../lib/svg-sanitize.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
import { registerToolProcessFn } from "../tool-factory.js";
|
||||
|
||||
@@ -74,7 +77,24 @@ export async function processBeautify(
|
||||
bgImageBuffer?: Buffer,
|
||||
): Promise<Buffer> {
|
||||
// 1. Decode & Prepare
|
||||
let buf = await autoOrient(await ensureSharpCompat(inputBuffer));
|
||||
let buf = inputBuffer;
|
||||
const validation = await validateImageBuffer(buf, _filename);
|
||||
if (validation.valid && validation.format === "heif") {
|
||||
buf = await decodeHeic(buf);
|
||||
}
|
||||
if (validation.valid && needsCliDecode(validation.format)) {
|
||||
try {
|
||||
const fileExt = _filename.split(".").pop()?.toLowerCase();
|
||||
buf = await decodeToSharpCompat(buf, validation.format, fileExt);
|
||||
} catch {
|
||||
await sharp(buf).metadata();
|
||||
}
|
||||
}
|
||||
if (validation.valid && validation.format === "svg") {
|
||||
buf = decompressSvgz(buf);
|
||||
buf = sanitizeSvg(buf);
|
||||
}
|
||||
buf = await autoOrient(buf);
|
||||
buf = await sharp(buf).ensureAlpha().png().toBuffer();
|
||||
|
||||
// 2. Apply Border Radius (skip for device frames that have their own bezels)
|
||||
|
||||
@@ -12,7 +12,7 @@ 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, ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
import { decodeHeic } from "../../lib/heic-converter.js";
|
||||
import { resolveOutputFormat } from "../../lib/output-format.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
import { updateSingleFileProgress } from "../progress.js";
|
||||
@@ -214,7 +214,19 @@ export function registerBlurFaces(app: FastifyInstance) {
|
||||
}),
|
||||
process: async (inputBuffer, settings, filename) => {
|
||||
const s = settings as { blurRadius?: number; sensitivity?: number };
|
||||
const orientedBuffer = await autoOrient(await ensureSharpCompat(inputBuffer));
|
||||
let decoded = inputBuffer;
|
||||
const validation = await validateImageBuffer(decoded, filename);
|
||||
if (validation.valid && validation.format === "heif") {
|
||||
decoded = await decodeHeic(decoded);
|
||||
}
|
||||
if (validation.valid && needsCliDecode(validation.format)) {
|
||||
try {
|
||||
decoded = await decodeToSharpCompat(decoded, validation.format);
|
||||
} catch {
|
||||
/* batch handler already decoded */
|
||||
}
|
||||
}
|
||||
const orientedBuffer = await autoOrient(decoded);
|
||||
const jobId = randomUUID();
|
||||
const workspacePath = await createWorkspace(jobId);
|
||||
const result = await blurFaces(orientedBuffer, join(workspacePath, "output"), {
|
||||
|
||||
@@ -8,8 +8,10 @@ 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 { encodeJxl } from "../../lib/format-encoders.js";
|
||||
import { ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
import { decodeHeic } from "../../lib/heic-converter.js";
|
||||
import { decompressSvgz, sanitizeSvg } from "../../lib/svg-sanitize.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
|
||||
// ── Template definitions (mirrors the frontend) ─────────────────────
|
||||
@@ -463,7 +465,42 @@ export function registerCollage(app: FastifyInstance) {
|
||||
.status(400)
|
||||
.send({ error: `Invalid file "${file.filename}": ${validation.reason}` });
|
||||
}
|
||||
file.buffer = await autoOrient(await ensureSharpCompat(file.buffer));
|
||||
if (validation.format === "heif") {
|
||||
try {
|
||||
file.buffer = await decodeHeic(file.buffer);
|
||||
} catch (err) {
|
||||
return reply.status(422).send({
|
||||
error: `Failed to decode "${file.filename}" (HEIC). Ensure libheif-examples is installed.`,
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (needsCliDecode(validation.format)) {
|
||||
try {
|
||||
const fileExt = file.filename.split(".").pop()?.toLowerCase();
|
||||
file.buffer = await decodeToSharpCompat(file.buffer, validation.format, fileExt);
|
||||
} catch {
|
||||
try {
|
||||
await sharp(file.buffer).metadata();
|
||||
} catch (err) {
|
||||
return reply.status(422).send({
|
||||
error: `Failed to decode "${file.filename}" (${validation.format.toUpperCase()})`,
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (validation.format === "svg") {
|
||||
try {
|
||||
file.buffer = decompressSvgz(file.buffer);
|
||||
file.buffer = sanitizeSvg(file.buffer);
|
||||
} catch (err) {
|
||||
return reply.status(400).send({
|
||||
error: `Invalid SVG "${file.filename}": ${err instanceof Error ? err.message : "Unknown error"}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
file.buffer = await autoOrient(file.buffer);
|
||||
}
|
||||
|
||||
let settings: z.infer<typeof settingsSchema>;
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import sharp from "sharp";
|
||||
import { validateImageBuffer } from "../../lib/file-validation.js";
|
||||
import { sanitizeFilename } from "../../lib/filename.js";
|
||||
import { ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
import { decodeToSharpCompat, needsCliDecode } from "../../lib/format-decoders.js";
|
||||
import { decodeHeic } from "../../lib/heic-converter.js";
|
||||
import { decompressSvgz, sanitizeSvg } from "../../lib/svg-sanitize.js";
|
||||
|
||||
/**
|
||||
* Simple k-means-like color quantization to extract dominant colors.
|
||||
@@ -70,8 +73,45 @@ export function registerColorPalette(app: FastifyInstance) {
|
||||
}
|
||||
|
||||
try {
|
||||
// Decode HEIC/HEIF if needed
|
||||
fileBuffer = await ensureSharpCompat(fileBuffer);
|
||||
const validation = await validateImageBuffer(fileBuffer, filename);
|
||||
if (!validation.valid) {
|
||||
return reply.status(400).send({ error: `Invalid image: ${validation.reason}` });
|
||||
}
|
||||
if (validation.format === "heif") {
|
||||
try {
|
||||
fileBuffer = await decodeHeic(fileBuffer);
|
||||
} catch (err) {
|
||||
return reply.status(422).send({
|
||||
error: "Failed to decode HEIC file. Ensure libheif-examples is installed.",
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (needsCliDecode(validation.format)) {
|
||||
try {
|
||||
const fileExt = filename.split(".").pop()?.toLowerCase();
|
||||
fileBuffer = await decodeToSharpCompat(fileBuffer, validation.format, fileExt);
|
||||
} catch {
|
||||
try {
|
||||
await sharp(fileBuffer).metadata();
|
||||
} catch (err) {
|
||||
return reply.status(422).send({
|
||||
error: `Failed to decode ${validation.format.toUpperCase()} file`,
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (validation.format === "svg") {
|
||||
try {
|
||||
fileBuffer = decompressSvgz(fileBuffer);
|
||||
fileBuffer = sanitizeSvg(fileBuffer);
|
||||
} catch (err) {
|
||||
return reply.status(400).send({
|
||||
error: err instanceof Error ? err.message : "Invalid SVG",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Resize to small image for analysis
|
||||
const raw = await sharp(fileBuffer)
|
||||
|
||||
@@ -4,7 +4,10 @@ import { join } from "node:path";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import sharp from "sharp";
|
||||
import { autoOrient } from "../../lib/auto-orient.js";
|
||||
import { ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
import { validateImageBuffer } from "../../lib/file-validation.js";
|
||||
import { decodeToSharpCompat, needsCliDecode } from "../../lib/format-decoders.js";
|
||||
import { decodeHeic } from "../../lib/heic-converter.js";
|
||||
import { decompressSvgz, sanitizeSvg } from "../../lib/svg-sanitize.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
|
||||
/**
|
||||
@@ -43,9 +46,85 @@ export function registerCompare(app: FastifyInstance) {
|
||||
}
|
||||
|
||||
try {
|
||||
// Decode HEIC/HEIF if needed, then normalize EXIF orientation
|
||||
bufferA = await autoOrient(await ensureSharpCompat(bufferA));
|
||||
bufferB = await autoOrient(await ensureSharpCompat(bufferB));
|
||||
const valA = await validateImageBuffer(bufferA, "image");
|
||||
if (!valA.valid) {
|
||||
return reply.status(400).send({ error: `Invalid first image: ${valA.reason}` });
|
||||
}
|
||||
if (valA.format === "heif") {
|
||||
try {
|
||||
bufferA = await decodeHeic(bufferA);
|
||||
} catch (err) {
|
||||
return reply.status(422).send({
|
||||
error: "Failed to decode first image (HEIC). Ensure libheif-examples is installed.",
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (needsCliDecode(valA.format)) {
|
||||
try {
|
||||
bufferA = await decodeToSharpCompat(bufferA, valA.format);
|
||||
} catch {
|
||||
try {
|
||||
await sharp(bufferA).metadata();
|
||||
} catch (err) {
|
||||
return reply.status(422).send({
|
||||
error: `Failed to decode first image (${valA.format.toUpperCase()})`,
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (valA.format === "svg") {
|
||||
try {
|
||||
bufferA = decompressSvgz(bufferA);
|
||||
bufferA = sanitizeSvg(bufferA);
|
||||
} catch (err) {
|
||||
return reply.status(400).send({
|
||||
error: err instanceof Error ? err.message : "Invalid SVG (first image)",
|
||||
});
|
||||
}
|
||||
}
|
||||
bufferA = await autoOrient(bufferA);
|
||||
|
||||
const valB = await validateImageBuffer(bufferB, "image");
|
||||
if (!valB.valid) {
|
||||
return reply.status(400).send({ error: `Invalid second image: ${valB.reason}` });
|
||||
}
|
||||
if (valB.format === "heif") {
|
||||
try {
|
||||
bufferB = await decodeHeic(bufferB);
|
||||
} catch (err) {
|
||||
return reply.status(422).send({
|
||||
error: "Failed to decode second image (HEIC). Ensure libheif-examples is installed.",
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (needsCliDecode(valB.format)) {
|
||||
try {
|
||||
bufferB = await decodeToSharpCompat(bufferB, valB.format);
|
||||
} catch {
|
||||
try {
|
||||
await sharp(bufferB).metadata();
|
||||
} catch (err) {
|
||||
return reply.status(422).send({
|
||||
error: `Failed to decode second image (${valB.format.toUpperCase()})`,
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (valB.format === "svg") {
|
||||
try {
|
||||
bufferB = decompressSvgz(bufferB);
|
||||
bufferB = sanitizeSvg(bufferB);
|
||||
} catch (err) {
|
||||
return reply.status(400).send({
|
||||
error: err instanceof Error ? err.message : "Invalid SVG (second image)",
|
||||
});
|
||||
}
|
||||
}
|
||||
bufferB = await autoOrient(bufferB);
|
||||
|
||||
// Normalize both to same size for comparison
|
||||
const metaA = await sharp(bufferA).metadata();
|
||||
|
||||
@@ -3,8 +3,11 @@ import sharp from "sharp";
|
||||
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 { sanitizeFilename } from "../../lib/filename.js";
|
||||
import { ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
import { decodeToSharpCompat, needsCliDecode } from "../../lib/format-decoders.js";
|
||||
import { decodeHeic } from "../../lib/heic-converter.js";
|
||||
import { decompressSvgz, sanitizeSvg } from "../../lib/svg-sanitize.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
threshold: z.number().min(0).max(20).default(8),
|
||||
@@ -150,9 +153,49 @@ export function registerFindDuplicates(app: FastifyInstance) {
|
||||
}
|
||||
|
||||
try {
|
||||
// Decode HEIC/HEIF if needed, then normalize EXIF orientation
|
||||
for (const file of files) {
|
||||
file.buffer = await autoOrient(await ensureSharpCompat(file.buffer));
|
||||
const validation = await validateImageBuffer(file.buffer, file.filename);
|
||||
if (!validation.valid) {
|
||||
return reply
|
||||
.status(400)
|
||||
.send({ error: `Invalid file "${file.filename}": ${validation.reason}` });
|
||||
}
|
||||
if (validation.format === "heif") {
|
||||
try {
|
||||
file.buffer = await decodeHeic(file.buffer);
|
||||
} catch (err) {
|
||||
return reply.status(422).send({
|
||||
error: `Failed to decode "${file.filename}" (HEIC). Ensure libheif-examples is installed.`,
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (needsCliDecode(validation.format)) {
|
||||
try {
|
||||
const fileExt = file.filename.split(".").pop()?.toLowerCase();
|
||||
file.buffer = await decodeToSharpCompat(file.buffer, validation.format, fileExt);
|
||||
} catch {
|
||||
try {
|
||||
await sharp(file.buffer).metadata();
|
||||
} catch (err) {
|
||||
return reply.status(422).send({
|
||||
error: `Failed to decode "${file.filename}" (${validation.format.toUpperCase()})`,
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (validation.format === "svg") {
|
||||
try {
|
||||
file.buffer = decompressSvgz(file.buffer);
|
||||
file.buffer = sanitizeSvg(file.buffer);
|
||||
} catch (err) {
|
||||
return reply.status(400).send({
|
||||
error: `Invalid SVG "${file.filename}": ${err instanceof Error ? err.message : "Unknown error"}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
file.buffer = await autoOrient(file.buffer);
|
||||
}
|
||||
|
||||
// Extract metadata, thumbnails, and compute hashes
|
||||
|
||||
@@ -3,8 +3,11 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||
import sharp from "sharp";
|
||||
import { z } from "zod";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { validateImageBuffer } from "../../lib/file-validation.js";
|
||||
import { decodeToSharpCompat, needsCliDecode } from "../../lib/format-decoders.js";
|
||||
import { encodeJxl } from "../../lib/format-encoders.js";
|
||||
import { ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
import { decodeHeic } from "../../lib/heic-converter.js";
|
||||
import { decompressSvgz, sanitizeSvg } from "../../lib/svg-sanitize.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
outputFormat: z.enum(["original", "jpeg", "png", "webp", "avif", "jxl"]).default("original"),
|
||||
@@ -103,8 +106,23 @@ export function registerImageToBase64(app: FastifyInstance) {
|
||||
try {
|
||||
const originalSize = buffer.length;
|
||||
|
||||
// Decode HEIC/HEIF to PNG for Sharp compatibility
|
||||
const decoded = await ensureSharpCompat(buffer);
|
||||
let decoded = buffer;
|
||||
const validation = await validateImageBuffer(buffer, filename);
|
||||
if (validation.valid && validation.format === "heif") {
|
||||
decoded = await decodeHeic(decoded);
|
||||
}
|
||||
if (validation.valid && needsCliDecode(validation.format)) {
|
||||
try {
|
||||
const fileExt = filename.split(".").pop()?.toLowerCase();
|
||||
decoded = await decodeToSharpCompat(decoded, validation.format, fileExt);
|
||||
} catch {
|
||||
await sharp(decoded).metadata();
|
||||
}
|
||||
}
|
||||
if (validation.valid && validation.format === "svg") {
|
||||
decoded = decompressSvgz(decoded);
|
||||
decoded = sanitizeSvg(decoded);
|
||||
}
|
||||
let pipeline = sharp(decoded);
|
||||
|
||||
// Get original metadata for dimensions
|
||||
|
||||
@@ -8,7 +8,7 @@ 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 { ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
import { decodeHeic } from "../../lib/heic-converter.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
@@ -66,7 +66,7 @@ export function registerInfo(app: FastifyInstance) {
|
||||
if (detectedFormat && needsCliDecode(detectedFormat)) {
|
||||
metaBuffer = await decodeToSharpCompat(fileBuffer, detectedFormat, ext);
|
||||
} else {
|
||||
metaBuffer = await ensureSharpCompat(fileBuffer);
|
||||
metaBuffer = await decodeHeic(fileBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,11 @@ import sharp from "sharp";
|
||||
import { z } from "zod";
|
||||
import { autoOrient } from "../../lib/auto-orient.js";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
import { validateImageBuffer } from "../../lib/file-validation.js";
|
||||
import { decodeToSharpCompat, needsCliDecode } from "../../lib/format-decoders.js";
|
||||
import { decodeHeic } from "../../lib/heic-converter.js";
|
||||
import { renderMemeTextSvg } from "../../lib/meme-text-renderer.js";
|
||||
import { decompressSvgz, sanitizeSvg } from "../../lib/svg-sanitize.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
import { registerToolProcessFn } from "../tool-factory.js";
|
||||
|
||||
@@ -178,7 +181,20 @@ export function registerMemeGenerator(app: FastifyInstance) {
|
||||
settingsSchema: settingsSchema as z.ZodType<unknown, z.ZodTypeDef, unknown>,
|
||||
process: async (inputBuffer: Buffer, settings: unknown, filename: string) => {
|
||||
const parsed = settingsSchema.parse(settings);
|
||||
const buf = await autoOrient(await ensureSharpCompat(inputBuffer));
|
||||
let buf = inputBuffer;
|
||||
const validation = await validateImageBuffer(buf, filename);
|
||||
if (validation.valid && validation.format === "heif") {
|
||||
buf = await decodeHeic(buf);
|
||||
}
|
||||
if (validation.valid && needsCliDecode(validation.format)) {
|
||||
try {
|
||||
const fileExt = filename.split(".").pop()?.toLowerCase();
|
||||
buf = await decodeToSharpCompat(buf, validation.format, fileExt);
|
||||
} catch {
|
||||
/* batch handler already decoded */
|
||||
}
|
||||
}
|
||||
buf = await autoOrient(buf);
|
||||
return processMeme(buf, parsed, filename);
|
||||
},
|
||||
});
|
||||
@@ -263,7 +279,46 @@ export function registerMemeGenerator(app: FastifyInstance) {
|
||||
if (!imageBuffer) {
|
||||
return reply.status(400).send({ error: "No image provided" });
|
||||
}
|
||||
imageBuffer = await autoOrient(await ensureSharpCompat(imageBuffer));
|
||||
const validation = await validateImageBuffer(imageBuffer, filename);
|
||||
if (!validation.valid) {
|
||||
return reply.status(400).send({ error: `Invalid image: ${validation.reason}` });
|
||||
}
|
||||
if (validation.format === "heif") {
|
||||
try {
|
||||
imageBuffer = await decodeHeic(imageBuffer);
|
||||
} catch (err) {
|
||||
return reply.status(422).send({
|
||||
error: "Failed to decode HEIC file. Ensure libheif-examples is installed.",
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (needsCliDecode(validation.format)) {
|
||||
try {
|
||||
const fileExt = filename.split(".").pop()?.toLowerCase();
|
||||
imageBuffer = await decodeToSharpCompat(imageBuffer, validation.format, fileExt);
|
||||
} catch {
|
||||
try {
|
||||
await sharp(imageBuffer).metadata();
|
||||
} catch (err) {
|
||||
return reply.status(422).send({
|
||||
error: `Failed to decode ${validation.format.toUpperCase()} file`,
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (validation.format === "svg") {
|
||||
try {
|
||||
imageBuffer = decompressSvgz(imageBuffer);
|
||||
imageBuffer = sanitizeSvg(imageBuffer);
|
||||
} catch (err) {
|
||||
return reply.status(400).send({
|
||||
error: err instanceof Error ? err.message : "Invalid SVG",
|
||||
});
|
||||
}
|
||||
}
|
||||
imageBuffer = await autoOrient(imageBuffer);
|
||||
|
||||
const output = await processMeme(imageBuffer, settings, filename, templateTextBoxes);
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ 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, ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
import { decodeHeic } from "../../lib/heic-converter.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
import { updateSingleFileProgress } from "../progress.js";
|
||||
import { registerToolProcessFn } from "../tool-factory.js";
|
||||
@@ -215,7 +215,19 @@ export function registerRedEyeRemoval(app: FastifyInstance) {
|
||||
format?: string;
|
||||
quality?: number;
|
||||
};
|
||||
const orientedBuffer = await autoOrient(await ensureSharpCompat(inputBuffer));
|
||||
let decoded = inputBuffer;
|
||||
const validation = await validateImageBuffer(decoded, filename);
|
||||
if (validation.valid && validation.format === "heif") {
|
||||
decoded = await decodeHeic(decoded);
|
||||
}
|
||||
if (validation.valid && needsCliDecode(validation.format)) {
|
||||
try {
|
||||
decoded = await decodeToSharpCompat(decoded, validation.format);
|
||||
} catch {
|
||||
/* batch handler already decoded */
|
||||
}
|
||||
}
|
||||
const orientedBuffer = await autoOrient(decoded);
|
||||
const jobId = randomUUID();
|
||||
const workspacePath = await createWorkspace(jobId);
|
||||
const result = await removeRedEye(orientedBuffer, join(workspacePath, "output"), {
|
||||
|
||||
@@ -6,9 +6,12 @@ import sharp from "sharp";
|
||||
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 { sanitizeFilename } from "../../lib/filename.js";
|
||||
import { decodeToSharpCompat, needsCliDecode } from "../../lib/format-decoders.js";
|
||||
import { encodeJxl } from "../../lib/format-encoders.js";
|
||||
import { ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
import { decodeHeic } from "../../lib/heic-converter.js";
|
||||
import { decompressSvgz, sanitizeSvg } from "../../lib/svg-sanitize.js";
|
||||
import { registerToolProcessFn } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
@@ -83,7 +86,54 @@ export function registerSplit(app: FastifyInstance) {
|
||||
}
|
||||
|
||||
try {
|
||||
fileBuffer = await autoOrient(await ensureSharpCompat(fileBuffer));
|
||||
const validation = await validateImageBuffer(fileBuffer, filename);
|
||||
if (!validation.valid) {
|
||||
return reply.status(400).send({ error: `Invalid image: ${validation.reason}` });
|
||||
}
|
||||
|
||||
if (validation.format === "heif") {
|
||||
try {
|
||||
fileBuffer = await decodeHeic(fileBuffer);
|
||||
} catch (err) {
|
||||
return reply.status(422).send({
|
||||
error: "Failed to decode HEIC file. Ensure libheif-examples is installed.",
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
const ext = filename.match(/\.[^.]+$/)?.[0];
|
||||
if (ext) filename = `${filename.slice(0, -ext.length)}.png`;
|
||||
}
|
||||
|
||||
if (needsCliDecode(validation.format)) {
|
||||
try {
|
||||
const fileExt = filename.split(".").pop()?.toLowerCase();
|
||||
fileBuffer = await decodeToSharpCompat(fileBuffer, validation.format, fileExt);
|
||||
} catch {
|
||||
try {
|
||||
await sharp(fileBuffer).metadata();
|
||||
} catch (err) {
|
||||
return reply.status(422).send({
|
||||
error: `Failed to decode ${validation.format.toUpperCase()} file`,
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
const ext = filename.match(/\.[^.]+$/)?.[0];
|
||||
if (ext) filename = `${filename.slice(0, -ext.length)}.png`;
|
||||
}
|
||||
|
||||
if (validation.format === "svg") {
|
||||
try {
|
||||
fileBuffer = decompressSvgz(fileBuffer);
|
||||
fileBuffer = sanitizeSvg(fileBuffer);
|
||||
} catch (err) {
|
||||
return reply.status(400).send({
|
||||
error: err instanceof Error ? err.message : "Invalid SVG",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fileBuffer = await autoOrient(fileBuffer);
|
||||
const metadata = await sharp(fileBuffer).metadata();
|
||||
const fullW = metadata.width ?? 0;
|
||||
const fullH = metadata.height ?? 0;
|
||||
|
||||
@@ -3,8 +3,11 @@ import sharp from "sharp";
|
||||
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 { sanitizeFilename } from "../../lib/filename.js";
|
||||
import { ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
import { decodeToSharpCompat, needsCliDecode } from "../../lib/format-decoders.js";
|
||||
import { decodeHeic } from "../../lib/heic-converter.js";
|
||||
import { decompressSvgz, sanitizeSvg } from "../../lib/svg-sanitize.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
position: z
|
||||
@@ -73,9 +76,86 @@ export function registerWatermarkImage(app: FastifyInstance) {
|
||||
}
|
||||
|
||||
try {
|
||||
// Decode HEIC/HEIF if needed, then normalize EXIF orientation
|
||||
mainBuffer = await autoOrient(await ensureSharpCompat(mainBuffer));
|
||||
watermarkBuffer = await autoOrient(await ensureSharpCompat(watermarkBuffer));
|
||||
const valMain = await validateImageBuffer(mainBuffer, filename);
|
||||
if (!valMain.valid) {
|
||||
return reply.status(400).send({ error: `Invalid image: ${valMain.reason}` });
|
||||
}
|
||||
if (valMain.format === "heif") {
|
||||
try {
|
||||
mainBuffer = await decodeHeic(mainBuffer);
|
||||
} catch (err) {
|
||||
return reply.status(422).send({
|
||||
error: "Failed to decode HEIC file. Ensure libheif-examples is installed.",
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (needsCliDecode(valMain.format)) {
|
||||
try {
|
||||
const ext = filename.split(".").pop()?.toLowerCase();
|
||||
mainBuffer = await decodeToSharpCompat(mainBuffer, valMain.format, ext);
|
||||
} catch {
|
||||
try {
|
||||
await sharp(mainBuffer).metadata();
|
||||
} catch (err) {
|
||||
return reply.status(422).send({
|
||||
error: `Failed to decode ${valMain.format.toUpperCase()} file`,
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (valMain.format === "svg") {
|
||||
try {
|
||||
mainBuffer = decompressSvgz(mainBuffer);
|
||||
mainBuffer = sanitizeSvg(mainBuffer);
|
||||
} catch (err) {
|
||||
return reply.status(400).send({
|
||||
error: err instanceof Error ? err.message : "Invalid SVG",
|
||||
});
|
||||
}
|
||||
}
|
||||
mainBuffer = await autoOrient(mainBuffer);
|
||||
|
||||
const valWm = await validateImageBuffer(watermarkBuffer, "watermark");
|
||||
if (!valWm.valid) {
|
||||
return reply.status(400).send({ error: `Invalid watermark image: ${valWm.reason}` });
|
||||
}
|
||||
if (valWm.format === "heif") {
|
||||
try {
|
||||
watermarkBuffer = await decodeHeic(watermarkBuffer);
|
||||
} catch (err) {
|
||||
return reply.status(422).send({
|
||||
error: "Failed to decode watermark (HEIC). Ensure libheif-examples is installed.",
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (needsCliDecode(valWm.format)) {
|
||||
try {
|
||||
watermarkBuffer = await decodeToSharpCompat(watermarkBuffer, valWm.format);
|
||||
} catch {
|
||||
try {
|
||||
await sharp(watermarkBuffer).metadata();
|
||||
} catch (err) {
|
||||
return reply.status(422).send({
|
||||
error: `Failed to decode watermark (${valWm.format.toUpperCase()})`,
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (valWm.format === "svg") {
|
||||
try {
|
||||
watermarkBuffer = decompressSvgz(watermarkBuffer);
|
||||
watermarkBuffer = sanitizeSvg(watermarkBuffer);
|
||||
} catch (err) {
|
||||
return reply.status(400).send({
|
||||
error: err instanceof Error ? err.message : "Invalid SVG (watermark)",
|
||||
});
|
||||
}
|
||||
}
|
||||
watermarkBuffer = await autoOrient(watermarkBuffer);
|
||||
|
||||
const mainImage = sharp(mainBuffer);
|
||||
const mainMeta = await mainImage.metadata();
|
||||
|
||||
@@ -29,7 +29,8 @@ import {
|
||||
} from "../lib/file-storage.js";
|
||||
import { validateImageBuffer } from "../lib/file-validation.js";
|
||||
import { sanitizeFilename } from "../lib/filename.js";
|
||||
import { ensureSharpCompat } from "../lib/heic-converter.js";
|
||||
import { decodeToSharpCompat, needsCliDecode } from "../lib/format-decoders.js";
|
||||
import { decodeHeic } from "../lib/heic-converter.js";
|
||||
import { isSvgBuffer, sanitizeSvg } from "../lib/svg-sanitize.js";
|
||||
import { hasEffectivePermission } from "../permissions.js";
|
||||
import { getAuthUser, requireAuth } from "../plugins/auth.js";
|
||||
@@ -386,8 +387,20 @@ export async function userFileRoutes(app: FastifyInstance): Promise<void> {
|
||||
const filePath = getStoredFilePath(file.storedName);
|
||||
|
||||
try {
|
||||
// Read file and decode HEIC/HEIF if needed before Sharp processing
|
||||
const fileBuffer = await ensureSharpCompat(await readFile(filePath));
|
||||
const rawBuffer = await readFile(filePath);
|
||||
const validation = await validateImageBuffer(rawBuffer, file.originalName);
|
||||
let decoded: Buffer<ArrayBuffer> = Buffer.from(rawBuffer);
|
||||
if (validation.valid && validation.format === "heif") {
|
||||
decoded = Buffer.from(await decodeHeic(rawBuffer));
|
||||
} else if (validation.valid && needsCliDecode(validation.format)) {
|
||||
try {
|
||||
const fileExt = file.originalName.split(".").pop()?.toLowerCase();
|
||||
decoded = Buffer.from(await decodeToSharpCompat(rawBuffer, validation.format, fileExt));
|
||||
} catch {
|
||||
// Sharp will attempt the raw buffer directly
|
||||
}
|
||||
}
|
||||
const fileBuffer = decoded;
|
||||
|
||||
const thumbnail = await sharp(fileBuffer)
|
||||
.resize(300, null, { withoutEnlargement: true })
|
||||
|
||||
Reference in New Issue
Block a user