This commit is contained in:
2026-05-20 18:08:37 +07:00
parent dd3fd889a3
commit 290d36e8cb
21 changed files with 1359 additions and 72 deletions

68
src/lib/image-resize.ts Normal file
View File

@@ -0,0 +1,68 @@
// 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<ResizeResult> {
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<Blob | null>((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<HTMLImageElement> {
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);
});
}