feat(svg-to-raster): extend settings schema with DPI, quality, and 4 new output formats (#49)

Add user-configurable DPI (36-1200) and quality (1-100) instead of
hardcoded values. Support avif, tiff, gif, heif output in addition to
png, jpg, webp. Width is now optional, defaulting to SVG intrinsic size
at the given DPI. Generate browser-previewable webp thumbnails for
non-browser formats (tiff, heif). Remove unused _contentType variable.

Co-authored-by: Siddharth Kumar Sah <siddharth123sk@gmail.com>
This commit is contained in:
stirling-image
2026-04-13 13:06:34 +08:00
committed by GitHub
co-authored by Siddharth Kumar Sah
parent 018fd943d8
commit df372ee1ca
+46 -15
View File
@@ -8,13 +8,15 @@ import { sanitizeSvg } from "../../lib/svg-sanitize.js";
import { createWorkspace } from "../../lib/workspace.js";
const settingsSchema = z.object({
width: z.number().min(1).max(8192).default(1024),
height: z.number().min(1).max(8192).optional(),
width: z.number().min(1).max(16384).optional(),
height: z.number().min(1).max(16384).optional(),
dpi: z.number().min(36).max(1200).default(300),
quality: z.number().min(1).max(100).default(90),
backgroundColor: z
.string()
.regex(/^#[0-9a-fA-F]{6,8}$/)
.default("#00000000"),
outputFormat: z.enum(["png", "jpg", "webp"]).default("png"),
outputFormat: z.enum(["png", "jpg", "webp", "avif", "tiff", "gif", "heif"]).default("png"),
});
/**
@@ -74,13 +76,12 @@ export function registerSvgToRaster(app: FastifyInstance) {
}
try {
let image = sharp(fileBuffer, { density: 300 }).resize(
settings.width,
settings.height ?? undefined,
{ fit: "inside" },
);
let image = sharp(fileBuffer, { density: settings.dpi });
if (settings.width || settings.height) {
image = image.resize(settings.width, settings.height, { fit: "inside" });
}
// Apply background if not transparent
if (settings.backgroundColor !== "#00000000") {
const bgR = parseInt(settings.backgroundColor.slice(1, 3), 16);
const bgG = parseInt(settings.backgroundColor.slice(3, 5), 16);
@@ -90,23 +91,35 @@ export function registerSvgToRaster(app: FastifyInstance) {
let buffer: Buffer;
let ext: string;
let _contentType: string;
switch (settings.outputFormat) {
case "jpg":
buffer = await image.jpeg({ quality: 90 }).toBuffer();
buffer = await image.jpeg({ quality: settings.quality }).toBuffer();
ext = "jpg";
_contentType = "image/jpeg";
break;
case "webp":
buffer = await image.webp({ quality: 90 }).toBuffer();
buffer = await image.webp({ quality: settings.quality }).toBuffer();
ext = "webp";
_contentType = "image/webp";
break;
case "avif":
buffer = await image.avif({ quality: settings.quality }).toBuffer();
ext = "avif";
break;
case "tiff":
buffer = await image.tiff({ quality: settings.quality }).toBuffer();
ext = "tiff";
break;
case "gif":
buffer = await image.gif().toBuffer();
ext = "gif";
break;
case "heif":
buffer = await image.heif({ quality: settings.quality }).toBuffer();
ext = "heif";
break;
default:
buffer = await image.png().toBuffer();
ext = "png";
_contentType = "image/png";
break;
}
@@ -116,9 +129,27 @@ export function registerSvgToRaster(app: FastifyInstance) {
const outputPath = join(workspacePath, "output", outFilename);
await writeFile(outputPath, buffer);
// Generate browser-previewable thumbnail for non-browser formats
const NON_PREVIEWABLE = new Set(["tiff", "heif"]);
let previewUrl: string | undefined;
if (NON_PREVIEWABLE.has(ext)) {
try {
const previewBuffer = await sharp(buffer)
.resize(1200, 1200, { fit: "inside" })
.webp({ quality: 80 })
.toBuffer();
const previewPath = join(workspacePath, "output", "preview.webp");
await writeFile(previewPath, previewBuffer);
previewUrl = `/api/v1/download/${jobId}/preview.webp`;
} catch {
// Non-fatal - frontend shows success card fallback
}
}
return reply.send({
jobId,
downloadUrl: `/api/v1/download/${jobId}/${encodeURIComponent(outFilename)}`,
previewUrl,
originalSize: fileBuffer.length,
processedSize: buffer.length,
});