UPNG.js 详细介绍:高性能 PNG / APNG 编解码库/无损/有损压缩完全指南

2026-07-14 102 浏览 0 评论

UPNG.js 是一款轻量级、纯 JavaScript 实现的 PNG/APNG 编码与解码库,作为专业在线图像编辑器 Photopea 的核心 PNG 引擎,它在实际生产环境中经过了大量验证。该库不依赖任何第三方依赖,可同时运行在浏览器端和 Node.js 环境中,支持无损压缩和有损压缩两种模式,能够处理包括动画 APNG、调色板图像、16 位深度图像在内的多种复杂 PNG 格式。

该项目由 Photopea 团队维护,源码体积小巧,压缩后仅约 20KB,却提供了完整的 PNG 规范实现。与其他同类库相比,UPNG.js 在压缩率和编解码速度之间取得了较好的平衡,尤其在 PNG 有损压缩方面表现突出,能够在视觉质量可接受的前提下,显著减小文件体积。

核心特性

UPNG.js 提供了丰富的功能特性,覆盖了 PNG 图像处理的主要场景:

  • 完整的 PNG 解码支持 :支持所有标准 PNG 颜色类型,包括灰度、RGB、索引色、灰度+Alpha、RGBA,位深度从 1 位到 16 位均可正确解析。
  • PNG 编码能力 :提供高级编码接口 UPNG.encode() 和低级编码接口 UPNG.encodeLL() ,前者适用于常规场景,后者允许精细控制通道数和位深度等参数。
  • APNG 动画支持 :支持解码和编码 APNG 动画图像,可读取帧延迟、帧偏移等元数据,也可将多帧像素数据编码为完整的 APNG 文件。
  • 无损与有损压缩 :通过 cnum 参数控制颜色数量,设置为 0 时启用无损压缩,设置为具体数值时启用有损压缩,数值越小文件体积越小。
  • 零依赖 :整个库不依赖任何外部库,可直接引入使用,减少了项目依赖体积和潜在的兼容性问题。
  • 跨环境兼容 :同时支持浏览器环境和 Node.js 环境,在浏览器中可直接通过 <script> 标签引入,在 Node.js 中可通过 npm 安装使用。

安装与引入

npm 安装

对于使用模块化开发的项目,可以通过 npm 安装 UPNG.js:

npm install upng-js

安装完成后,使用 ES Module 或 CommonJS 方式引入:

// ES Module
import UPNG from 'upng-js';

// CommonJS
const UPNG = require('upng-js');

浏览器直接引入

对于传统的前端项目,可以直接下载 UPNG.js 源码文件,通过 <script> 标签引入到页面中:

<script src="UPNG.js"></script>

引入后,全局对象 UPNG 即可在脚本中直接使用。

核心 API 详解

UPNG.decode(buffer)

decode 方法用于解码 PNG 或 APNG 文件,接收一个包含 PNG 数据的 ArrayBuffer 作为参数,返回一个图像对象,包含以下属性:

  • width :图像宽度,单位为像素
  • height :图像高度,单位为像素
  • depth :位深度,常见值为 8 或 16
  • ctype :颜色类型,1 表示灰度,2 表示 RGB,3 表示索引色,4 表示灰度+Alpha,6 表示 RGBA
  • frames :帧数据数组,单帧 PNG 只有一个元素,APNG 包含多个帧
  • tabs :PNG 文件中的辅助数据块,如 pHYs、iCCP 等

UPNG.toRGBA8(img)

toRGBA8 方法将解码后的图像对象转换为 RGBA8 格式的像素数据数组。每个元素是一个 ArrayBuffer ,包含对应帧的像素数据,每个像素占 4 个字节,依次为 R、G、B、A 四个通道,每个通道 8 位。

对于单帧 PNG,返回的数组只有一个元素;对于 APNG 动画,返回的数组长度等于帧数。该方法是连接解码和编码的桥梁,通常在解码后调用,将像素数据转换为统一的 RGBA8 格式,便于后续处理或重新编码。

UPNG.encode(imgs, w, h, cnum, [dels])

