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`] negotiates format and converts
21//!   pixel data in one call, returning `Cow::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`]**: 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;
400pub(crate) mod f16_scalar;
401pub(crate) mod negotiate;
402
403pub mod adapt;
404// Internal only — zero consumers today. Kept for a future caller;
405// see zenpixels CLAUDE.md YAGNI section.
406/// Internal: runtime op tracer for in-repo tests. Gated on
407/// `feature = "__trace_ops"`. The `__` prefix and `#[doc(hidden)]`
408/// signal that this is unstable internal surface — see module docs.
409#[doc(hidden)]
410#[path = "__trace_ops.rs"]
411pub mod __trace_ops;
412#[allow(dead_code)]
413pub(crate) mod builtin_profiles;
414pub mod cms;
415#[allow(
416    dead_code,
417    unused_variables,
418    clippy::needless_return,
419    clippy::excessive_precision,
420    clippy::derivable_impls
421)]
422pub(crate) mod cms_lite;
423#[cfg(feature = "cms-moxcms")]
424pub mod cms_moxcms;
425pub mod converter;
426pub mod ext;
427#[allow(
428    dead_code,
429    unexpected_cfgs,
430    unused_variables,
431    clippy::needless_return,
432    clippy::excessive_precision,
433    clippy::derivable_impls
434)]
435pub(crate) mod fast_gamut;
436
437/// Bench-only shims for u16 RGB gamut hybrid kernels — comparative
438/// benchmarks for {LUT, poly} × {LUT, poly} decode/encode combinations.
439/// Gated behind `__bench_u16_hybrids` so the surface stays out of
440/// non-bench builds. Not public API in any sense beyond the bench file.
441#[cfg(feature = "__bench_u16_hybrids")]
442#[doc(hidden)]
443pub mod __bench_u16_hybrids {
444    pub fn lut_lut(m: &[[f32; 3]; 3], src: &[u16], dst: &mut [u16]) {
445        crate::fast_gamut::__bench_u16_lut_decode_lut_encode(m, src, dst)
446    }
447    pub fn lut_poly(m: &[[f32; 3]; 3], src: &[u16], dst: &mut [u16]) {
448        crate::fast_gamut::__bench_u16_lut_decode_poly_encode(m, src, dst)
449    }
450    pub fn poly_lut(m: &[[f32; 3]; 3], src: &[u16], dst: &mut [u16]) {
451        crate::fast_gamut::__bench_u16_poly_decode_lut_encode(m, src, dst)
452    }
453    pub fn poly_poly(m: &[[f32; 3]; 3], src: &[u16], dst: &mut [u16]) {
454        crate::fast_gamut::__bench_u16_poly_decode_poly_encode(m, src, dst)
455    }
456}
457
458/// Bench-only shims exposing the crate-private `scan` fused kernel to
459/// `benches/bench_load_bearing.rs` for pass-structure comparison.
460/// Gated behind `__bench_scan`; not public API in any sense beyond the
461/// bench file.
462#[cfg(feature = "__bench_scan")]
463#[doc(hidden)]
464pub mod __bench_scan {
465    pub use crate::scan::{FusedRequest, FusedResult, fused_predicates_rgba8_cg};
466}
467pub mod gamut;
468pub mod hdr;
469pub mod icc_profiles;
470pub mod load_bearing;
471pub mod oklab;
472pub mod orient;
473pub mod output;
474#[cfg(feature = "pipeline")]
475pub mod pipeline;
476// Crate-internal SIMD predicates backing `load_bearing` -- the module
477// stays private so the per-(layout × channel-type) kernel set never
478// becomes public API surface. Consume via `PixelSliceLoadBearingExt`.
479mod scan;
480
481// Re-export key conversion types at crate root.
482pub use adapt::{adapt_for_encode_explicit, try_adapt_in_place};
483pub use convert::{ConvertPlan, convert_row};
484pub use converter::RowConverter;
485pub use error::ConvertError;
486pub use negotiate::{
487    ConversionCost, ConvertIntent, FormatOption, Provenance, best_match, best_match_with,
488    conversion_cost, conversion_cost_with_provenance, ideal_format, negotiate,
489};
490#[cfg(feature = "pipeline")]
491pub use pipeline::{
492    CodecFormats, ConversionPath, FormatEntry, LossBucket, MatrixStats, OpCategory, OpRequirement,
493    PathEntry, QualityThreshold, generate_path_matrix, matrix_stats, optimal_path,
494};
495
496// Re-export extension traits.
497#[cfg(feature = "rgb")]
498pub use ext::PixelBufferConvertTypedExt;
499pub use ext::{ColorPrimariesExt, PixelBufferConvertExt, TransferFunctionExt};
500
501// Re-export gamut conversion utilities.
502pub use gamut::{
503    GamutMatrix, apply_matrix_f32, apply_matrix_row_f32, apply_matrix_row_rgba_f32,
504    conversion_matrix,
505};
506
507// Re-export the load-bearing analysis surface: one trait, one report.
508// The scan-level predicates stay crate-internal.
509pub use load_bearing::{LoadBearingReport, PixelBufferLoadBearingExt, PixelSliceLoadBearingExt};
510
511// Re-export HDR types and tone mapping.
512#[cfg(feature = "std")]
513pub use hdr::exposure_tonemap;
514// Anchor-aware HDR PQ quantizer — reads `DiffuseWhite` from the source's
515// `ColorContext` (default BT.2408 = 203); the canonical linear→PQ16 encoder.
516// (The no-allocation `quantize_into` sibling stays `pub(crate)` until a concrete
517// consumer or the §3.2 PixelBuffer-level surface lands — see hdr.rs.)
518pub use hdr::quantize_to;
519// `HdrMetadata` is re-exported but deprecated (see the `hdr` module); the
520// re-export itself names it, hence the allow.
521#[allow(deprecated)]
522pub use hdr::{
523    ContentLightLevel, HdrMetadata, MasteringDisplay, reinhard_inverse, reinhard_tonemap,
524};
525
526// Re-export CMS traits, enums, and implementations.
527#[allow(deprecated)]
528pub use cms::{
529    CmsPluginError, ColorManagement, ColorPriority, PluggableCms, RenderingIntent, RowTransform,
530    RowTransformMut,
531};
532// TODO: pub use cms_lite::ZenCmsLite once benchmarked on aarch64.
533#[cfg(feature = "cms-moxcms")]
534pub use cms_moxcms::MoxCms;
535
536// Re-export output types.
537pub use output::finalize_for_output_with;
538#[allow(deprecated)]
539pub use output::{EncodeReady, OutputMetadata, OutputProfile, finalize_for_output};