zenpixels

Pixel format types and transfer-function-aware conversion for Rust image codecs.
A JPEG decoder gives you RGB8 in sRGB. An AVIF decoder gives you RGBA16 in BT.2020 PQ. A resize library wants RGBF32 in linear light. Without shared types, every codec pair needs hand-rolled conversion — and gets transfer functions wrong, silently drops alpha, or writes "sRGB" in the ICC profile while the pixels are linear.
zenpixels makes pixel format descriptions first-class types that travel with the data. The conversion crate handles transfer functions, gamut matrices, depth scaling, and alpha compositing so codecs don't have to.
Two crates: zenpixels (types, buffers, metadata) and zenpixels-convert (all the math). Both are no_std + alloc, forbid(unsafe_code), no system dependencies.
# Types only — for codec crates
= "0.2"
# Types + conversion — for processing pipelines
= "0.2"
Quick start
Format conversion
use ;
// Pick the cheapest target format the encoder supports
let target = best_match
.ok_or?;
// Pre-compute the plan, then convert row by row — no per-row allocation
let mut converter = new?;
for y in 0..height
Color profile conversion
Built-in: named-profile pairs (sRGB ↔ Display P3 ↔ BT.2020 ↔ Adobe RGB) use hardcoded matrices with fused SIMD kernels — LUT-decode + SIMD matrix + SIMD polynomial encode for u16, fused matlut for u8. No CMS backend needed.
use ;
use ;
let p3 = RGB8_SRGB.with_primaries;
let srgb = RGB8_SRGB;
let mut conv = new_explicit?;
conv.convert_row;
Custom ICC profiles (vendor-specific, LUT-based, perceptual intent) need a
CMS backend. Pass one via PluggableCms — the plan delegates to the plugin
when the profiles differ, or uses the built-in matlut fast path when both
sides are well-known.
use ;
let cms: &dyn PluggableCms = &MoxCms; // or any backend
let mut conv = new_explicit_with_cms?;
For HDR and wide-gamut pipelines that defer tone/gamut mapping, use
with_clip_out_of_gamut(false) — f32 sRGB transfers then preserve
negative and supernormal values through the conversion.
Type system
The core split: what the bytes are vs. what the bytes mean.
PixelFormat is a flat enum of byte layouts — channel count, depth, memory order. No color semantics.
Rgb8, Rgba8, Rgb16, Rgba16, RgbF16, RgbaF16, RgbF32, RgbaF32,
Gray8, Gray16, GrayF16, GrayAF16, GrayF32, GrayA8, GrayA16, GrayAF32,
Bgra8, Rgbx8, Bgrx8, Cmyk8, OklabF32, OklabaF32
F16 variants are descriptor-only today — typed Pixel impls land when Rust
stable ships native f16.
PixelDescriptor wraps a PixelFormat with everything needed to interpret the color data:
Every buffer carries one. Every codec declares which ones it produces and consumes. Predefined constants for the common cases:
RGB8_SRGB // u8 RGB, sRGB transfer, BT.709 primaries
RGBAF32_LINEAR // f32 RGBA, linear light
BGRA8_SRGB // u8 BGRA (Windows/DirectX order)
OKLABF32 // f32 Oklab L,a,b
CICP and ICC
Cicp carries ITU-T H.273 code points (used by AVIF, HEIF, JPEG XL, AV1). Named constants for SRGB, DISPLAY_P3, BT2100_PQ, BT2100_HLG. Human-readable name lookups via color_primaries_name() etc.
ColorContext bundles ICC profile bytes and/or CICP codes. Travels with pixel data via Arc — cheap to clone, cheap to share across pipeline stages.
ColorOrigin is the immutable provenance record: how the source file described its color, not what the pixels currently are. Used at encode time to decide whether to re-embed the original profile.
Orientation
Orientation is the canonical EXIF orientation enum for the zen ecosystem. #[repr(u8)] with EXIF values 1-8, so o as u8 gives the tag value directly.
All 8 elements of the D4 dihedral group, with full composition algebra:
use Orientation;
let combined = Rotate90.then;
assert_eq!;
let undone = Rotate90.compose;
assert_eq!;
let = Rotate90.output_dimensions;
assert_eq!;
Pixel buffers
PixelBuffer, PixelSlice, and PixelSliceMut carry their PixelDescriptor and optional ColorContext. Generic over P: Pixel for compile-time type safety, with zero-cost .erase() / .try_typed::<Q>() for dynamic dispatch.
// Typed buffer — format enforced at compile time
let buf = from_pixels?;
// Type-erased for codec dispatch
let erased = buf.erase;
// Recover the type
let typed = erased..unwrap;
Data access
Row-level: row(y) returns pixel bytes without padding. row_with_stride(y) includes padding.
Bulk: as_strided_bytes() returns the full backing &[u8] including stride padding — zero-copy passthrough to GPU uploads, codec writers, or anything that takes a buffer + stride. as_contiguous_bytes() returns Some only when rows are tightly packed. contiguous_bytes() returns Cow — borrows when tight, copies to strip padding otherwise.
Views: sub_rows(y, count) and crop_view(x, y, w, h) are zero-copy. crop_copy() allocates.
Allocation
try_new() for tight stride, try_new_simd_aligned() for SIMD-aligned rows, from_vec() to wrap an existing allocation. All constructors validate dimensions, stride, and alignment. into_vec() recovers the allocation for pool reuse.
Interop
With imgref feature: From<ImgRef<P>>, From<ImgVec<P>>, as_imgref(), try_as_imgref::<P>() and mutable counterparts. With rgb feature: Pixel impls for Rgb<u8>, Rgba<u8>, Gray<u8>, BGRA<u8>, and their u16/f32 variants.
Conversion
zenpixels-convert re-exports everything from zenpixels, so downstream code can depend on it alone.
Row conversion
RowConverter pre-computes a conversion plan from a source/target descriptor pair. Three tiers:
- Direct kernels for common pairs (byte swizzle, depth shift, transfer function LUTs)
- Composed plans for less common pairs (e.g.,
RGB8_SRGBtoRGBA16_LINEAR) - Hub path through linear sRGB f32 as universal fallback
Format negotiation
The cost model separates effort (CPU work) from loss (information destroyed). ConvertIntent controls weighting:
| Intent | Effort | Loss | Use case |
|---|---|---|---|
Fastest |
4x | 1x | Encoding — get there fast |
LinearLight |
1x | 4x | Resize, blur — need linear math |
Blend |
1x | 4x | Compositing — premultiplied alpha |
Perceptual |
1x | 3x | Color grading, sharpening |
Provenance tracking lets the cost model know that f32 data decoded from a u8 JPEG has zero loss converting back to u8.
Three entry points: best_match() (simple), best_match_with() (with consumer costs), negotiate() (full control with provenance).
No silent lossy conversions
Every operation that destroys information requires an explicit policy via ConvertOptions:
- Alpha removal:
DiscardIfOpaque,CompositeOnto { r, g, b },DiscardUnchecked, orForbid - Depth reduction:
Round,Truncate, orForbid - RGB to gray: requires explicit luma coefficients (
Bt709,Bt601,Bt2020, orDisplayP3), orNoneto forbid. Y' (encoded luma) semantic — round-trips bit-exactly forR==G==B.
Convenience constructors: ConvertOptions::forbid_lossy() (safe default) and ConvertOptions::permissive() (sensible lossy defaults), with with_alpha_policy(), with_depth_policy(), etc. for customization.
Atomic output assembly
finalize_for_output couples converted pixels with matching encoder metadata in one step. Prevents the bug where pixel values don't match the embedded ICC/CICP profile.
Gamut, HDR, Oklab
Gamut matrices — 3x3 row-major f32 between BT.709, Display P3, BT.2020. No CMS needed for named-profile conversions.
HDR — Reinhard and exposure tone mapping, ContentLightLevel and MasteringDisplay metadata.
Oklab — primaries-aware rgb_to_lms_matrix() / lms_to_rgb_matrix(), scalar rgb_to_oklab() / oklab_to_rgb(), public LMS/XYZ/Oklab matrices. Non-sRGB sources get correct LMS matrices without an intermediate sRGB step.
CMS — PluggableCms trait (dyn-compatible, accepts ColorProfileSource directly — CICP, named profiles, or raw ICC bytes) plugs an external backend into RowConverter. RowTransformMut is the &mut self row-level transform returned by plugins. The cms-moxcms feature provides a concrete backend using moxcms, supporting u8/u16/f32 transforms with automatic profile identification. The older ColorManagement / RowTransform traits (ICC-bytes-only, &self) are retained for backward compatibility.
ICC identification — zenpixels::icc::identify_common(icc_bytes) recognizes 183 well-known RGB + 21 grayscale profiles via normalized FNV-1a hash lookup (~100ns). Returns primaries, transfer function, and IdentificationUse (whether matrix+TRC substitution is safe vs CMS-only). Covers sRGB, Display P3, BT.2020, Adobe RGB variants across ICC v2–v5.
Pipeline planner
CodecFormats declares each codec's decode outputs and encode inputs, ICC/CICP support, effective bits, and overshoot behavior. The pipeline feature enables the format registry, operation requirements, and path solver for multi-step conversion planning.
Planar support
With the planar feature: PlaneLayout, PlaneDescriptor, PlaneSemantic, Subsampling (4:2:0/4:2:2/4:4:4/4:1:1), YuvMatrix, and MultiPlaneImage container. Handles YCbCr, Oklab planes, gain maps, and separate alpha planes.
Features
zenpixels
| Feature | Default | What it enables |
|---|---|---|
std |
yes | Standard library (currently a no-op; everything is no_std + alloc) |
icc |
yes | icc module — hash-based ICC profile identification (~100ns) |
rgb |
Pixel impls for rgb crate types, typed from_pixels() constructors |
|
imgref |
From<ImgRef> / From<ImgVec> conversions (implies rgb) |
|
planar |
Multi-plane image types (YCbCr, Oklab, gain maps) | |
serde |
Serialize/Deserialize derives on all core types |
zenpixels-convert
| Feature | Default | What it enables |
|---|---|---|
std |
yes | Standard library |
rgb |
Pixel impls for rgb crate types, typed convenience methods (to_rgb8(), to_rgba8(), etc.) |
|
imgref |
ImgRef/ImgVec conversions (implies rgb) |
|
planar |
Multi-plane image types | |
pipeline |
Pipeline planner: format registry, operation requirements, path solver | |
cms-moxcms |
ICC profile transforms via moxcms (implies std) |
|
serde |
Forwards to zenpixels/serde |
Build time
zenpixels itself compiles in ~0.28s (release, 7950X). The cold cargo build --release -p zenpixels wall is ~1.9s, but 1.6s of that is the serial prerequisite chain proc-macro2 → syn → bytemuck_derive → bytemuck — costs most real Rust projects already pay for something else. Edit-rebuild cycles only pay the 0.3s.
MSRV
zenpixels requires Rust 1.85+. zenpixels-convert requires Rust 1.89+ (for the safe SIMD intrinsics it uses). 2024 edition.
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
Apache-2.0 OR MIT