/** * Downscale an image File to a JPEG Blob (max 1600px on the long edge, quality 0.82) before chat upload, * so image blobs stay ~100-400KB in sqlite instead of multi-MB phone-camera shots. GIFs pass through * unchanged (a canvas re-encode would kill the animation). Returns { blob, width, height }; falls back * to the original file if canvas/toBlob is unavailable. Shared by both chat surfaces so it can't drift. */ export interface DownscaledImage { blob: Blob; width: number; height: number } export const downscaleImage = (file: File, maxDim = 1600, quality = 0.82): Promise => { return new Promise((resolve) => { const fallback = () => resolve({ blob: file, width: 0, height: 0 }) try { if (typeof document === 'undefined' || !file.type || file.type === 'image/gif') return fallback() const url = URL.createObjectURL(file) const img = new Image() img.onload = () => { try { let width = img.naturalWidth || img.width let height = img.naturalHeight || img.height if (!width || !height) { URL.revokeObjectURL(url); return fallback() } const scale = Math.min(1, maxDim / Math.max(width, height)) width = Math.round(width * scale) height = Math.round(height * scale) const canvas = document.createElement('canvas') canvas.width = width canvas.height = height const ctx = canvas.getContext('2d') if (!ctx) { URL.revokeObjectURL(url); return fallback() } ctx.drawImage(img, 0, 0, width, height) canvas.toBlob((blob) => { URL.revokeObjectURL(url) if (blob) resolve({ blob, width, height }) else fallback() }, 'image/jpeg', quality) } catch { URL.revokeObjectURL(url); fallback() } } img.onerror = () => { URL.revokeObjectURL(url); fallback() } img.src = url } catch { fallback() } }) }