Files
dukang/packages/shared-ui/src/compressImage.ts
T

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);
}