encode 方法是最常用的编码接口,用于将 RGBA8 格式的像素数据编码为 PNG 或 APNG 文件,返回一个 ArrayBuffer 。参数说明如下:

  • imgs :像素数据数组,每个元素是一个 ArrayBuffer ,包含一帧的 RGBA8 像素数据。单帧 PNG 传入单元素数组,APNG 传入多元素数组。
  • w :图像宽度,单位为像素
  • h :图像高度,单位为像素
  • cnum :颜色数量,控制压缩质量。设置为 0 时启用无损压缩,保留所有颜色;设置为具体数值时启用有损压缩,数值越小颜色越少,文件体积越小,画质损失越大。常用值为 256,对应 8 位索引色。
  • dels :可选参数,帧延迟数组,单位为毫秒,仅在编码 APNG 时使用。数组长度应与帧数相同,每个元素对应一帧的显示时长。

UPNG.encodeLL(imgs, w, h, cc, ac, depth, [dels])

encodeLL 是低级编码接口,提供了更精细的控制能力,适用于需要指定位深度、通道数等专业场景。参数说明如下:

  • imgs :像素数据数组,与 encode 方法相同
  • w :图像宽度
  • h :图像高度
  • cc :颜色通道数,1 表示灰度,3 表示 RGB
  • ac :Alpha 通道数,0 表示无 Alpha,1 表示有 Alpha
  • depth :位深度,可设置为 1、2、4、8、16
  • dels :可选参数,帧延迟数组,与 encode 方法相同

通过组合 ccacdepth 参数,可以生成不同颜色类型和位深度的 PNG 文件,例如灰度 8 位、RGB 16 位、RGBA 8 位等。

实战示例代码

PNG 解码并在 Canvas 上显示

以下示例演示如何使用 UPNG.js 解码一个 PNG 文件,并将其绘制到 Canvas 元素上:

// 获取 Canvas 上下文
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

// 加载并解码 PNG 文件
fetch('example.png')
  .then(response => response.arrayBuffer())
  .then(buffer => {
    // 解码 PNG 数据
    const img = UPNG.decode(buffer);

    // 转换为 RGBA8 格式
    const rgba8 = UPNG.toRGBA8(img);

    // 设置 Canvas 尺寸
    canvas.width = img.width;
    canvas.height = img.height;

    // 创建 ImageData 并填充像素数据
    const imageData = ctx.createImageData(img.width, img.height);
    const data = new Uint8Array(rgba8[0]);
    imageData.data.set(data);

    // 绘制到 Canvas
    ctx.putImageData(imageData, 0, 0);
  });

PNG 无损压缩

以下示例演示如何对 Canvas 中的图像进行无损压缩,生成 PNG 文件并下载:

function compressPNGLossless(canvas) {
  const ctx = canvas.getContext('2d');
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

  // 获取像素数据的 ArrayBuffer
  const pixelBuffer = imageData.data.buffer;

  // 使用无损模式编码(cnum = 0)
  const pngBuffer = UPNG.encode(
    [pixelBuffer],
    canvas.width,
    canvas.height,
    0
  );

  // 创建 Blob 并生成下载链接
  const blob = new Blob([pngBuffer], { type: 'image/png' });
  const url = URL.createObjectURL(blob);

  // 触发下载
  const link = document.createElement('a');
  link.href = url;
  link.download = 'compressed.png';
  link.click();

  // 释放 URL 对象
  URL.revokeObjectURL(url);
}

PNG 有损压缩

以下示例演示如何对用户上传的 PNG 图片进行有损压缩,并显示压缩前后的大小对比:

async function compressPNGLossy(file, quality = 0.8) {
  // 读取文件为 ArrayBuffer
  const arrayBuffer = await file.arrayBuffer();

  // 解码原始 PNG
  const decoded = UPNG.decode(arrayBuffer);
  const rgba8 = UPNG.toRGBA8(decoded);

  // 计算颜色数量,quality 取值 0-1,值越小压缩越强
  const cnum = Math.floor(256 * quality);

  // 使用有损模式编码
  const compressed = UPNG.encode(
    rgba8,
    decoded.width,
    decoded.height,
    cnum
  );

  // 计算压缩率
  const originalSize = arrayBuffer.byteLength;
  const compressedSize = compressed.byteLength;
  const ratio = ((1 - compressedSize / originalSize) * 100).toFixed(2);

  console.log(`原始大小: ${originalSize} 字节`);
  console.log(`压缩后大小: ${compressedSize} 字节`);
  console.log(`压缩率: ${ratio}%`);

  return compressed;
}

