Cached at:
05/24/26, 12:35 AM
# Image - Bun
Source: [https://bun.com/docs/runtime/image](https://bun.com/docs/runtime/image)
> ## Documentation Index Fetch the complete documentation index at:[https://bun\.com/docs/llms\.txt](https://bun.com/docs/llms.txt) Use this file to discover all available pages before exploring further\.
`Bun\.Image`is a chainable image pipeline for decoding, resizing, rotating, and re\-encoding JPEG, PNG, WebP, HEIC, and AVIF — built on libjpeg\-turbo, spng, libwebp, and SIMD geometry kernels, with zero npm dependencies and no native addon build step\.
```
await Bun.file("photo.jpg").image().resize(400, 400, { fit: "inside" }).webp({ quality: 80 }).write("thumb.webp");
```
The API is shaped after[Sharp](https://sharp.pixelplumbing.com/): construct from an input, chain transforms, pick an output format, then`await`a terminal method\. Nothing runs until the terminal is awaited, and the work executes off the JavaScript thread\.
## Input
The constructor accepts a path, bytes, or a`Blob`— including`Bun\.file\(\)`and`Bun\.s3\(\)`\.`Blob\#image\(\)`is shorthand for`new Bun\.Image\(blob\)`:
```
new Bun.Image("./photo.jpg"); // file path
new Bun.Image(buffer); // Buffer / ArrayBuffer / TypedArray
new Bun.Image(Bun.file("photo.jpg")); // BunFile (read lazily, off-thread)
Bun.file("photo.jpg").image(); // same as above
Bun.s3("bucket/photo.jpg").image(); // S3File
```
The format is sniffed from the bytes — extensions and`Content\-Type`are ignored\.**Path strings are filesystem paths\.**Don’t pass user\-controlled strings directly to the constructor — that’s an arbitrary\-file\-read primitive\. Read untrusted input into a`Buffer`\(e\.g\. via`fetch`/`Bun\.file`with your own validation\) and pass the bytes\.When passing a`TypedArray`/`ArrayBuffer`, don’t mutate it while a terminal is pending — decode runs off\-thread and borrows the bytes\.`SharedArrayBuffer`and resizable buffers are refused; use`buf\.slice\(\)`to pass a fixed view\.A second`options`argument guards against decompression bombs and controls EXIF handling:
```
new Bun.Image(input, {
// Reject if width*height > this. Checked after reading the header,
// before allocating the pixel buffer. Default matches Sharp (~268 MP).
maxPixels: 4096 * 4096,
// Apply JPEG EXIF Orientation before any other op. Default: true.
autoOrient: true,
});
```
Read`width`,`height`, and`format`without decoding pixel data:
```
const { width, height, format } = await new Bun.Image(input).metadata();
// => { width: 1920, height: 1080, format: "jpeg" }
```
## Resize
```
img.resize(800); // width 800, keep aspect ratio
img.resize(800, 600); // exactly 800×600 (stretch)
img.resize(800, 600, { fit: "inside" }); // fit within 800×600
img.resize(800, 600, { withoutEnlargement: true }); // never upscale
img.resize(800, 600, { filter: "mitchell" });
```
`fit`Behavior`"fill"`\(default\)Stretch to exactly`width × height``"inside"`Preserve aspect ratio; result fits*within*the box
`filter`selects the resampling kernel\. The default`"lanczos3"`is the right choice for photographs\.
FilterUse when`"lanczos3"`*\(default\)*General\-purpose, sharpest for photos`"lanczos2"`Slightly softer, fewer ringing artifacts`"mitchell"`Smooth gradients; the classic bicubic compromise`"cubic"`Catmull\-Rom — sharper than Mitchell, can ring`"mks2013"`/`"mks2021"`”Magic Kernel Sharp”; used by Facebook/Instagram`"bilinear"`/`"linear"`Fast, soft`"box"`Area\-average; good for large integer downscales`"nearest"`Pixel art / hard edges
When the source is a JPEG and the target is at most half the source size, decode skips straight to the nearest M/8 IDCT scale, so generating a thumbnail from a 24 MP photo never materializes the full\-resolution buffer\.
## Rotate · flip
```
img.rotate(90); // 90° clockwise (multiples of 90 only)
img.flip(); // mirror vertically (about the x-axis)
img.flop(); // mirror horizontally (about the y-axis)
```
## Modulate
```
img.modulate({
brightness: 1.2, // 1 = unchanged
saturation: 0, // 0 = greyscale, 1 = unchanged, >1 = boost
});
```
## Output formats
Calling a format method sets the encode target; without one, the source format is reused\.
```
img.jpeg({ quality: 85 }); // 1–100, default 80
img.png({ compressionLevel: 6 }); // zlib level 0–9
img.png({ palette: true, colors: 64, dither: true }); // indexed PNG
img.webp({ quality: 80 });
img.webp({ lossless: true });
img.heic({ quality: 80 }); // macOS / Windows only
img.avif({ quality: 60 }); // macOS / Windows only
```
`palette: true`quantizes to a ≤256\-color palette and emits an indexed \(color\-type 3\) PNG, optionally with Floyd–Steinberg`dither`\. This is typically 3–5× smaller than truecolor for screenshots and UI assets\.
## Terminals
A pipeline does no work until one of these is awaited:
```
await img.bytes(); // Uint8Array
await img.buffer(); // Buffer
await img.blob(); // Blob with .type set to the output MIME
await img.toBase64(); // string
await img.dataurl(); // "data:image/png;base64,…"
await img.write("out.webp"); // number (bytes written)
await img.write(Bun.s3("bucket/out.webp"));
```
`\.write\(\)`accepts the same destinations as`Bun\.write`— a path string,`Bun\.file\(\)`,`Bun\.s3\(\)`, or an fd\. If you didn’t chain a format method and the destination is a path string, the extension picks one \(`\.jpg`/`\.png`/`\.webp`/`\.heic`/`\.avif`\)\.
## Placeholders
For a low\-quality placeholder to inline in HTML before the real image loads,`\.placeholder\(\)`returns a[ThumbHash](https://evanw.github.io/thumbhash/)\-rendered ≤32px blur as a`data:`URL — ~400–700 bytes, no client\-side decoder needed:
```
const lqip = await Bun.file("hero.jpg").image().placeholder();
// <img src={lqip} … /> — then swap to the real URL on load.
```
For coarse\-to\-fine rendering of the image*itself*, encode a progressive JPEG:
```
img.jpeg({ progressive: true });
```
After the first terminal resolves,`img\.width`and`img\.height`reflect the*output*dimensions \(they’re`\-1`before\)\.
## `Bun\.serve`integration
A`Bun\.Image`pipeline is a valid`Response`body and sets`Content\-Type`automatically\. To keep the encode off the JS thread in a server handler, await a terminal first:
```
Bun.serve({
routes: {
"/avatar/:id": async req => {
// Validate before touching the filesystem (see the Input note above).
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);
},
},
});
```
Passing the pipeline directly \(`new Response\(img\)`\) also works, but currently runs the encode synchronously during body init\.
## Clipboard
```
const img = Bun.Image.fromClipboard();
if (img) {
const png = await img.resize(800, 800, { fit: "inside" }).png().bytes();
}
```
`fromClipboard\(\)`reads PNG, TIFF, HEIC, JPEG, WebP, GIF, or BMP from the system pasteboard on macOS and Windows; the regular decode pipeline takes it from there\. Returns`null`if there’s no image, and always`null`on Linux — call`wl\-paste`/`xclip`yourself and pass the bytes to the constructor\.For a passive “image in clipboard, press ⌘V” hint, poll`clipboardChangeCount\(\)`\(a single integer read\) and call`hasClipboardImage\(\)`only when it moves; macOS has no clipboard\-change notification, so this is the documented pattern\.
## Platform backends
LinuxmacOSWindowsJPEG / PNG / WebPlibjpeg\-turbo · spng · libwebpsamesameBMP / GIF \(decode\)built\-inImageIOWICTIFF \(decode\)❌ImageIOWICResize / rotate / flipHighway SIMDAccelerate vImageHighway SIMDHEIC / AVIF❌`ERR\_IMAGE\_FORMAT\_UNSUPPORTED`ImageIO ²WIC ¹Clipboard❌ returns`null`NSPasteboardWin32
¹ Windows requires the**HEIF Image Extensions**/**AV1 Video Extension**from the Microsoft Store\. ² AVIF*encode*needs an OS AV1 encoder — Apple Silicon M3\+ only\. Intel Mac and M1/M2 reject with`ERR\_IMAGE\_FORMAT\_UNSUPPORTED`; AVIF*decode*works everywhere ImageIO does \(macOS 13\+\)\.When a system\-backend format isn’t available on the current machine, the terminal rejects with`error\.code === "ERR\_IMAGE\_FORMAT\_UNSUPPORTED"`— branch on that to fall back to a portable format:
```
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;
});
```
Formats handled by the system backend \(TIFF, HEIC, AVIF, clipboard\) inherit the**OS’s**patch level — keep macOS / Windows updated\. JPEG, PNG, and WebP go through the same statically\-linked codecs on every platform, so encoded output is byte\-identical across Linux, macOS, and Windows\. To force the portable Highway path for geometry too — e\.g\. for golden\-image tests — set the process\-global backend:
```
Bun.Image.backend = "bun"; // default is "system" on macOS/Windows
```