純前端實現 JPG 圖片壓縮 | canvas

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

前言

在迭代圖牀應用時,需要用到圖片壓縮,在之前分享了使用 UPNG.js 壓縮 PNG 圖片,這裏記錄分享一下如何處理 JPG 圖片。

蒐羅調研了一圈,JPG 圖片的處理,基本都是圍繞 canvas 展開的。

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

使用

支持 npmcdn 兩種引入方式。

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 環境也可工作。

使用

同樣的也支持 npmcdn 兩種引入方式。

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 處理時,需考慮下面幾個方面的問題:

  1. 大小限制:詳見 不同瀏覽器和設備上 canvas 大小限制
  2. 信息保留:EXIF 信息,正確識別與處理圖片方向;
  3. 設備兼容性:移動端設備瀏覽器定製內核相對多, 邊界情況較多(相關 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

最後

後續將繼續學習&探索一下 GIFMP4 轉 GIF 等常用的動圖前端處理實現的方式。

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