feat: full HEIF/HEIC support, content-aware resize performance fix, UI improvements

- Add bidirectional HEIF support: decode (input) and encode (output) via system heif-convert/heif-enc
- Add server-side WebP preview generation for non-browser-previewable formats (HEIC, TIFF)
- Fix content-aware resize failing on HEIF input (decode before passing to caire)
- Fix content-aware resize timeout on large images by downscaling to max 1200px and using JPEG intermediate
- Add HEIF as target format in convert tool
- Add loading spinner for HEIF preview decode in file store
- Fix file picker not accepting HEIF files (explicit .heic,.heif,.hif extensions)
- Extend frontend timeout for medium tools to 180s with 45s progress animation
- Redesign rotate controls with preset buttons and compact flip section
- Remove misleading savings percentage from convert tool
This commit is contained in:
Siddharth Kumar Sah
2026-04-11 23:27:44 +08:00
parent e0869477d4
commit 6f5283019b
18 changed files with 425 additions and 86 deletions
@@ -6,6 +6,7 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod";
import { autoOrient } from "../../lib/auto-orient.js";
import { validateImageBuffer } from "../../lib/file-validation.js";
import { decodeHeic } from "../../lib/heic-converter.js";
import { createWorkspace } from "../../lib/workspace.js";
import { registerToolProcessFn } from "../tool-factory.js";
@@ -59,6 +60,20 @@ export function registerContentAwareResize(app: FastifyInstance) {
return reply.status(400).send({ error: `Invalid image: ${validation.reason}` });
}
// Decode HEIC/HEIF input (caire can't read HEIF containers)
if (validation.format === "heif") {
try {
fileBuffer = await decodeHeic(fileBuffer);
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 HEIC/HEIF file",
details: err instanceof Error ? err.message : String(err),
});
}
}
// Validate settings
let settings: Settings;
try {
@@ -143,7 +158,13 @@ export function registerContentAwareResize(app: FastifyInstance) {
settingsSchema,
process: async (inputBuffer, settings, filename) => {
const s = settings as Settings;
const orientedBuffer = await autoOrient(inputBuffer);
// Decode HEIC/HEIF for pipeline/batch mode
const ext = filename.split(".").pop()?.toLowerCase() ?? "";
let buf = inputBuffer;
if (["heic", "heif", "hif"].includes(ext)) {
buf = await decodeHeic(buf);
}
const orientedBuffer = await autoOrient(buf);
const jobId = randomUUID();
const workspacePath = await createWorkspace(jobId);
const result = await seamCarve(orientedBuffer, join(workspacePath, "output"), {
+3 -2
View File
@@ -15,10 +15,11 @@ const FORMAT_CONTENT_TYPES: Record<string, string> = {
tiff: "image/tiff",
gif: "image/gif",
heic: "image/heic",
heif: "image/heif",
};
const settingsSchema = z.object({
format: z.enum(["jpg", "png", "webp", "avif", "tiff", "gif", "heic"]),
format: z.enum(["jpg", "png", "webp", "avif", "tiff", "gif", "heic", "heif"]),
quality: z.number().min(1).max(100).optional(),
});
@@ -31,7 +32,7 @@ export function registerConvert(app: FastifyInstance) {
const image = sharp(inputBuffer, sharpOpts);
let buffer: Buffer;
if (settings.format === "heic") {
if (settings.format === "heic" || settings.format === "heif") {
// Sharp cannot encode HEVC. Convert to PNG first, then use heif-enc.
const pngBuffer = await image.png().toBuffer();
buffer = await encodeHeic(pngBuffer, settings.quality);