图片处理工具实现
在本节中,我们将实现在线工具箱的图片处理功能。主要包括以下功能点:
- 图片压缩
- 支持多种图片格式
- 可调节压缩质量
- 批量处理功能
- 格式转换
- 支持 JPG、PNG、WebP 等格式互转
- 保持图片质量
- 简单编辑
- 裁剪功能
- 旋转功能
- 调整尺寸
技术选型
为了实现这些功能,我们将使用以下技术和库:
- Sharp:高性能的 Node.js 图片处理库
- Vue Use:提供图片拖拽和文件处理的 Composables
- Canvas API:用于图片预览和简单编辑
- Web Workers:处理大图片时避免阻塞主线程
页面实现
基础布局
<script setup lang="ts">import { ref } from 'vue'import { useFileDialog } from '@vueuse/core'
// 文件选择相关const { files, open } = useFileDialog({ accept: 'image/*', multiple: true})
// 选中的图片列表const selectedImages = ref<File[]>([])
// 监听文件选择watch(files, (newFiles) => { if (newFiles) { selectedImages.value = Array.from(newFiles) }})</script>
<template> <div class="container mx-auto px-4 py-8"> <h1 class="text-3xl font-bold mb-8">图片处理工具</h1>
<!-- 工具选择 --> <div class="tabs tabs-boxed mb-8"> <a class="tab tab-active">压缩</a> <a class="tab">转换</a> <a class="tab">编辑</a> </div>
<!-- 图片上传区域 --> <div class="border-2 border-dashed border-base-300 rounded-lg p-8 text-center cursor-pointer" @click="open()" @drop.prevent="selectedImages = Array.from($event.dataTransfer?.files || [])" @dragover.prevent > <div class="text-lg mb-4"> 点击或拖拽图片到此处 </div> <div class="text-sm text-base-content/60"> 支持 JPG、PNG、WebP 等格式 </div> </div>
<!-- 图片预览列表 --> <div v-if="selectedImages.length" class="mt-8 grid grid-cols-2 md:grid-cols-4 gap-4"> <div v-for="(image, index) in selectedImages" :key="index" class="relative aspect-square rounded-lg overflow-hidden" > <img :src="URL.createObjectURL(image)" class="w-full h-full object-cover" @load="URL.revokeObjectURL($event.target.src)" > </div> </div> </div></template>
图片压缩功能
首先,我们需要安装必要的依赖:
npm install sharp @vueuse/core
创建图片压缩的 Composable:
import { ref } from 'vue'import sharp from 'sharp'
export function useImageCompressor() { const compressing = ref(false) const progress = ref(0)
const compressImage = async (file: File, quality: number = 80) => { compressing.value = true progress.value = 0
try { const buffer = await file.arrayBuffer() const image = sharp(buffer)
// 获取图片信息 const metadata = await image.metadata()
// 根据原始格式选择压缩方法 let processed switch (metadata.format) { case 'jpeg': case 'jpg': processed = await image .jpeg({ quality }) .toBuffer() break case 'png': processed = await image .png({ quality: quality / 100 }) .toBuffer() break case 'webp': processed = await image .webp({ quality }) .toBuffer() break default: throw new Error(`不支持的格式: ${metadata.format}`) }
// 创建压缩后的文件 const compressedFile = new File( [processed], `compressed_${file.name}`, { type: file.type } )
progress.value = 100 return compressedFile } catch (error) { console.error('压缩失败:', error) throw error } finally { compressing.value = false } }
return { compressing, progress, compressImage }}
格式转换功能
创建格式转换的 Composable:
import { ref } from 'vue'import sharp from 'sharp'
export function useImageConverter() { const converting = ref(false)
const convertImage = async ( file: File, format: 'jpeg' | 'png' | 'webp', options: sharp.OutputOptions = {} ) => { converting.value = true
try { const buffer = await file.arrayBuffer() const image = sharp(buffer)
// 转换格式 let processed switch (format) { case 'jpeg': processed = await image .jpeg(options) .toBuffer() break case 'png': processed = await image .png(options) .toBuffer() break case 'webp': processed = await image .webp(options) .toBuffer() break }
// 确定新文件的扩展名 const ext = format === 'jpeg' ? 'jpg' : format const newName = file.name.replace(/\.[^.]+$/, `.${ext}`)
// 创建转换后的文件 const convertedFile = new File( [processed], newName, { type: `image/${format}` } )
return convertedFile } catch (error) { console.error('转换失败:', error) throw error } finally { converting.value = false } }
return { converting, convertImage }}
图片编辑功能
创建图片编辑的 Composable:
import { ref, computed } from 'vue'
export function useImageEditor() { const canvas = ref<HTMLCanvasElement | null>(null) const ctx = computed(() => canvas.value?.getContext('2d'))
// 图片旋转 const rotateImage = (degree: number) => { if (!canvas.value || !ctx.value) return
const { width, height } = canvas.value
// 保存当前画布内容 const imageData = ctx.value.getImageData(0, 0, width, height)
// 调整画布大小 if (degree === 90 || degree === 270) { canvas.value.width = height canvas.value.height = width }
// 执行旋转 ctx.value.save() ctx.value.translate(canvas.value.width / 2, canvas.value.height / 2) ctx.value.rotate((degree * Math.PI) / 180) ctx.value.drawImage( canvas.value, -width / 2, -height / 2 ) ctx.value.restore() }
// 图片裁剪 const cropImage = (x: number, y: number, width: number, height: number) => { if (!canvas.value || !ctx.value) return
const imageData = ctx.value.getImageData(x, y, width, height) canvas.value.width = width canvas.value.height = height ctx.value.putImageData(imageData, 0, 0) }
// 调整尺寸 const resizeImage = (width: number, height: number) => { if (!canvas.value || !ctx.value) return
const tempCanvas = document.createElement('canvas') tempCanvas.width = canvas.value.width tempCanvas.height = canvas.value.height const tempCtx = tempCanvas.getContext('2d')
if (!tempCtx) return
tempCtx.drawImage(canvas.value, 0, 0)
canvas.value.width = width canvas.value.height = height
ctx.value.drawImage( tempCanvas, 0, 0, tempCanvas.width, tempCanvas.height, 0, 0, width, height ) }
return { canvas, rotateImage, cropImage, resizeImage }}
使用示例
<script setup lang="ts">import { ref } from 'vue'import { useImageCompressor } from '~/composables/useImageCompressor'import { useImageConverter } from '~/composables/useImageConverter'import { useImageEditor } from '~/composables/useImageEditor'
const { compressing, progress, compressImage } = useImageCompressor()const { converting, convertImage } = useImageConverter()const { canvas, rotateImage, cropImage, resizeImage } = useImageEditor()
// 压缩质量设置const quality = ref(80)
// 目标格式设置const targetFormat = ref<'jpeg' | 'png' | 'webp'>('jpeg')
// 处理压缩const handleCompress = async (file: File) => { try { const compressed = await compressImage(file, quality.value) // 处理压缩后的文件... } catch (error) { console.error('压缩失败:', error) }}
// 处理转换const handleConvert = async (file: File) => { try { const converted = await convertImage(file, targetFormat.value) // 处理转换后的文件... } catch (error) { console.error('转换失败:', error) }}</script>
<template> <div class="space-y-8"> <!-- 压缩设置 --> <div class="form-control"> <label class="label"> <span class="label-text">压缩质量</span> <span class="label-text-alt">{{ quality }}%</span> </label> <input v-model="quality" type="range" min="0" max="100" class="range" > </div>
<!-- 格式选择 --> <div class="form-control"> <label class="label"> <span class="label-text">目标格式</span> </label> <select v-model="targetFormat" class="select select-bordered"> <option value="jpeg">JPG</option> <option value="png">PNG</option> <option value="webp">WebP</option> </select> </div>
<!-- 编辑工具栏 --> <div class="btn-group"> <button class="btn" @click="rotateImage(-90)" > 向左旋转 </button> <button class="btn" @click="rotateImage(90)" > 向右旋转 </button> </div>
<!-- 画布容器 --> <div class="relative aspect-video bg-base-200 rounded-lg overflow-hidden"> <canvas ref="canvas" class="w-full h-full object-contain" /> </div> </div></template>
性能优化
为了提高图片处理的性能,我们可以采取以下措施:
- 使用 Web Workers
import sharp from 'sharp'
self.addEventListener('message', async (e) => { const { file, type, options } = e.data
try { const buffer = await file.arrayBuffer() const image = sharp(buffer)
let result switch (type) { case 'compress': result = await image .jpeg({ quality: options.quality }) .toBuffer() break case 'convert': result = await image[options.format](options) .toBuffer() break