DEV Community

Cover image for 圖片壓縮:用 Compressor.js 自動調整品質壓縮至指定大小
Let's Write
Let's Write

Posted on • Originally published at letswrite.tw

圖片壓縮:用 Compressor.js 自動調整品質壓縮至指定大小

本篇要解決的問題

很多網站功能會需要處理使用者上傳的圖片,比方讓使用者上傳會員照片。

但隨著手機相機愈做愈好,拍出來的照片隨便都是幾 MB,直接上傳的話,耗時也佔空間。

雖然網路上搜尋有許多圖片壓縮工具,但大多只能設定固定的壓縮的品質,無法保證壓縮後的檔案大小符合需求。

本筆記文將使用 Compressor.js 套件,實作一個圖片壓縮功能,符合以下需求:

  • 自動嘗試不同的壓縮品質,直到檔案小於指定大小(ex: 600KB)為止。
  • 將圖片轉換為 WebP 格式。
  • 長、寬限制最大尺寸。

這樣就可以確保壓縮後的圖片既符合檔案大小限制,又保有良好的質感。


核心概念:遞迴壓縮

一般網路上的圖片壓縮只能一次性設定品質參數,本篇用了 comporess.js 後,採用 遞迴嘗試 的方式,執行方式如下:

  1. 從最高品質(quality = 1.0)開始壓縮。
  2. 檢查壓縮後的檔案大小。
  3. 如果超過目標大小 600KB,則降低品質(減少 0.05)後重新壓縮。
  4. 重複步驟 2-3,直到檔案符合大小要求,或品質降到下限(0.40)。

這種方法能夠在保證檔案大小的前提下,盡可能保留圖片品質。


引入 Compressor.js

首先,我們需要引入 Compressor.js 這個圖片壓縮套件。

它可以在瀏覽器端直接處理圖片:無需後端支援:

<script src="https://cdn.jsdelivr.net/npm/compressorjs@1.2.1/dist/compressor.min.js"></script>
Enter fullscreen mode Exit fullscreen mode

上面是直接引用 CDN 的方式,官方文件 也有其他引用方式,可以自己的需求引用。


建立 HTML

我們需要一個檔案上傳的 file input,以及一個用來預覽壓縮結果的 div:

<input type="file" id="upload" accept="image/*" />
<img id="preview" style="max-width:300px" />
Enter fullscreen mode Exit fullscreen mode

實作智慧壓縮函式

這是整個方案的核心函式,接受四個參數:

  • file:要壓縮的原始檔案。
  • targetSize:目標檔案大小,預設 600KB。
  • floor:品質下限,預設 0.40。
  • step:每次品質遞減幅度,預設 0.05。
function compressWithFloor(
  file,
  targetSize = 600 * 1024,
  floor = 0.4,
  step = 0.05
) {
  return new Promise((resolve, reject) => {
    let q = 1.0;

    const attempt = () => {
      new Compressor(file, {
        quality: q,
        mimeType: "image/webp", // 強制轉換為 WebP 格式
        maxWidth: 2560, // 最大寬度限制
        maxHeight: 1440, // 最大高度限制
        success(blob) {
          console.log(
            `q=${q.toFixed(2)}  size=${Math.round(blob.size / 1024)}KB`
          );
          if (blob.size > targetSize && q - step >= floor) {
            q = +(q - step).toFixed(2);
            attempt(); // 繼續壓縮
          } else {
            resolve(blob); // 符合大小或到達品質下限
          }
        },
        error(err) {
          reject(err);
        },
      });
    };

    attempt();
  });
}
Enter fullscreen mode Exit fullscreen mode

參數說明

1. 強制輸出 WebP 格式

mimeType: 'image/webp' 會將所有圖片(包括 PNG、JPEG)都轉換為 WebP。

WebP 是 Google 開發的現代圖片格式。在相同品質下。檔案大小通常比 JPEG 小 25-35%。

2. 尺寸限制

maxWidth: 2560maxHeight: 1440 確保圖片不會超過 2K 解析度。

如果原圖尺寸較小,不會被放大;

如果較大,會等比例縮小。

這對於手機拍攝的 4K、8K 照片特別有用。

3. 遞迴壓縮邏輯

if (blob.size > targetSize && q - step >= floor) {
  q = +(q - step).toFixed(2);
  attempt(); // 繼續壓縮
}
Enter fullscreen mode Exit fullscreen mode

這段程式碼檢查兩個條件:

  1. 檔案是否仍大於目標大小?
  2. 品質是否還有下降空間?

只要 1、2 都符合,就會降低品質並重新嘗試壓縮。


Blob 轉 Base64 輔助函式

為了在瀏覽器中預覽和下載圖片,或是調用 API 傳到後端,我們需要將壓縮後的 Blob 物件轉換為 Base64 格式:

function blobToBase64(blob) {
  return new Promise((resolve) => {
    const r = new FileReader();
    r.onloadend = () => resolve(r.result);
    r.readAsDataURL(blob);
  });
}
Enter fullscreen mode Exit fullscreen mode

整合上傳與下載功能

最後,我們監聽檔案上傳事件,執行壓縮流程,並自動觸發下載:

document.getElementById("upload").addEventListener("change", async (e) => {
  const file = e.target.files[0];
  if (!file) return;

  try {
    const compressed = await compressWithFloor(file, 600 * 1024, 0.4, 0.05);
    console.log("final size:", Math.round(compressed.size / 1024), "KB");

    const base64 = await blobToBase64(compressed);
    document.getElementById("preview").src = base64;

    // 取出原始檔名(去掉副檔名)
    const originalName = file.name.replace(/\.[^/.]+$/, "");
    const newFileName = `${originalName}-compress.webp`;

    // 建立下載連結並自動觸發
    const link = document.createElement("a");
    link.href = base64;
    link.download = newFileName;
    link.click();
  } catch (err) {
    console.error("壓縮失敗:", err);
  }
});
Enter fullscreen mode Exit fullscreen mode

