纯前端实现 PNG 图片压缩 | UPNG.js

73 阅读4分钟

在线 Demo 体验地址 →: demos.sugarat.top/pages/png-c…

前言

最近在迭代自己的 图床 应用,由于使用时间的累计,存储空间占用越来越大了,在做 Web 应用的时候会随手拿 tinypng 压缩一下图片。

想着给咱图床也加个压缩的功能,这样上传/访问也能省点 💰。

图片类型众多,常用的主要就是PNG/JPG/GIF

个人使用频率最高的场景是截图上传,格式为PNG,就先拿 PNG 试手。调研了一圈开源里最流行的就是使用 UPNG.js 进行 PNG 的压缩。

如何判断图片是 PNG

第一步当然是判断图片类型,不然 UPNG.js 就不能正常工作咯,通过文件后缀 .png 判断肯定是不靠谱的。

搜索了解了一下,可以使用 魔数 判断:一个PNG文件的前8个字节是固定的

PNG 的前 8 个字节是(16进制表示):89 50 4E 47 0D 0A 1A 0A

我们可以拿工具看一下,我这里用 VS Code 插件 Hex Editor 查看一个 PNG 图片的 16 进制表示信息。

可以看到前八个字节和上面表示的一样。

于是可以根据这个特性判断,于是就有如下的判断代码。

async function isPNG(file: File) {
  // 提取前8个字节
  const arraybuffer = await file.slice(0, 8).arrayBuffer()

  // PNG 的前8字节16进制表示
  const signature = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]
  // const signature = [137, 80, 78, 71, 13, 10, 26, 10]

  // 转为 8位无符号整数数组 方便对比
  // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
  const source = new Uint8Array(arraybuffer)

  // 逐个字节对比
  for (let i = 0; i < signature.length; i++) {
    if (source[i] !== signature[i]) {
      return false
    }
  }
  return true
}

UPNG.js

简介

一个轻量且极速的 PNG/APNG 编码和解码库,Photopea 图像编辑器的主要 PNG 引擎。

npm 加载

官方提供了 npm 包,简单引入即可使用。

安装依赖

npm install upng-js

核心方法就 3 个,依次调用即可

  • UPNG.decode(buffer)
  • UPNG.toRGBA8(img)
  • UPNG.encode(imgs, w, h, cnum, [dels])
    • cnum:0 表示无损压缩,256表示有损,可以调整这个值来控制压缩质量。

注意:压缩并不意味着一定小,对于一些已经很简单且小的图片,压缩后可能反而更大。

下面是这个方法的最简实现。

import UPNG from 'upng-js'

async function compressPNG(file: File) {
  const arrayBuffer = await file.arrayBuffer()
  const decoded = UPNG.decode(arrayBuffer)
  const rgba8 = UPNG.toRGBA8(decoded)

  // 关键的压缩方
  // 这里 保持宽高不变,保持80%的质量(接近于 tinypng 的压缩效果)
  const compressed = UPNG.encode(
    rgba8,
    decoded.width,
    decoded.height,
    256 * 0.8
  )
  return new File([compressed], file.name, { type: 'image/png' })
}

其中压缩后的宽高,压缩质量都是可以调整的。

可配置封装

下面方法(TS 实现),提供了一些常用的配置选项。

import UPNG from 'upng-js'

interface CompressOptions {
  /**
   * 压缩质量([0,1])
   * @default 0.8
   */
  quality?: number
  /**
   * 压缩后更大是否使用原图
   * @default true
   */
  noCompressIfLarger?: boolean
  /**
   * 压缩后的新宽度
   * @default 原尺寸
   */
  width?: number
  /**
   * 压缩后新高度
   * @default 原尺寸
   */
  height?: number
}
async function compressPNGImage(file: File, ops: CompressOptions = {}) {
  const { width, height, quality = 0.8, noCompressIfLarger = true } = ops

  const arrayBuffer = await file.arrayBuffer()
  const decoded = UPNG.decode(arrayBuffer)
  const rgba8 = UPNG.toRGBA8(decoded)

  const compressed = UPNG.encode(
    rgba8,
    width || decoded.width,
    height || decoded.height,
    256 * quality
  )

  const newFile = new File([compressed], file.name, { type: 'image/png' })

  if (!noCompressIfLarger) {
    return newFile
  }

  return file.size > newFile.size ? newFile : file
}

CDN 加载

不通过 npm 安装,也可以使用 <script> 标签的方式进行全局引入。

可以使用Static file提供的 CDN 资源。

只需在 HTML 模板顶部 head 中加入如下资源即可使用。

<head>
  <script src="https://cdn.staticfile.net/pako/1.0.5/pako.min.js"></script>
  <script src="https://cdn.staticfile.net/upng-js/2.1.0/UPNG.min.js"></script>
</head>

PNG 格式化使用 Inflate 算法。这部分调用 Pako.js 实现,所以需要额外前置引入。

引入后,将在 window 上绑定 UPNG 变量,使用和上述 npm 给到的例子完全一致。

代码里调用方式如下

window.UPNG.encode

// 省略 window
UPNG.encode

完整 demo

笔者将本节内容整理成了一个 Demo,可以直接在线体验。

在线 Demo 体验地址 →: demos.sugarat.top/pages/png-c…

大概界面如下:

纯血 HTML/CSS/JS,复制粘贴就能运行。

完整源码见:GitHub:ATQQ/demos - png-compress

最后

后续将继续学习&探索一下其它格式的纯前端压缩实现(JPG,GIF,MP4转GIF)。