純前端實現 PNG 圖片壓縮 | UPNG.js

在線 Demo 體驗地址 →: https://demos.sugarat.top/pages/png-compress/

前言

最近在迭代自己的 圖牀 應用,由於使用時間的累計,存儲空間佔用越來越大了,在做 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 體驗地址 →: https://demos.sugarat.top/pages/png-compress/

大概界面如下:

純血 HTML/CSS/JS,複製粘貼就能運行。

完整源碼見:GitHub:ATQQ/demos - png-compress

最後

後續將繼續學習&探索一下其它格式的純前端壓縮實現(JPG,GIF,MP4轉GIF)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章