feat: add HEIC/HEIF format support for input and output

Add bidirectional HEIC support using system libheif CLI tools (heif-enc/heif-dec)
for HEVC encoding/decoding, since Sharp's bundled libheif only supports AV1.

- HEIC input: all tools now accept iPhone HEIC photos via heif-dec pre-processing
- HEIC output: convert tool produces true HEIC (HEVC) via heif-enc
- Docker: adds libheif-examples package for heif-enc/heif-dec CLI tools
- Tests: full conversion matrix (7x7), unit tests, and Playwright e2e tests
- Docs: updated OpenAPI spec, image-engine docs, getting-started, llms-full.txt
This commit is contained in:
Siddharth Kumar Sah
2026-04-04 21:33:48 +08:00
parent 84283c16bd
commit 6717c17a26
21 changed files with 193 additions and 20 deletions
+13 -3
View File
@@ -3,6 +3,7 @@ import { convert } from "@stirling-image/image-engine";
import type { FastifyInstance } from "fastify";
import sharp from "sharp";
import { z } from "zod";
import { encodeHeic } from "../../lib/heic-converter.js";
import { isSvgBuffer } from "../../lib/svg-sanitize.js";
import { createToolRoute } from "../tool-factory.js";
@@ -13,10 +14,11 @@ const FORMAT_CONTENT_TYPES: Record<string, string> = {
avif: "image/avif",
tiff: "image/tiff",
gif: "image/gif",
heic: "image/heic",
};
const settingsSchema = z.object({
format: z.enum(["jpg", "png", "webp", "avif", "tiff", "gif"]),
format: z.enum(["jpg", "png", "webp", "avif", "tiff", "gif", "heic"]),
quality: z.number().min(1).max(100).optional(),
});
@@ -27,8 +29,16 @@ export function registerConvert(app: FastifyInstance) {
process: async (inputBuffer, settings, filename) => {
const sharpOpts = isSvgBuffer(inputBuffer) ? { density: 300 } : undefined;
const image = sharp(inputBuffer, sharpOpts);
const result = await convert(image, settings);
const buffer = await result.toBuffer();
let buffer: Buffer;
if (settings.format === "heic") {
// Sharp cannot encode HEVC. Convert to PNG first, then use heif-enc.
const pngBuffer = await image.png().toBuffer();
buffer = await encodeHeic(pngBuffer, settings.quality);
} else {
const result = await convert(image, settings);
buffer = await result.toBuffer();
}
// Change filename extension to match the output format
const ext = extname(filename);