aa
This commit is contained in:
68
src/lib/image-resize.ts
Normal file
68
src/lib/image-resize.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user