feat(upload): compress images over 10MB before OSS upload (v3.4.13)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
/** 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);
|
||||
}
|
||||
Reference in New Issue
Block a user