thread-master/apps/web/src/lib/memberAvatar.ts

65 lines
2.1 KiB
TypeScript
Raw Normal View History

2026-07-10 05:10:31 +00:00
/** 會員頭像:本機選檔 → 壓縮成可存 localStorage 的 data URL */
const MAX_INPUT_BYTES = 5 * 1024 * 1024;
const MAX_EDGE = 256;
const JPEG_QUALITY = 0.86;
function loadImageFromFile(file: File): Promise<HTMLImageElement> {
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<string> {
if (!file.type.startsWith("image/")) {
throw new Error("請選擇圖片檔JPGPNGWebP");
}
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;
}