otf-pixels 0.2.0

A streaming, demand-driven image processing engine.
Documentation

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 otf_pixels::{EncodeOptions, Fit, Format, Image, Modulate, ResizeOptions};

let webp = Image::open("photo.jpg")?
    .resize_with(800, 600, ResizeOptions::default().with_fit(Fit::Cover))
    .modulate(Modulate::identity().with_saturation(0.0)?)
    .output(Format::WebP, EncodeOptions::with_quality(80)?)
    .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

cargo add otf-pixels

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

tsr ci        # fmt, clippy, tests, docs, feature combinations
tsr interop   # cross-check output against the reference libraries
tsr fuzz      # a short run of every fuzz target (nightly + cargo-fuzz)
tsr bench     # benchmarks, compared with libvips when it is installed

Tasks are defined in tasks.toml and run with tsr.

License

Apache-2.0 — see LICENSE and NOTICE.