/** * 图片压缩工具 —— 头像上传前的前端预压缩,降低网络延迟 * * 头像展示尺寸很小(会话列表 38px、成员/消息 30px、编辑预览 96px), * 上传前压成 AVATAR_UPLOAD_SIZE 见方的 JPEG,可显著减少请求体体积。 */ /** 头像上传目标边长(正方形,px) */ export const AVATAR_UPLOAD_SIZE = 50 /** 载入图片为可绘制源(优先 ImageBitmap,回退 HTMLImageElement) */ async function LoadImageSource(file: File): Promise { if (typeof createImageBitmap === 'function') { return await createImageBitmap(file) } const url = URL.createObjectURL(file) try { return await new Promise((resolve, reject) => { const el = new Image() el.onload = () => resolve(el) el.onerror = () => reject(new Error('图片解码失败')) el.src = url }) } finally { URL.revokeObjectURL(url) } } /** * 将图片居中裁剪为正方形并缩放为 Size×Size 的 JPEG File。 * * - 透明区域以白色填充(JPEG 不支持透明,避免出现黑底) * - 非图片、canvas 不可用或解码失败时原样返回,保证上传不中断 */ export async function compressImageSquare(file: File, Size = AVATAR_UPLOAD_SIZE): Promise { if (!file.type.startsWith('image/')) return file try { const source = await LoadImageSource(file) const canvas = document.createElement('canvas') canvas.width = Size canvas.height = Size const ctx = canvas.getContext('2d') if (!ctx) return file // 居中裁剪正方形后缩放 const side = Math.min(source.width, source.height) const sx = (source.width - side) / 2 const sy = (source.height - side) / 2 ctx.fillStyle = '#ffffff' ctx.fillRect(0, 0, Size, Size) ctx.imageSmoothingEnabled = true ctx.imageSmoothingQuality = 'high' ctx.drawImage(source, sx, sy, side, side, 0, 0, Size, Size) if (typeof ImageBitmap !== 'undefined' && source instanceof ImageBitmap) source.close() const blob = await new Promise((resolve) => canvas.toBlob(resolve, 'image/jpeg', 0.9)) if (!blob) return file const base = file.name.replace(/\.[^.]+$/, '') return new File([blob], `${base}.jpg`, { type: 'image/jpeg', lastModified: Date.now() }) } catch { return file } }