gongxiangp2/lib/image-compress.ts
chenpei 4e89b57798 feat(upload): optimize upload UI and performance
- Expand batch upload list area and optimize preview image layout
- Improve upload speed tracking and display (server-side & client-side)
- Reduce network detection log noise
- Optimize Docker build process (caching, env validation)
- Add shadow environment deployment support
2026-01-28 15:07:50 +08:00

142 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 前端图片压缩工具
* 在浏览器中压缩图片,减少上传数据量,提高处理速度
*/
interface CompressOptions {
maxWidth?: number
maxHeight?: number
quality?: number
format?: 'image/jpeg' | 'image/webp' | 'image/png'
}
/**
* 压缩图片文件
* @param file 原始图片文件
* @param options 压缩选项
* @returns 压缩后的 Blob
*/
export async function compressImage(
file: File,
options: CompressOptions = {}
): Promise<Blob> {
const {
maxWidth = 800,
maxHeight = 2000,
quality = 0.85,
format = 'image/jpeg',
} = options
return new Promise((resolve, reject) => {
const img = new Image()
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
if (!ctx) {
reject(new Error('无法创建 Canvas 上下文'))
return
}
img.onload = () => {
try {
// 计算新尺寸(保持比例)
let { width, height } = img
// 如果图片尺寸已经在限制内,且文件不大,直接返回
if (width <= maxWidth && height <= maxHeight && file.size < 500 * 1024) {
// ⚠️ 内存泄漏修复:释放 Object URL(R20)
URL.revokeObjectURL(img.src)
resolve(file)
return
}
// 计算缩放比例
let scale = 1
if (width > maxWidth) {
scale = Math.min(scale, maxWidth / width)
}
if (height > maxHeight) {
scale = Math.min(scale, maxHeight / height)
}
const newWidth = Math.round(width * scale)
const newHeight = Math.round(height * scale)
// 设置 canvas 尺寸
canvas.width = newWidth
canvas.height = newHeight
// 绘制图片(使用高质量缩放)
ctx.imageSmoothingEnabled = true
ctx.imageSmoothingQuality = 'high'
ctx.drawImage(img, 0, 0, newWidth, newHeight)
// 转换为 Blob
canvas.toBlob(
(blob) => {
// ⚠️ 内存泄漏修复:处理完成后释放 Object URL(R20)
URL.revokeObjectURL(img.src)
if (blob) {
console.log(
`[前端压缩] ${file.name}: ${(file.size / 1024).toFixed(2)}KB -> ${(blob.size / 1024).toFixed(2)}KB (${width}x${height} -> ${newWidth}x${newHeight})`
)
resolve(blob)
} else {
reject(new Error('图片压缩失败'))
}
},
format,
quality
)
} catch (error) {
// ⚠️ 内存泄漏修复:错误时也要释放 Object URL(R20)
URL.revokeObjectURL(img.src)
reject(error)
}
}
img.onerror = () => {
// ⚠️ 内存泄漏修复:加载失败时释放 Object URL(R20)
URL.revokeObjectURL(img.src)
reject(new Error('图片加载失败'))
}
// 加载图片
img.src = URL.createObjectURL(file)
})
}
/**
* 将 Blob 转换为 File 对象
*/
export function blobToFile(blob: Blob, fileName: string): File {
return new File([blob], fileName, { type: blob.type })
}
/**
* 批量压缩图片
*/
export async function compressImages(
files: File[],
options: CompressOptions = {}
): Promise<File[]> {
const compressedFiles: File[] = []
for (let i = 0; i < files.length; i++) {
const file = files[i]
try {
const compressedBlob = await compressImage(file, options)
const compressedFile = blobToFile(compressedBlob, file.name)
compressedFiles.push(compressedFile)
} catch (error) {
console.error(`[前端压缩] 压缩 ${file.name} 失败:`, error)
// 压缩失败时使用原文件
compressedFiles.push(file)
}
}
return compressedFiles
}