Skip to main content

zenpixels_convert/
lib.rs

1//! Transfer-function-aware pixel conversion for zenpixels.
2//!
3//! This crate provides all the conversion logic that was split out of the
4//! `zenpixels` interchange crate: row-level format conversion, gamut mapping,
5//! codec format negotiation, and HDR tone mapping.
6//!
7//! # Re-exports
8//!
9//! All interchange types from `zenpixels` are re-exported at the crate root,
10//! so downstream code can depend on `zenpixels-convert` alone.
11//!
12//! # Core concepts
13//!
14//! - **Format negotiation**: [`best_match`] picks the cheapest conversion
15//!   target from a codec's supported formats for a given source descriptor.
16//!
17//! - **Row conversion**: [`RowConverter`] pre-computes a conversion plan and
18//!   converts rows with no per-row allocation, using SIMD where available.
19//!
20//! - **Codec helpers**: [`adapt::adapt_for_encode_cow`] negotiates format and converts
21//!   pixel data in one call, returning [`PixelCow::Borrowed`] when the input
22//!   already matches a supported format.
23//!
24//! - **Extension traits**: [`TransferFunctionExt`], [`ColorPrimariesExt`],
25//!   and `PixelBufferConvertExt` add conversion methods to interchange types.
26//!
27//! # Codec compliance guide
28//!
29//! This section describes how to write a codec that integrates correctly with
30//! the zenpixels ecosystem. A "codec" here means any decoder or encoder crate
31//! that produces or consumes pixel data.
32//!
33//! ## Design principles
34//!
35//! 1. **Codecs own I/O; zenpixels-convert owns pixel math.** A codec reads
36//!    and writes its container format. All pixel format conversion, transfer
37//!    function application, gamut mapping, and alpha handling is done by
38//!    `zenpixels-convert`. Codecs should not re-implement conversion logic.
39//!
40//! 2. **`PixelFormat` is byte layout; `PixelDescriptor` is full meaning.**
41//!    `PixelFormat` describes the physical byte arrangement (channel count,
42//!    order, depth). `PixelDescriptor` adds color interpretation: transfer
43//!    function, primaries, alpha mode, signal range. Codecs register
44//!    `PixelDescriptor` values because negotiation needs the full picture.
45//!
46//! 3. **No silent lossy conversions.** Every operation that destroys
47//!    information (alpha removal, depth reduction, gamut clipping) requires
48//!    an explicit policy via [`ConvertOptions`]. Codecs must not silently
49//!    clamp, truncate, or discard data.
50//!
51//! 4. **Pixels and metadata travel together.** [`ColorContext`] rides on
52//!    [`PixelBuffer`] via `Arc` so ICC/CICP metadata follows pixel data
53//!    through the pipeline. [`finalize_for_output`] couples converted pixels
54//!    with matching encoder metadata atomically.
55//!
56//! 5. **Provenance enables lossless round-trips.** The cost model tracks
57//!    where data came from ([`Provenance`]). A JPEG u8 decoded to f32 for
58//!    resize reports zero loss when converting back to u8, because the
59//!    origin precision was u8 all along.
60//!
61//! ## The pixel lifecycle
62//!
63//! Every image processing pipeline follows this flow:
64//!
65//! ```text
66//! ┌──────────┐    ┌───────────┐    ┌───────────┐    ┌──────────┐
67//! │  Decode   │───>│ Negotiate │───>│  Convert  │───>│  Encode  │
68//! │          │    │           │    │           │    │          │
69//! │ Produces: │    │ Picks:    │    │ Uses:     │    │ Consumes:│
70//! │ PixelBuf  │    │ best fmt  │    │ RowConv.  │    │ EncReady │
71//! │ ColorCtx  │    │ from list │    │ per-row   │    │ metadata │
72//! │ ColorOrig │    │           │    │           │    │          │
73//! └──────────┘    └───────────┘    └───────────┘    └──────────┘
74//! ```
75//!
76//! ### Step 1: Decode
77//!
78//! The decoder produces pixel data in one of its natively supported formats,
79//! wraps it in a [`PixelBuffer`], and extracts color metadata from the file.
80//!
81//! ```rust,ignore
82//! // Decode raw pixels
83//! let pixels: Vec<u8> = my_codec_decode(&file_bytes)?;
84//! let desc = PixelDescriptor::RGB8_SRGB;
85//! let buffer = PixelBuffer::from_vec(pixels, width, height, desc)?;
86//!
87//! // Extract color metadata for CMS integration
88//! let color_ctx = match (icc_chunk, cicp_chunk) {
89//!     (Some(icc), Some(cicp)) =>
90//!         Some(Arc::new(ColorContext::from_icc_and_cicp(icc, cicp))),
91//!     (Some(icc), None) =>
92//!         Some(Arc::new(ColorContext::from_icc(icc))),
93//!     (None, Some(cicp)) =>
94//!         Some(Arc::new(ColorContext::from_cicp(cicp))),
95//!     (None, None) => None,
96//! };
97//!
98//! // Track provenance for re-encoding decisions
99//! let origin = ColorOrigin::from_icc_and_cicp(icc, cicp);
100//! // or: ColorOrigin::from_icc(icc)
101//! // or: ColorOrigin::from_cicp(cicp)
102//! // or: ColorOrigin::from_gama_chrm()  // PNG gAMA+cHRM
103//! // or: ColorOrigin::assumed()         // no color metadata in file
104//! ```
105//!
106//! **Rules for decoders:**
107//!
108//! - Register only formats the decoder natively produces. Do not list formats
109//!   that require internal conversion — let the caller convert via
110//!   [`RowConverter`]. If your JPEG decoder outputs u8 sRGB only, register
111//!   `RGB8_SRGB`, not `RGBF32_LINEAR`.
112//!
113//! - Extract all available color metadata. Both ICC and CICP can coexist
114//!   (AVIF/HEIF containers carry both). Record all of it on [`ColorContext`].
115//!
116//! - Build a [`ColorOrigin`] that records *how* the file described its color,
117//!   not what the pixels are. This is immutable and used only at encode time
118//!   for provenance decisions (e.g., "re-embed the original ICC profile").
119//!
120//! - Set `effective_bits` correctly on `FormatEntry`. A 10-bit AVIF source
121//!   decoded to u16 has `effective_bits = 10`, not 16. A JPEG decoded to f32
122//!   with debiased dequantization has `effective_bits = 10`. Getting this
123//!   wrong makes the cost model over- or under-value precision.
124//!
125//! - Set `can_overshoot = true` only when output values exceed `[0.0, 1.0]`.
126//!   This is rare — only JPEG f32 decode with preserved IDCT ringing.
127//!
128//! ### Step 2: Negotiate
129//!
130//! Before encoding, the pipeline must pick a format the encoder accepts.
131//! Negotiation uses the two-axis cost model (effort vs. loss) weighted by
132//! [`ConvertIntent`].
133//!
134//! Three entry points, from simplest to most flexible:
135//!
136//! - **[`best_match`]**: Pass a source descriptor, a list of supported
137//!   descriptors, and an intent. Good for simple encode paths.
138//!
139//! - **[`best_match_with`]**: Like `best_match`, but each candidate carries
140//!   a consumer cost ([`FormatOption`]). Use this when the encoder has fast
141//!   internal conversion paths (e.g., a JPEG encoder with a fused f32→u8+DCT
142//!   kernel can advertise `RGBF32_LINEAR` with low consumer cost).
143//!
144//! - **[`negotiate()`](fn@crate::negotiate)**: Full control. Explicit [`Provenance`] (so the cost
145//!   model knows the data's true origin) plus consumer costs. Use this in
146//!   processing pipelines where data has been widened from a lower-precision
147//!   source (e.g., JPEG u8 decoded to f32 for resize — provenance says "u8
148//!   origin", so converting back to u8 reports zero loss).
149//!
150//! ```rust,ignore
151//! // Simple: "what format should I encode to?"
152//! let target = best_match(
153//!     buffer.descriptor(),
154//!     &encoder_supported,
155//!     ConvertIntent::Fastest,
156//! ).ok_or("no compatible format")?;
157//!
158//! // With provenance: "this f32 data came from u8 JPEG"
159//! let provenance = Provenance::with_origin_depth(ChannelType::U8);
160//! let target = negotiate(
161//!     current_desc,
162//!     provenance,
163//!     options.iter().copied(),
164//!     ConvertIntent::Fastest,
165//! );
166//! ```
167//!
168//! **Rules for negotiation:**
169//!
170//! - Use [`ConvertIntent::Fastest`] when encoding. The encoder knows what it
171//!   wants; get there with minimal work.
172//!
173//! - Use [`ConvertIntent::LinearLight`] for resize, blur, anti-aliasing.
174//!   These operations need linear light for gamma-correct results.
175//!
176//! - Use [`ConvertIntent::Blend`] for compositing. This ensures premultiplied
177//!   alpha for correct Porter-Duff math.
178//!
179//! - Use [`ConvertIntent::Perceptual`] for sharpening, contrast, saturation.
180//!   These are perceptual operations that work best in sRGB or Oklab space.
181//!
182//! - Track provenance when data has been widened. If you decoded a JPEG (u8)
183//!   into f32 for processing, tell the cost model via
184//!   `Provenance::with_origin_depth(ChannelType::U8)`. Otherwise it will
185//!   penalize the f32→u8 conversion as lossy when it's actually a lossless
186//!   round-trip.
187//!
188//! - If an operation genuinely expands the data's gamut (e.g., saturation
189//!   boost in BT.2020 that pushes colors outside sRGB), call
190//!   [`Provenance::invalidate_primaries`] with the current working primaries.
191//!   Otherwise the cost model will incorrectly report gamut narrowing as
192//!   lossless.
193//!
194//! ### Step 3: Convert
195//!
196//! Once a target format is chosen, convert pixel data row-by-row.
197//!
198//! ```rust,ignore
199//! let converter = RowConverter::new(source_desc, target_desc)?;
200//! for y in 0..height {
201//!     converter.convert_row(src_row, dst_row, width);
202//! }
203//! ```
204//!
205//! Or use the convenience function that combines negotiation and conversion:
206//!
207//! ```rust,ignore
208//! let adapted = adapt_for_encode(
209//!     raw_bytes, descriptor, width, rows, stride,
210//!     &encoder_supported,
211//! )?;
212//! // adapted.data is Cow::Borrowed if no conversion needed
213//! ```
214//!
215//! **Rules for conversion:**
216//!
217//! - Use [`RowConverter`], not hand-rolled conversion. It handles transfer
218//!   functions, gamut matrices, alpha mode changes, depth scaling, Oklab,
219//!   and byte swizzle correctly. It pre-computes the plan so there is
220//!   zero per-row overhead.
221//!
222//! - For policy-sensitive conversions (when you need to control what lossy
223//!   operations are allowed), use [`adapt_for_encode_explicit`] with
224//!   [`ConvertOptions`]. This validates policies *before* doing work and
225//!   returns specific errors like [`ConvertError::AlphaNotOpaque`] or
226//!   [`ConvertError::DepthReductionForbidden`].
227//!
228//! - The conversion system handles three tiers internally:
229//!   (a) Direct SIMD kernels for common pairs (byte swizzle, depth shift,
230//!   transfer LUTs).
231//!   (b) Composed multi-step plans for less common pairs.
232//!   (c) Hub path through linear sRGB f32 as a universal fallback.
233//!
234//! - **Signal range never converts — it refuses.** There are no
235//!   Narrow↔Full (limited↔full / studio↔full swing) conversion kernels, so
236//!   any plan whose endpoints differ in [`SignalRange`] fails with
237//!   [`ConvertError::NoPath`] instead of relabeling unscaled values
238//!   (which would lift or crush blacks). Narrow data passes through only
239//!   verbatim — same range on both sides (value-preserving steps like
240//!   swizzles are fine). If you hit the refusal, either present a
241//!   same-range target or expand the data upstream where the semantics
242//!   are known.
243//!
244//! ### Step 4: Encode
245//!
246//! The encoder receives pixel data in a format it natively supports and
247//! must embed correct color metadata.
248//!
249//! For the atomic path (recommended), use [`finalize_for_output`]:
250//!
251//! ```rust,ignore
252//! let ready = finalize_for_output(
253//!     &buffer,
254//!     &color_origin,
255//!     OutputProfile::SameAsOrigin,
256//!     target_format,
257//!     &cms,
258//! )?;
259//!
260//! // Pixels and metadata are guaranteed to match
261//! encoder.write_pixels(ready.pixels())?;
262//! encoder.write_icc(ready.metadata().icc.as_deref())?;
263//! encoder.write_cicp(ready.metadata().cicp)?;
264//! ```
265//!
266//! **Rules for encoders:**
267//!
268//! - Register only formats the encoder natively accepts. If your JPEG encoder
269//!   takes u8 sRGB, register `RGB8_SRGB`. Don't also list `RGBA8_SRGB`
270//!   unless you actually handle RGBA natively (not just by stripping alpha
271//!   internally). Let the conversion system handle format changes.
272//!
273//! - If you have fast internal conversion paths, advertise them via
274//!   [`FormatOption::with_cost`]. Example: a JPEG encoder with a fused
275//!   f32→DCT path can accept `RGBF32_LINEAR` at `ConversionCost::new(5, 0)`,
276//!   so negotiation will route f32 data directly to the encoder instead of
277//!   doing a redundant f32→u8 conversion first.
278//!
279//! - Use [`finalize_for_output`] to bundle pixels and metadata atomically.
280//!   This prevents the most common color management bug: pixel values that
281//!   don't match the embedded ICC/CICP.
282//!
283//! - Always embed color metadata when the format supports it. Check
284//!   `CodecFormats::icc_encode` and `CodecFormats::cicp` for your codec's
285//!   capabilities. Omitting color metadata causes browsers and OS viewers
286//!   to assume sRGB, which corrupts Display P3 and HDR content.
287//!
288//! - The [`OutputProfile`] enum controls what gets embedded:
289//!   - [`OutputProfile::SameAsOrigin`]: Re-embed the original ICC/CICP from
290//!     the source file. Used for transcoding without color changes.
291//!   - [`OutputProfile::Named`]: Use a well-known CICP profile (sRGB, P3,
292//!     BT.2020). Uses hardcoded gamut matrices, no CMS needed.
293//!   - [`OutputProfile::Icc`]: Use specific ICC profile bytes. Requires a
294//!     [`ColorManagement`] implementation.
295//!
296//! ## Format registry
297//!
298//! Every codec must declare its capabilities in a `CodecFormats` struct,
299//! typically as a `pub static`. This serves as the single source of truth
300//! for what the codec can produce and consume. See the `pipeline::registry`
301//! module (requires the `pipeline` feature) for the full format table and
302//! examples for each codec.
303//!
304//! ```rust,ignore
305//! pub static MY_CODEC: CodecFormats = CodecFormats {
306//!     name: "mycodec",
307//!     decode_outputs: &[
308//!         FormatEntry::standard(PixelDescriptor::RGB8_SRGB),
309//!         FormatEntry::standard(PixelDescriptor::RGBA8_SRGB),
310//!     ],
311//!     encode_inputs: &[
312//!         FormatEntry::standard(PixelDescriptor::RGB8_SRGB),
313//!     ],
314//!     icc_decode: true,   // extracts ICC profiles
315//!     icc_encode: true,   // embeds ICC profiles
316//!     cicp: false,        // no CICP support
317//! };
318//! ```
319//!
320//! ## Cost model
321//!
322//! Format negotiation uses a two-axis cost model separating **effort**
323//! (CPU work) from **loss** (information destroyed). These are independent:
324//! a fast conversion can be very lossy (f32 HDR → u8 clamp), and a slow
325//! conversion can be lossless (u8 sRGB → f32 linear).
326//!
327//! [`ConvertIntent`] controls how the axes are weighted:
328//!
329//! | Intent         | Effort weight | Loss weight | Use case |
330//! |----------------|---------------|-------------|----------|
331//! | `Fastest`      | 4x            | 1x          | Encoding |
332//! | `LinearLight`  | 1x            | 4x          | Resize, blur |
333//! | `Blend`        | 1x            | 4x          | Compositing |
334//! | `Perceptual`   | 1x            | 3x          | Color grading |
335//!
336//! Cost components are additive: total = transfer_cost + depth_cost +
337//! layout_cost + alpha_cost + primaries_cost + consumer_cost +
338//! suitability_loss. The lowest-scoring candidate wins.
339//!
340//! ## CMS integration
341//!
342//! Named profile conversions (sRGB ↔ Display P3 ↔ BT.2020) use hardcoded
343//! 3×3 gamut matrices and need no CMS backend. ICC-to-ICC transforms
344//! require a [`ColorManagement`] implementation, which is a compile-time
345//! feature (e.g., `cms-moxcms`, `cms-lcms2`).
346//!
347//! Codecs that handle ICC profiles must:
348//! 1. Extract ICC bytes on decode and store them on [`ColorContext`].
349//! 2. Record provenance on [`ColorOrigin`].
350//! 3. On encode, let [`finalize_for_output`] handle the ICC transform
351//!    (if the target profile differs from the source) or pass-through
352//!    (if `SameAsOrigin`).
353//!
354//! ## Error handling
355//!
356//! [`ConvertError`] provides specific variants so codecs can handle each
357//! failure mode:
358//!
359//! - [`ConvertError::NoMatch`] — no supported format works for this source.
360//!   The codec's format list may be too restrictive.
361//! - [`ConvertError::NoPath`] — no conversion kernel exists between formats.
362//!   Unusual; most pairs are covered.
363//! - [`ConvertError::AlphaNotOpaque`] — `DiscardIfOpaque` policy was set
364//!   but the data has semi-transparent pixels.
365//! - [`ConvertError::DepthReductionForbidden`] — `Forbid` policy prevents
366//!   narrowing (e.g., f32→u8).
367//! - [`ConvertError::AllocationFailed`] — buffer allocation failed (OOM).
368//! - [`ConvertError::CmsError`] — CMS transform failed (invalid ICC profile,
369//!   unsupported color space, etc.).
370//!
371//! Codecs should match on specific variants and return actionable errors
372//! to callers. Do not flatten `ConvertError` into a generic string.
373//!
374//! ## Checklist
375//!
376//! - [ ] Declare `CodecFormats` with correct `effective_bits` and `can_overshoot`
377//! - [ ] Decode: extract ICC + CICP → [`ColorContext`]
378//! - [ ] Decode: record provenance → [`ColorOrigin`]
379//! - [ ] Encode: negotiate via [`best_match`] or [`adapt::adapt_for_encode`]
380//! - [ ] Encode: convert via [`RowConverter`] (not hand-rolled)
381//! - [ ] Encode: embed metadata via [`finalize_for_output`]
382//! - [ ] Encode: embed ICC/CICP when the format supports it
383//! - [ ] Handle [`ConvertError`] variants specifically
384//! - [ ] Test round-trip: native format → encode → decode = lossless
385//! - [ ] Test negotiation: `best_match(my_format, my_supported, Fastest)` picks identity
386
387#![cfg_attr(not(feature = "std"), no_std)]
388#![forbid(unsafe_code)]
389
390extern crate alloc;
391
392whereat::define_at_crate_info!(path = "zenpixels-convert/");
393
394// Re-export all interchange types from zenpixels.
395pub use zenpixels::*;
396
397// Conversion modules.
398pub(crate) mod convert;
399pub mod error;
400/// Resource estimation — predict peak memory + wall-clock time for a
401/// `ConvertPlan` before running it. See [`ResourceEstimate`],
402/// [`ComputeEnvironment`], [`ImageCharacteristics`],
403/// [`ConvertPlan::estimate`](crate::ConvertPlan::estimate), and
404/// [`ConvertPlan::estimate_in`](crate::ConvertPlan::estimate_in).
405///
406/// These estimate types are defined locally here. `zenpixels-convert`
407/// is a foundation crate and does NOT depend on `zencodec` — the layering
408/// rule keeps codec abstractions strictly above pixel math. There is
409/// currently no `From` conversion to/from the similarly-named
410/// `zencodec::estimate::*` types — the shapes have diverged (for example,
411/// `zencodec`'s `ResourceEstimate` additionally tracks `cpu_ms` and
412/// `peak_memory_bytes_max`, and its `ImageCharacteristics` tracks
413/// `frame_count`, none of which exist here). A `decode → convert → encode`
414/// pipeline bridging the two currently has to map fields by hand.
415pub mod estimate;
416pub use estimate::{ComputeEnvironment, ImageCharacteristics, ResourceEstimate, SimdTier};
417pub(crate) mod f16_scalar;
418pub(crate) mod negotiate;
419
420pub mod adapt;
421// Internal only — zero consumers today. Kept for a future caller;
422// see zenpixels CLAUDE.md YAGNI section.
423/// Internal: runtime op tracer for in-repo tests. Gated on
424/// `feature = "__trace_ops"`. The `__` prefix and `#[doc(hidden)]`
425/// signal that this is unstable internal surface — see module docs.
426#[doc(hidden)]
427#[path = "__trace_ops.rs"]
428pub mod __trace_ops;
429#[allow(dead_code)]
430pub(crate) mod builtin_profiles;
431pub mod cms;
432#[allow(
433    dead_code,
434    unused_variables,
435    clippy::needless_return,
436    clippy::excessive_precision,
437    clippy::derivable_impls
438)]
439pub(crate) mod cms_lite;
440#[cfg(feature = "cms-moxcms")]
441pub mod cms_moxcms;
442pub mod converter;
443pub mod ext;
444#[allow(
445    dead_code,
446    unexpected_cfgs,
447    unused_variables,
448    clippy::needless_return,
449    clippy::excessive_precision,
450    clippy::derivable_impls
451)]
452pub(crate) mod fast_gamut;
453
454/// Bench-only shims for u16 RGB gamut hybrid kernels — comparative
455/// benchmarks for {LUT, poly} × {LUT, poly} decode/encode combinations.
456/// Gated behind `__bench_u16_hybrids` so the surface stays out of
457/// non-bench builds. Not public API in any sense beyond the bench file.
458#[cfg(feature = "__bench_u16_hybrids")]
459#[doc(hidden)]
460pub mod __bench_u16_hybrids {
461    pub fn lut_lut(m: &[[f32; 3]; 3], src: &[u16], dst: &mut [u16]) {
462        crate::fast_gamut::__bench_u16_lut_decode_lut_encode(m, src, dst)
463    }
464    pub fn lut_poly(m: &[[f32; 3]; 3], src: &[u16], dst: &mut [u16]) {
465        crate::fast_gamut::__bench_u16_lut_decode_poly_encode(m, src, dst)
466    }
467    pub fn poly_lut(m: &[[f32; 3]; 3], src: &[u16], dst: &mut [u16]) {
468        crate::fast_gamut::__bench_u16_poly_decode_lut_encode(m, src, dst)
469    }
470    pub fn poly_poly(m: &[[f32; 3]; 3], src: &[u16], dst: &mut [u16]) {
471        crate::fast_gamut::__bench_u16_poly_decode_poly_encode(m, src, dst)
472    }
473}
474
475/// Bench-only shims exposing the crate-private `scan` fused kernel to
476/// `benches/bench_load_bearing.rs` for pass-structure comparison.
477/// Gated behind `__bench_scan`; not public API in any sense beyond the
478/// bench file.
479#[cfg(feature = "__bench_scan")]
480#[doc(hidden)]
481pub mod __bench_scan {
482    pub use crate::scan::{FusedRequest, FusedResult, fused_predicates_rgba8_cg};
483}
484pub mod gamut;
485pub mod hdr;
486pub mod icc_profiles;
487pub mod load_bearing;
488pub mod oklab;
489pub mod orient;
490pub mod output;
491#[cfg(feature = "pipeline")]
492pub mod pipeline;
493// Crate-internal SIMD predicates backing `load_bearing` -- the module
494// stays private so the per-(layout × channel-type) kernel set never
495// becomes public API surface. Consume via `PixelSliceLoadBearingExt`.
496mod scan;
497
498// Re-export key conversion types at crate root.
499#[allow(deprecated)]
500pub use adapt::adapt_for_encode_explicit;
501pub use adapt::{adapt_for_encode_explicit_cow, try_adapt_in_place};
502#[cfg(feature = "hdr-experimental")]
503pub use convert::HdrConfig;
504pub use convert::{ConvertPlan, convert_row, requires_cms};
505pub use converter::RowConverter;
506pub use error::ConvertError;
507pub use negotiate::{
508    ConversionCost, ConvertIntent, FormatOption, Provenance, best_match, best_match_with,
509    conversion_cost, conversion_cost_with_provenance, ideal_format, negotiate,
510};
511#[cfg(feature = "pipeline")]
512pub use pipeline::{
513    CodecFormats, ConversionPath, FormatEntry, LossBucket, MatrixStats, OpCategory, OpRequirement,
514    PathEntry, QualityThreshold, generate_path_matrix, matrix_stats, optimal_path,
515};
516
517// Re-export extension traits.
518#[cfg(feature = "rgb")]
519pub use ext::PixelBufferConvertTypedExt;
520#[cfg(feature = "hdr-experimental")]
521pub use ext::PixelBufferHdrConvertExt;
522pub use ext::{ColorPrimariesExt, PixelBufferConvertExt, TransferFunctionExt};
523
524// Re-export gamut conversion utilities.
525pub use gamut::{
526    GamutMatrix, apply_matrix_f32, apply_matrix_row_f32, apply_matrix_row_rgba_f32,
527    conversion_matrix,
528};
529
530// Re-export the load-bearing analysis surface: one trait, one report.
531// The scan-level predicates stay crate-internal.
532pub use load_bearing::{LoadBearingReport, PixelBufferLoadBearingExt, PixelSliceLoadBearingExt};
533
534// Re-export HDR types and tone mapping.
535// `exposure_tonemap`, `reinhard_inverse`, `reinhard_tonemap`, and `HdrMetadata`
536// are deprecated (see the `hdr` module). The re-exports themselves name them,
537// hence the per-statement `#[allow(deprecated)]`. For production HDR→SDR tone
538// mapping use the `zentone` crate (`zentone::Bt2446A`, `Bt2408Tonemapper`, …).
539#[allow(deprecated)]
540#[cfg(feature = "std")]
541pub use hdr::exposure_tonemap;
542// Anchor-aware HDR PQ quantizer — reads `DiffuseWhite` from the source's
543// `ColorContext` (default BT.2408 = 203); the canonical linear→PQ16 encoder.
544// (The no-allocation `quantize_into` sibling stays `pub(crate)` until a concrete
545// consumer or the §3.2 PixelBuffer-level surface lands — see hdr.rs.)
546pub use hdr::quantize_to;
547#[allow(deprecated)]
548pub use hdr::{
549    ContentLightLevel, HdrMetadata, MasteringDisplay, reinhard_inverse, reinhard_tonemap,
550};
551
552// Re-export CMS traits, enums, and implementations.
553#[allow(deprecated)]
554pub use cms::{
555    CmsPluginError, ColorManagement, ColorPriority, PluggableCms, RenderingIntent, RowTransform,
556    RowTransformMut,
557};
558// TODO: pub use cms_lite::ZenCmsLite once benchmarked on aarch64.
559#[cfg(feature = "cms-moxcms")]
560pub use cms_moxcms::MoxCms;
561
562// Re-export output types.
563pub use output::finalize_for_output_with;
564#[allow(deprecated)]
565pub use output::{EncodeReady, OutputMetadata, OutputProfile, finalize_for_output};