/** * Client-side image optimization. * * Resizes images and converts them to WebP before upload, * reducing bandwidth and storage costs significantly. */ interface OptimizedVariant { blob: Blob; width: number; height: number; suffix: string; } export interface OptimizedImage { original: { width: number; height: number }; variants: OptimizedVariant[]; } const VARIANTS = [ { suffix: "thumb", maxWidth: 400, quality: 0.7 }, { suffix: "medium", maxWidth: 1000, quality: 0.8 }, { suffix: "full", maxWidth: 2000, quality: 0.85 }, ] as const; /** * Optimize an image file: resize to 3 WebP variants + extract dimensions. * Returns the variants sorted small to large. */ export async function optimizeImage(file: File): Promise { const bitmap = await createImageBitmap(file); const { width: origW, height: origH } = bitmap; const variants: OptimizedVariant[] = []; for (const { suffix, maxWidth, quality } of VARIANTS) { // Skip variant if original is smaller than this tier if (origW <= maxWidth && suffix !== "full") continue; const scale = Math.min(1, maxWidth / origW); const w = Math.round(origW * scale); const h = Math.round(origH * scale); const canvas = new OffscreenCanvas(w, h); const ctx = canvas.getContext("2d"); if (!ctx) throw new Error("Canvas 2D context unavailable"); ctx.drawImage(bitmap, 0, 0, w, h); const blob = await canvas.convertToBlob({ type: "image/webp", quality, }); variants.push({ blob, width: w, height: h, suffix }); } // Always ensure at least one variant (the full size) if (variants.length === 0) { const canvas = new OffscreenCanvas(origW, origH); const ctx = canvas.getContext("2d"); if (!ctx) throw new Error("Canvas 2D context unavailable"); ctx.drawImage(bitmap, 0, 0); const blob = await canvas.convertToBlob({ type: "image/webp", quality: 0.85, }); variants.push({ blob, width: origW, height: origH, suffix: "full" }); } bitmap.close(); return { original: { width: origW, height: origH }, variants, }; } /** * Check if the browser supports WebP encoding via OffscreenCanvas. */ export function supportsWebpOptimization(): boolean { try { return typeof OffscreenCanvas !== "undefined"; } catch { return false; } }