/** * 图片压缩工具 - 使用原生 Canvas API * 将图片压缩到指定最大尺寸(1024x1024px) */ export interface ResizeOptions { maxWidth?: number maxHeight?: number quality?: number outputFormat?: 'image/jpeg' | 'image/png' | 'image/webp' mode?: 'contain' | 'cover' | 'crop' // contain: 等比缩放, cover: 填充裁剪, crop: 居中裁剪 } const DEFAULT_OPTIONS: Required = { maxWidth: 1024, maxHeight: 1024, quality: 0.92, outputFormat: 'image/jpeg', mode: 'contain' } /** * 使用 Canvas API 压缩图片 * @param file 原始图片文件 * @param options 压缩选项 * @returns 压缩后的 Blob */ export async function resizeImageWithCanvas( file: File, options: ResizeOptions = {} ): Promise { const opts = { ...DEFAULT_OPTIONS, ...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 context not available')) return } const objectUrl = URL.createObjectURL(file) img.onload = () => { try { let { width, height } = img // 如果图片尺寸已经小于等于目标尺寸,直接返回原图 if (width <= opts.maxWidth && height <= opts.maxHeight) { URL.revokeObjectURL(objectUrl) // 直接读取原文件为 blob file.arrayBuffer().then(buffer => { resolve(new Blob([buffer], { type: file.type })) }).catch(reject) return } // 计算目标尺寸 let targetWidth = width let targetHeight = height let sourceX = 0 let sourceY = 0 let sourceWidth = width let sourceHeight = height if (opts.mode === 'contain') { // 等比缩放,保持宽高比 const scale = Math.min( opts.maxWidth / width, opts.maxHeight / height, 1 // 不放大 ) targetWidth = Math.round(width * scale) targetHeight = Math.round(height * scale) } else if (opts.mode === 'cover') { // 填充模式:裁剪以填满目标尺寸 const scale = Math.max( opts.maxWidth / width, opts.maxHeight / height ) targetWidth = opts.maxWidth targetHeight = opts.maxHeight sourceWidth = Math.round(opts.maxWidth / scale) sourceHeight = Math.round(opts.maxHeight / scale) sourceX = Math.round((width - sourceWidth) / 2) sourceY = Math.round((height - sourceHeight) / 2) } else if (opts.mode === 'crop') { // 居中裁剪到目标尺寸 const scale = Math.max( opts.maxWidth / width, opts.maxHeight / height ) const scaledWidth = width * scale const scaledHeight = height * scale targetWidth = opts.maxWidth targetHeight = opts.maxHeight sourceWidth = width sourceHeight = height // 计算裁剪区域 if (scaledWidth > opts.maxWidth) { sourceX = Math.round((scaledWidth - opts.maxWidth) / 2 / scale) sourceWidth = Math.round(opts.maxWidth / scale) } if (scaledHeight > opts.maxHeight) { sourceY = Math.round((scaledHeight - opts.maxHeight) / 2 / scale) sourceHeight = Math.round(opts.maxHeight / scale) } } // 设置 canvas 尺寸 canvas.width = targetWidth canvas.height = targetHeight // 设置图片质量(仅对 JPEG 有效) if (opts.outputFormat === 'image/jpeg') { ctx.fillStyle = '#FFFFFF' ctx.fillRect(0, 0, targetWidth, targetHeight) } // 绘制图片 ctx.drawImage( img, sourceX, sourceY, sourceWidth, sourceHeight, 0, 0, targetWidth, targetHeight ) // 转换为 Blob canvas.toBlob( (blob) => { // 清理对象 URL URL.revokeObjectURL(objectUrl) if (blob) { resolve(blob) } else { reject(new Error('Failed to create blob')) } }, opts.outputFormat, opts.quality ) } catch (error) { URL.revokeObjectURL(objectUrl) reject(error) } } img.onerror = () => { URL.revokeObjectURL(objectUrl) reject(new Error('Failed to load image')) } // 加载图片 img.src = objectUrl }) } /** * 压缩图片文件,返回新的 File 对象 */ export async function compressImageFile( file: File, options: ResizeOptions = {} ): Promise { const blob = await resizeImageWithCanvas(file, options) const opts = { ...DEFAULT_OPTIONS, ...options } // 确定文件扩展名 let ext = 'jpg' if (opts.outputFormat === 'image/png') { ext = 'png' } else if (opts.outputFormat === 'image/webp') { ext = 'webp' } // 创建新的 File 对象,保持原始文件名但更新扩展名 const fileName = file.name.replace(/\.[^.]+$/, '') + '.' + ext return new File([blob], fileName, { type: opts.outputFormat, lastModified: Date.now() }) }