From 078b3446602f3bf706e216ed87c4dc35954adfca Mon Sep 17 00:00:00 2001 From: Siddharth Kumar Sah Date: Mon, 13 Apr 2026 12:29:41 +0800 Subject: [PATCH] feat(svg-to-raster): extend settings schema with DPI, quality, and 4 new output formats 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. --- apps/api/src/routes/tools/svg-to-raster.ts | 61 ++++++++++++++++------ 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/apps/api/src/routes/tools/svg-to-raster.ts b/apps/api/src/routes/tools/svg-to-raster.ts index e68d25a9..471bb85d 100644 --- a/apps/api/src/routes/tools/svg-to-raster.ts +++ b/apps/api/src/routes/tools/svg-to-raster.ts @@ -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, });