| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317 |
- /**
- * 图片压缩工具
- * 用于压缩大于1MB的图片到0.8MB-1MB之间,保持原文件格式
- */
- /**
- * 压缩图片到目标大小范围
- * @param file - 原始图片文件
- * @param targetMinSize - 目标最小大小(字节)
- * @param targetMaxSize - 目标最大大小(字节)
- * @param quality - 压缩质量(0-1),默认0.9
- * @returns Promise<File> - 压缩后的文件
- */
- async function compressImageToTargetSize(
- file: File,
- targetMinSize: number,
- targetMaxSize: number,
- quality: number = 0.9
- ): Promise<File> {
- return new Promise((resolve, reject) => {
- const canvas = document.createElement('canvas')
- const ctx = canvas.getContext('2d')
- const img = new Image()
- img.onload = () => {
- try {
- const originalWidth = img.width
- const originalHeight = img.height
-
- // 先尝试保持原尺寸,只调整质量
- canvas.width = originalWidth
- canvas.height = originalHeight
- if (ctx) {
- ctx.imageSmoothingEnabled = true
- ctx.imageSmoothingQuality = 'high'
- }
- // 绘制原始尺寸的图片
- ctx?.drawImage(img, 0, 0, originalWidth, originalHeight)
- // 转换为blob
- canvas.toBlob(
- (blob) => {
- if (!blob) {
- reject(new Error('图片压缩失败'))
- return
- }
- // 检查压缩结果是否在目标范围内
- if (blob.size >= targetMinSize && blob.size <= targetMaxSize) {
- const compressedFile = new File([blob], file.name, {
- type: file.type,
- lastModified: Date.now()
- })
- resolve(compressedFile)
- } else if (blob.size > targetMaxSize) {
- // 如果还是太大,需要调整尺寸
- compressWithSizeAdjustment(file, img, targetMinSize, targetMaxSize, quality, resolve, reject)
- } else {
- // 如果太小了,提高质量重新压缩
- compressWithHigherQuality(file, img, targetMinSize, targetMaxSize, quality, resolve, reject)
- }
- },
- file.type,
- quality
- )
- } catch (error) {
- reject(new Error('图片压缩过程中发生错误: ' + error.message))
- }
- }
- img.onerror = () => {
- reject(new Error('图片加载失败'))
- }
- // 创建图片URL
- const reader = new FileReader()
- reader.onload = (e) => {
- img.src = e.target?.result as string
- }
- reader.onerror = () => {
- reject(new Error('文件读取失败'))
- }
- reader.readAsDataURL(file)
- })
- }
- /**
- * 通过调整尺寸进行压缩
- */
- function compressWithSizeAdjustment(
- file: File,
- img: HTMLImageElement,
- targetMinSize: number,
- targetMaxSize: number,
- quality: number,
- resolve: (file: File) => void,
- reject: (error: Error) => void
- ) {
- const canvas = document.createElement('canvas')
- const ctx = canvas.getContext('2d')
-
- const originalWidth = img.width
- const originalHeight = img.height
-
- // 计算目标尺寸,使用更保守的缩放策略
- const targetSize = (targetMinSize + targetMaxSize) / 2
- const sizeRatio = targetSize / file.size
-
- // 使用更温和的缩放比例,避免过度压缩
- let scale = Math.sqrt(sizeRatio) * 1.2 // 比理论值稍大一些,避免过度压缩
-
- // 确保缩放比例在合理范围内
- scale = Math.max(0.5, Math.min(1.0, scale))
-
- const targetWidth = Math.floor(originalWidth * scale)
- const targetHeight = Math.floor(originalHeight * scale)
-
- canvas.width = targetWidth
- canvas.height = targetHeight
-
- if (ctx) {
- ctx.imageSmoothingEnabled = true
- ctx.imageSmoothingQuality = 'high'
- }
-
- // 绘制缩放后的图片
- ctx?.drawImage(img, 0, 0, targetWidth, targetHeight)
-
- canvas.toBlob(
- (blob) => {
- if (!blob) {
- reject(new Error('图片压缩失败'))
- return
- }
-
- const compressedFile = new File([blob], file.name, {
- type: file.type,
- lastModified: Date.now()
- })
-
- // 检查结果,如果还是太大,继续调整
- if (blob.size > targetMaxSize && scale > 0.5) {
- // 递归调用,进一步缩小
- const newScale = scale * 0.9
- const newWidth = Math.floor(originalWidth * newScale)
- const newHeight = Math.floor(originalHeight * newScale)
-
- canvas.width = newWidth
- canvas.height = newHeight
- ctx?.drawImage(img, 0, 0, newWidth, newHeight)
-
- canvas.toBlob(
- (newBlob) => {
- if (newBlob) {
- const newFile = new File([newBlob], file.name, {
- type: file.type,
- lastModified: Date.now()
- })
- resolve(newFile)
- } else {
- resolve(compressedFile) // 如果二次压缩失败,返回第一次的结果
- }
- },
- file.type,
- quality
- )
- } else {
- resolve(compressedFile)
- }
- },
- file.type,
- quality
- )
- }
- /**
- * 通过提高质量重新压缩(当压缩结果太小时)
- */
- function compressWithHigherQuality(
- file: File,
- img: HTMLImageElement,
- targetMinSize: number,
- targetMaxSize: number,
- currentQuality: number,
- resolve: (file: File) => void,
- reject: (error: Error) => void
- ) {
- const canvas = document.createElement('canvas')
- const ctx = canvas.getContext('2d')
-
- canvas.width = img.width
- canvas.height = img.height
-
- if (ctx) {
- ctx.imageSmoothingEnabled = true
- ctx.imageSmoothingQuality = 'high'
- }
-
- // 提高质量,但不超过0.95
- const newQuality = Math.min(0.95, currentQuality + 0.1)
-
- ctx?.drawImage(img, 0, 0, img.width, img.height)
-
- canvas.toBlob(
- (blob) => {
- if (!blob) {
- reject(new Error('图片压缩失败'))
- return
- }
-
- const compressedFile = new File([blob], file.name, {
- type: file.type,
- lastModified: Date.now()
- })
-
- // 如果提高质量后仍然太小,或者已经达到最高质量,直接返回
- if (blob.size < targetMinSize && newQuality < 0.95) {
- compressWithHigherQuality(file, img, targetMinSize, targetMaxSize, newQuality, resolve, reject)
- } else {
- resolve(compressedFile)
- }
- },
- file.type,
- newQuality
- )
- }
- /**
- * 压缩图片
- * @param file - 原始图片文件
- * @param maxSize - 最大文件大小(字节),默认1MB
- * @param quality - 压缩质量(0-1),默认0.9
- * @returns Promise<File> - 压缩后的文件
- */
- export async function compressImage(
- file: File,
- maxSize: number = 1 * 1024 * 1024,
- quality: number = 0.9
- ): Promise<File> {
- // 如果文件小于等于最大限制,直接返回原文件
- if (file.size <= maxSize) {
- return file
- }
- // 对于大于1MB的文件,设置目标大小为0.8MB-1MB之间
- const targetMinSize = 800 * 1024 // 800KB
- const targetMaxSize = maxSize // 1MB
- return compressImageToTargetSize(file, targetMinSize, targetMaxSize, quality)
- }
- /**
- * 批量压缩图片
- * @param files - 图片文件数组
- * @param maxSize - 最大文件大小(字节),默认1MB
- * @param quality - 压缩质量(0-1),默认0.8
- * @returns Promise<File[]> - 压缩后的文件数组
- */
- export async function compressImages(
- files: File[],
- maxSize: number = 1 * 1024 * 1024,
- quality: number = 0.8
- ): Promise<File[]> {
- const results: File[] = []
-
- for (const file of files) {
- try {
- const compressedFile = await compressImage(file, maxSize, quality)
- results.push(compressedFile)
- } catch (error) {
- console.error(`压缩文件 ${file.name} 失败:`, error)
- // 压缩失败时保留原文件
- results.push(file)
- }
- }
-
- return results
- }
- /**
- * 获取图片信息
- * @param file - 图片文件
- * @returns Promise<{width: number, height: number, size: number, type: string}>
- */
- export function getImageInfo(file: File): Promise<{
- width: number
- height: number
- size: number
- type: string
- }> {
- return new Promise((resolve, reject) => {
- const img = new Image()
-
- img.onload = () => {
- resolve({
- width: img.width,
- height: img.height,
- size: file.size,
- type: file.type
- })
- }
-
- img.onerror = () => {
- reject(new Error('无法获取图片信息'))
- }
-
- const reader = new FileReader()
- reader.onload = (e) => {
- img.src = e.target?.result as string
- }
- reader.onerror = () => {
- reject(new Error('文件读取失败'))
- }
- reader.readAsDataURL(file)
- })
- }
|