跳转到主要内容

crayonxiaoxin

从零构建纯浏览器端图片处理工具:ImgTools 的技术实践

前言

本文分享了 ImgTools(一个纯浏览器端图片处理工具)的完整实现过程。从技术选型到架构设计,从核心引擎到多模式 UI,涵盖了我在这过程中遇到的关键问题和解决方案。

为什么做这个工具

日常开发中经常需要压缩图片、转换格式、制作 Favicon,每次都要打开各种在线工具。这些工具往往需要上传文件到服务器,既慢又不安全。市面上已有的浏览器端方案要么功能单一,要么 WASM 引擎体积过大。我希望做一个本地运行、功能完整、且够快的工具。

技术选型

图像引擎:wasm-vips

方案优点缺点
Canvas API无需额外依赖,简单场景开发快格式支持有限,压缩质量不可控
Squoosh 风格(多 WASM 编解码器)每个格式独立优化5-8 个 WASM 包,集成复杂
wasm-vips(libvips WASM 移植)单一库覆盖所有格式,API 统一WASM 约 5MB

最终选择了 wasm-vips。它是 libvips 的 WASM 编译版本,一个库就能搞定 JPEG/PNG/WebP/AVIF/BMP/TIFF/GIF/SVG 的读写和压缩。API 统一,维护成本低。

前端框架

选择了 Vue 3 + Vite + TypeScript + Pinia。Vite 对 WASM 有原生支持,vue-tsc 提供类型检查,Pinia 的 Composition API 风格和 setup 语法配合得很好。

架构设计

分层结构

Components (UI) → Composables (逻辑) → Stores (状态) → Core (引擎)
  • core/ — wasm-vips 封装、格式矩阵、处理流水线
  • composables/ — 图片处理逻辑、批量导出
  • stores/ — Pinia 全局状态
  • components/ — UI 组件

核心流水线

File → ArrayBuffer → vips.Image.newFromBuffer → 解码
  → resize/crop 变换 → writeToBuffer('.jpg[Q=80]') → 编码
  → Blob → 预览 / 下载

编码方式使用 libvips 的内联格式字符串,如 .jpg[Q=80,optimize_coding=true],而不是各个格式独立的 save 方法。

关键问题与解决方案

1. SharedArrayBuffer 与跨域隔离

wasm-vips 使用 SharedArrayBuffer 实现多线程加速。浏览器要求页面设置两个 HTTP 头:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

配置在 vite.config.ts(开发环境)和 vercel.json(生产环境)。如果漏掉,wasm-vips 初始化时会报 DataCloneError

2. WASM 初始化管理

wasm-vips 是 ~5MB 的 WASM 文件,初始化需要 1-3 秒。我做了几个设计:

  • Promise 守卫 — 防止并发初始化
  • 30 秒超时 — 加载失败时给出明确错误信息
  • 事件监听机制 — 组件通过 onVipsReady 订阅初始化完成事件

3. 自动降质

JPEG 二次编码时,如果质量设得比原图高,结果会反而更大。我实现了自动降质逻辑:

用户设 Q=80 → 编码 → 结果比原图大?
  → 自动降到 Q=70 → 还大?→ 继续降到 Q=60…
  → 直到文件真正变小或 Q≤10

这样用户永远不用担心”越压越大”的问题。

4. PNG 有损压缩

PNG 是天生无损的,但通过调色板量化(palette quantization)可以实现有损压缩:

// 有损 PNG:减少颜色数量
img.writeToBuffer('.png[palette=true,Q=80,dither=1.0,compression=9]')
// 无损 PNG:最高 zlib 压缩
img.writeToBuffer('.png[compression=9]')

5. Favicon 与 ICO 格式

wasm-vips 没有编译 ImageMagick 支持,所以无法直接生成 .ico 文件。解决方案是自己实现 ICO 打包:ICO 格式本质上是多张 PNG 的容器。我用 JS 实现了二进制打包。

6. PDF 转图片

wasm-vips 不直接支持 PDF。方案是 PDF.js + wasm-vips 组合:

PDF → PDF.js 逐页渲染到 Canvas
  → getImageData() 获取像素
  → wasm-vips newFromMemory() 编码为 PNG/JPEG/WebP

7. 动态 WASM 模块

部署到 Vercel 后,wasm-vips 的动态模块(vips-jxl.wasmvips-heif.wasm)报了 404。原因是 Vite 构建时没有包含这些运行时加载的文件。修复方式:复制到 public/ 目录。

多模式架构

模式功能
压缩质量/有损无损/自动降质
转换格式互转,最大质量编码
Favicon裁剪 + 多尺寸 PNG/ICO/ZIP
PDF逐页/长图,精度可调

每个模式共享底层的处理引擎,但 UI 布局各自独立设计。

其他特性

  • 暗黑模式 — CSS 变量 + 系统偏好检测 + 手动切换
  • 多语言 — vue-i18n,自动检测浏览器语言,支持简中/繁中/英文
  • PWA — vite-plugin-pwa,预缓存 ~15MB 资源,支持离线使用
  • Vue Router(History 模式) — URL 直接对应模式,如 /compress/pdf

总结

ImgTools 的核心思路是:选择单一成熟的 WASM 引擎(wasm-vips),在其基础上构建清晰的架构分层,针对每个模式做独立的 UI 优化。

技术栈选择上,Vue 3 + Vite + TypeScript 的组合对于 WASM 项目非常友好,Vite 对 WASM 的动态导入和资产处理都相当成熟。

踩过的坑主要集中在 wasm-vips 的浏览器兼容性(COOP/COEP)、动态 WASM 模块的构建问题、以及 PDF.js 的内存管理上。这些经验对于其他 WASM 项目也有参考价值。


项目地址:ImgTools

在线体验:https://img.rmb.ee

讨论

还没有留言,来留下第一条评论吧!

留下足迹