// Client-side image normalisation. Resizes to MAX_EDGE px on the long axis // and re-encodes to WebP (smaller than JPEG at equivalent quality). Output // is typically <200KB even from a 12MP iPhone photo. // // CLAUDE.md: "resize about max 1200px and compress below 1MB before upload, // use canvas API at client". export const MAX_EDGE = 1200; export const OUTPUT_MIME = "image/webp"; export const OUTPUT_QUALITY = 0.85; export const HARD_LIMIT_BYTES = 1024 * 1024; // 1MB after re-encode export const ACCEPTED_TYPES = ["image/jpeg", "image/png", "image/webp"]; export type ResizeResult = { blob: Blob; width: number; height: number; bytes: number; mimeType: string; }; export async function resizeImage(file: File): Promise { if (!ACCEPTED_TYPES.includes(file.type)) { throw new Error( "Định dạng không hỗ trợ. Hãy chọn JPG, PNG hoặc WebP (HEIC không xử lý được trực tiếp).", ); } const img = await loadImage(file); const scale = Math.min(1, MAX_EDGE / Math.max(img.width, img.height)); const w = Math.max(1, Math.round(img.width * scale)); const h = Math.max(1, Math.round(img.height * scale)); const canvas = document.createElement("canvas"); canvas.width = w; canvas.height = h; const ctx = canvas.getContext("2d"); if (!ctx) throw new Error("Trình duyệt không hỗ trợ canvas."); ctx.imageSmoothingQuality = "high"; ctx.drawImage(img, 0, 0, w, h); URL.revokeObjectURL(img.src); const blob = await new Promise((resolve) => canvas.toBlob(resolve, OUTPUT_MIME, OUTPUT_QUALITY), ); if (!blob) throw new Error("Không re-encode được ảnh."); if (blob.size > HARD_LIMIT_BYTES) { throw new Error("Ảnh vẫn quá lớn sau khi nén (> 1MB). Hãy chọn ảnh khác."); } return { blob, width: w, height: h, bytes: blob.size, mimeType: OUTPUT_MIME, }; } function loadImage(file: File): Promise { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = () => reject(new Error("Không đọc được ảnh.")); img.src = URL.createObjectURL(file); }); }