jingrowtools/src/shared/utils/imageResize.ts
2026-01-02 18:33:52 +08:00

185 lines
5.2 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.

/**
* 图片压缩工具 - 使用原生 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<ResizeOptions> = {
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<Blob> {
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<File> {
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()
})
}