|
@@ -0,0 +1,317 @@
|
|
|
|
|
+/**
|
|
|
|
|
+ * 图片压缩工具
|
|
|
|
|
+ * 用于压缩大于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)
|
|
|
|
|
+ })
|
|
|
|
|
+}
|