Pixels
Streaming image processing for Rust: lazy, tiled, pure Rust
crates.io | Docs | Embedding guide
An Open Tech Foundation project
An image is a lazy graph of operations. Pixels are pulled through it in tiles only when the output asks for them, so memory stays bounded by the tiles in flight, not by the size of the image.
use ;
let webp = open?
.resize_with
.modulate
.output
.bytes?;
Nothing runs until bytes(). Rows then stream through resize, modulate and
the WebP encoder in one pass, and a JPEG much larger than its output decodes
at reduced scale.
[!NOTE] Pre-1.0. The API can change between minor versions; see the changelog.
Why Pixels
| Need | How Pixels meets it |
|---|---|
| 🧵 Streaming, not eager | Rust crates such as image decode the whole image, then apply one op at a time. Pixels evaluates on demand, tile by tile. |
| 🦀 Pure Rust | No libvips, OpenCV or system libraries; builds the same on every platform. |
| 🔒 Safe on hostile input | unsafe is forbidden in every crate; malformed files are errors, never panics; size limits are enforced before allocating. |
| 🧪 Checked against references | Every codec is cross-checked against libpng, libjpeg, libgif, libtiff, libwebp and libaom. |
| 🚦 Concurrency with no setup | Every call shares one process-wide worker pool; no per-request threads to manage. |
Compared with other libraries
| Pixels | libvips / sharp | ImageMagick | Pillow | image (Rust) |
|
|---|---|---|---|---|---|
| Written in | Rust | C | C | Python + C | Rust |
| Native dependencies | None | libjpeg-turbo, libwebp, libspng… | Many delegate libraries | libjpeg, zlib, libwebp… | None |
| Evaluation | Lazy, demand-driven tiles | Lazy, demand-driven regions | Eager | Eager | Eager |
| Peak memory | Tiles in flight | Regions in flight | Whole image | Whole image | Whole image |
| Memory-safe | Yes, no unsafe |
No | No | No (C core) | Yes |
| Formats | 7 | Dozens | 200+ | 30+ | About 15 |
| Maturity | New (0.x) | Decades | Decades | Decades | Mature |
Pixels follows libvips' design. What it adds is that design in pure, safe Rust, with no native libraries to build, ship or patch.
Speed today
Time for one job (best of 10), quality 80, same input files, i7-8700K (12 threads):
| Workload | Pixels | sharp 0.35 (libvips 8.18) | Pillow 11 | image 0.25 |
|---|---|---|---|---|
| 12 MP JPEG → 400 px WebP | 56 ms | 23 ms | 24 ms | — (no lossy WebP) |
| 12 MP JPEG → 400 px JPEG | 40 ms | 16 ms | 16 ms | 201 ms |
| 1280×800 PNG → 320 px JPEG | 20 ms | 15 ms | 27 ms | 24 ms |
The C libraries are still faster on JPEG and WebP, where they use libjpeg-turbo's and libwebp's hand-written SIMD. For 40 concurrent jobs against Bun, Deno and Node, see ES-Runtime's benchmarks.
Used by
ES-Runtime (repo), the Open Tech Foundation's server JavaScript runtime, builds its image module on Pixels.
| Aspect | Detail |
|---|---|
| What | runtime:images: decode, transform and encode JPEG, PNG, WebP, AVIF, GIF and TIFF, with a Bun.Image-style chain |
| Why Pixels | Its codecs are Pixels' own, written in Rust, where Bun and sharp call C libraries |
| Safe for uploads | Images over 268 megapixels are refused at the header, before any memory is allocated |
| Learn more | Images guide · API · Benchmarks |
Install
Requires Rust 1.85+. Every codec is on by default; for a smaller build, use
default-features = false and enable only the formats you need.
| Feature | Format |
|---|---|
png, gif, tiff, webp, avif, raw |
Each codec on its own |
jpeg |
Baseline JPEG |
jpeg-progressive |
Progressive JPEG decode, the build's only third-party codec (jpeg-decoder) |
Formats
| Format | Decode | Encode |
|---|---|---|
| PNG | All bit depths, palettes, transparency, interlaced | 8/16-bit, every filter |
| JPEG | Baseline, scaled (1/2, 1/4, 1/8); progressive via feature | Baseline, chroma subsampling |
| WebP | Lossy, lossless, alpha; first frame of animations | Lossy (default) or lossless |
| AVIF | Still images, 8/10/12-bit | Lossy, 8-bit |
| GIF | Every frame, interlace, disposal; first frame composited | Single frame, quantized palette |
| TIFF | Baseline 6.0, strips and tiles, LZW/Deflate/PackBits | Strips or tiles, Deflate |
| Raw | Caller-described pixels | Packed pixels |
The format is identified from the file's first bytes, never from its name.
Operations
| Operation | Method |
|---|---|
| Resize (seven filters, fit modes) | resize, resize_with, thumbnail |
| Geometry | crop, rotate (multiples of 90°), flip, flop, orient |
| Colour | modulate, to_srgb, to_pixel_format, flatten, extract_channel |
| Filters | blur, sharpen, convolve |
| Compositing | composite, composite_with |
| Output | output(format, options) then bytes() or write(sink) |
Chaining never fails: an error is carried to bytes() or write() and
returned there.
Concurrency
Call output(...).bytes() from any number of threads; every call shares one
pool of workers, one per core. Don't create a scheduler per request. The
embedding guide
covers limits, errors and fixed-size pools.
Crates
| Crate | Role |
|---|---|
otf-pixels |
Start here. The chainable Image API over everything below |
otf-pixels-core |
Op graph, tiles, scheduler, and the codec and op traits |
otf-pixels-ops |
Resize, rotate, colour, filters, compositing |
otf-pixels-compress |
DEFLATE, zlib, LZW and checksums |
otf-pixels-codec-* |
One crate per format: png, jpeg, webp, avif, gif, tiff, raw |
Design
| Pillar | In one line |
|---|---|
| Lazy op graph | Chaining builds an immutable graph; nothing runs until an output pulls. |
| Demand-driven tiles | The output asks for regions, and only the tiles they need are computed, in parallel. |
| Streaming I/O | Sources are readers and sinks are writers; codecs that can't stream buffer internally. |
| Hybrid typing | One dynamic Image at the API; typed kernels inside, chosen once per tile. |
| Own the codecs | Every format written from scratch except progressive JPEG decode. |
GPU compute is planned for v2 (ADR-0007).
Documentation
| Doc | Purpose |
|---|---|
| EMBEDDING.md | Putting Pixels behind a runtime or server: threading, limits, errors |
| ARCHITECTURE.md | Layers, graph, scheduler, backends |
| SPEC.md | API contracts, formats, guarantees, safety limits |
| ROADMAP.md | v1 and v2 scope |
| docs/adr/ | One record per architecture decision |
| CHANGELOG.md | What changed, per release |
Develop
Tasks are defined in tasks.toml and run with tsr.