本篇要解決的問題
很多網站功能會需要處理使用者上傳的圖片,比方讓使用者上傳會員照片。
但隨著手機相機愈做愈好,拍出來的照片隨便都是幾 MB,直接上傳的話,耗時也佔空間。
雖然網路上搜尋有許多圖片壓縮工具,但大多只能設定固定的壓縮的品質,無法保證壓縮後的檔案大小符合需求。
本筆記文將使用 Compressor.js 套件,實作一個圖片壓縮功能,符合以下需求:
- 自動嘗試不同的壓縮品質,直到檔案小於指定大小(ex: 600KB)為止。
- 將圖片轉換為 WebP 格式。
- 長、寬限制最大尺寸。
這樣就可以確保壓縮後的圖片既符合檔案大小限制,又保有良好的質感。
核心概念:遞迴壓縮
一般網路上的圖片壓縮只能一次性設定品質參數,本篇用了 comporess.js 後,採用 遞迴嘗試 的方式,執行方式如下:
- 從最高品質(
quality = 1.0
)開始壓縮。 - 檢查壓縮後的檔案大小。
- 如果超過目標大小 600KB,則降低品質(減少 0.05)後重新壓縮。
- 重複步驟 2-3,直到檔案符合大小要求,或品質降到下限(0.40)。
這種方法能夠在保證檔案大小的前提下,盡可能保留圖片品質。
引入 Compressor.js
首先,我們需要引入 Compressor.js 這個圖片壓縮套件。
它可以在瀏覽器端直接處理圖片:無需後端支援:
<script src="https://cdn.jsdelivr.net/npm/compressorjs@1.2.1/dist/compressor.min.js"></script>
上面是直接引用 CDN 的方式,官方文件 也有其他引用方式,可以自己的需求引用。
建立 HTML
我們需要一個檔案上傳的 file input,以及一個用來預覽壓縮結果的 div:
<input type="file" id="upload" accept="image/*" />
<img id="preview" style="max-width:300px" />
實作智慧壓縮函式
這是整個方案的核心函式,接受四個參數:
-
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();
});
}
參數說明
1. 強制輸出 WebP 格式
mimeType: 'image/webp'
會將所有圖片(包括 PNG、JPEG)都轉換為 WebP。
WebP 是 Google 開發的現代圖片格式。在相同品質下。檔案大小通常比 JPEG 小 25-35%。
2. 尺寸限制
maxWidth: 2560
和 maxHeight: 1440
確保圖片不會超過 2K 解析度。
如果原圖尺寸較小,不會被放大;
如果較大,會等比例縮小。
這對於手機拍攝的 4K、8K 照片特別有用。
3. 遞迴壓縮邏輯
if (blob.size > targetSize && q - step >= floor) {
q = +(q - step).toFixed(2);
attempt(); // 繼續壓縮
}
這段程式碼檢查兩個條件:
- 檔案是否仍大於目標大小?
- 品質是否還有下降空間?
只要 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);
});
}
整合上傳與下載功能
最後,我們監聽檔案上傳事件,執行壓縮流程,並自動觸發下載:
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);
}
});
檔名處理邏輯
程式會自動處理檔名:
- 移除原始副檔名:
file.name.replace(/\.[^/.]+$/, "")
。 - 加上
-compress.webp
後綴。
例如:vacation.jpg
→ vacation-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
如果原圖品質很差,即使降到最低品質仍超過 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>
Demo 與原始碼
想要實際體驗圖片壓縮工具的話,以下是完整的 Demo 頁面和原始碼供大家使用。
使用前,如果這個工具對你有幫助,歡迎到 GitHub 給我一顆星星 ⭐,你的小小動作,對本站都是大大的鼓勵。
Demo:Demo
GitHub 原始碼:GitHub Repository
Top comments (0)