122 lines
3.7 KiB
TypeScript
122 lines
3.7 KiB
TypeScript
import { probeImageTransparency } from './imageTransparency'
|
|
|
|
const IMAGE_UPLOAD_MAX_SIDE: Record<string, number> = {
|
|
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<string, number> = {
|
|
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<HTMLImageElement>((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<Blob | null>(resolve => {
|
|
canvas.toBlob(resolve, type, quality)
|
|
})
|
|
}
|
|
|
|
function replaceFileExt(filename: string, ext: string) {
|
|
const base = filename.replace(/\.[^.]+$/, '')
|
|
return `${base || 'image'}.${ext}`
|
|
}
|