logo
打造你的在线工具箱

在本节中,我们将实现在线工具箱的图片处理功能。主要包括以下功能点:

  • 图片压缩
    • 支持多种图片格式
    • 可调节压缩质量
    • 批量处理功能
  • 格式转换
    • 支持 JPG、PNG、WebP 等格式互转
    • 保持图片质量
  • 简单编辑
    • 裁剪功能
    • 旋转功能
    • 调整尺寸

技术选型

为了实现这些功能,我们将使用以下技术和库:

  • Sharp:高性能的 Node.js 图片处理库
  • Vue Use:提供图片拖拽和文件处理的 Composables
  • Canvas API:用于图片预览和简单编辑
  • Web Workers:处理大图片时避免阻塞主线程

页面实现

基础布局

<!-- pages/tools/image.vue -->
<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:

// composables/useImageCompressor.ts
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:

// composables/useImageConverter.ts
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:

// composables/useImageEditor.ts
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
  }
}

使用示例

<!-- components/ImageProcessor.vue -->
<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>

性能优化

为了提高图片处理的性能,我们可以采取以下措施:

  1. 使用 Web Workers
// workers/imageProcessor.worker.ts
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