Expand description
Transfer-function-aware pixel conversion for zenpixels.
This crate provides all the conversion logic that was split out of the
zenpixels interchange crate: row-level format conversion, gamut mapping,
codec format negotiation, and HDR tone mapping.
§Re-exports
All interchange types from zenpixels are re-exported at the crate root,
so downstream code can depend on zenpixels-convert alone.
§Core concepts
-
Format negotiation:
best_matchpicks the cheapest conversion target from a codec’s supported formats for a given source descriptor. -
Row conversion:
RowConverterpre-computes a conversion plan and converts rows with no per-row allocation, using SIMD where available. -
Codec helpers:
adapt::adapt_for_encodenegotiates format and converts pixel data in one call, returningCow::Borrowedwhen the input already matches a supported format. -
Extension traits:
TransferFunctionExt,ColorPrimariesExt, andPixelBufferConvertExtadd conversion methods to interchange types.
§Codec compliance guide
This section describes how to write a codec that integrates correctly with the zenpixels ecosystem. A “codec” here means any decoder or encoder crate that produces or consumes pixel data.
§Design principles
-
Codecs own I/O; zenpixels-convert owns pixel math. A codec reads and writes its container format. All pixel format conversion, transfer function application, gamut mapping, and alpha handling is done by
zenpixels-convert. Codecs should not re-implement conversion logic. -
PixelFormatis byte layout;PixelDescriptoris full meaning.PixelFormatdescribes the physical byte arrangement (channel count, order, depth).PixelDescriptoradds color interpretation: transfer function, primaries, alpha mode, signal range. Codecs registerPixelDescriptorvalues because negotiation needs the full picture. -
No silent lossy conversions. Every operation that destroys information (alpha removal, depth reduction, gamut clipping) requires an explicit policy via
ConvertOptions. Codecs must not silently clamp, truncate, or discard data. -
Pixels and metadata travel together.
ColorContextrides onPixelBufferviaArcso ICC/CICP metadata follows pixel data through the pipeline.finalize_for_outputcouples converted pixels with matching encoder metadata atomically. -
Provenance enables lossless round-trips. The cost model tracks where data came from (
Provenance). A JPEG u8 decoded to f32 for resize reports zero loss when converting back to u8, because the origin precision was u8 all along.
§The pixel lifecycle
Every image processing pipeline follows this flow:
┌──────────┐ ┌───────────┐ ┌───────────┐ ┌──────────┐
│ Decode │───>│ Negotiate │───>│ Convert │───>│ Encode │
│ │ │ │ │ │ │ │
│ Produces: │ │ Picks: │ │ Uses: │ │ Consumes:│
│ PixelBuf │ │ best fmt │ │ RowConv. │ │ EncReady │
│ ColorCtx │ │ from list │ │ per-row │ │ metadata │
│ ColorOrig │ │ │ │ │ │ │
└──────────┘ └───────────┘ └───────────┘ └──────────┘§Step 1: Decode
The decoder produces pixel data in one of its natively supported formats,
wraps it in a PixelBuffer, and extracts color metadata from the file.
// Decode raw pixels
let pixels: Vec<u8> = my_codec_decode(&file_bytes)?;
let desc = PixelDescriptor::RGB8_SRGB;
let buffer = PixelBuffer::from_vec(pixels, width, height, desc)?;
// Extract color metadata for CMS integration
let color_ctx = match (icc_chunk, cicp_chunk) {
(Some(icc), Some(cicp)) =>
Some(Arc::new(ColorContext::from_icc_and_cicp(icc, cicp))),
(Some(icc), None) =>
Some(Arc::new(ColorContext::from_icc(icc))),
(None, Some(cicp)) =>
Some(Arc::new(ColorContext::from_cicp(cicp))),
(None, None) => None,
};
// Track provenance for re-encoding decisions
let origin = ColorOrigin::from_icc_and_cicp(icc, cicp);
// or: ColorOrigin::from_icc(icc)
// or: ColorOrigin::from_cicp(cicp)
// or: ColorOrigin::from_gama_chrm() // PNG gAMA+cHRM
// or: ColorOrigin::assumed() // no color metadata in fileRules for decoders:
-
Register only formats the decoder natively produces. Do not list formats that require internal conversion — let the caller convert via
RowConverter. If your JPEG decoder outputs u8 sRGB only, registerRGB8_SRGB, notRGBF32_LINEAR. -
Extract all available color metadata. Both ICC and CICP can coexist (AVIF/HEIF containers carry both). Record all of it on
ColorContext. -
Build a
ColorOriginthat records how the file described its color, not what the pixels are. This is immutable and used only at encode time for provenance decisions (e.g., “re-embed the original ICC profile”). -
Set
effective_bitscorrectly onFormatEntry. A 10-bit AVIF source decoded to u16 haseffective_bits = 10, not 16. A JPEG decoded to f32 with debiased dequantization haseffective_bits = 10. Getting this wrong makes the cost model over- or under-value precision. -
Set
can_overshoot = trueonly when output values exceed[0.0, 1.0]. This is rare — only JPEG f32 decode with preserved IDCT ringing.
§Step 2: Negotiate
Before encoding, the pipeline must pick a format the encoder accepts.
Negotiation uses the two-axis cost model (effort vs. loss) weighted by
ConvertIntent.
Three entry points, from simplest to most flexible:
-
best_match: Pass a source descriptor, a list of supported descriptors, and an intent. Good for simple encode paths. -
best_match_with: Likebest_match, but each candidate carries a consumer cost (FormatOption). Use this when the encoder has fast internal conversion paths (e.g., a JPEG encoder with a fused f32→u8+DCT kernel can advertiseRGBF32_LINEARwith low consumer cost). -
negotiate: Full control. ExplicitProvenance(so the cost model knows the data’s true origin) plus consumer costs. Use this in processing pipelines where data has been widened from a lower-precision source (e.g., JPEG u8 decoded to f32 for resize — provenance says “u8 origin”, so converting back to u8 reports zero loss).
// Simple: "what format should I encode to?"
let target = best_match(
buffer.descriptor(),
&encoder_supported,
ConvertIntent::Fastest,
).ok_or("no compatible format")?;
// With provenance: "this f32 data came from u8 JPEG"
let provenance = Provenance::with_origin_depth(ChannelType::U8);
let target = negotiate(
current_desc,
provenance,
options.iter().copied(),
ConvertIntent::Fastest,
);Rules for negotiation:
-
Use
ConvertIntent::Fastestwhen encoding. The encoder knows what it wants; get there with minimal work. -
Use
ConvertIntent::LinearLightfor resize, blur, anti-aliasing. These operations need linear light for gamma-correct results. -
Use
ConvertIntent::Blendfor compositing. This ensures premultiplied alpha for correct Porter-Duff math. -
Use
ConvertIntent::Perceptualfor sharpening, contrast, saturation. These are perceptual operations that work best in sRGB or Oklab space. -
Track provenance when data has been widened. If you decoded a JPEG (u8) into f32 for processing, tell the cost model via
Provenance::with_origin_depth(ChannelType::U8). Otherwise it will penalize the f32→u8 conversion as lossy when it’s actually a lossless round-trip. -
If an operation genuinely expands the data’s gamut (e.g., saturation boost in BT.2020 that pushes colors outside sRGB), call
Provenance::invalidate_primarieswith the current working primaries. Otherwise the cost model will incorrectly report gamut narrowing as lossless.
§Step 3: Convert
Once a target format is chosen, convert pixel data row-by-row.
let converter = RowConverter::new(source_desc, target_desc)?;
for y in 0..height {
converter.convert_row(src_row, dst_row, width);
}Or use the convenience function that combines negotiation and conversion:
let adapted = adapt_for_encode(
raw_bytes, descriptor, width, rows, stride,
&encoder_supported,
)?;
// adapted.data is Cow::Borrowed if no conversion neededRules for conversion:
-
Use
RowConverter, not hand-rolled conversion. It handles transfer functions, gamut matrices, alpha mode changes, depth scaling, Oklab, and byte swizzle correctly. It pre-computes the plan so there is zero per-row overhead. -
For policy-sensitive conversions (when you need to control what lossy operations are allowed), use
adapt_for_encode_explicitwithConvertOptions. This validates policies before doing work and returns specific errors likeConvertError::AlphaNotOpaqueorConvertError::DepthReductionForbidden. -
The conversion system handles three tiers internally: (a) Direct SIMD kernels for common pairs (byte swizzle, depth shift, transfer LUTs). (b) Composed multi-step plans for less common pairs. (c) Hub path through linear sRGB f32 as a universal fallback.
§Step 4: Encode
The encoder receives pixel data in a format it natively supports and must embed correct color metadata.
For the atomic path (recommended), use finalize_for_output:
let ready = finalize_for_output(
&buffer,
&color_origin,
OutputProfile::SameAsOrigin,
target_format,
&cms,
)?;
// Pixels and metadata are guaranteed to match
encoder.write_pixels(ready.pixels())?;
encoder.write_icc(ready.metadata().icc.as_deref())?;
encoder.write_cicp(ready.metadata().cicp)?;Rules for encoders:
-
Register only formats the encoder natively accepts. If your JPEG encoder takes u8 sRGB, register
RGB8_SRGB. Don’t also listRGBA8_SRGBunless you actually handle RGBA natively (not just by stripping alpha internally). Let the conversion system handle format changes. -
If you have fast internal conversion paths, advertise them via
FormatOption::with_cost. Example: a JPEG encoder with a fused f32→DCT path can acceptRGBF32_LINEARatConversionCost::new(5, 0), so negotiation will route f32 data directly to the encoder instead of doing a redundant f32→u8 conversion first. -
Use
finalize_for_outputto bundle pixels and metadata atomically. This prevents the most common color management bug: pixel values that don’t match the embedded ICC/CICP. -
Always embed color metadata when the format supports it. Check
CodecFormats::icc_encodeandCodecFormats::cicpfor your codec’s capabilities. Omitting color metadata causes browsers and OS viewers to assume sRGB, which corrupts Display P3 and HDR content. -
The
OutputProfileenum controls what gets embedded:OutputProfile::SameAsOrigin: Re-embed the original ICC/CICP from the source file. Used for transcoding without color changes.OutputProfile::Named: Use a well-known CICP profile (sRGB, P3, BT.2020). Uses hardcoded gamut matrices, no CMS needed.OutputProfile::Icc: Use specific ICC profile bytes. Requires aColorManagementimplementation.
§Format registry
Every codec must declare its capabilities in a CodecFormats struct,
typically as a pub static. This serves as the single source of truth
for what the codec can produce and consume. See the pipeline::registry
module (requires the pipeline feature) for the full format table and
examples for each codec.
pub static MY_CODEC: CodecFormats = CodecFormats {
name: "mycodec",
decode_outputs: &[
FormatEntry::standard(PixelDescriptor::RGB8_SRGB),
FormatEntry::standard(PixelDescriptor::RGBA8_SRGB),
],
encode_inputs: &[
FormatEntry::standard(PixelDescriptor::RGB8_SRGB),
],
icc_decode: true, // extracts ICC profiles
icc_encode: true, // embeds ICC profiles
cicp: false, // no CICP support
};§Cost model
Format negotiation uses a two-axis cost model separating effort (CPU work) from loss (information destroyed). These are independent: a fast conversion can be very lossy (f32 HDR → u8 clamp), and a slow conversion can be lossless (u8 sRGB → f32 linear).
ConvertIntent controls how the axes are weighted:
| Intent | Effort weight | Loss weight | Use case |
|---|---|---|---|
Fastest | 4x | 1x | Encoding |
LinearLight | 1x | 4x | Resize, blur |
Blend | 1x | 4x | Compositing |
Perceptual | 1x | 3x | Color grading |
Cost components are additive: total = transfer_cost + depth_cost + layout_cost + alpha_cost + primaries_cost + consumer_cost + suitability_loss. The lowest-scoring candidate wins.
§CMS integration
Named profile conversions (sRGB ↔ Display P3 ↔ BT.2020) use hardcoded
3×3 gamut matrices and need no CMS backend. ICC-to-ICC transforms
require a ColorManagement implementation, which is a compile-time
feature (e.g., cms-moxcms, cms-lcms2).
Codecs that handle ICC profiles must:
- Extract ICC bytes on decode and store them on
ColorContext. - Record provenance on
ColorOrigin. - On encode, let
finalize_for_outputhandle the ICC transform (if the target profile differs from the source) or pass-through (ifSameAsOrigin).
§Error handling
ConvertError provides specific variants so codecs can handle each
failure mode:
ConvertError::NoMatch— no supported format works for this source. The codec’s format list may be too restrictive.ConvertError::NoPath— no conversion kernel exists between formats. Unusual; most pairs are covered.ConvertError::AlphaNotOpaque—DiscardIfOpaquepolicy was set but the data has semi-transparent pixels.ConvertError::DepthReductionForbidden—Forbidpolicy prevents narrowing (e.g., f32→u8).ConvertError::AllocationFailed— buffer allocation failed (OOM).ConvertError::CmsError— CMS transform failed (invalid ICC profile, unsupported color space, etc.).
Codecs should match on specific variants and return actionable errors
to callers. Do not flatten ConvertError into a generic string.
§Checklist
-
Declare
CodecFormatswith correcteffective_bitsandcan_overshoot -
Decode: extract ICC + CICP →
ColorContext -
Decode: record provenance →
ColorOrigin -
Encode: negotiate via
best_matchoradapt::adapt_for_encode -
Encode: convert via
RowConverter(not hand-rolled) -
Encode: embed metadata via
finalize_for_output - Encode: embed ICC/CICP when the format supports it
-
Handle
ConvertErrorvariants specifically - Test round-trip: native format → encode → decode = lossless
-
Test negotiation:
best_match(my_format, my_supported, Fastest)picks identity
Re-exports§
pub use adapt::adapt_for_encode_explicit;pub use converter::RowConverter;pub use error::ConvertError;pub use ext::ColorPrimariesExt;pub use ext::PixelBufferConvertExt;pub use ext::TransferFunctionExt;pub use gamut::GamutMatrix;pub use gamut::apply_matrix_f32;pub use gamut::apply_matrix_row_f32;pub use gamut::apply_matrix_row_rgba_f32;pub use gamut::conversion_matrix;pub use hdr::exposure_tonemap;pub use hdr::HdrMetadata;pub use hdr::reinhard_inverse;pub use hdr::reinhard_tonemap;pub use cms::ColorManagement;pub use cms::RowTransform;pub use output::EncodeReady;pub use output::OutputMetadata;pub use output::OutputProfile;pub use output::finalize_for_output;
Modules§
- adapt
- Codec adapter functions — the fastest path to a compliant encoder.
- buffer
- Opaque pixel buffer abstraction.
- cicp
- CICP (Coding-Independent Code Points) color description.
- cms
- Color Management System (CMS) traits.
- color
- Color profile types for CMS integration.
- converter
- Pre-computed row converter.
- descriptor
- Pixel format descriptor types.
- error
- Error types for pixel format conversion.
- ext
- Extension traits that add conversion methods to zenpixels interchange types.
- gamut
- Color gamut conversion matrices for BT.709, BT.2020, and Display P3.
- hdr
- HDR processing utilities.
- oklab
- Oklab color space conversion constants and scalar reference functions.
- orientation
- EXIF orientation as an element of the D4 dihedral group.
- output
- Atomic output preparation for encoders.
- pixel_
types - Custom pixel types for gray+alpha formats.
- policy
- Conversion policy types for explicit control over lossy operations.
Macros§
- at
- Start tracing an error with crate metadata for repository links.
Structs§
- At
- An error with location tracking - wraps any error type.
- Bgrx
- 32-bit BGR pixel with padding byte (BGRx).
- Cicp
- CICP color description (ITU-T H.273).
- Color
Context - Color space metadata for pixel data.
- Color
Origin - Immutable record of how the source file described its color.
- Content
Light Level - HDR content light level metadata (CEA-861.3 / CTA-861-H).
- Conversion
Cost - Two-axis conversion cost: computational effort vs. information loss.
- Convert
Options - Explicit options for pixel format conversion. All lossy operations require a policy choice — no silent defaults.
- Convert
Plan - Pre-computed conversion plan.
- Format
Option - A supported format with optional consumer-provided cost override.
- Gray
Alpha8 - Grayscale + alpha, 8-bit.
- Gray
Alpha16 - Grayscale + alpha, 16-bit.
- Gray
Alpha F32 - Grayscale + alpha, f32.
- Mastering
Display - Mastering display color volume metadata (SMPTE ST 2086).
- Pixel
Buffer - Owned pixel buffer with format metadata.
- Pixel
Descriptor - Compact pixel format descriptor.
- Pixel
Slice - Borrowed view of pixel data.
- Pixel
Slice Mut - Mutable borrowed view of pixel data.
- Provenance
- Tracks where pixel data came from, so the cost model can distinguish “f32 that was widened from u8 JPEG” (lossless round-trip back to u8) from “f32 that was decoded from a 16-bit EXR” (lossy truncation to u8).
- Rgbx
- 32-bit RGB pixel with padding byte (RGBx).
Enums§
- Alpha
Mode - Alpha channel interpretation.
- Alpha
Policy - Policy for alpha channel removal. Required when converting from a layout with alpha to one without.
- Buffer
Error - Errors from pixel buffer operations.
- Byte
Order - RGB-family byte order. Only meaningful when color model is RGB.
- Channel
Layout - Channel layout (number and meaning of channels).
- Channel
Type - Channel storage type.
- Color
Model - What the channels represent, independent of channel count or byte order.
- Color
Primaries - Color primaries (CIE xy chromaticities of R, G, B).
- Color
Profile Source - A source color profile — either ICC bytes or CICP parameters.
- Color
Provenance - How the source file described its color information.
- Convert
Intent - What the consumer plans to do with the converted pixels.
- Depth
Policy - Policy for bit depth reduction (U16→U8, etc.).
- Gray
Expand - How to expand grayscale channels to RGB.
- Luma
Coefficients - Luma coefficients for RGB→Gray conversion.
- Named
Profile - Well-known color profiles that any CMS should recognize.
- Orientation
- Image orientation — the 8-element D4 dihedral group.
- Pixel
Format - Physical pixel layout for match-based format dispatch.
- Signal
Range - Signal range for pixel values.
- Transfer
Function - Electro-optical transfer function.
Traits§
- Pixel
- Compile-time pixel format descriptor.
- Result
AtExt - Extension trait for adding location tracking to
Result<T, At<E>>.
Functions§
- at
- Wrap any value in
At<E>and capture the caller’s location. - best_
match - Pick the best conversion target from
supportedfor the given source. - best_
match_ with - Pick the best conversion target from
options, accounting for consumer-provided cost overrides. - conversion_
cost - Compute the two-axis conversion cost for
from→to. - conversion_
cost_ with_ provenance - Compute the two-axis conversion cost with explicit provenance.
- convert_
row - Convert one row of
widthpixels using a pre-computed plan. - ideal_
format - Recommend the ideal working format for a given intent, based on the source format.
- negotiate
- Fully-parameterized format negotiation.