影像
使用快速的原生管道解碼、轉換和編碼影像
Bun.Image 是一個可鏈式呼叫的影像管道,用於解碼、調整大小、旋轉以及重新編碼 JPEG、PNG、WebP、HEIC 和 AVIF。它基於 libjpeg-turbo、spng、libwebp 和 SIMD 幾何核心建置,無需 npm 相依,也無需建置原生外掛。
await Bun.file("photo.jpg").image().resize(400, 400, { fit: "inside" }).webp({ quality: 80 }).write("thumb.webp");
此 API 參考了 Sharp 的設計:從輸入構造例項,鏈式呼叫轉換,選擇輸出格式,然後 await 一個終結方法。只有在你等待終結方法時才會執行操作,且處理會在 JavaScript 執行緒之外執行。
輸入
建構函式接受路徑、位元組或 Blob——包括 Bun.file() 和 Bun.s3.file()。Blob#image() 是 new Bun.Image(blob) 的簡寫:
new Bun.Image("./photo.jpg"); // 檔案路徑
new Bun.Image(buffer); // Buffer / ArrayBuffer / TypedArray
new Bun.Image(Bun.file("photo.jpg")); // BunFile(延遲讀取、線上程外執行)
Bun.file("photo.jpg").image(); // 與上面相同
Bun.s3.file("bucket/photo.jpg").image(); // S3File
Bun 會從位元組中檢測格式,並忽略副檔名和 Content-Type。
**路徑字串是檔案系統路徑。**不要將使用者控制的字串直接傳給建構函式——這會產生任意檔案讀取原語。請先使用 fetch 或 Bun.file 將不受信任的輸入讀入 Buffer,並進行你自己的驗證,然後再傳入這些位元組。
傳入 TypedArray/ArrayBuffer 時,終結方法正在執行期間不要修改它——解碼會線上程外執行,並借用這些位元組。Bun 不接受 SharedArrayBuffer 和可調整大小的緩衝區;請使用 buf.slice() 傳入固定檢視。
當 libjpeg-turbo 僅發出警告就能完成解碼時,Bun 會解碼損壞的 JPEG。例如,段之間有多餘位元組、缺少影像結束標記,以及掃描資料被截斷。libjpeg-turbo 會將沒有資料的塊填充為純灰色。漸進式檔案若在第一次掃描後被截斷,則每個塊都有資料,因此 Bun 會傳回細節較少的完整影像。當 libjpeg-turbo 無法完成解碼時,終結方法會以 ERR_IMAGE_DECODE_FAILED 拒絕。metadata() 只讀取頭部,因此即使終結方法會拒絕,它也可能成功解析。
第二個 options 引數用於防護解壓炸彈並控制 EXIF 處理:
new Bun.Image(input, {
// 如果 width*height > 這個值則拒絕。會在讀取頭部後、
// 分配畫素緩衝區之前檢查。預設值與 Sharp 一致(約 268 MP)。
maxPixels: 4096 * 4096,
// 在任何其他操作之前應用 JPEG EXIF 方向。預設:true。
autoOrient: true,
});
後設資料
無需解碼畫素資料即可讀取 width、height 和 format:
const { width, height, format } = await new Bun.Image(input).metadata();
// => { width: 1920, height: 1080, format: "jpeg" }
調整大小
img.resize(800); // 寬度為 800,保持寬高比
img.resize(800, 600); // 精確為 800×600(拉伸)
img.resize(800, 600, { fit: "inside" }); // 適配到 800×600 內
img.resize(800, 600, { withoutEnlargement: true }); // 永不放大
img.resize(800, 600, { filter: "mitchell" });
fit | 行為 |
|---|---|
"fill"(預設) | 拉伸到精確的 width × height |
"inside" | 保持寬高比;結果適配於該邊界內 |
filter 用於選擇重取樣核心。預設的 "lanczos3" 是照片的最佳選擇。
| 濾鏡 | 適用場景 |
|---|---|
"lanczos3" (預設) | 通用,照片最銳利 |
"lanczos2" | 略柔和,振鈴偽影更少 |
"mitchell" | 平滑漸變;經典的雙三次折中方案 |
"cubic" | Catmull-Rom——比 Mitchell 更銳利,可能產生振鈴 |
"mks2013" / "mks2021" | “Magic Kernel Sharp”;Facebook/Instagram 使用 |
"bilinear" / "linear" | 快速,柔和 |
"box" | 區域平均;適合大倍率整數縮小 |
"nearest" | 畫素風格 / 硬邊緣 |
當源影像是 JPEG 且目標尺寸不超過源影像的一半時,解碼會直接跳到最接近的 M/8 IDCT 縮放級別。因此,從一張 24 MP 照片生成縮圖時,不會建立全解析度緩衝區。
旋轉 · 翻轉
img.rotate(90); // 順時針旋轉 90°(僅支援 90 的倍數)
img.flip(); // 垂直映象(繞 x 軸)
img.flop(); // 水平映象(繞 y 軸)
調節
img.modulate({
brightness: 1.2, // 1 = 不變
saturation: 0, // 0 = 灰度,1 = 不變,>1 = 增強
});
輸出格式
呼叫格式方法會設定編碼目標;如果沒有呼叫格式方法,Bun 會沿用源格式。
img.jpeg({ quality: 85 }); // 1–100,預設 80
img.png({ compressionLevel: 6 }); // zlib 級別 0–9
img.png({ palette: true, colors: 64, dither: true }); // 索引 PNG
img.webp({ quality: 80 });
img.webp({ lossless: true });
img.heic({ quality: 80 }); // 僅 macOS / Windows
img.avif({ quality: 60 }); // 僅 macOS / Windows
palette: true 會將影像量化為不超過 256 種顏色的調色盤,並輸出索引(color-type 3)PNG,可選擇使用 Floyd–Steinberg dither。對於螢幕截圖和 UI 資源,索引 PNG 通常比真彩色影像小 3–5 倍。
終結方法
管道在等待以下任一方法之前不會執行任何操作:
await img.bytes(); // Uint8Array
await img.buffer(); // Buffer
await img.blob(); // Blob,.type 設定為輸出 MIME
await img.toBase64(); // string
await img.dataurl(); // "data:image/png;base64,…"
await img.write("out.webp"); // number(寫入的位元組數)
await img.write(Bun.s3.file("bucket/out.webp"));
.write() 接受與 Bun.write 相同的目標型別——路徑字串、Bun.file()、Bun.s3.file() 或檔案描述符。如果你沒有鏈式呼叫格式方法,並且目標是路徑字串,則副檔名會決定格式(.jpg/.png/.webp/.heic/.avif)。
佔位符
.placeholder() 會傳回一個低品質佔位圖,可在真實影像載入前內嵌到 HTML 中。佔位圖是以 data: URL 呈現的 ThumbHash 渲染的 ≤32px 模糊影像。大小約為 400–700 位元組,無需客戶端解碼器:
const lqip = await Bun.file("hero.jpg").image().placeholder();
// <img src={lqip} … /> — 然後在載入時切換到真實 URL。
如果想對影像 本身 進行由粗到細的渲染,請編碼為漸進式 JPEG:
img.jpeg({ progressive: true });
第一個終結方法解析後,img.width 和 img.height 反映的是 輸出 尺寸(在此之前它們為 -1)。
Bun.serve 整合
Bun.Image 管道是一個有效的 Response body,並會自動設定 Content-Type。為了在伺服器端處理程序中讓編碼保持在 JS 執行緒之外,請先 await 一個終結方法:
Bun.serve({
routes: {
"/avatar/:id": async req => {
// 在接觸檔案系統之前先驗證(見上面的輸入說明)。
if (!/^[a-z0-9]+$/.test(req.params.id)) return new Response(null, { status: 400 });
const out = await Bun.file(`avatars/${req.params.id}.png`).image().resize(128, 128).webp().blob();
return new Response(out);
},
},
});
直接傳遞管道(new Response(img))同樣有效,但會在正文初始化期間同步執行編碼。
剪貼簿
const img = Bun.Image.fromClipboard();
if (img) {
const png = await img.resize(800, 800, { fit: "inside" }).png().bytes();
}
fromClipboard() 會從 macOS 和 Windows 的系統剪貼簿讀取 PNG、TIFF、HEIC、JPEG、WebP、GIF 或 BMP;之後再交給常規解碼管道處理。如果沒有影像則傳回 null,而在 Linux 上始終傳回 null——請自行呼叫 wl-paste/xclip 並將位元組傳給建構函式。
對於“剪貼簿中有影像,按 ⌘V”這種被動提示,請輪詢 clipboardChangeCount()(一次整數讀取),並僅在值發生變化時呼叫 hasClipboardImage()。輪詢是文件推薦的模式,因為 macOS 沒有剪貼簿更改通知。
平臺後端
| Linux | macOS | Windows | |
|---|---|---|---|
| JPEG / PNG / WebP | libjpeg-turbo · spng · libwebp | same | same |
| BMP / GIF (decode) | built-in | ImageIO | WIC |
| TIFF (decode) | ❌ | ImageIO | WIC |
| Resize / rotate / flip | Highway SIMD | Accelerate vImage ³ | Highway SIMD |
| HEIC / AVIF | ❌ ERR_IMAGE_FORMAT_UNSUPPORTED | ImageIO ² | WIC ¹ |
| Clipboard | ❌ returns null | NSPasteboard | Win32 |
¹ Windows 需要從 Microsoft Store 獲取 HEIF Image Extensions / AV1 Video Extension。
² AVIF 編碼 需要作業系統 AV1 編碼器——僅限 Apple Silicon M3+。Intel Mac 和 M1/M2 會以 ERR_IMAGE_FORMAT_UNSUPPORTED 拒絕;AVIF 解碼 在 ImageIO 支援的所有環境中均可使用(macOS 13+)。
³ 僅在使用預設 lanczos3 濾鏡時,調整大小才會使用 vImage;在 macOS 上,其他所有 filter 都會使用 Highway SIMD。
噹噹前計算機上無法使用系統後端格式時,終結方法會以 error.code === "ERR_IMAGE_FORMAT_UNSUPPORTED" 拒絕。請根據該錯誤程式碼分支處理,以回退到可移植格式:
const out = await img
.avif({ quality: 50 })
.bytes()
.catch(e => {
if (e.code === "ERR_IMAGE_FORMAT_UNSUPPORTED") return img.webp({ quality: 80 }).bytes();
throw e;
});
由系統後端處理的格式(TIFF、HEIC、AVIF、剪貼簿)會採用作業系統的補丁級別,因此請保持 macOS / Windows 為最新版本。JPEG、PNG 和 WebP 在所有平臺上都使用相同的靜態連結編解碼器,因此在 Linux、macOS 和 Windows 上編碼的輸出逐位元組相同。若要讓幾何處理也強制使用可移植的 Highway 路徑(例如用於黃金影像測試),請設定行程全域性後端:
Bun.Image.backend = "bun"; // macOS/Windows 上預設是 "system"