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

粥里有勺糖發表於2024-03-16

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

前言

最近在迭代自己的 圖床 應用,由於使用時間的累計,儲存空間佔用越來越大了,在做 Web 應用的時候會隨手拿 tinypng 壓縮一下圖片。

想著給咱圖床也加個壓縮的功能,這樣上傳/訪問也能省點 💰。

圖片型別眾多,常用的主要就是PNG/JPG/GIF

個人使用頻率最高的場景是截圖上傳,格式為PNG,就先拿 PNG 試手。調研了一圈開源裡最流行的就是使用 UPNG.js 進行 PNG 的壓縮。

  • 官方對比 tinypng 介紹
  • 官方線上示例 Demo

如何判斷圖片是 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)。

相關文章