feat: add AVIF output format support across 6 remaining tools (#85)

Closes #73

AVIF was already supported in the core engine, convert, compress,
optimize-for-web, upscale, erase-object, svg-to-raster, and
pdf-to-image tools. This adds AVIF as an output format option to
the 6 tools that were missing it: split, collage, stitch,
image-to-base64, noise-removal, and red-eye-removal.

For each tool, both the frontend format selector (with quality
slider for AVIF's lossy encoding) and the backend Zod schema +
Sharp .avif() encoding were updated. AVIF defaults: quality from
the user slider, effort 4 (balanced encode speed).

Also fixes pre-existing Biome formatting violations in 5 files
that were blocking a clean lint pass.
This commit is contained in:
Ashim
2026-04-21 23:34:48 +08:00
committed by GitHub
parent 7d422c2fd0
commit ba26ea4bc7
13 changed files with 49 additions and 26 deletions
+5 -1
View File
@@ -332,7 +332,7 @@ const settingsSchema = z.object({
cornerRadius: z.number().min(0).max(500).default(0),
backgroundColor: z.string().default("#FFFFFF"),
aspectRatio: z.string().default("free"),
outputFormat: z.enum(["png", "jpeg", "webp"]).default("png"),
outputFormat: z.enum(["png", "jpeg", "webp", "avif"]).default("png"),
quality: z.number().min(1).max(100).default(90),
});
@@ -633,6 +633,10 @@ export function registerCollage(app: FastifyInstance) {
pipeline = pipeline.webp({ quality: settings.quality });
outputExt = "webp";
break;
case "avif":
pipeline = pipeline.avif({ quality: settings.quality, effort: 4 });
outputExt = "avif";
break;
default:
pipeline = pipeline.png();
outputExt = "png";
+5 -1
View File
@@ -5,7 +5,7 @@ import { z } from "zod";
import { ensureSharpCompat } from "../../lib/heic-converter.js";
const settingsSchema = z.object({
outputFormat: z.enum(["original", "jpeg", "png", "webp"]).default("original"),
outputFormat: z.enum(["original", "jpeg", "png", "webp", "avif"]).default("original"),
quality: z.number().int().min(1).max(100).default(80),
maxWidth: z.number().int().min(0).default(0),
maxHeight: z.number().int().min(0).default(0),
@@ -140,6 +140,10 @@ export function registerImageToBase64(app: FastifyInstance) {
outputBuffer = await pipeline.webp({ quality: opts.quality }).toBuffer();
mimeType = "image/webp";
break;
case "avif":
outputBuffer = await pipeline.avif({ quality: opts.quality, effort: 4 }).toBuffer();
mimeType = "image/avif";
break;
default:
outputBuffer = await pipeline.toBuffer();
mimeType = detectMimeType(ext);
+3 -2
View File
@@ -19,7 +19,7 @@ const settingsSchema = z.object({
strength: z.union([z.number(), z.string()]).transform(Number).default(50),
detailPreservation: z.union([z.number(), z.string()]).transform(Number).default(50),
colorNoise: z.union([z.number(), z.string()]).transform(Number).default(30),
format: z.enum(["original", "png", "jpeg", "webp"]).default("original"),
format: z.enum(["original", "png", "jpeg", "webp", "avif"]).default("original"),
quality: z.union([z.number(), z.string()]).transform(Number).default(90),
});
@@ -165,7 +165,7 @@ export function registerNoiseRemoval(app: FastifyInstance) {
strength: z.union([z.number(), z.string()]).transform(Number).default(50),
detailPreservation: z.union([z.number(), z.string()]).transform(Number).default(50),
colorNoise: z.union([z.number(), z.string()]).transform(Number).default(30),
format: z.enum(["original", "png", "jpeg", "webp"]).default("original"),
format: z.enum(["original", "png", "jpeg", "webp", "avif"]).default("original"),
quality: z.union([z.number(), z.string()]).transform(Number).default(90),
}),
process: async (inputBuffer, settings, filename) => {
@@ -188,6 +188,7 @@ export function registerNoiseRemoval(app: FastifyInstance) {
jpeg: "image/jpeg",
jpg: "image/jpeg",
webp: "image/webp",
avif: "image/avif",
};
return {
buffer: result.buffer,
+10 -3
View File
@@ -14,7 +14,7 @@ const settingsSchema = z.object({
rows: z.number().min(1).max(100).default(3),
tileWidth: z.number().min(10).optional(),
tileHeight: z.number().min(10).optional(),
outputFormat: z.enum(["original", "png", "jpg", "webp"]).default("original"),
outputFormat: z.enum(["original", "png", "jpg", "webp", "avif"]).default("original"),
quality: z.number().min(1).max(100).default(90),
});
@@ -29,6 +29,7 @@ function resolveOutputFormat(
png: { sharpFormat: "png", ext: ".png" },
jpg: { sharpFormat: "jpeg", ext: ".jpg" },
webp: { sharpFormat: "webp", ext: ".webp" },
avif: { sharpFormat: "avif", ext: ".avif" },
};
return map[outputFormat] ?? { sharpFormat: null, ext: originalExt };
}
@@ -137,9 +138,12 @@ export function registerSplit(app: FastifyInstance) {
let pipeline = sharp(fileBuffer).extract({ left, top, width: w, height: h });
if (sharpFormat) {
const formatOpts: Record<string, unknown> = {};
if (sharpFormat === "jpeg" || sharpFormat === "webp") {
if (sharpFormat === "jpeg" || sharpFormat === "webp" || sharpFormat === "avif") {
formatOpts.quality = settings.quality;
}
if (sharpFormat === "avif") {
formatOpts.effort = 4;
}
pipeline = pipeline.toFormat(sharpFormat, formatOpts);
}
@@ -220,9 +224,12 @@ export function registerSplit(app: FastifyInstance) {
let pipeline = sharp(inputBuffer).extract({ left, top, width: w, height: h });
if (sharpFormat) {
const formatOpts: Record<string, unknown> = {};
if (sharpFormat === "jpeg" || sharpFormat === "webp") {
if (sharpFormat === "jpeg" || sharpFormat === "webp" || sharpFormat === "avif") {
formatOpts.quality = settings.quality;
}
if (sharpFormat === "avif") {
formatOpts.effort = 4;
}
pipeline = pipeline.toFormat(sharpFormat, formatOpts);
}
+5 -1
View File
@@ -23,7 +23,7 @@ const settingsSchema = z.object({
.string()
.regex(/^#[0-9a-fA-F]{6}$/)
.default("#FFFFFF"),
format: z.enum(["png", "jpeg", "webp"]).default("png"),
format: z.enum(["png", "jpeg", "webp", "avif"]).default("png"),
quality: z.number().min(1).max(100).default(90),
});
@@ -200,6 +200,8 @@ export function registerStitch(app: FastifyInstance) {
pipeline = pipeline.jpeg({ quality: settings.quality });
} else if (settings.format === "webp") {
pipeline = pipeline.webp({ quality: settings.quality });
} else if (settings.format === "avif") {
pipeline = pipeline.avif({ quality: settings.quality, effort: 4 });
} else {
pipeline = pipeline.png();
}
@@ -230,6 +232,8 @@ export function registerStitch(app: FastifyInstance) {
.toBuffer();
} else if (settings.format === "webp") {
result = await sharp(result).webp({ quality: settings.quality }).toBuffer();
} else if (settings.format === "avif") {
result = await sharp(result).avif({ quality: settings.quality, effort: 4 }).toBuffer();
}
}