線上 Demo 體驗地址 →: https://demos.sugarat.top/pages/jpg-compress/
前言
在迭代圖床應用時,需要用到圖片壓縮,在之前分享了使用 UPNG.js 壓縮 PNG 圖片,這裡記錄分享一下如何處理 JPG 圖片。
蒐羅調研了一圈,JPG 圖片的處理,基本都是圍繞 canvas 展開的。
- 掘金:前端實現圖片壓縮技術調研
- 相關開源庫(近期還有迭代維護的):Compressor.js,browser-image-compression。
如何判斷圖片是 JPG
同樣第一步當然是判斷圖片型別,不然就沒法正常的做後續處理了。
搜尋瞭解了一下,JPG 圖片的前三位元組是固定的(16進製表示):FF D8 FF
。
下圖是 VS Code 外掛 Hex Editor 檢視一個 JPG 圖片的 16 進製表示資訊。
於是可以根據這個特性判斷,於是就有如下的判斷程式碼。
function isJPG(file) {
// 提取前3個位元組
const arraybuffer = await file.slice(0, 3).arrayBuffer()
// JPG 的前3位元組16進製表示
const signature = [0xFF, 0xD8, 0xFF]
// 轉為 8位無符號整數陣列 方便對比
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
const source = new Uint8Array(arraybuffer)
// 逐個位元組對比
return source.every((value, index) => value === signature[index])
}
當然社群也有現成的 is-jpg 庫可以使用。
可看判斷程式碼還是很簡單的。
下面將先介紹一下上述兩個開源庫的簡單用法與特色,最後再介紹一下直接使用 canvas API
壓縮的方式以及注意事項。
Compressor.js
簡介
JavaScript 影像壓縮工具。使用瀏覽器原生的
canvas.toBlob API
實現壓縮,有失真壓縮
,非同步
,在不同的瀏覽器壓縮效果有所出入。一般可以用來在上傳之前在客戶端預壓縮影像。
官方示例站點:Compressor.js PlayGround
使用
支援 npm
和 cdn
兩種引入方式。
npm 載入
# 安裝依賴
npm install compressorjs
// 專案中引入使用
import Compressor from 'compressorjs'
cdn 載入
<!-- html head 中引入 -->
<script src='https://cdn.staticfile.net/compressorjs/1.2.1/compressor.min.js'></script>
<!-- 專案中直接使用 Compressor 即可 -->
簡單使用方式如下
// file 是待壓縮圖片的檔案物件
new Compressor(file, {
quality: 0.8,
success(result) {
// result 是壓縮後的圖片內容
}
})
其餘的 option 選項可以參考官方文件,主要是尺寸大小,壓縮質量效果,圖片資訊的保留等細節的調節。
簡單封裝
可以簡單用 Promise
封裝一下,使用更加方便。
async function compressJPGByCompressor(file, ops) {
return new Promise((resolve, reject) => {
new Compressor(file, {
...ops,
success(result) {
resolve(result)
},
error(err) {
reject(err)
}
})
})
}
當然這種不支援 Promise
的回撥用法函式用 Promise.withResolvers 包裝最合適不過了。
當然瀏覽器不支援這個API的話 需要引入 polyfill
才行(可以從 core-js
中引入,或自己簡單實現一下)。
function compressJPGByCompressor(file, ops) {
const { promise, resolve, reject } = Promise.withResolvers()
new Compressor(file, {
...ops,
success(result) {
resolve(result)
},
error(err) {
reject(err)
}
})
return promise
}
browser-image-compression
簡介
瀏覽器中實現圖片壓縮,透過降低解析度或大小來壓縮 jpeg、png、webp 和 bmp 影像;支援使用 Web Worker 實現多執行緒的非阻塞壓縮。
官方示例站點:compression PlayGround
其中多執行緒壓縮使用 OffscreenCanvas: 一個可以脫離螢幕渲染的 canvas 物件。在 web worker
環境也可工作。
使用
同樣的也支援 npm
和 cdn
兩種引入方式。
npm 載入
# 安裝依賴
npm install browser-image-compression
// 專案中引入使用
import imageCompression from 'browser-image-compression'
cdn 載入
<!-- html head 中引入 -->
<script src="https://cdn.staticfile.net/browser-image-compression/2.0.2/browser-image-compression.min.js"></script>
<!-- 專案中直接使用 imageCompression 即可 -->
簡單使用方式如下
imageCompression(file, {
// 設定壓縮後的最大大小,單位是 MB(會根據目標自動調整圖片質量或者尺寸)
maxSizeMB: 1,
// 如果希望透過百分比控制質量,只需簡單計算一下即可
// maxSizeMB: Math.round(file.size / (1024 * 1024) * quality),
// 也可設定壓縮後最大的寬或者高 (自動應用於圖片中較長的那一邊)
// maxWidthOrHeight: 1920,
}).then((result) => {
// result 為壓縮後的結果
})
可以看出來使用非常簡單:
- 調整尺寸就使用
maxWidthOrHeight
; - 保持原尺寸就調整
maxSizeMB
的值。
簡單封裝
function compressImageByImageCompression(file, options = {}) {
const { width, height, quality = 0.8, ...ops } = options
return window.imageCompression(file, {
maxSizeMB: Math.round(file.size / (1024 * 1024) * quality),
maxWidthOrHeight: width || height || undefined,
libURL: 'https://cdn.staticfile.net/browser-image-compression/2.0.2/browser-image-compression.js',
...ops
})
}
這樣呼叫起來更加方便靈活。
注意事項
預設是開啟的多執行緒壓縮,會從 https://cdn.jsdelivr.net
拉取 worker 指令碼。
如果存在網路原因訪問不通暢,可以透過 options.libURL
替換為自定義的指令碼位置,比如使用 Staticfile CDN 資源。
imageCompression(file, {
// ...省略其它配置
libURL: 'https://cdn.staticfile.net/browser-image-compression/2.0.2/browser-image-compression.js'
})
canvas api
主流的 JPG 純前端壓縮方案,基本都是藉助 canvas 實現的,區別就在於邊界場景是否考慮周全,配套的特效能否滿足將使用的場景。
使用
先建立 Image
物件,獲取圖片的基本資訊
下面是使用 URL.createObjectURL
建立資源連結的方式:
const img = new Image()
// 圖片完成載入
img.onload = () => {
// 獲取圖片寬高
const { width, height } = img
// 後續就可以使用 canvas 進行進一步的壓縮處理
}
img.src = URL.createObjectURL(file)
當然這裡也可以用 FileReader
,此時程式碼看上去多2行(hhh)
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = function (event) {
img.src = event.target.result
}
緊接著就可以使用 canvas
進行影像的繪製(img 完成載入後)
// 建立 canvas 元素
const canvas = document.createElement('canvas')
// 獲取畫布的2D渲染上下文
const ctx = canvas.getContext('2d')
// 設定 canvas 的寬高與圖片一致
canvas.width = img.width
canvas.height = img.height
// 在 canvas 上繪製圖片(待繪製的圖片,畫布上的起始座標,繪製的寬高)
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
// 如果把元素插入到頁面中,則可以看到 canvas 繪製的圖片
// document.body.appendChild(canvas);
接下來最核心的就行呼叫 canvas.toDataURL(type, quality)
進行"壓縮"了。
// 只需要設定圖片格式,與圖片質量 兩個引數即可
const compressedDataUrl = canvas.toDataURL('image/jpeg', 0.8)
接下來就需要將 compressedDataUrl
轉化為 blob
或者 file
物件。
DataUrl
格式如下。
data:image/jpeg;base64,XXXX
# 資料識別符號:以"data:"開頭
# MIME型別描述:指示資料的型別,"image/jpeg"表示JPEG影像
# 資料編碼:以base64編碼表示,"XXXX"是 base64 編碼資料部分
咱們先把mimetype
,decodedData
這 2 部分提取出來
const [dataDescription, base64Data] = compressedDataUrl.split(',')
// 檔案型別
const mimetype = dataDescription.match(/:(.*?);/)[1]
// 解碼 base64 資料
const decodedData = atob(base64Data)
最後將解碼的 base64
資料轉成 file
即可。
let n = decodedData.length
// 建立等位元組大小的 Uint8Array
const u8arr = new Uint8Array(n)
// 遍歷賦值
while (n--) {
u8arr[n] = decodedData.charCodeAt(n)
}
// 透過 Uint8Array 建立 File 物件
const result = new File([u8arr], file.name, { type: mimetype })
簡單封裝
完整程式碼如下:
async function compressImageByCanvas(file, options = {}) {
const { quality } = options
let { width, height } = options
let _resolve, _reject
const promise = new Promise((resolve, reject) => {
_resolve = resolve
_reject = reject
})
const img = new Image()
img.onload = function () {
// 如果只指定了寬度或高度,則另一個按比例縮放
if (width && !height) {
height = Math.round(img.height * (width / img.width))
}
else if (!width && height) {
width = Math.round(img.width * (height / img.height))
}
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = width || img.width
canvas.height = height || img.height
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
const compressedDataUrl = canvas.toDataURL('image/jpeg', quality)
_resolve(dataURItoFile(compressedDataUrl, file.name))
}
img.src = createObjectURL(file)
return promise
}
function dataURItoFile(dataURI, fileName) {
const [dataDescription, base64Data] = dataURI.split(',')
const mimetype = dataDescription.match(/:(.*?);/)[1]
const decodedData = atob(base64Data)
let n = decodedData.length
const u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = decodedData.charCodeAt(n)
}
return new File([u8arr], fileName, { type: mimetype })
}
相容性問題
筆者並沒有深入測試 canvas 壓縮的相容性問題,但從社群的幾個前端處理 JPG 庫裡的 README 描述與 issues 等可以歸納出使用 canvas
處理時,需考慮下面幾個方面的問題:
- 大小限制:詳見 不同瀏覽器和裝置上 canvas 大小限制;
- 資訊保留:
EXIF
資訊,正確識別與處理圖片方向; - 裝置相容性:移動端裝置瀏覽器定製核心相對多, 邊界情況較多(相關 API 的支援程度,canvas 差異性表現)。
參考:browser-image-compression
, Compressor.js
, localResizeIMG
完整 demo
筆者將本節內容整理成了一個 Demo,可以直接線上體驗。
線上 Demo 體驗地址 →: https://demos.sugarat.top/pages/jpg-compress/
大概介面如下(可修改配置切換壓縮方案,對比效果):
純血 HTML/CSS/JS,複製貼上就能執行。
完整原始碼見:GitHub:ATQQ/demos - jpg-compress
最後
後續將繼續學習&探索一下 GIF
,MP4 轉 GIF
等常用的動圖前端處理實現的方式。