Files
calendar/packages/lib/server/imageUtils.ts
Benny JooandGitHub 8e73421094 perf: do not use next/dist/server image optimizer (#23207)
* write image utils

* refactor

* better comment
2025-08-25 15:13:44 +05:30

146 lines
4.7 KiB
TypeScript

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<string | null> {
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 };
}