我如何构建了一个实际上根本看不到你的文件的图像转换器

发布: (2026年4月7日 GMT+8 13:58)
8 分钟阅读
原文: Dev.to

Source: Dev.to

我厌倦了把图片上传到随机的在线转换器

并不是因为它们慢——虽然确实慢——而是每次把文件拖进这些网站时,我根本不知道文件在另一端会发生什么。服务条款总是三页长,且在恰到好处的地方含糊其辞。

“我们可能会使用上传的内容来改进我们的服务。”
当然可以。

于是我自己动手做了一个。在此过程中,我发现浏览器的能力远超大多数开发者对它的评价。

工作原理(内部实现)

服务器端转换的问题

大多数图像转换工具遵循相同的架构:

  1. 您将文件上传到他们的服务器。
  2. 他们的后端运行 ImageMagick(或类似的工具)。
  3. 转换后的文件被写入他们的存储。
  4. 您下载它。
  5. 他们会删除它……大概。

大概”是我无法跨越的那一步。即使把隐私问题抛在一边,这种做法也存在真实的性能瓶颈:你的瓶颈是 上传速度,而不是设备本身的处理能力。现代笔记本可以在毫秒级完成 JPEG 编码。仅仅为了转码而让文件往返服务器,毫无意义。

我想要的替代方案很简单:在浏览器中、在用户自己的机器上完成全部操作。 不上传。不需要服务器。不再猜测。

浏览器实际上能做什么

结果是,能做的相当多。

现代浏览器提供原生的 Canvas API,能够处理图像的编码与解码。一个基础的 JPG → PNG 转换流程如下:

async function convertImage(file, targetFormat) {
  const bitmap = await createImageBitmap(file);

  const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
  const ctx = canvas.getContext('2d');
  ctx.drawImage(bitmap, 0, 0);

  const blob = await canvas.convertToBlob({
    type: `image/${targetFormat}`,
    quality: 0.92,
  });

  return blob;
}

这就是全部过程。文件从用户磁盘进入浏览器内存,重新编码后作为下载返回——期间没有任何网络请求。

提示: 在转换文件时打开 DevTools 的 Network(网络)面板,你会看到没有任何上传请求。这不是政策,而是技术现实。

更有趣的地方:HEIC

JPG、PNG 和 WebP 相对简单,因为浏览器原生支持它们。HEIC(High Efficiency Image Container)则是另一番景象。

  • HEIC 是自 iOS 11 起 Apple 的默认照片格式。
  • 与 JPG 在相同质量下提供更好的压缩,并支持深度图、Live Photos 等。
  • Windows 和大多数网络服务都不知道如何处理它——把 HEIC 上传到互联网上一半的网站都会报错。

浏览器并不原生解码 HEIC,因此我需要采用另一种方式:WebAssembly

import { libheif } from 'libheif-js';

async function decodeHEIC(file) {
  const decoder = new libheif.HeifDecoder();
  const buffer = await file.arrayBuffer();
  const data = new Uint8Array(buffer);

  const images = decoder.decode(data);
  const image = images[0];

  const width = image.get_width();
  const height = image.get_height();

  const pixelData = await new Promise((resolve, reject) => {
    image.display(
      { data: new Uint8ClampedArray(width * height * 4), width, height },
      (result) => {
        if (!result) reject(new Error('HEIC decode failed'));
        else resolve(result);
      }
    );
  });

  const canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;
  const ctx = canvas.getContext('2d');
  const imageData = new ImageData(pixelData.data, width, height);
  ctx.putImageData(imageData, 0, 0);

  return new Promise((resolve) =>
    canvas.toBlob(resolve, 'image/jpeg', 0.92)
  );
}

WASM 模块(libheif-js)在浏览器标签页中直接运行编译好的 C++ HEIC 解码器。这是真正的图像处理——解码 Apple 的专有格式——使用用户的 CPU,完全不涉及服务器。

第一次让它跑通时,我觉得有点荒唐。你基本上是在浏览器标签页里运行本地代码。但这正是 WebAssembly 的用途所在。

性能现实

有一点我没预料到:本地处理往往 更快,不仅仅是出于隐私考虑。

场景典型耗时(现代机器)
基于 Canvas 的 JPG → PNG(3–5 MB)

| 100–300 ms | | HEIC → JPEG via WASM(大型 iPhone 照片) | 1–3 s | | Server‑side(上传 + 处理 + 下载) | > 3 s(取决于连接) |

当服务器端占优势的情况是批量处理大量大型文件时,此时服务器硬件的并行化能力起关键作用。对于一次性转换,本地处理更有优势。

我对浏览器能力的认识

构建这个功能改变了我对哪些工作应该放在服务器上的看法。很多工具会说“上传到我们的服务器,我们来处理”,但其实并不一定需要这么做。浏览器具备:

  • 功能强大的 2‑D Canvas API,内置编解码支持。
  • 用于计算密集型任务的 WebAssembly。
  • 用于读取本地文件的 File API 和 FileReader。
  • 用于在不将所有内容加载到内存的情况下处理大文件的 Streams API。

把文件发送到服务器的冲动往往是习惯,而非必要。只要用户已经在本地拥有文件并且只需要转换,完全可以认真考虑本地处理。

接下来的计划

我接下来要添加的格式是 AVIF。相较于 WebP,它的压缩提升是真实可见的——测试显示在相同质量下,摄影内容的文件大小可减少 20–30 %。挑战在于 AVIF 编码比 WebP 更加耗算,但采用相同的浏览器端方案(Canvas + WASM),仍然可以在本地完成,而无需离开用户的机器。

Overview

WASM 方法需要更仔细的优化,以避免让用户等待。

当前工作

  • 批量转换 使用合适的队列。
  • 更好地处理异常的颜色配置文件(例如,新款 iPhone 的宽色域图像,在某些情况下会表现异常)。

演示

如果您想看到实际效果,请访问我构建的实时工具 imageconvert.website
该站点包括:

  • 转换器
  • HEIC 工具
  • 压缩器

所有这些组件都实现了上述工作流程。

欢迎在评论中随时提问有关实现细节的问题!

0 浏览
Back to Blog

相关文章

阅读更多 »