import sharp from "sharp"; // Maximum allowed size for SVG data (5MB) const MAX_SVG_SIZE = 5 * 1024 * 1024; const JPEG = "image/jpeg"; const PNG = "image/png"; const WEBP = "image/webp"; const AVIF = "image/avif"; const GIF = "image/gif"; const SVG = "image/svg+xml"; /** * Converts an SVG image to PNG format * @param data Base64 encoded image data * @returns Base64 encoded PNG data or a placeholder image if conversion fails */ export const convertSvgToPng = async (data: string) => { if (data.startsWith("data:image/svg+xml;base64,")) { try { const base64Data = data.replace(/^data:image\/svg\+xml;base64,/, ""); const buffer = Buffer.from(base64Data, "base64"); // Check if the SVG data exceeds the size limit if (buffer.length > MAX_SVG_SIZE) { throw new Error("SVG data exceeds maximum allowed size"); } const pngBuffer = await sharp(buffer).png().toBuffer(); return `data:image/png;base64,${pngBuffer.toString("base64")}`; } catch (error) { console.error("Error converting SVG to PNG", error); // Return a 1x1 transparent PNG as placeholder return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="; } } return data; }; /** * Detect content type from image buffer. * Simplified version of Next.js image optimizer's detectContentType function (https://github.com/vercel/next.js/blob/9436dce61f1a3ff9478261dc2eba47e0527acf3d/packages/next/src/server/image-optimizer.ts#L160). * Supports common web image formats (JPEG, PNG, GIF, WEBP, AVIF, SVG) and drops * irrelevant formats like PDF, ICO, TIFF, etc. that aren't used for logos. */ export async function detectContentType(buffer: Buffer): Promise { if ([0xff, 0xd8, 0xff].every((b, i) => buffer[i] === b)) { return JPEG; } if ([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a].every((b, i) => buffer[i] === b)) { return PNG; } if ([0x47, 0x49, 0x46, 0x38].every((b, i) => buffer[i] === b)) { return GIF; } if ([0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50].every((b, i) => !b || buffer[i] === b)) { return WEBP; } if ([0x3c, 0x3f, 0x78, 0x6d, 0x6c].every((b, i) => buffer[i] === b)) { return SVG; } if ([0x3c, 0x73, 0x76, 0x67].every((b, i) => buffer[i] === b)) { return SVG; } if ([0, 0, 0, 0, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x66].every((b, i) => !b || buffer[i] === b)) { return AVIF; } // Fallback to sharp metadata detection try { const meta = await sharp(buffer).metadata(); switch (meta?.format) { case "avif": return AVIF; case "webp": return WEBP; case "png": return PNG; case "jpeg": case "jpg": return JPEG; case "gif": return GIF; case "svg": return SVG; default: return null; } } catch { return null; } } /** * Resize an image buffer while preserving the original format. * Simplified version of Next.js image optimizer's optimizeImage function (https://github.com/vercel/next.js/blob/9436dce61f1a3ff9478261dc2eba47e0527acf3d/packages/next/src/server/image-optimizer.ts#L640). * Supports common web image formats (JPEG, PNG, WEBP, AVIF) with format-specific * optimization settings. Drops advanced options like limitInputPixels, sequentialRead, * and timeout that aren't needed for logo processing. Uses failOnError: false for * better robustness with potentially malformed images. */ export async function resizeImage(params: { buffer: Buffer; width: number; height?: number; quality?: number; contentType?: string; }): Promise<{ buffer: Buffer; contentType: string }> { const { buffer, width, height, quality = 100 } = params; let { contentType } = params; // Auto-detect content type if not provided if (!contentType) { contentType = (await detectContentType(buffer)) ?? PNG; } const transformer = sharp(buffer).rotate(); if (height) { transformer.resize(width, height); } else { transformer.resize(width, undefined, { withoutEnlargement: true }); } // Apply format-specific optimization (preserving original format) if (contentType === AVIF) { transformer.avif({ quality: Math.max(quality - 20, 1), effort: 3, }); } else if (contentType === WEBP) { transformer.webp({ quality }); } else if (contentType === PNG) { transformer.png({ quality }); } else if (contentType === JPEG) { transformer.jpeg({ quality, mozjpeg: true }); } else { // For unknown formats, default to PNG transformer.png({ quality }); contentType = PNG; } const optimizedBuffer = await transformer.toBuffer(); return { buffer: optimizedBuffer, contentType }; }