perf: do not use next/dist/server image optimizer (#23207)
* write image utils * refactor * better comment
This commit is contained in:
@@ -193,25 +193,27 @@ async function getHandler(request: NextRequest) {
|
||||
const response = await fetch(filteredLogo);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
let buffer: Buffer = Buffer.from(arrayBuffer);
|
||||
let contentType = response.headers.get("content-type") || "image/png";
|
||||
|
||||
// If we need to resize the team logos (via Next.js' built-in image processing)
|
||||
// Resize the team logos if needed
|
||||
if (teamLogos[logoDefinition.source] && logoDefinition.w) {
|
||||
const { detectContentType, optimizeImage } = await import("next/dist/server/image-optimizer");
|
||||
|
||||
buffer = await optimizeImage({
|
||||
const { resizeImage } = await import("@calcom/lib/server/imageUtils");
|
||||
const { buffer: outBuffer, contentType: outContentType } = await resizeImage({
|
||||
buffer,
|
||||
contentType: (await detectContentType(buffer)) ?? "image/jpeg",
|
||||
quality: 100,
|
||||
width: logoDefinition.w,
|
||||
height: logoDefinition.h, // optional
|
||||
height: logoDefinition.h,
|
||||
quality: 100,
|
||||
contentType,
|
||||
});
|
||||
buffer = outBuffer;
|
||||
contentType = outContentType;
|
||||
}
|
||||
|
||||
// Create a new response with the image buffer
|
||||
const imageResponse = new NextResponse(buffer as BodyInit);
|
||||
|
||||
// Set the appropriate headers
|
||||
imageResponse.headers.set("Content-Type", response.headers.get("content-type") || "image/png");
|
||||
imageResponse.headers.set("Content-Type", contentType);
|
||||
imageResponse.headers.set("Cache-Control", "s-maxage=86400, stale-while-revalidate=60");
|
||||
|
||||
return imageResponse;
|
||||
|
||||
@@ -3,6 +3,13 @@ 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
|
||||
@@ -29,3 +36,110 @@ export const convertSvgToPng = async (data: string) => {
|
||||
}
|
||||
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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user