LemonArt
← 返回技术博客
图片格式10 分钟阅读

Web 图片格式转换原理:Canvas、Blob 与自定义编码器如何协作的完整解析

从浏览器原生编码能力到 BMP、TIFF、QOI、PPM 等扩展格式,本文说明 LemonArt 如何构建可扩展的图片格式转换链路,包括编码器注册机制、质量参数映射与批量任务的内存优化策略。

“把 PNG 转成 WebP”表面上只是调用一次 toBlob,但一个支持多种格式的图片工具还要处理透明度、色彩、质量参数、浏览器能力差异和不可逆压缩。LemonArt 采用原生优先、编码器补充的策略,在兼容性和格式覆盖之间取得平衡。

一、原生 Canvas 编码是默认路径

浏览器提供 canvas.toBlob 和 canvas.toDataURL,可以稳定生成 PNG、JPEG 和 WebP,在较新的浏览器中也可能支持 AVIF。LemonArt 会先使用目标 MIME 类型请求原生编码,再检查返回结果是否真的使用了目标格式。仅仅传入 image/avif 并不代表所有浏览器都会生成 AVIF,能力检测必须以实际结果为准。

质量参数对 JPEG、WebP 和 AVIF 的含义并不完全相同,因此界面统一显示质量百分比,内部再根据编码器的约定做映射。PNG 是无损格式,质量滑块对它通常没有视觉意义。

toBlob 也不是所有环境都有。LemonArt 的 canvasToBlob 先走 toBlob,缺失时回退到 toDataURL,把 Base64 手动解码成 Uint8Array 再包成 Blob;两条路径共用同一个 MIME 验证函数,回退路径同样不会放过类型不匹配的结果。

const validate = (blob: Blob | null) => {
  if (!blob || blob.type !== mime) reject(new Error('unsupported format'))
  else resolve(blob)
}
if (typeof canvas.toBlob === 'function') {
  canvas.toBlob(validate, mime, quality)
} else {
  const dataUrl = canvas.toDataURL(mime, quality)
  const [header, encoded] = dataUrl.split(',', 2)
  const binary = atob(encoded)
  const bytes = new Uint8Array(binary.length)
  for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
  validate(new Blob([bytes], { type: mime }))
}ts

二、为什么还需要项目内置编码器

浏览器原生编码格式有限,而且同一格式在不同浏览器上的支持时间不同。为了覆盖开发者和设计工作流中常见的 BMP、TIFF、TGA、DDS、ICO、QOI、PPM、PGM、PBM、PAM、XPM 和 Farbfeld,LemonArt 在像素层构建了轻量编码器。它们接收 RGBA 像素和尺寸,返回 Blob,不影响上层转换流程。

扩展格式的目标不是替代专业桌面软件,而是让用户在不安装工具的情况下快速生成可互操作的测试样本、纹理文件和科学图像数据。输出面板会明确显示格式,不支持的浏览器原生输入也会给出提示。

三、透明度和色彩需要提前定义规则

JPG 不保存 Alpha 通道,导出时必须把透明区域合成到背景色,否则不同浏览器可能出现黑色或白色背景。PNG、WebP、ICO 和部分自定义格式可以保留透明度。LemonArt 在转换前统一读取 RGBA 像素,在编码器边界决定是否丢弃 Alpha,避免不同功能重复实现同一规则。

对于 SVG,项目使用嵌入的 PNG 数据生成可携带的 SVG 文档。它适合把位图放入矢量容器,但并不会把照片自动变成真正的路径矢量图,产品文案需要准确区分两者。

四、批量转换如何保持稳定

批量任务最容易出现的问题是同时解码大量大图,导致内存峰值过高。LemonArt 按队列逐个处理文件,更新单项进度并释放临时 Object URL;某一张图片失败时不会中断其余任务,用户可以单独重试或下载成功结果。

文件名只用于展示和下载,格式判断同时参考 MIME 和扩展名。对 HEIC、PSD、RAW 等浏览器不能直接解码的格式,工具会保留清晰错误提示,而不是生成一个无法打开的空文件。

五、以 BMP 为例看内置编码器的细节

BMP 看起来是最简单的位图格式,但把它写对需要处理五个细节:文件头与信息头共 54 字节;每行像素按 4 字节对齐补齐(rowSize = floor((24w+31)/32)*4);像素按 BGR 而非 RGB 存储;负高度表示自顶向下存储;以及不透明的 BMP 用 24 位深,因此透明像素必须先合成到背景色。LemonArt 的 encodeBmp 用一个 readCanvas 读取 RGBA 像素,再逐字节写入 DataView,全程不依赖任何第三方库。

编码器被设计成接收 canvas 或 RGBA 像素的纯函数,返回 Blob。所有扩展格式(QOI、PPM、PGM、TGA、ICO、DDS、TIFF、Farbfeld 等)共用同一套输入约定,上层转换流程不关心某个格式的内部字节序,新增格式只需要追加一个编码函数。

const rowSize = Math.floor((24 * width + 31) / 32) * 4
const buffer = new ArrayBuffer(54 + rowSize * height)
const view = new DataView(buffer)
view.setUint16(0, 0x4d42, true)       // 'BM'
view.setUint32(14, 40, true)          // BITMAPINFOHEADER
view.setInt32(22, -height, true)      // 负值 = top-down
view.setUint16(28, 24, true)          // 24 位深,无 Alpha
const alpha = data[src + 3] / 255
view.setUint8(offset++, compositeChannel(data[src + 2], alpha)) // Bts

要点总结

  • 先验证浏览器实际编码结果,再决定是否回退。
  • 把自定义编码器设计成接收 RGBA 像素的纯函数,便于扩展格式。
  • 位图格式的字节序、行填充与颜色顺序是正确性的关键,必须用规范逐项核对。
  • 批量处理要逐项排队、释放临时资源,并允许部分成功。
© 2024 LemonArt让图片处理变得更简单本地运行 · 无需注册