import { probeImageTransparency } from './imageTransparency' const IMAGE_UPLOAD_MAX_SIDE: Record = { avatar: 512, chat: 1280, announcement: 1600, 'home-banner': 1920, 'aw-recycle': 1920, 'cooperation-feedback': 1920, listing: 1920, dispute: 1920, handoff: 1920, realname: 1920, qrcode: 2560, } const IMAGE_UPLOAD_QUALITY: Record = { avatar: 0.82, chat: 0.8, announcement: 0.84, 'home-banner': 0.84, 'aw-recycle': 0.84, 'cooperation-feedback': 0.84, listing: 0.84, dispute: 0.86, handoff: 0.86, realname: 0.86, qrcode: 0.95, } export interface UploadEncoding { maxSide: number quality: number outputType: 'image/webp' | 'image/png' extension: 'webp' | 'png' } /** * 决定上传前的压缩参数。 * * 关键点:带透明通道的图(例如热区透明按钮图、透明 Logo)必须输出 PNG, * 否则压缩过程会把透明区域压成实色(历史上就是先铺白底再编码,导致透明按钮图变白底方块)。 */ export function resolveUploadEncoding(scene: string, preserveAlpha: boolean): UploadEncoding { const maxSide = IMAGE_UPLOAD_MAX_SIDE[scene] ?? 1600 const quality = IMAGE_UPLOAD_QUALITY[scene] ?? 0.82 return { maxSide, quality, outputType: preserveAlpha ? 'image/png' : 'image/webp', extension: preserveAlpha ? 'png' : 'webp', } } export async function optimizeImageForUpload(file: File, scene: string) { if (!file.type.startsWith('image/')) return file if (!['image/jpeg', 'image/png', 'image/webp'].includes(file.type)) return file if (typeof document === 'undefined') return file try { // 先判断原图是否真的有透明像素:有则全程保留 alpha const probe = await probeImageTransparency(file) const preserveAlpha = Boolean(probe?.hasTransparentPixels) const { maxSide, quality, outputType, extension } = resolveUploadEncoding(scene, preserveAlpha) const image = await loadImage(file) const { width, height } = fitSize(image.naturalWidth, image.naturalHeight, maxSide) const canvas = document.createElement('canvas') canvas.width = width canvas.height = height const context = canvas.getContext('2d') if (!context) return file // 不铺白底:canvas 本身保留透明,输出 PNG / WebP 都能带 alpha context.clearRect(0, 0, width, height) context.drawImage(image, 0, 0, width, height) const blob = await canvasToBlob(canvas, outputType, quality) if (!blob || blob.size >= file.size) return file return new File([blob], replaceFileExt(file.name, extension), { type: outputType, lastModified: Date.now(), }) } catch { return file } } function loadImage(file: File) { return new Promise((resolve, reject) => { const url = URL.createObjectURL(file) const image = new Image() image.onload = () => { URL.revokeObjectURL(url) resolve(image) } image.onerror = () => { URL.revokeObjectURL(url) reject(new Error('图片读取失败')) } image.src = url }) } function fitSize(width: number, height: number, maxSide: number) { if (width <= 0 || height <= 0) return { width: 1, height: 1 } const scale = Math.min(1, maxSide / Math.max(width, height)) return { width: Math.max(1, Math.round(width * scale)), height: Math.max(1, Math.round(height * scale)), } } function canvasToBlob(canvas: HTMLCanvasElement, type: string, quality: number) { return new Promise(resolve => { canvas.toBlob(resolve, type, quality) }) } function replaceFileExt(filename: string, ext: string) { const base = filename.replace(/\.[^.]+$/, '') return `${base || 'image'}.${ext}` }