/** * Downscale an image File to a max dimension and re-encode as JPEG. * Phone photos routinely exceed the backend's upload limit; this keeps * uploads well under it while preserving enough detail for captioning. */ export async function downscaleImage( file: File, maxDim = 1536, quality = 0.85, ): Promise { const bitmap = await loadImageBitmap(file); const { width, height } = scaleDimensions(bitmap.width, bitmap.height, maxDim); const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); if (!ctx) throw new Error('Canvas 2D context unavailable'); ctx.drawImage(bitmap, 0, 0, width, height); const blob = await new Promise((resolve, reject) => { canvas.toBlob( (b) => (b ? resolve(b) : reject(new Error('canvas.toBlob returned null'))), 'image/jpeg', quality, ); }); const name = file.name.replace(/\.[^.]+$/, '') + '.jpg'; return new File([blob], name, { type: 'image/jpeg' }); } function scaleDimensions(w: number, h: number, maxDim: number): { width: number; height: number } { if (w <= maxDim && h <= maxDim) return { width: w, height: h }; const ratio = w > h ? maxDim / w : maxDim / h; return { width: Math.round(w * ratio), height: Math.round(h * ratio) }; } async function loadImageBitmap(file: File): Promise { if ('createImageBitmap' in window) { return createImageBitmap(file); } return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = () => reject(new Error('Failed to load image')); img.src = URL.createObjectURL(file); }); }