zenpixels
Pixel format interchange types and transfer-function-aware conversion for Rust image codecs.
Two crates, one split: zenpixels defines the types, zenpixels-convert does the math. Codecs depend on the interchange crate for zero-cost format descriptions. Processing pipelines pull in the conversion crate when they need to change pixel formats, apply transfer functions, or negotiate the cheapest encode path.
Both crates are no_std + alloc, forbid(unsafe_code), and have no system dependencies.
Why
Image codecs produce pixels in different formats. A JPEG decoder gives you RGB8 in sRGB. An AVIF decoder might give you RGBA16 in BT.2020 PQ. A resize library wants RGBF32 in linear light. A PNG encoder takes RGBA8 or RGBA16.
Without a shared type system, every codec pair needs hand-rolled conversion code. Transfer functions get applied in the wrong order (or not at all). Alpha gets silently discarded. Metadata says sRGB but the pixels are linear. These bugs are subtle, hard to test, and widespread.
zenpixels fixes this by making pixel format descriptions first-class types that travel with the pixel data. The conversion crate handles all the math — transfer functions, gamut matrices, depth scaling, alpha compositing — so codecs don't have to.
The type system
The core design separates what the bytes are from what the bytes mean.
PixelFormat — byte layout
PixelFormat is a flat enum describing the physical pixel layout: channel count, channel depth, and byte order. Nothing about color interpretation.
Rgb8, Rgba8, Rgb16, Rgba16, RgbF32, RgbaF32,
Gray8, Gray16, GrayF32, GrayA8, GrayA16, GrayAF32,
Bgra8, Rgbx8, Bgrx8, OklabF32, OklabaF32
Use this for exhaustive match dispatch over known layouts. It answers questions like "how many bytes per pixel?" and "is there an alpha channel?" — but not "is this sRGB or linear?" or "are these BT.709 or BT.2020 primaries?"
PixelDescriptor — full meaning
PixelDescriptor wraps a PixelFormat with everything needed to interpret the color data correctly:
This is the unit of currency in the zen ecosystem. Every buffer carries one. Every codec declares which ones it produces and consumes. The conversion system uses pairs of them to build conversion plans.
40+ predefined constants follow a naming convention: {FORMAT}_{TRANSFER} for concrete descriptors, {FORMAT} for transfer-agnostic ones.
// Concrete — transfer function is known
RGB8_SRGB // u8 RGB, sRGB transfer, BT.709 primaries
RGBAF32_LINEAR // f32 RGBA, linear light, BT.709 primaries
BGRA8_SRGB // u8 BGRA, sRGB transfer (Windows/DirectX order)
// Transfer-agnostic — for negotiation when transfer doesn't matter
RGB8 // u8 RGB, transfer unknown
RGBA16 // u16 RGBA, transfer unknown
// Perceptual color
OKLABF32 // f32 Oklab L,a,b
OKLABAF32 // f32 Oklab L,a,b + alpha
Building blocks
Each axis of PixelDescriptor is its own enum:
ChannelType — storage per channel: U8, U16, F16, F32.
ChannelLayout — what the channels represent: Gray, GrayAlpha, Rgb, Rgba, Bgra, Oklab, OklabA.
TransferFunction — the electro-optical transfer function: Linear, Srgb, Bt709, Pq (HDR10), Hlg, Unknown.
ColorPrimaries — RGB chromaticities: Bt709 (sRGB), DisplayP3, Bt2020, Unknown. Discriminant values match CICP codes. Supports gamut containment queries (Bt2020.contains(DisplayP3) is true).
AlphaMode — how to interpret the alpha channel: Straight (unassociated), Premultiplied (associated), Opaque (all 0xFF), Undefined (padding, as in RGBX/BGRX). Wrapped in Option — None means no alpha channel exists at all.
SignalRange — Full (0–255 for u8) or Narrow (16–235 luma, 16–240 chroma). Matters for video formats.
Cicp
ITU-T H.273 code points, used by HEIF, AVIF, JPEG XL, and AV1 to signal color space in-band.
// Named constants for common profiles
SRGB // (1, 13, 6, true)
DISPLAY_P3 // (12, 13, 0, true)
BT2100_PQ // (9, 16, 9, true)
BT2100_HLG // (9, 18, 9, true)
Human-readable name lookups: color_primaries_name(), transfer_characteristics_name(), matrix_coefficients_name().
ColorContext
Bundles ICC profile bytes and/or CICP codes. Travels with pixel data via Option<Arc<ColorContext>> on buffers. Cheap to clone, cheap to share across pipeline stages.
let ctx = from_icc_and_cicp;
assert!;
let tf = ctx.transfer_function; // derived from CICP if available
ColorOrigin is the immutable provenance record — it tracks how the source file described its color (ICC, CICP, gAMA+cHRM, or assumed), not what the pixels currently are. Used at encode time to decide whether to re-embed the original profile.
Pixel buffers
PixelBuffer, PixelSlice, and PixelSliceMut are always available (no feature gate). They provide format-aware pixel storage that carries its own PixelDescriptor and optional ColorContext.
Typed vs. type-erased
Buffers are generic over P: Pixel. When P is a concrete type, format correctness is enforced at compile time. Call .erase() to get a type-erased buffer for dynamic dispatch, and .try_typed::<Q>() to recover the type.
use ;
use Rgba;
// Typed — format enforced at compile time
let buf = from_pixels;
// Type-erased for codec dispatch
let erased = buf.erase;
assert_eq!;
// Recover the type
let typed = erased..unwrap;
The Pixel trait
Maps concrete pixel types to their PixelDescriptor. Open trait — custom types can implement it.
Built-in impls for Rgbx and Bgrx (32-bit SIMD-friendly padded types) and GrayAlpha8/GrayAlpha16/GrayAlphaF32 are always available. With the rgb feature, you also get impls for Rgb<u8>, Rgba<u8>, Gray<u8>, BGRA<u8>, and their u16/f32 variants.
Buffer operations
PixelBuffer<P> (owned):
try_new(w, h, desc)— tight stridetry_new_simd_aligned(w, h, desc)— 32-byte aligned rows for SIMDfrom_vec(data, w, h, desc)— wrap existing allocationfrom_pixels(pixels, w, h)— from typedVec<P>(requiresrgbfeature)into_vec()— recover the allocation for pool reuse
PixelSlice<'a, P> (borrowed, immutable) and PixelSliceMut<'a, P> (borrowed, mutable):
row(y)/row_mut(y)— raw byte access to a single rowrow_pixels(y)— typed&[P]via bytemuck (whenP: Pixel)sub_rows(y, count)— zero-copy vertical slicecrop_view(x, y, w, h)— zero-copy rectangle view
With the imgref feature, From<ImgRef<P>> and From<ImgVec<P>> conversions are available for interop with the imgref ecosystem.
Conversion (zenpixels-convert)
All the pixel math lives here. Re-exports everything from zenpixels, so downstream code can depend on this crate alone.
Row conversion
RowConverter pre-computes a conversion plan from a source/target PixelDescriptor pair. No per-row allocation or dispatch.
use ;
// Pick the cheapest format the encoder supports
let target = best_match
.ok_or?;
// Convert row by row
let converter = new?;
for y in 0..height
Three tiers, from fastest to most general:
- Direct SIMD kernels for common pairs (byte swizzle, depth shift, transfer function LUTs).
- Composed multi-step plans for less common pairs (e.g.,
RGB8_SRGBtoRGBA16_LINEAR). - Hub path through linear sRGB f32 as a universal fallback.
The converter picks the fastest tier that covers the requested pair.
Format negotiation
The cost model separates effort (CPU work) from loss (information destroyed). ConvertIntent controls how they're weighted:
| Intent | Effort | Loss | Use case |
|---|---|---|---|
Fastest |
4x | 1x | Encoding — get there fast |
LinearLight |
1x | 4x | Resize, blur — need linear math |
Blend |
1x | 4x | Compositing — need 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. Without this, the model would penalize the round-trip as lossy.
No silent lossy conversions
Every operation that destroys information requires an explicit policy via ConvertOptions:
- Alpha removal:
DiscardIfOpaque(error if not opaque),CompositeOnto { r, g, b }(flatten onto background),DiscardUnchecked, orForbid. - Depth reduction:
Round,Truncate, orForbid. - RGB to gray: requires explicit luma coefficients (
Bt709orBt601), orNoneto forbid.
Atomic output assembly
finalize_for_output couples converted pixels with matching encoder metadata in one step. Prevents the most common color management bug: pixel values that don't match the embedded ICC/CICP profile.
Additional capabilities
- Gamut matrices — 3x3 row-major f32 conversion matrices between named primaries. No CMS needed for sRGB/Display P3/BT.2020 conversions.
- HDR — Reinhard and exposure tone mapping, content light level metadata.
- Oklab — primaries-aware
rgb_to_lms_matrix(). Non-sRGB sources get correct LMS matrices without an intermediate sRGB step. - CMS traits —
ColorManagementandRowTransformfor ICC-to-ICC transforms via external CMS backends. - Codec format registry —
CodecFormatsstruct where each codec declares its decode outputs and encode inputs, ICC/CICP support, effective bits, and whether values can overshoot[0.0, 1.0].
Planar support
With the planar feature, zenpixels handles multi-plane images: YCbCr 4:2:0/4:2:2/4:4:4, Oklab planes, gain maps, and separate alpha planes.
PlaneLayout—Interleaved { channels }orPlanar { planes, relationship }.PlaneDescriptor— per-plane semantic label, channel type, and subsampling factors.PlaneSemantic—Luma,ChromaCb,ChromaCr,OklabL,OklabA,OklabB,Alpha,GainMap,Red,Green,Blue,Depth.Subsampling—S444,S422,S420,S411withh_factor()/v_factor()accessors.YuvMatrix—Identity,Bt601,Bt709,Bt2020withrgb_to_y_coeffs().MultiPlaneImage— bundles aPlaneLayout, per-planePixelBuffers, and sharedColorContext.
Features
zenpixels
| Feature | What it enables |
|---|---|
std |
Standard library (default; currently a no-op, everything is no_std + alloc) |
rgb |
Pixel impls for rgb crate types, typed from_pixels() constructors |
imgref |
From<ImgRef> / From<ImgVec> conversions (implies rgb) |
buffer |
Convenience: enables both rgb and imgref |
planar |
Multi-plane image types (YCbCr, Oklab, gain maps) |
zenpixels-convert
| Feature | What it enables |
|---|---|
std |
Standard library (default) |
buffer |
PixelBufferConvertExt — convert_to(), to_rgb8(), to_rgba8(), etc. |
codec |
Format registry and negotiation |
Ecosystem
zenpixels is the pixel format layer for the zen codec family:
- zenjpeg — JPEG
- zenpng — PNG
- zenwebp — WebP
- zenjxl — JPEG XL
- zenavif — AVIF
- zengif — GIF
- zenbitmaps — BMP/TIFF
- zenresize — image resizing
- zencodecs — unified codec dispatch
All zen codecs use PixelDescriptor to describe their output and CodecFormats to declare their capabilities. The conversion crate handles all format bridging between them.
MSRV
Rust 1.93+, 2024 edition.
License
MIT OR Apache-2.0