fa6fb80d19
Co-authored-by: Cursor <cursoragent@cursor.com>
91 lines
2.8 KiB
TypeScript
91 lines
2.8 KiB
TypeScript
/** OSS 单文件上限(与后端 OSS_MAX_UPLOAD_BYTES 默认一致) */
|
|
export const DEFAULT_OSS_MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
|
|
|
|
function isCompressibleImage(file: File): boolean {
|
|
if (!file.type.startsWith('image/')) return false;
|
|
if (file.type === 'image/svg+xml' || file.type === 'image/gif') return false;
|
|
return true;
|
|
}
|
|
|
|
function loadImage(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;
|
|
});
|
|
}
|
|
|
|
function canvasToBlob(canvas: HTMLCanvasElement, type: string, quality: number): Promise<Blob> {
|
|
return new Promise((resolve, reject) => {
|
|
canvas.toBlob(
|
|
(blob) => (blob ? resolve(blob) : reject(new Error('图片压缩失败'))),
|
|
type,
|
|
quality,
|
|
);
|
|
});
|
|
}
|
|
|
|
async function renderScaled(
|
|
img: HTMLImageElement,
|
|
maxEdge: number,
|
|
quality: number,
|
|
mime: string,
|
|
): Promise<Blob> {
|
|
const w = img.naturalWidth;
|
|
const h = img.naturalHeight;
|
|
const scale = Math.min(1, maxEdge / Math.max(w, h));
|
|
const cw = Math.max(1, Math.round(w * scale));
|
|
const ch = Math.max(1, Math.round(h * scale));
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = cw;
|
|
canvas.height = ch;
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) throw new Error('图片压缩失败');
|
|
ctx.drawImage(img, 0, 0, cw, ch);
|
|
return canvasToBlob(canvas, mime, quality);
|
|
}
|
|
|
|
/**
|
|
* 超过 maxBytes 的图片用 canvas 压缩后再上传;不可压缩或非图片原样返回。
|
|
*/
|
|
export async function compressImageFileIfNeeded(
|
|
file: File,
|
|
maxBytes = DEFAULT_OSS_MAX_UPLOAD_BYTES,
|
|
): Promise<File> {
|
|
if (!isCompressibleImage(file) || file.size <= maxBytes) return file;
|
|
|
|
const img = await loadImage(file);
|
|
const mime = file.type === 'image/png' || file.type === 'image/webp' ? 'image/jpeg' : file.type || 'image/jpeg';
|
|
const edges = [4096, 3072, 2048, 1536, 1280, 1024, 800];
|
|
let best: Blob | null = null;
|
|
|
|
outer: for (const edge of edges) {
|
|
for (let quality = 0.92; quality >= 0.5; quality -= 0.08) {
|
|
const blob = await renderScaled(img, edge, quality, mime);
|
|
if (!best || blob.size < best.size) best = blob;
|
|
if (blob.size <= maxBytes) {
|
|
best = blob;
|
|
break outer;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!best || best.size >= file.size) return file;
|
|
|
|
const ext = mime === 'image/png' ? '.png' : '.jpg';
|
|
const baseName = file.name.replace(/\.[^.]+$/, '') || 'image';
|
|
return new File([best], `${baseName}${ext}`, { type: mime, lastModified: Date.now() });
|
|
}
|
|
|
|
export function formatOssMaxSizeMb(maxBytes = DEFAULT_OSS_MAX_UPLOAD_BYTES): number {
|
|
return Math.floor(maxBytes / 1024 / 1024);
|
|
}
|