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
zenpixels = "0.2"
# Types + conversion — for processing pipelines
zenpixels-convert = "0.2"use zenpixels_convert::{RowConverter, best_match, ConvertIntent};
// Pick the cheapest target format the encoder supports
let target = best_match(source_desc, &encoder_formats, ConvertIntent::Fastest)
.ok_or("no compatible format")?;
// Pre-compute the plan, then convert row by row — no per-row allocation
let mut converter = RowConverter::new(source_desc, target)?;
for y in 0..height {
converter.convert_row(src_row, dst_row, width);
}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 zenpixels::{PixelDescriptor, ColorPrimaries};
use zenpixels_convert::{RowConverter, ConvertOptions};
let p3 = PixelDescriptor::RGB8_SRGB.with_primaries(ColorPrimaries::DisplayP3);
let srgb = PixelDescriptor::RGB8_SRGB;
let mut conv = RowConverter::new_explicit(
p3, srgb, &ConvertOptions::permissive(),
)?;
conv.convert_row(p3_row, srgb_row, width);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 zenpixels_convert::{RowConverter, ConvertOptions, cms::PluggableCms};
let cms: &dyn PluggableCms = &MoxCms; // or any backend
let mut conv = RowConverter::new_explicit_with_cms(
source_desc, target_desc,
&ConvertOptions::permissive(),
Some(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.
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:
pub struct PixelDescriptor {
pub format: PixelFormat,
pub transfer: TransferFunction, // Linear, Srgb, Bt709, Pq, Gamma22, Hlg, Unknown
pub alpha: Option<AlphaMode>, // Straight, Premultiplied, Opaque, Undefined
pub primaries: ColorPrimaries, // Bt709, DisplayP3, Bt2020, AdobeRgb, Unknown
pub signal_range: SignalRange, // Full or Narrow
}Every buffer carries one. Every codec declares which ones it produces and consumes. Predefined constants for the common cases:
PixelDescriptor::RGB8_SRGB // u8 RGB, sRGB transfer, BT.709 primaries
PixelDescriptor::RGBAF32_LINEAR // f32 RGBA, linear light
PixelDescriptor::BGRA8_SRGB // u8 BGRA (Windows/DirectX order)
PixelDescriptor::OKLABF32 // f32 Oklab L,a,bCicp 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 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 zenpixels::Orientation;
let combined = Orientation::Rotate90.then(Orientation::FlipH);
assert_eq!(combined, Orientation::Transpose);
let undone = Orientation::Rotate90.compose(Orientation::Rotate90.inverse());
assert_eq!(undone, Orientation::Identity);
let (w, h) = Orientation::Rotate90.output_dimensions(1920, 1080);
assert_eq!((w, h), (1080, 1920));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 = PixelBuffer::<Rgba<u8>>::from_pixels(pixels, width, height)?;
// Type-erased for codec dispatch
let erased = buf.erase();
// Recover the type
let typed = erased.try_typed::<Rgba<u8>>().unwrap();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.
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.
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.
zenpixels-convert re-exports everything from zenpixels, so downstream code can depend on it alone.
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
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).
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.
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 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.
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.
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.
| 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 |
| 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 |
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.
zenpixels requires Rust 1.85+. zenpixels-convert requires Rust 1.89+ (for the safe SIMD intrinsics it uses). 2024 edition.
| 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
archmage · magetypes · enough · whereat · zenbench · cargo-copter
And other projects · GitHub @imazen · GitHub @lilith · lib.rs/~lilith · NuGet (over 30 million downloads / 87 packages)
Apache-2.0 OR MIT