Skip to content

imazen/zenpixels

Repository files navigation

zenpixels CI crates.io lib.rs docs.rs license

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"

Quick start

Format conversion

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);
}

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 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.

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:

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,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 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));

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 = 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();

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:

  1. Direct kernels for common pairs (byte swizzle, depth shift, transfer function LUTs)
  2. Composed plans for less common pairs (e.g., RGB8_SRGB to RGBA16_LINEAR)
  3. 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, or Forbid
  • Depth reduction: Round, Truncate, or Forbid
  • RGB to gray: requires explicit luma coefficients (Bt709, Bt601, Bt2020, or DisplayP3), or None to forbid. Y' (encoded luma) semantic — round-trips bit-exactly for R==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.

CMSPluggableCms 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 identificationzenpixels::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

About

No description, website, or topics provided.

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

Watchers

Forks

Packages

 
 
 

Contributors