// 使用示例
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', (e) => {
  const file = e.target.files[0];
  if (file && file.type === 'image/png') {
    compressPNGLossy(file, 0.7);
  }
});

APNG 动画编码

以下示例演示如何将多帧图像数据编码为 APNG 动画文件:

function createAPNG(frames, delays, width, height) {
  // frames: 像素数据 ArrayBuffer 数组
  // delays: 帧延迟数组,单位为毫秒
  // width, height: 图像尺寸

  // 编码为 APNG
  const apngBuffer = UPNG.encode(
    frames,
    width,
    height,
    0,    // 无损压缩
    delays
  );

  // 创建 Blob
  const blob = new Blob([apngBuffer], { type: 'image/png' });
  return blob;
}

// 生成示例帧数据
function generateSampleFrames(width, height) {
  const frames = [];
  const colors = [
    [255, 0, 0, 255],    // 红色
    [0, 255, 0, 255],    // 绿色
    [0, 0, 255, 255],    // 蓝色
    [255, 255, 0, 255]   // 黄色
  ];

  for (let c = 0; c < colors.length; c++) {
    const frame = new Uint8Array(width * height * 4);
    const color = colors[c];

    for (let i = 0; i < frame.length; i += 4) {
      frame[i] = color[0];     // R
      frame[i + 1] = color[1]; // G
      frame[i + 2] = color[2]; // B
      frame[i + 3] = color[3]; // A
    }

    frames.push(frame.buffer);
  }

  return frames;
}

// 使用示例
const width = 200;
const height = 200;
const frames = generateSampleFrames(width, height);
const delays = [500, 500, 500, 500]; // 每帧显示 500ms

const apngBlob = createAPNG(frames, delays, width, height);
const url = URL.createObjectURL(apngBlob);

// 可以将 url 赋值给 img 元素的 src 属性查看效果

APNG 动画解码与播放

以下示例演示如何解码 APNG 动画并在 Canvas 上逐帧播放:

class APNGPlayer {
  constructor(canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
    this.frames = [];
    this.delays = [];
    this.currentFrame = 0;
    this.playing = false;
    this.timer = null;
  }

  async load(url) {
    const response = await fetch(url);
    const buffer = await response.arrayBuffer();

    // 解码 APNG
    const apng = UPNG.decode(buffer);
    const rgba8 = UPNG.toRGBA8(apng);

    // 设置 Canvas 尺寸
    this.canvas.width = apng.width;
    this.canvas.height = apng.height;

    // 存储帧数据
    this.frames = rgba8.map(data => new Uint8Array(data));
    this.delays = apng.frames.map(frame => frame.delay);

    // 显示第一帧
    this.showFrame(0);
  }

  showFrame(index) {
    if (index < 0 || index >= this.frames.length) return;

    this.currentFrame = index;
    const imageData = this.ctx.createImageData(
      this.canvas.width,
      this.canvas.height
    );
    imageData.data.set(this.frames[index]);
    this.ctx.putImageData(imageData, 0, 0);
  }

  play() {
    if (this.playing || this.frames.length === 0) return;

    this.playing = true;
    const nextFrame = () => {
      if (!this.playing) return;

      this.currentFrame = (this.currentFrame + 1) % this.frames.length;
      this.showFrame(this.currentFrame);

      const delay = this.delays[this.currentFrame] || 100;
      this.timer = setTimeout(nextFrame, delay);
    };

    const delay = this.delays[this.currentFrame] || 100;
    this.timer = setTimeout(nextFrame, delay);
  }

  pause() {
    this.playing = false;
    if (this.timer) {
      clearTimeout(this.timer);
      this.timer = null;
    }
  }