檔名處理邏輯

程式會自動處理檔名:

  • 移除原始副檔名:file.name.replace(/\.[^/.]+$/, "")
  • 加上 -compress.webp 後綴。

例如:vacation.jpgvacation-compress.webp


壓縮流程示例

假設上傳一張 5MB 的照片,壓縮過程可能如下:

q=1.00  size=1200KB  → 超過 600KB,繼續
q=0.95  size=950KB   → 超過 600KB,繼續
q=0.90  size=750KB   → 超過 600KB,繼續
q=0.85  size=580KB   → 符合要求,完成!
final size: 580 KB
Enter fullscreen mode Exit fullscreen mode

如果原圖品質很差,即使降到最低品質仍超過 600KB,程式也會在 q=0.40 時停止,避免過度壓縮導致圖片難以辨識。


完整程式碼

以下是完整的可執行範例:

<!DOCTYPE html>
<html lang="zh-TW">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Let's write - 圖片壓縮</title>
  </head>
  <body>
    <h1>Let's write - 圖片壓縮工具</h1>
    <p>筆記文:<a href="https://www.letswrite.tw/compressjs/" target="_blank"></a></p>
    <input type="file" id="upload" accept="image/*" />
    <img id="preview" style="max-width:300px; margin-top:20px;" />

    <script src="https://cdn.jsdelivr.net/npm/compressorjs@1.2.1/dist/compressor.min.js"></script>
    <script>
      function compressWithFloor(
        file,
        targetSize = 600 * 1024,
        floor = 0.4,
        step = 0.05
      ) {
        return new Promise((resolve, reject) => {
          let q = 1.0;

          const attempt = () => {
            new Compressor(file, {
              quality: q,
              mimeType: "image/webp",
              maxWidth: 2560,
              maxHeight: 1440,
              success(blob) {
                console.log(
                  `q=${q.toFixed(2)}  size=${Math.round(blob.size / 1024)}KB`
                );
                if (blob.size > targetSize && q - step >= floor) {
                  q = +(q - step).toFixed(2);
                  attempt();
                } else {
                  resolve(blob);
                }
              },
              error(err) {
                reject(err);
              },
            });
          };

          attempt();
        });
      }

      function blobToBase64(blob) {
        return new Promise((resolve) => {
          const r = new FileReader();
          r.onloadend = () => resolve(r.result);
          r.readAsDataURL(blob);
        });
      }

      // 依序嘗試 q=1.00, 0.95, 0.90, ... ,直到 <=600KB  q<0.40
    function compressWithFloor(
      file,
      targetSize = 600 * 1024,
      floor = 0.4,
      step = 0.05,
    ) {
      return new Promise((resolve, reject) => {
        let q = 1.0;

        const attempt = () => {
          new Compressor(file, {
            quality: q,
            mimeType: "image/webp", // 強制轉 WebP
            maxWidth: 2560, // 最大寬度限制
            maxHeight: 1440, // 最大高度限制
            success(blob) {
              console.log(
                `q=${q.toFixed(2)}  size=${Math.round(blob.size / 1024)}KB`,
              );
              if (blob.size > targetSize && q - step >= floor) {
                q = +(q - step).toFixed(2);
                attempt(); // 繼續壓縮
              } else {
                resolve(blob); // 符合大小或到達品質下限
              }
            },
            error(err) {
              reject(err);
            },
          });
        };

        attempt();
      });
    }

    // Blob -> base64
    function blobToBase64(blob) {
      return new Promise((resolve) => {
        const r = new FileReader();
        r.onloadend = () => resolve(r.result);
        r.readAsDataURL(blob);
      });
    }

    document.getElementById("upload").addEventListener("change", async (e) => {
      const file = e.target.files[0];
      if (!file) return;

      const targetSize = 600 * 1024;

      // 檢查原始檔案大小
      if (file.size <= targetSize) {
        console.log(
          `原始檔案大小 ${Math.round(file.size / 1024)}KB,已小於目標大小 ${Math.round(targetSize / 1024)}KB,不需壓縮`,
        );
        alert(`圖片大小已符合要求 (${Math.round(file.size / 1024)}KB),無需壓縮`);

        // 仍然顯示預覽
        const base64 = await blobToBase64(file);
        document.getElementById("preview").src = base64;
        return;
      }

      try {
        const compressed = await compressWithFloor(file, targetSize, 0.4, 0.05);
        console.log("final size:", Math.round(compressed.size / 1024), "KB");
        const base64 = await blobToBase64(compressed);
        document.getElementById("preview").src = base64;
        const originalName = file.name.replace(/\.[^/.]+$/, "");
        const newFileName = `${originalName}-compress.webp`;
        const link = document.createElement("a");
        link.href = base64;
        link.download = newFileName;
        link.click();
      } catch (err) {
        console.error("壓縮失敗:", err);
      }
    });

    </script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Demo 與原始碼

想要實際體驗圖片壓縮工具的話,以下是完整的 Demo 頁面和原始碼供大家使用。

使用前,如果這個工具對你有幫助,歡迎到 GitHub 給我一顆星星 ⭐,你的小小動作,對本站都是大大的鼓勵。

DemoDemo

GitHub 原始碼GitHub Repository

Top comments (0)