線上 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)。