/** 會員頭像:本機選檔 → 壓縮成可存 localStorage 的 data URL */ const MAX_INPUT_BYTES = 5 * 1024 * 1024; const MAX_EDGE = 256; const JPEG_QUALITY = 0.86; function loadImageFromFile(file: File): Promise { return new Promise((resolve, reject) => { const url = URL.createObjectURL(file); const img = new Image(); img.onload = () => { URL.revokeObjectURL(url); resolve(img); }; img.onerror = () => { URL.revokeObjectURL(url); reject(new Error("無法讀取圖片")); }; img.src = url; }); } /** * 將本機圖片裁成正方形、縮到 ≤256px,輸出 JPEG data URL。 * mock 階段不真上傳;存進租戶會員紀錄。 */ export async function fileToMemberAvatarDataUrl(file: File): Promise { if (!file.type.startsWith("image/")) { throw new Error("請選擇圖片檔(JPG/PNG/WebP)"); } if (file.size > MAX_INPUT_BYTES) { throw new Error("圖片請在 5MB 以內"); } const img = await loadImageFromFile(file); const srcW = img.naturalWidth || img.width; const srcH = img.naturalHeight || img.height; if (!srcW || !srcH) throw new Error("無法讀取圖片尺寸"); const side = Math.min(srcW, srcH); const sx = Math.floor((srcW - side) / 2); const sy = Math.floor((srcH - side) / 2); const edge = Math.min(MAX_EDGE, side); const canvas = document.createElement("canvas"); canvas.width = edge; canvas.height = edge; const ctx = canvas.getContext("2d"); if (!ctx) throw new Error("瀏覽器不支援圖片處理"); ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = "high"; ctx.drawImage(img, sx, sy, side, side, 0, 0, edge, edge); const dataUrl = canvas.toDataURL("image/jpeg", JPEG_QUALITY); if (!dataUrl.startsWith("data:image/")) throw new Error("壓縮失敗"); if (dataUrl.length > 400_000) { // 再壓一輪 const smaller = canvas.toDataURL("image/jpeg", 0.7); if (smaller.length > 400_000) throw new Error("頭像仍過大,請換較簡單的圖"); return smaller; } return dataUrl; }