zenpixels-convert

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
[]
= "0.2.14"
Common feature combinations:
# Plus the rgb crate's typed buffers and convenience methods
= { = "0.2.14", = ["rgb"] }
# Plus a pluggable CMS backend (moxcms) for arbitrary ICC profiles
= { = "0.2.14", = ["cms-moxcms"] }
# Size-sensitive / wasm builds (drops the bundled CICP->ICC database)
= { = "0.2.14", = false, = ["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 ;
// 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 (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 ;
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, 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 ;
let cms: &dyn PluggableCms = &MoxCms;
let mut conv = new_explicit_with_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, orForbid. - Depth reduction:
Round,Truncate, orForbid. - RGB → gray: requires explicit luma coefficients (
Bt709,Bt601,Bt2020, orDisplayP3), orNoneto forbid. Uses Y' (encoded luma) semantics — round-trips bit-exactly whenR == 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 |
4× | 1× | Encoding — get there fast |
LinearLight |
1× | 4× | Resize, blur — need linear math |
Blend |
1× | 4× | Compositing — premultiplied alpha |
Perceptual |
1× | 3× | 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:
- Direct kernels for common pairs (byte swizzle, depth shift, transfer-function LUTs).
- Composed plans for less common pairs (e.g.
RGB8_SRGB→RGBA16_LINEAR). - 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/GamutMatrixandapply_matrix_*row kernels. No CMS needed for named-profile conversions. - HDR — Reinhard (
reinhard_tonemap/reinhard_inverse) andexposure_tonemaptone mapping;ContentLightLevelandMasteringDisplaymetadata;quantize_tois the anchor-aware linear→PQ16 quantizer (readsDiffuseWhitefrom the source'sColorContext, BT.2408 default of 203). - Oklab — primaries-aware
rgb_to_lms_matrix()/lms_to_rgb_matrix(), scalarrgb_to_oklab()/oklab_to_rgb(). Non-sRGB sources get correct LMS matrices without an intermediate sRGB step.
CICP / ICC
- ICC identification —
zenpixels::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_cicpreads embeddedcICPtags directly. - CICP → ICC synthesis —
icc_profiles::synthesize_icc_for_cicp(Cicp)resolves an embeddable ICC profile for any assigned H.273 combination from the bundled compressed database (icc-dbfeature, on by default; ~36 KB asset, lazily decoded, no CMS required, PQ/HLG HDR included).synthesize_gray_icc_for_cicpis the GRAY-class sibling for single-channel output. The sRGB default answersNotNeeded. Withicc-dboff, these answerNeedsCmsoutside 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):
orient::apply_orientation— fresh buffer; SIMD-tiled transpose for 4-byte pixels (behind thefast-transposefeature; portable scalar otherwise).orient::apply_orientation_into— caller-provided target, no allocation.orient::apply_orientation_in_place— reuses the buffer's own allocation.
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):
= { = "0.2.14", = 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.