parabun:image
import image from "parabun:image";A from-scratch image module with the codecs and operations Sharp covers, baked into the runtime. libjpeg-turbo, libpng, libwebp, and libsharpyuv are vendored statically — no npm install sharp, no Node-ABI-versioned binary distribution.
Images are Image objects: { width, height, channels, data: Uint8Array | Uint8ClampedArray }. Most operations return a new Image; the source is unchanged.
Codec I/O
Section titled “Codec I/O”decode(bytes)
Section titled “decode(bytes)”Auto-detects format from magic bytes. JPEG, PNG, and WebP supported. Returns an Image with the file’s native channel layout (RGB, RGBA, grayscale, grayscale-alpha).
const bytes = await Bun.file("photo.jpg").bytes();const img = image.decode(bytes);// { width: 1920, height: 1280, channels: 3, data: Uint8Array }encode(img, opts)
Section titled “encode(img, opts)”const webp = image.encode(img, { format: "webp", quality: 85 });const png = image.encode(img, { format: "png" });const jpg = image.encode(img, { format: "jpeg", quality: 92, progressive: true });await Bun.write("photo.webp", webp);| Option | Description |
|---|---|
format | "jpeg" | "png" | "webp". |
quality | 0–100 for JPEG / WebP. PNG ignores. |
progressive | JPEG only. |
lossless | WebP only. Disables quality. |
Geometric transforms
Section titled “Geometric transforms”resize(img, opts)
Section titled “resize(img, opts)”const small = image.resize(img, { width: 800, height: 600, kernel: "lanczos" });const fit = image.resize(img, { width: 800, fit: "contain" });| Option | Description |
|---|---|
width, height | At least one required. The other is computed to preserve aspect unless fit: "fill". |
kernel | "bilinear" | "lanczos". Lanczos is sharper; bilinear is faster. |
fit | "contain" (default), "cover", "fill", "inside", "outside" — same semantics as Sharp. |
background | When fit: "contain" letterboxes, this is the fill color. Default [0, 0, 0, 0]. |
rotate(img, degrees, opts?)
Section titled “rotate(img, degrees, opts?)”90 / 180 / 270 are exact transposes; arbitrary angles use bilinear interpolation. opts.background for the corner fill on non-quadrant rotations.
flip(img, axis)
Section titled “flip(img, axis)”axis is "horizontal", "vertical", or "both".
crop(img, { x, y, width, height })
Section titled “crop(img, { x, y, width, height })”Bounds are clamped to the image; out-of-bounds reads return the edge pixel.
Filters
Section titled “Filters”blur(img, { sigma })
Section titled “blur(img, { sigma })”Separable Gaussian. Two 1-D passes. Edge mode: clamp.
boxBlur(img, { radius })
Section titled “boxBlur(img, { radius })”Faster, lower-quality alternative — good enough for cheap previews and the prefilter in sharpen.
sharpen(img, opts?)
Section titled “sharpen(img, opts?)”Unsharp mask:
const sharp = image.sharpen(img, { amount: 1.5, sigma: 1.0, threshold: 0 });| Option | Default | Description |
|---|---|---|
amount | 1.0 | Strength of the high-pass add. |
sigma | 1.0 | Gaussian blur radius for the prefilter. |
threshold | 0 | Suppress edges below this magnitude (avoids amplifying noise). |
edgeDetect(img, opts?)
Section titled “edgeDetect(img, opts?)”Sobel. Returns a single-channel image of gradient magnitude. opts.normalize rescales the output to [0, 255].
adjust(img, { brightness?, contrast?, saturation? })
Section titled “adjust(img, { brightness?, contrast?, saturation? })”Each value is a multiplier — 1.0 is no change, 1.5 is 50% boost. Saturation operates in HSL; brightness / contrast are linear in the RGB space.
hueShift(img, degrees)
Section titled “hueShift(img, degrees)”YIQ rotation matrix. Preserves luma + saturation, rotates the chrominance angle. Pure hue shift; nothing else moves.
toGrayscale(img)
Section titled “toGrayscale(img)”ITU-R BT.601 luma weights (0.299 R + 0.587 G + 0.114 B). Returns a single-channel image.
invert(img) / threshold(img, value)
Section titled “invert(img) / threshold(img, value)”Component-wise. threshold returns binary 0/255 per pixel based on luma.
histogram(img)
Section titled “histogram(img)”Returns { r: Uint32Array(256), g: Uint32Array(256), b: Uint32Array(256), a?: Uint32Array(256) } — per-channel value distribution. Useful for auto-levels / tone curves.
Compositing
Section titled “Compositing”composite(dst, src, opts?)
Section titled “composite(dst, src, opts?)”Porter-Duff source-over. Both images can have alpha.
const stamped = image.composite(canvas, watermark, { x: 20, y: 20, opacity: 0.6 });| Option | Description |
|---|---|
x, y | Top-left of src in dst coordinates. Default 0. |
opacity | Multiplier on src’s alpha. Default 1.0. |
mode | "over" (default). Other Porter-Duff ops are pending. |
Pipeline — chained operations
Section titled “Pipeline — chained operations”For longer chains, image.pipeline(img) returns a builder that defers work until the terminal call. This is where Sharp’s lazy decode→transform→encode buffer sharing happens — operations short-circuit allocations of intermediate images.
const out = await image.pipeline(img) .resize({ width: 1024 }) .blur({ sigma: 1.5 }) .sharpen({ amount: 1.2 }) .encode({ format: "webp", quality: 85 });
await Bun.write("processed.webp", out);The terminal calls are .encode(opts), .toBuffer() (raw bytes), or .toImage() (back to a plain Image).
Performance
Section titled “Performance”CPU release build, 4096² RGBA on a 16-core x86:
| Operation | parabun:image | Sharp | Speedup |
|---|---|---|---|
| Gaussian blur, σ=4 | 38 ms | 137 ms | 3.6× |
| Lanczos 4096²→2048² | 67 ms | 163 ms | 2.4× |
| Bilinear 4096²→1024² | 9 ms | 121 ms | 13.4× |
| End-to-end JPEG decode → blur → WebP encode | similar | similar | within ~10% |
The kernel speedups are on isolated operations. End-to-end Sharp wins because libvips’s lazy buffer chaining short-circuits intermediate decode + transform + encode allocations; image.pipeline closes most of that gap.
Limits
Section titled “Limits”- AVIF decode/encode lands when
libavif+aom(ordav1d) are vendored. Tracked. - TIFF, GIF (animated), HEIF: not yet.
- ICC color management: not yet — output is sRGB-assumed.
- The pipeline builder fuses adjacent compatible kernels (e.g. resize + sharpen) but not all of them. This expands over time.