  stop() {
    this.pause();
    this.showFrame(0);
  }
}

// 使用示例
const canvas = document.getElementById('apngCanvas');
const player = new APNGPlayer(canvas);

player.load('animation.png').then(() => {
  player.play();
});

// 暂停播放
// player.pause();

// 停止播放
// player.stop();

技术原理

UPNG.js 的实现遵循 PNG 规范,解码过程主要包括以下步骤:首先读取 PNG 文件头和数据块,解析 IHDR 块获取图像基本信息,然后根据颜色类型和位深度解析 IDAT 块中的压缩数据,经过 Deflate 解压、滤波处理、调色板查找等步骤,最终还原为像素数据。

编码过程则是解码的逆过程:将像素数据进行滤波处理,然后使用 Deflate 算法压缩,封装为 PNG 文件格式的各个数据块。对于有损压缩,UPNG.js 内置了颜色量化算法,能够将真彩色图像量化为指定数量的调色板颜色,通过减少颜色数量来提高压缩率。

在 APNG 支持方面,UPNG.js 能够解析 acTL、fcTL、fdAT 等 APNG 专用数据块,提取动画的帧数、循环次数、每帧的延迟和偏移等信息。编码时则会按照 APNG 规范生成相应的数据块,确保生成的动画文件能够被支持 APNG 的浏览器和图像查看器正确播放。

性能特点

UPNG.js 在性能方面表现出色,主要体现在以下几个方面:

编解码速度较快,对于常规尺寸的图像能够在毫秒级完成处理,适合在前端实时处理场景中使用。压缩率表现良好,尤其是在有损压缩模式下,通过颜色量化和优化的 Deflate 参数,能够在视觉质量损失较小的情况下,实现较高的压缩比。内存占用较低,处理过程中不会产生大量的临时对象,适合处理较大尺寸的图像。

由于采用纯 JavaScript 实现,UPNG.js 的性能受 JavaScript 引擎执行效率的影响。在现代浏览器中,得益于 V8 等引擎的 JIT 编译优化,性能能够满足大多数应用场景的需求。对于特别大的图像或批量处理场景,可能需要考虑使用 Web Worker 来避免阻塞主线程。

适用场景

UPNG.js 适用于多种前端图像处理场景,包括但不限于:

前端图片压缩工具,在用户上传图片前进行压缩,减少上传流量和服务器存储压力。Canvas 图像导出,将 Canvas 绘制内容导出为 PNG 文件,相比浏览器原生的 toDataURL 方法,能够获得更高的压缩率和更多的控制选项。APNG 动画制作与播放,在网页中实现轻量级的动画效果,替代部分 GIF 动画场景,获得更好的画质和更小的文件体积。图像格式转换,将其他格式的图像转换为 PNG,或在不同 PNG 格式之间进行转换。在线图像编辑器,作为 PNG 格式的底层处理引擎,支持图像的打开、编辑和保存操作。

注意事项

使用 UPNG.js 时,有几个方面需要注意:

encode 方法的 imgs 参数要求传入 ArrayBuffer 数组,而不是 Uint8Array 数组。如果手中只有 Uint8Array ,需要通过 .buffer 属性获取底层的 ArrayBuffer 。有损压缩并不总是能减小文件体积,对于颜色数量本来就很少的简单图像,有损压缩后文件可能反而变大,建议根据实际图像内容选择合适的压缩模式。APNG 的帧延迟单位是毫秒,但 PNG 规范中实际存储的是百分之一秒,UPNG.js 在内部进行了转换,使用时直接传入毫秒值即可。处理大尺寸图像时,可能会占用较多内存,建议根据实际情况限制图像尺寸或采用分块处理的方式。

UPNG.js 是一个专注于 PNG 处理的专业库,它的出现为前端开发者提供了一个强大的图像处理工具。无论是简单的图片压缩,还是复杂的动画制作,UPNG.js 都能提供稳定可靠的支持。随着 Web 应用的功能越来越丰富,这类纯 JavaScript 实现的图像处理库将在更多场景中发挥作用。


发布评论

发布评论前请先 登录
0 评论
点赞
收藏

评论列表 0

暂无评论