zenpixels-convert 0.2.14

Transfer-function-aware pixel conversion, gamut mapping, and codec format negotiation for zenpixels
Documentation

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

Transfer-function-aware pixel conversion, gamut mapping, and codec format negotiation for Rust image pipelines.

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-convert is the math half of the zenpixels ecosystem. zenpixels carries the format/color types (PixelBuffer, PixelDescriptor, PixelFormat, TransferFunction, ColorPrimaries, ColorContext); this crate handles transfer functions, gamut matrices, depth scaling, alpha compositing, format negotiation, and HDR tone mapping so codecs don't have to. It re-exports everything from zenpixels at its crate root, so downstream pipelines can depend on zenpixels-convert alone.

no_std + alloc, #![forbid(unsafe_code)], no system dependencies. SIMD where available, runtime-dispatched.

Install

[dependencies]
zenpixels-convert = "0.2.14"

Common feature combinations:

# Plus the rgb crate's typed buffers and convenience methods
zenpixels-convert = { version = "0.2.14", features = ["rgb"] }

# Plus a pluggable CMS backend (moxcms) for arbitrary ICC profiles
zenpixels-convert = { version = "0.2.14", features = ["cms-moxcms"] }

# Size-sensitive / wasm builds (drops the bundled CICP->ICC database)
zenpixels-convert = { version = "0.2.14", default-features = false, features = ["std"] }

Quick start

Negotiate a format, then convert row by row

best_match picks the cheapest target format an encoder supports for a given source descriptor; RowConverter pre-computes the conversion plan once and converts rows with no per-row allocation.

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 (no CMS needed for named profiles)

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

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, and uses the built-in matlut fast path when both sides are well-known. The cms-moxcms feature provides a concrete backend (MoxCms) built on moxcms.

use zenpixels_convert::{RowConverter, ConvertOptions, MoxCms, cms::PluggableCms};

let cms: &dyn PluggableCms = &MoxCms;
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, build ConvertOptions with gamut clipping disabled so f32 sRGB transfers preserve negative and supernormal values through the conversion.

No silent lossy conversions

Every operation that destroys information requires an explicit policy via ConvertOptions (from zenpixels, re-exported here):

  • Alpha removal: DiscardIfOpaque, CompositeOnto { r, g, b }, DiscardUnchecked, or Forbid.
  • Depth reduction: Round, Truncate, or Forbid.
  • RGB → gray: requires explicit luma coefficients (Bt709, Bt601, Bt2020, or DisplayP3), or None to forbid. Uses Y' (encoded luma) semantics — round-trips bit-exactly when R == G == B.

Convenience constructors: ConvertOptions::forbid_lossy() (safe default) and ConvertOptions::permissive() (sensible lossy defaults), plus with_alpha_policy() / with_depth_policy() for customization.

Format negotiation

The cost model separates effort (CPU work) from loss (information destroyed). ConvertIntent controls the weighting:

Intent Effort Loss Use case
Fastest Encoding — get there fast
LinearLight Resize, blur — need linear math
Blend Compositing — premultiplied alpha
Perceptual 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).

Row conversion tiers

RowConverter pre-computes a conversion plan from a source/target descriptor pair, choosing one of 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_SRGBRGBA16_LINEAR).
  3. Hub path through linear sRGB f32 as the universal fallback.

