{"slug": "bun-image", "title": "Bun.Image", "summary": "Bun.Image is a chainable image processing pipeline built into the Bun runtime that can decode, resize, rotate, and re-encode JPEG, PNG, WebP, HEIC, and AVIF formats without any npm dependencies or native addon build steps. The API is modeled after Sharp, allowing users to construct an image from a file path, bytes, or Blob, chain transformation operations like resize and rotate, select an output format, and then await a terminal method to execute the work off the JavaScript thread. All processing runs lazily—nothing executes until a terminal method like `.bytes()`, `.buffer()`, `.blob()`, or `.toBase64()` is awaited.", "body_md": "Documentation Index\nFetch the complete documentation index at: https://bun.com/docs/llms.txt\nUse this file to discover all available pages before exploring further.\nBun.Image\nis 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.\nawait Bun.file(\"photo.jpg\").image().resize(400, 400, { fit: \"inside\" }).webp({ quality: 80 }).write(\"thumb.webp\");\nThe API is shaped after Sharp: construct from an input, chain transforms, pick an output format, then await\na terminal method. Nothing runs until the terminal is awaited, and the work executes off the JavaScript thread.\nThe constructor accepts a path, bytes, or a Blob\n— including Bun.file()\nand Bun.s3()\n. Blob#image()\nis shorthand for new Bun.Image(blob)\n:\nnew Bun.Image(\"./photo.jpg\"); // file path\nnew Bun.Image(buffer); // Buffer / ArrayBuffer / TypedArray\nnew Bun.Image(Bun.file(\"photo.jpg\")); // BunFile (read lazily, off-thread)\nBun.file(\"photo.jpg\").image(); // same as above\nBun.s3(\"bucket/photo.jpg\").image(); // S3File\nThe format is sniffed from the bytes — extensions and Content-Type\nare ignored.\nPath 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\n(e.g. via fetch\n/Bun.file\nwith your own validation) and pass the bytes.\nWhen passing a TypedArray\n/ArrayBuffer\n, don’t mutate it while a terminal is pending — decode runs off-thread and borrows the bytes. SharedArrayBuffer\nand resizable buffers are refused; use buf.slice()\nto pass a fixed view.\nA second options\nargument guards against decompression bombs and controls EXIF handling:\nnew Bun.Image(input, {\n// Reject if width*height > this. Checked after reading the header,\n// before allocating the pixel buffer. Default matches Sharp (~268 MP).\nmaxPixels: 4096 * 4096,\n// Apply JPEG EXIF Orientation before any other op. Default: true.\nautoOrient: true,\n});\nRead width\n, height\n, and format\nwithout decoding pixel data:\nconst { width, height, format } = await new Bun.Image(input).metadata();\n// => { width: 1920, height: 1080, format: \"jpeg\" }\nResize\nimg.resize(800); // width 800, keep aspect ratio\nimg.resize(800, 600); // exactly 800×600 (stretch)\nimg.resize(800, 600, { fit: \"inside\" }); // fit within 800×600\nimg.resize(800, 600, { withoutEnlargement: true }); // never upscale\nimg.resize(800, 600, { filter: \"mitchell\" });\nfilter\nselects the resampling kernel. The default \"lanczos3\"\nis the right choice for photographs.\nWhen 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.\nRotate · flip\nimg.rotate(90); // 90° clockwise (multiples of 90 only)\nimg.flip(); // mirror vertically (about the x-axis)\nimg.flop(); // mirror horizontally (about the y-axis)\nModulate\nimg.modulate({\nbrightness: 1.2, // 1 = unchanged\nsaturation: 0, // 0 = greyscale, 1 = unchanged, >1 = boost\n});\nCalling a format method sets the encode target; without one, the source format is reused.\nimg.jpeg({ quality: 85 }); // 1–100, default 80\nimg.png({ compressionLevel: 6 }); // zlib level 0–9\nimg.png({ palette: true, colors: 64, dither: true }); // indexed PNG\nimg.webp({ quality: 80 });\nimg.webp({ lossless: true });\nimg.heic({ quality: 80 }); // macOS / Windows only\nimg.avif({ quality: 60 }); // macOS / Windows only\npalette: true\nquantizes to a ≤256-color palette and emits an indexed (color-type 3) PNG, optionally with Floyd–Steinberg dither\n. This is typically 3–5× smaller than truecolor for screenshots and UI assets.\nTerminals\nA pipeline does no work until one of these is awaited:\nawait img.bytes(); // Uint8Array\nawait img.buffer(); // Buffer\nawait img.blob(); // Blob with .type set to the output MIME\nawait img.toBase64(); // string\nawait img.dataurl(); // \"data:image/png;base64,…\"\nawait img.write(\"out.webp\"); // number (bytes written)\nawait img.write(Bun.s3(\"bucket/out.webp\"));\n.write()\naccepts the same destinations as Bun.write\n— a path string, Bun.file()\n, Bun.s3()\n, or an fd. If you didn’t chain a format method and the destination is a path string, the extension picks one (.jpg\n/.png\n/.webp\n/.heic\n/.avif\n).\nPlaceholders\nFor a low-quality placeholder to inline in HTML before the real image loads, .placeholder()\nreturns a ThumbHash-rendered ≤32px blur as a data:\nURL — ~400–700 bytes, no client-side decoder needed:\nconst lqip = await Bun.file(\"hero.jpg\").image().placeholder();\n// <img src={lqip} … /> — then swap to the real URL on load.\nFor coarse-to-fine rendering of the image itself, encode a progressive JPEG:\nimg.jpeg({ progressive: true });\nAfter the first terminal resolves, img.width\nand img.height\nreflect the output dimensions (they’re -1\nbefore).\nBun.serve\nintegration\nA Bun.Image\npipeline is a valid Response\nbody and sets Content-Type\nautomatically. To keep the encode off the JS thread in a server handler, await a terminal first:\nBun.serve({\nroutes: {\n\"/avatar/:id\": async req => {\n// Validate before touching the filesystem (see the Input note above).\nif (!/^[a-z0-9]+$/.test(req.params.id)) return new Response(null, { status: 400 });\nconst out = await Bun.file(`avatars/${req.params.id}.png`).image().resize(128, 128).webp().blob();\nreturn new Response(out);\n},\n},\n});\nPassing the pipeline directly (new Response(img)\n) also works, but currently runs the encode synchronously during body init.\nClipboard\nconst img = Bun.Image.fromClipboard();\nif (img) {\nconst png = await img.resize(800, 800, { fit: \"inside\" }).png().bytes();\n}\nfromClipboard()\nreads 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\nif there’s no image, and always null\non Linux — call wl-paste\n/xclip\nyourself and pass the bytes to the constructor.\nFor a passive “image in clipboard, press ⌘V” hint, poll clipboardChangeCount()\n(a single integer read) and call hasClipboardImage()\nonly when it moves; macOS has no clipboard-change notification, so this is the documented pattern.\n¹ Windows requires the HEIF Image Extensions / AV1 Video Extension from the Microsoft Store.\n² AVIF encode needs an OS AV1 encoder — Apple Silicon M3+ only. Intel Mac and M1/M2 reject with ERR_IMAGE_FORMAT_UNSUPPORTED\n; AVIF decode works everywhere ImageIO does (macOS 13+).\nWhen a system-backend format isn’t available on the current machine, the terminal rejects with error.code === \"ERR_IMAGE_FORMAT_UNSUPPORTED\"\n— branch on that to fall back to a portable format:\nconst out = await img\n.avif({ quality: 50 })\n.bytes()\n.catch(e => {\nif (e.code === \"ERR_IMAGE_FORMAT_UNSUPPORTED\") return img.webp({ quality: 80 }).bytes();\nthrow e;\n});\nFormats 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:\nBun.Image.backend = \"bun\"; // default is \"system\" on macOS/Windows", "url": "https://wpnews.pro/news/bun-image", "canonical_source": "https://bun.com/docs/runtime/image", "published_at": "2026-05-23 22:57:51+00:00", "updated_at": "2026-05-24 00:03:43.499983+00:00", "lang": "en", "topics": ["developer-tools", "open-source"], "entities": ["Bun", "Sharp", "libjpeg-turbo", "spng", "libwebp"], "alternates": {"html": "https://wpnews.pro/news/bun-image", "markdown": "https://wpnews.pro/news/bun-image.md", "text": "https://wpnews.pro/news/bun-image.txt", "jsonld": "https://wpnews.pro/news/bun-image.jsonld"}}