zenbitmaps

Fast, safe bitmap format codecs. no_std + alloc, forbid(unsafe_code), panic-free.
| Format | Feature | Decode | Encode | Detection |
|---|---|---|---|---|
| PNM (PBM/PGM/PPM/PAM/PFM) | (default) | 1327 GiB/s | 10.5 GiB/s | P1-P7/Pf/PF magic |
| Farbfeld | (default) | 3.04 GiB/s | 1.50 GiB/s | farbfeld magic |
| TGA | tga |
4.85 GiB/s | 5.88 GiB/s | Header heuristic + v2 footer |
| BMP | bmp |
4.66 GiB/s | 1.23 GiB/s | BM magic |
| QOI | qoi |
1.32 GiB/s | 870 MiB/s | qoif magic |
| Radiance HDR | hdr |
1.05 GiB/s | 361 MiB/s | #?RADIANCE / #?RGBE |
1 megapixel RGB8, single-threaded. PPM decode is zero-copy. See benchmarks.
Getting started
[]
= "0.1" # PNM + farbfeld
= { = "0.1", = ["bmp"] } # + BMP
= { = "0.1", = ["qoi"] } # + QOI (via rapid-qoi)
= { = "0.1", = ["tga"] } # + TGA
= { = "0.1", = ["hdr"] } # + Radiance HDR
= { = "0.1", = ["all"] } # everything
Quick example
use *;
use Unstoppable;
// Encode pixels to PPM
let pixels = vec!; // 2 RGB pixels
let encoded = encode_ppm?;
// Decode (auto-detects format from magic bytes)
let decoded = decode?;
assert!; // zero-copy for PPM with maxval=255
assert_eq!;
# Ok::
Format detection
detect_format() identifies the format from magic bytes without decoding:
use *;
match detect_format
decode() uses this internally and dispatches to the right codec.
Supported formats
PNM family (always available):
- P1 (PBM ASCII), P4 (PBM binary) — 1-bit black/white
- P2 (PGM ASCII), P5 (PGM binary) — grayscale, 8-bit and 16-bit
- P3 (PPM ASCII), P6 (PPM binary) — RGB, 8-bit and 16-bit
- P7 (PAM) — arbitrary channels (grayscale, RGB, RGBA), 8-bit and 16-bit
- PFM — floating-point grayscale and RGB (32-bit per channel)
- Decode: all 9 variants. Encode: P5/P6/P7/PFM (binary)
- Magic:
P1-P7/Pf/PF
Farbfeld (always available):
- RGBA 16-bit per channel, big-endian
- Magic:
farbfeld
BMP (bmp feature):
- All standard bit depths: 1, 2, 4, 8, 16, 24, 32
- Compression: uncompressed, RLE4, RLE8, BITFIELDS
- Palette expansion, bottom-up/top-down, grayscale detection
BmpPermissivenesslevels: Strict, Standard (default), Permissive- Native byte order decoding via
decode_bmp_native()(skips BGR→RGB swizzle) - Magic:
BM
QOI (qoi feature, via rapid-qoi):
- RGB8 and RGBA8, lossless
- Row-level streaming decode via
decode_range - Streaming encode via
push_rows/finish - Magic:
qoif
TGA (tga feature):
- Uncompressed and RLE-compressed (types 1-3, 9-11)
- True color (15/16/24/32-bit), grayscale, color-mapped
- All image origins (top/bottom, left/right)
- Fast path: memcpy + SIMD batch BGR→RGB swizzle for 24/32-bit
- Detection: header heuristic (TGA has no magic bytes)
Radiance HDR (hdr feature):
- RGBE format with new-style per-channel RLE
- Decodes to
RgbF32(linear float) - RGBE↔f32 via IEEE 754 bit manipulation (no libm, no unsafe)
- Encodes from
RgbF32orRgb8 - Magic:
#?RADIANCE/#?RGBE
Zero-copy decoding
PNM files with maxval=255 (the common case) decode to a borrowed slice into your input buffer. No allocation, no copy. Formats requiring transformation (BMP row flip, farbfeld endian swap, 16-bit, non-255 maxval, PFM) allocate.
With the rgb feature, as_pixels() gives you a zero-copy typed view:
let decoded = decode?;
let pixels: & = decoded.as_pixels?; // zero-copy reinterpret
With the imgref feature, as_imgref() gives you a zero-copy 2D view:
let decoded = decode?;
let img: ImgRef = decoded.as_imgref?; // zero-copy 2D view
to_imgvec() is also available when you need an owned copy.
BGRA pipeline
BMP files store pixels in BGR/BGRA order. Use decode_bmp_native() to skip the BGR→RGB swizzle and work directly in native byte order:
let decoded = decode_bmp_native?;
// decoded.layout is Bgr8, Bgra8, or Gray8
// All encoders accept BGR/BGRA input — swizzle happens automatically
let pam = encode_pam?;
PGM, PPM, PAM, farbfeld, and BMP encoders all accept Bgr8, Bgra8, and Bgrx8 input.
Typed pixel API (rgb feature)
With the rgb feature, you get type-safe pixel encode/decode using the rgb crate's types:
use *;
use Unstoppable;
let pixels = vec!;
let encoded = encode_ppm_pixels?;
let = ?;
# Ok::
Available types: RGB8, RGBA8, BGR8, BGRA8 (type aliases for rgb crate types).
ImgRef/ImgVec API (imgref feature)
With the imgref feature (implies rgb), you can work with 2D image buffers that handle stride/padding:
let img = new;
let encoded = encode_ppm_img?;
let decoded_img = ?;
decode_into() decodes directly into a pre-allocated ImgRefMut buffer, handling arbitrary stride.
Cooperative cancellation
Every function takes a stop parameter implementing enough::Stop. Pass Unstoppable when you don't need cancellation. For server use, pass a token that checks a shutdown flag — decode/encode will bail out promptly via BitmapError::Cancelled.
Resource limits
use *;
use Unstoppable;
let limits = Limits ;
# let data = encode_ppm.unwrap;
let decoded = decode_with_limits?;
# Ok::
Features
| Feature | What it adds |
|---|---|
| (default) | PNM (P1-P7/PFM) + farbfeld decode/encode |
bmp |
BMP decode/encode (all bit depths, RLE, bitfields, palettes) |
qoi |
QOI decode/encode via rapid-qoi (streaming, lossless) |
tga |
TGA decode/encode (truecolor, grayscale, color-mapped, RLE) |
hdr |
Radiance HDR decode/encode (RGBE, RLE, f32 output) |
simd |
SIMD-accelerated BGR↔RGB swizzle via garb |
rgb |
Typed pixel API (RGB8, RGBA8, as_pixels(), encode_*_pixels()) |
imgref |
2D buffer API (ImgVec/ImgRef, as_imgref(), decode_into()) — implies rgb |
zencodec |
zencodec trait integration: streaming decode/encode, probe, CICP (implies rgb + imgref) |
std |
Enable std support (not required — no_std + alloc by default) |
all |
All format + pixel API features |
API
All public functions are flat, one-shot calls at crate root.
Decode (auto-detect):
detect_format(data)— identify format from magic bytesdecode(data, stop)— auto-detect and decodedecode_with_limits(data, limits, stop)
Decode (format-specific):
decode_farbfeld/decode_farbfeld_with_limitsdecode_bmp/decode_bmp_with_limits— RGB output (bmp)decode_bmp_native/decode_bmp_native_with_limits— BGR output (bmp)decode_bmp_permissive/..._with_limits(bmp)decode_qoi/decode_qoi_with_limits(qoi)decode_tga/decode_tga_with_limits(tga)decode_hdr/decode_hdr_with_limits(hdr)probe_bmp(data)— BMP metadata without decode (bmp)
Encode (raw bytes):
encode_ppm,encode_pgm,encode_pam,encode_pfm— PNM familyencode_farbfeld— farbfeldencode_bmp,encode_bmp_rgba— BMP (bmp)encode_qoi— QOI (qoi)encode_tga— TGA (tga)encode_hdr— Radiance HDR (hdr)
Typed pixel (rgb): decode_pixels, encode_ppm_pixels, encode_pam_pixels, etc.
ImgRef/ImgVec (imgref): decode_img, decode_into, encode_ppm_img, etc.
Types:
DecodeOutput<'a>— decoded image (.pixels(),.width,.height,.layout,.is_borrowed(),.as_pixels(),.as_imgref(),.to_imgvec())ImageFormat— format enum (Pnm, Bmp, Farbfeld, Qoi, Tga, Hdr)PixelLayout— pixel format (Gray8, Gray16, Rgb8, Rgba8, Rgba16, Bgr8, Bgra8, Bgrx8, GrayF32, RgbF32)BmpPermissiveness— decode strictness (Strict, Standard, Permissive) (bmp)Limits— resource limits (max width/height/pixels/memory)BitmapError— error type,#[non_exhaustive]
Performance
1000x1000 RGB8, single-threaded, default target (no -C target-cpu=native):
| PPM | TGA | BMP | Farbfeld | QOI | HDR | |
|---|---|---|---|---|---|---|
| Decode | 1327 GiB/s | 4.85 GiB/s | 4.66 GiB/s | 3.04 GiB/s | 1.32 GiB/s | 1.05 GiB/s |
| Encode | 10.5 GiB/s | 5.88 GiB/s | 1.23 GiB/s | 1.50 GiB/s | 870 MiB/s | 361 MiB/s |
PPM decode is zero-copy (returns a borrowed slice — no allocation). TGA decode uses memcpy + batch BGR swizzle for 24/32-bit uncompressed images. The simd feature accelerates BGR↔RGB swizzle via garb for TGA and QOI encode paths.
Run: cargo bench --bench codecs --all-features
Credits
- PNM: draws from zune-ppm by Caleb Etemesi (MIT/Apache-2.0/Zlib)
- BMP: forked from zune-bmp 0.5.2 by Caleb Etemesi (MIT/Apache-2.0/Zlib)
- Farbfeld: forked from zune-farbfeld 0.5.2 by Caleb Etemesi (MIT/Apache-2.0/Zlib)
- QOI: uses rapid-qoi by Zakarum (MIT/Apache-2.0)
- TGA, HDR: from-scratch implementations, no external dependencies
AI-Generated Code Notice
Developed with Claude (Anthropic). Not all code manually reviewed. Review critical paths before production use.
Image tech I maintain
| State of the art codecs* | zenjpeg · zenpng · zenwebp · zengif · zenavif (rav1d-safe · zenrav1e · zenavif-parse · zenavif-serialize) · zenjxl (jxl-encoder · zenjxl-decoder) · zentiff · zenbitmaps · heic · zenraw · zenpdf · ultrahdr · mozjpeg-rs · webpx |
| Compression | zenflate · zenzop |
| Processing | zenresize · zenfilters · zenquant · zenblend |
| Metrics | zensim · fast-ssim2 · butteraugli · resamplescope-rs · codec-eval · codec-corpus |
| Pixel types & color | zenpixels · zenpixels-convert · linear-srgb · garb |
| Pipeline | zenpipe · zencodec · zencodecs · zenlayout · zennode |
| ImageResizer | ImageResizer (C#) — 24M+ NuGet downloads across all packages |
| Imageflow | Image optimization engine (Rust) — .NET · node · go — 9M+ NuGet downloads across all packages |
| Imageflow Server | The fast, safe image server (Rust+C#) — 552K+ NuGet downloads, deployed by Fortune 500s and major brands |
* as of 2026
General Rust awesomeness
archmage · magetypes · enough · whereat · zenbench · cargo-copter
And other projects · GitHub @imazen · GitHub @lilith · lib.rs/~lilith · NuGet (over 30 million downloads / 87 packages)
License
MIT OR Apache-2.0