Gamut, HDR, Oklab

  • Gamut matrices — 3×3 row-major f32 between BT.709, Display P3, BT.2020 via conversion_matrix / GamutMatrix and apply_matrix_* row kernels. No CMS needed for named-profile conversions.
  • HDR — Reinhard (reinhard_tonemap / reinhard_inverse) and exposure_tonemap tone mapping; ContentLightLevel and MasteringDisplay metadata; quantize_to is the anchor-aware linear→PQ16 quantizer (reads DiffuseWhite from the source's ColorContext, BT.2408 default of 203).
  • Oklab — primaries-aware rgb_to_lms_matrix() / lms_to_rgb_matrix(), scalar rgb_to_oklab() / oklab_to_rgb(). Non-sRGB sources get correct LMS matrices without an intermediate sRGB step.

CICP / ICC

  • ICC identificationzenpixels::icc::identify_common(icc_bytes) recognizes hundreds of well-known RGB + grayscale profiles via a normalized hash lookup (~100ns), returning primaries, transfer function, and whether matrix+TRC substitution is safe vs CMS-only. extract_cicp reads embedded cICP tags directly.
  • CICP → ICC synthesisicc_profiles::synthesize_icc_for_cicp(Cicp) resolves an embeddable ICC profile for any assigned H.273 combination from the bundled compressed database (icc-db feature, on by default; ~36 KB asset, lazily decoded, no CMS required, PQ/HLG HDR included). synthesize_gray_icc_for_cicp is the GRAY-class sibling for single-channel output. The sRGB default answers NotNeeded. With icc-db off, these answer NeedsCms outside the sRGB default and the bundled consts.

Orientation

The buffer-baking half of the zen orientation story lives here (the Orientation enum and its composition algebra are in zenpixels):

Atomic output assembly

finalize_for_output couples converted pixels with matching encoder metadata in one step, preventing the bug where pixel values don't match the embedded ICC/CICP profile. Returns an EncodeReady bundle.

Pipeline planner (pipeline feature)

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 a path solver (optimal_path, generate_path_matrix) for multi-step conversion planning.

Load-bearing analysis

PixelSliceLoadBearingExt / PixelBufferLoadBearingExt run SIMD predicates over a buffer to report which channels actually carry information (e.g. is the alpha fully opaque, is the image grayscale), returning a LoadBearingReport used to drive lossless format-reduction decisions.

Key types and functions

Item What it does
RowConverter Pre-computed per-row conversion plan (new, new_explicit, new_explicit_with_cms, convert_row, convert_rows)
best_match / best_match_with / negotiate Format negotiation entry points
ConvertIntent Effort-vs-loss weighting for negotiation
Provenance / ConversionCost / FormatOption Cost-model inputs and results
ConvertOptions (re-export) Explicit lossy-operation policy
ConvertPlan / convert_row Lower-level plan + stateless row convert
adapt::adapt_for_encode / adapt_for_encode_explicit / try_adapt_in_place One-call negotiate-and-convert helpers
PluggableCms / RowTransformMut / MoxCms Pluggable CMS backend interface + moxcms impl
GamutMatrix / conversion_matrix / apply_matrix_* Gamut matrix construction and application
quantize_to / reinhard_tonemap / exposure_tonemap HDR quantization and tone mapping
finalize_for_output / EncodeReady / OutputMetadata Atomic pixels-plus-metadata output assembly
TransferFunctionExt / ColorPrimariesExt / PixelBufferConvertExt Conversion methods bolted onto interchange types
LoadBearingReport / PixelSliceLoadBearingExt Channel-information analysis
ConvertError Conversion error type

Features

Feature Default What it enables
std yes Standard library
icc-db yes Bundled CICP→ICC profile database (~36 KB asset) behind synthesize_icc_for_cicp / synthesize_gray_icc_for_cicp; disable for size-sensitive builds (they answer NeedsCms instead)
fast-transpose SIMD transpose kernels (x86-64 AVX2 + aarch64 NEON) for apply_orientation*; portable scalar otherwise
avx512 16-wide AVX-512F f16 conversion kernels (runtime-dispatched)
rgb Pixel impls for the rgb crate's types + typed convenience methods (to_rgb8(), to_rgba8(), …)
imgref ImgRef / ImgVec conversions (implies rgb)
planar Multi-plane image types (YCbCr, Oklab planes, gain maps)
pipeline Pipeline planner: format registry, operation requirements, path solver
cms-moxcms ICC profile transforms via moxcms (implies std)
serde Forwards to zenpixels/serde

no_std

zenpixels-convert is no_std + alloc by default-construction (std is on by default but everything works without it). For no_std, set default-features = false and re-enable only what you need (note that cms-moxcms requires std):

zenpixels-convert = { version = "0.2.14", default-features = false }

MSRV

Rust 1.89+ (for the safe SIMD intrinsics it uses), 2024 edition. The companion zenpixels crate requires 1.85+.

See also

  • zenpixels — the pixel format/color type system this crate operates on.
  • linear-srgb — the linear↔sRGB transfer math used internally.

License

Apache-2.0 OR MIT.