69 lines
2.2 KiB
TypeScript
69 lines
2.2 KiB
TypeScript
// 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);
|
|
});
|
|
}
|