import sharp from "sharp"; export type WorkbookSourceImage = { bytes: Uint8Array; contentType: string; width?: number | null; height?: number | null; description: string; }; export type WorkbookImageNormalizationOptions = { maxDimension?: number; outputFormat?: "png" | "jpeg"; jpegQuality?: number; }; const NORMALIZABLE_IMAGE = /^image\/(?:jpe?g|png|webp|gif|tiff?|avif|heic|heif)$/i; function hasImageSignature(bytes: Uint8Array) { if (bytes.length < 4) return false; if (bytes[0] === 0xff && bytes[1] === 0xd8) return true; if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) { return true; } if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46) return true; if ( bytes.length >= 12 && String.fromCharCode(...bytes.slice(0, 4)) === "RIFF" && String.fromCharCode(...bytes.slice(8, 12)) === "WEBP" ) { return true; } return ( (bytes[0] === 0x49 && bytes[1] === 0x49 && bytes[2] === 0x2a && bytes[3] === 0x00) || (bytes[0] === 0x4d && bytes[1] === 0x4d && bytes[2] === 0x00 && bytes[3] === 0x2a) ); } /** * Excel viewers disagree on whether JPEG EXIF orientation should be applied. * Bake that orientation into the pixels before the image enters the workbook. * Callers may also resize and encode large source images as JPEG to keep the * generated workbook within the upload limit. */ export async function normalizeWorkbookImage( image: WorkbookSourceImage, options: WorkbookImageNormalizationOptions = {}, ): Promise { if (!NORMALIZABLE_IMAGE.test(image.contentType) && !hasImageSignature(image.bytes)) { return image; } try { let normalized = sharp(image.bytes, { animated: false }).rotate(); if (options.maxDimension && options.maxDimension > 0) { normalized = normalized.resize({ width: Math.floor(options.maxDimension), height: Math.floor(options.maxDimension), fit: "inside", withoutEnlargement: true, }); } normalized = options.outputFormat === "jpeg" ? normalized .flatten({ background: "#ffffff" }) .jpeg({ quality: Math.min(95, Math.max(50, options.jpegQuality ?? 82)), mozjpeg: true, }) : normalized.png({ compressionLevel: 6 }); const { data, info } = await normalized.toBuffer({ resolveWithObject: true }); return { ...image, bytes: new Uint8Array(data), contentType: options.outputFormat === "jpeg" ? "image/jpeg" : "image/png", width: info.width, height: info.height, }; } catch { return image; } }