Skip to main content

zenpixels_convert/
registry.rs

1//! Codec format registry — static tables of what each codec can produce and consume.
2//!
3//! These tables are derived from the actual `supported_descriptors()` in each
4//! zen* codec's zencodec-types integration. Extended (internal-API-only) formats
5//! are listed separately where they exist.
6//!
7//! # How to register a codec
8//!
9//! Every codec declares a [`CodecFormats`] constant that lists every pixel
10//! format the decoder can produce and the encoder can accept. The format
11//! negotiation system ([`super::best_match`], [`super::negotiate`]) uses
12//! these tables to pick the cheapest conversion path.
13//!
14//! ```rust,ignore
15//! pub static MY_CODEC: CodecFormats = CodecFormats {
16//!     name: "mycodec",
17//!     decode_outputs: &[
18//!         FormatEntry::standard(PixelDescriptor::RGB8_SRGB),
19//!         FormatEntry::standard(PixelDescriptor::RGBA8_SRGB),
20//!     ],
21//!     encode_inputs: &[
22//!         FormatEntry::standard(PixelDescriptor::RGB8_SRGB),
23//!     ],
24//!     icc_decode: true,
25//!     icc_encode: true,
26//!     cicp: false,
27//! };
28//! ```
29//!
30//! # Rules for format entries
31//!
32//! **Only register formats the codec handles natively.** If your decoder
33//! outputs u8 sRGB and your caller needs f32, that is a conversion handled
34//! by [`RowConverter`](super::RowConverter). Don't add `RGBF32_LINEAR` to
35//! your decode list unless your decoder genuinely outputs f32 data. Listing
36//! non-native formats causes double conversions (codec converts internally,
37//! then the negotiator converts again).
38//!
39//! **Decode and encode lists can differ.** A JPEG decoder produces u8 only,
40//! but a JPEG encoder may accept u8, u16, and f32 (converting internally
41//! before DCT). List what each side actually handles.
42//!
43//! **Set `icc_decode`/`icc_encode`/`cicp` accurately.** These booleans
44//! tell the pipeline whether ICC and CICP metadata can round-trip through
45//! the codec. A codec that silently drops ICC profiles must set
46//! `icc_encode: false`, even if the format spec supports ICC.
47//!
48//! # Effective bits
49//!
50//! The `effective_bits` field tracks the actual precision of data within its
51//! container type. Two u16 values may have different effective precision:
52//! - PNG 16-bit: u16 with 16 effective bits (full range)
53//! - AVIF 10-bit decoded to u16: 10 effective bits (top bits are replicated)
54//! - Farbfeld: u16 with 16 effective bits
55//!
56//! This matters for provenance: converting a 10-bit-effective u16 to u8 loses
57//! only 2 bits, not 8.
58//!
59//! When `effective_bits` is wrong, the cost model over- or under-values
60//! precision during negotiation. Examples:
61//!
62//! - A GIF decoder that outputs u8 converted from palette: `effective_bits = 8`
63//!   (the u8 values are already the final precision).
64//! - A JPEG decoder with debiased dequantization producing f32: `effective_bits = 10`
65//!   (10 bits of real precision in the f32 container).
66//! - A PNG decoder that expands 1-bit gray to u8: `effective_bits = 8`
67//!   (the values are scaled to fill the u8 range).
68//!
69//! Use [`FormatEntry::standard`](FormatEntry) for the common case where
70//! effective bits match the container (u8→8, u16→16, f32→32). Use
71//! [`FormatEntry::with_bits`](FormatEntry) when they differ.
72//!
73//! # Overshoot
74//!
75//! Set `can_overshoot = true` only when output pixel values exceed
76//! `[0.0, 1.0]` for float formats or the full integer range for integer
77//! formats. The only known case is JPEG f32 decode with preserved IDCT
78//! ringing — the inverse DCT produces values beyond the nominal range.
79//! Most codecs clamp to nominal range and should set `can_overshoot = false`
80//! (the default from [`FormatEntry::standard`](FormatEntry)).
81
82use crate::{ChannelLayout, ChannelType, ColorPrimaries, PixelDescriptor, TransferFunction};
83
84/// A format a codec can produce (decode) or consume (encode).
85#[derive(Clone, Copy, Debug)]
86pub struct FormatEntry {
87    /// The pixel descriptor for this format.
88    pub descriptor: PixelDescriptor,
89
90    /// Effective precision bits within the container type.
91    ///
92    /// Usually matches the container (u8=8, u16=16, f32=32), but can differ:
93    /// - AVIF 10-bit source decoded to u8: effective_bits = 8 (precision lost)
94    /// - JPEG f32 precise decode: effective_bits = 10 (debiased dequant)
95    /// - PNG 1-bit gray decoded to u8: effective_bits = 8 (scaled to fill range)
96    /// - Farbfeld u16: effective_bits = 16 (full range)
97    pub effective_bits: u8,
98
99    /// Whether output values can exceed the nominal range.
100    ///
101    /// JPEG f32 decode preserves IDCT ringing, producing values outside [0.0, 1.0].
102    /// Most codecs clamp to nominal range.
103    pub can_overshoot: bool,
104}
105
106impl FormatEntry {
107    /// Create a format entry with standard precision (matches container type).
108    const fn standard(descriptor: PixelDescriptor) -> Self {
109        let effective_bits = match descriptor.channel_type() {
110            ChannelType::U8 => 8,
111            ChannelType::U16 => 16,
112            ChannelType::F16 => 11,
113            ChannelType::F32 => 32,
114            _ => 0,
115        };
116        Self {
117            descriptor,
118            effective_bits,
119            can_overshoot: false,
120        }
121    }
122
123    /// Create a format entry with custom effective bits.
124    const fn with_bits(descriptor: PixelDescriptor, effective_bits: u8) -> Self {
125        Self {
126            descriptor,
127            effective_bits,
128            can_overshoot: false,
129        }
130    }
131
132    /// Create a format entry that can overshoot nominal range.
133    const fn overshoot(descriptor: PixelDescriptor, effective_bits: u8) -> Self {
134        Self {
135            descriptor,
136            effective_bits,
137            can_overshoot: true,
138        }
139    }
140}
141
142/// Static description of a codec's format capabilities.
143#[derive(Clone, Debug)]
144pub struct CodecFormats {
145    /// Codec name (e.g. "jpeg", "png").
146    pub name: &'static str,
147    /// Formats the decoder can produce (via zencodec `supported_descriptors`).
148    pub decode_outputs: &'static [FormatEntry],
149    /// Formats the encoder can accept (via zencodec `supported_descriptors`).
150    pub encode_inputs: &'static [FormatEntry],
151    /// Whether ICC profiles are extracted on decode.
152    pub icc_decode: bool,
153    /// Whether ICC profiles can be embedded on encode.
154    pub icc_encode: bool,
155    /// Whether CICP signaling is supported.
156    pub cicp: bool,
157}
158
159// ═══════════════════════════════════════════════════════════════════════
160// JPEG (zenjpeg)
161// ═══════════════════════════════════════════════════════════════════════
162//
163// Decode: u8 sRGB only via zencodec API. Internal API also supports:
164//   - SrgbF32: f32 sRGB gamma, unclamped (preserves IDCT ringing)
165//   - LinearF32: f32 linear, unclamped
166//   - SrgbF32Precise: f32 sRGB with debiased dequantization (~10-bit effective)
167//   - LinearF32Precise: f32 linear with debiased dequantization
168// All f32 decode paths can produce values outside [0.0, 1.0] (overshoot).
169//
170// Encode: accepts u8 sRGB, u16 sRGB, f32 linear. Internal conversion for
171// non-native formats.
172
173static JPEG_DECODE: &[FormatEntry] = &[
174    FormatEntry::with_bits(PixelDescriptor::RGB8_SRGB, 8),
175    FormatEntry::with_bits(PixelDescriptor::RGBA8_SRGB, 8),
176    FormatEntry::with_bits(PixelDescriptor::GRAY8_SRGB, 8),
177    FormatEntry::with_bits(PixelDescriptor::BGRA8_SRGB, 8),
178];
179
180static JPEG_ENCODE: &[FormatEntry] = &[
181    FormatEntry::standard(PixelDescriptor::RGB8_SRGB),
182    FormatEntry::standard(PixelDescriptor::RGBA8_SRGB),
183    FormatEntry::standard(PixelDescriptor::GRAY8_SRGB),
184    FormatEntry::standard(PixelDescriptor::BGRA8_SRGB),
185    FormatEntry::standard(PixelDescriptor::RGB16_SRGB),
186    FormatEntry::standard(PixelDescriptor::RGBA16_SRGB),
187    FormatEntry::standard(PixelDescriptor::GRAY16_SRGB),
188    FormatEntry::standard(PixelDescriptor::RGBF32_LINEAR),
189    FormatEntry::standard(PixelDescriptor::RGBAF32_LINEAR),
190    FormatEntry::standard(PixelDescriptor::GRAYF32_LINEAR),
191];
192
193/// Extended JPEG decode formats available via internal API (not zencodec).
194///
195/// These require using zenjpeg's `OutputTarget` directly rather than the
196/// zencodec `DecoderConfig` interface. They provide higher quality decode
197/// at the cost of API portability.
198///
199/// **Deblocking/debiased/XYB decode modes** produce f32 output where u8
200/// would truncate precision — the extended precision from debiased
201/// dequantization or XYB inverse transform cannot fit in 8 bits.
202pub static JPEG_DECODE_EXTENDED: &[FormatEntry] = &[
203    // SrgbF32: f32 sRGB gamma, unclamped integer IDCT, preserves ringing
204    FormatEntry::overshoot(
205        PixelDescriptor::new_full(
206            ChannelType::F32,
207            ChannelLayout::Rgb,
208            None,
209            TransferFunction::Srgb,
210            ColorPrimaries::Bt709,
211        ),
212        8,
213    ),
214    // LinearF32: f32 linear light, unclamped integer IDCT + sRGB→linear
215    FormatEntry::overshoot(PixelDescriptor::RGBF32_LINEAR, 8),
216    // SrgbF32Precise: f32 sRGB with Laplacian dequantization biases (~10-bit effective)
217    FormatEntry::overshoot(
218        PixelDescriptor::new_full(
219            ChannelType::F32,
220            ChannelLayout::Rgb,
221            None,
222            TransferFunction::Srgb,
223            ColorPrimaries::Bt709,
224        ),
225        10,
226    ),
227    // LinearF32Precise: f32 linear with Laplacian biases (~10-bit effective)
228    FormatEntry::overshoot(PixelDescriptor::RGBF32_LINEAR, 10),
229    // Grayscale variants
230    FormatEntry::overshoot(PixelDescriptor::GRAYF32_LINEAR, 8),
231];
232
233pub static JPEG: CodecFormats = CodecFormats {
234    name: "jpeg",
235    decode_outputs: JPEG_DECODE,
236    encode_inputs: JPEG_ENCODE,
237    icc_decode: true,
238    icc_encode: true,
239    cicp: false,
240};
241
242// ═══════════════════════════════════════════════════════════════════════
243// PNG (zenpng)
244// ═══════════════════════════════════════════════════════════════════════
245//
246// Full format support: u8, u16, f32. Sub-8-bit (1/2/4-bit) sources are
247// scaled up to fill u8 range.
248//
249// u16 uses full 16 bits. f32 output is clamped to [0.0, 1.0], no overshoot.
250
251static PNG_FORMATS: &[FormatEntry] = &[
252    FormatEntry::standard(PixelDescriptor::RGB8_SRGB),
253    FormatEntry::standard(PixelDescriptor::RGBA8_SRGB),
254    FormatEntry::standard(PixelDescriptor::GRAY8_SRGB),
255    FormatEntry::standard(PixelDescriptor::GRAYA8_SRGB),
256    FormatEntry::standard(PixelDescriptor::BGRA8_SRGB),
257    FormatEntry::standard(PixelDescriptor::RGB16_SRGB),
258    FormatEntry::standard(PixelDescriptor::RGBA16_SRGB),
259    FormatEntry::standard(PixelDescriptor::GRAY16_SRGB),
260    FormatEntry::standard(PixelDescriptor::GRAYA16_SRGB),
261    FormatEntry::standard(PixelDescriptor::RGBF32_LINEAR),
262    FormatEntry::standard(PixelDescriptor::RGBAF32_LINEAR),
263    FormatEntry::standard(PixelDescriptor::GRAYF32_LINEAR),
264    FormatEntry::standard(PixelDescriptor::GRAYAF32_LINEAR),
265];
266
267pub static PNG: CodecFormats = CodecFormats {
268    name: "png",
269    decode_outputs: PNG_FORMATS,
270    encode_inputs: PNG_FORMATS,
271    icc_decode: true,
272    icc_encode: true,
273    cicp: true,
274};
275
276// ═══════════════════════════════════════════════════════════════════════
277// GIF (zengif)
278// ═══════════════════════════════════════════════════════════════════════
279//
280// Always 8-bit indexed palette. Decode composites frames to RGBA8 natively.
281// f32 decode is a conversion from RGBA8. Encode requires quantization.
282// No ICC or CICP (GIF spec doesn't support color management).
283
284static GIF_DECODE: &[FormatEntry] = &[
285    FormatEntry::with_bits(PixelDescriptor::RGBA8_SRGB, 8),
286    FormatEntry::with_bits(PixelDescriptor::RGB8_SRGB, 8),
287    FormatEntry::with_bits(PixelDescriptor::GRAY8_SRGB, 8),
288    FormatEntry::with_bits(PixelDescriptor::BGRA8_SRGB, 8),
289    // f32 outputs are converted from 8-bit source — effective precision is 8 bits
290    FormatEntry::with_bits(PixelDescriptor::RGBF32_LINEAR, 8),
291    FormatEntry::with_bits(PixelDescriptor::RGBAF32_LINEAR, 8),
292    FormatEntry::with_bits(PixelDescriptor::GRAYF32_LINEAR, 8),
293];
294
295static GIF_ENCODE: &[FormatEntry] = &[
296    FormatEntry::standard(PixelDescriptor::RGBA8_SRGB),
297    FormatEntry::standard(PixelDescriptor::RGB8_SRGB),
298    FormatEntry::standard(PixelDescriptor::GRAY8_SRGB),
299    FormatEntry::standard(PixelDescriptor::BGRA8_SRGB),
300];
301
302pub static GIF: CodecFormats = CodecFormats {
303    name: "gif",
304    decode_outputs: GIF_DECODE,
305    encode_inputs: GIF_ENCODE,
306    icc_decode: false,
307    icc_encode: false,
308    cicp: false,
309};
310
311// ═══════════════════════════════════════════════════════════════════════
312// WebP (zenwebp)
313// ═══════════════════════════════════════════════════════════════════════
314//
315// Only RGB8/RGBA8 via zencodec API. The encoder and decoder internally
316// handle BGRA8, GRAY8, and f32 linear via conversion, but these are not
317// in supported_descriptors(). ICC roundtrip supported.
318
319static WEBP_FORMATS: &[FormatEntry] = &[
320    FormatEntry::standard(PixelDescriptor::RGB8_SRGB),
321    FormatEntry::standard(PixelDescriptor::RGBA8_SRGB),
322];
323
324pub static WEBP: CodecFormats = CodecFormats {
325    name: "webp",
326    decode_outputs: WEBP_FORMATS,
327    encode_inputs: WEBP_FORMATS,
328    icc_decode: true,
329    icc_encode: true,
330    cicp: false,
331};
332
333// ═══════════════════════════════════════════════════════════════════════
334// AVIF (zenavif)
335// ═══════════════════════════════════════════════════════════════════════
336//
337// Decode: AV1 supports 8/10/12-bit, but zenavif always outputs u8 sRGB
338// via zencodec API. 10/12-bit sources are scaled to u16 internally then
339// converted to u8. CICP decoded from container or AV1 config.
340//
341// The effective bits of the u8 output are 8 regardless of AV1 source depth
342// (precision is lost in the u8 conversion).
343
344static AVIF_FORMATS: &[FormatEntry] = &[
345    FormatEntry::with_bits(PixelDescriptor::RGB8_SRGB, 8),
346    FormatEntry::with_bits(PixelDescriptor::RGBA8_SRGB, 8),
347];
348
349pub static AVIF: CodecFormats = CodecFormats {
350    name: "avif",
351    decode_outputs: AVIF_FORMATS,
352    encode_inputs: AVIF_FORMATS,
353    icc_decode: true,
354    icc_encode: false,
355    cicp: true,
356};
357
358// ═══════════════════════════════════════════════════════════════════════
359// JPEG XL (zenjxl)
360// ═══════════════════════════════════════════════════════════════════════
361//
362// JXL supports arbitrary bit depths natively. The zencodec integration
363// decodes to u8 by default, with f32 linear for HDR content.
364// f32 output is clamped to [0.0, 1.0] at encode time.
365// Full ICC roundtrip, CICP via ICC metadata.
366
367static JXL_FORMATS: &[FormatEntry] = &[
368    FormatEntry::standard(PixelDescriptor::RGB8_SRGB),
369    FormatEntry::standard(PixelDescriptor::RGBA8_SRGB),
370    FormatEntry::standard(PixelDescriptor::GRAY8_SRGB),
371    FormatEntry::standard(PixelDescriptor::GRAYA8_SRGB),
372    FormatEntry::standard(PixelDescriptor::BGRA8_SRGB),
373    FormatEntry::standard(PixelDescriptor::RGBF32_LINEAR),
374    FormatEntry::standard(PixelDescriptor::RGBAF32_LINEAR),
375    FormatEntry::standard(PixelDescriptor::GRAYF32_LINEAR),
376    FormatEntry::standard(PixelDescriptor::GRAYAF32_LINEAR),
377];
378
379pub static JXL: CodecFormats = CodecFormats {
380    name: "jxl",
381    decode_outputs: JXL_FORMATS,
382    encode_inputs: JXL_FORMATS,
383    icc_decode: true,
384    icc_encode: true,
385    cicp: true,
386};
387
388// ═══════════════════════════════════════════════════════════════════════
389// BMP (zenbitmaps)
390// ═══════════════════════════════════════════════════════════════════════
391//
392// Native format is BGR/BGRA. Supports 1-32 bit input but always outputs
393// u8 via zencodec. No color management.
394
395static BMP_FORMATS: &[FormatEntry] = &[
396    FormatEntry::standard(PixelDescriptor::RGB8_SRGB),
397    FormatEntry::standard(PixelDescriptor::RGBA8_SRGB),
398    FormatEntry::standard(PixelDescriptor::BGRA8_SRGB),
399];
400
401pub static BMP: CodecFormats = CodecFormats {
402    name: "bmp",
403    decode_outputs: BMP_FORMATS,
404    encode_inputs: BMP_FORMATS,
405    icc_decode: false,
406    icc_encode: false,
407    cicp: false,
408};
409
410// ═══════════════════════════════════════════════════════════════════════
411// Farbfeld (zenbitmaps)
412// ═══════════════════════════════════════════════════════════════════════
413//
414// Always RGBA u16 big-endian on disk. Lossless format, 16 effective bits.
415// Can output u8 and grayscale via conversion.
416
417static FARBFELD_FORMATS: &[FormatEntry] = &[
418    FormatEntry::standard(PixelDescriptor::RGBA16_SRGB),
419    FormatEntry::with_bits(PixelDescriptor::RGBA8_SRGB, 8),
420    FormatEntry::with_bits(PixelDescriptor::RGB8_SRGB, 8),
421    FormatEntry::with_bits(PixelDescriptor::GRAY8_SRGB, 8),
422];
423
424pub static FARBFELD: CodecFormats = CodecFormats {
425    name: "farbfeld",
426    decode_outputs: FARBFELD_FORMATS,
427    encode_inputs: FARBFELD_FORMATS,
428    icc_decode: false,
429    icc_encode: false,
430    cicp: false,
431};
432
433// ═══════════════════════════════════════════════════════════════════════
434// PNM (zenbitmaps)
435// ═══════════════════════════════════════════════════════════════════════
436//
437// Covers PGM (P5), PPM (P6), PAM (P7), and PFM.
438// Variable bit depth: P5/P6/P7 support maxval 1–65535.
439// PFM is f32 [0.0, 1.0]. No color management.
440
441static PNM_FORMATS: &[FormatEntry] = &[
442    FormatEntry::standard(PixelDescriptor::RGB8_SRGB),
443    FormatEntry::standard(PixelDescriptor::RGBA8_SRGB),
444    FormatEntry::standard(PixelDescriptor::RGBA16_SRGB),
445    FormatEntry::standard(PixelDescriptor::GRAY8_SRGB),
446    FormatEntry::standard(PixelDescriptor::GRAYA8_SRGB),
447    FormatEntry::standard(PixelDescriptor::BGRA8_SRGB),
448    FormatEntry::standard(PixelDescriptor::RGBF32_LINEAR),
449    FormatEntry::standard(PixelDescriptor::RGBAF32_LINEAR),
450    FormatEntry::standard(PixelDescriptor::GRAYF32_LINEAR),
451    FormatEntry::standard(PixelDescriptor::GRAYAF32_LINEAR),
452];
453
454pub static PNM: CodecFormats = CodecFormats {
455    name: "pnm",
456    decode_outputs: PNM_FORMATS,
457    encode_inputs: PNM_FORMATS,
458    icc_decode: false,
459    icc_encode: false,
460    cicp: false,
461};
462
463// ═══════════════════════════════════════════════════════════════════════
464// All codecs
465// ═══════════════════════════════════════════════════════════════════════
466
467/// All registered codecs.
468pub static ALL_CODECS: &[&CodecFormats] =
469    &[&JPEG, &PNG, &GIF, &WEBP, &AVIF, &JXL, &BMP, &FARBFELD, &PNM];
470
471#[cfg(test)]
472mod tests {
473    use super::*;
474
475    #[test]
476    fn all_codecs_have_decode_and_encode() {
477        for codec in ALL_CODECS {
478            assert!(
479                !codec.decode_outputs.is_empty(),
480                "{} has no decode outputs",
481                codec.name
482            );
483            assert!(
484                !codec.encode_inputs.is_empty(),
485                "{} has no encode inputs",
486                codec.name
487            );
488        }
489    }
490
491    #[test]
492    fn effective_bits_within_container() {
493        for codec in ALL_CODECS {
494            for entry in codec
495                .decode_outputs
496                .iter()
497                .chain(codec.encode_inputs.iter())
498            {
499                let container_bits = match entry.descriptor.channel_type() {
500                    ChannelType::U8 => 8,
501                    ChannelType::U16 => 16,
502                    ChannelType::F16 => 16,
503                    ChannelType::F32 => 32,
504                    _ => 0,
505                };
506                assert!(
507                    entry.effective_bits <= container_bits,
508                    "{}: effective_bits {} > container {} for {:?}",
509                    codec.name,
510                    entry.effective_bits,
511                    container_bits,
512                    entry.descriptor
513                );
514            }
515        }
516    }
517
518    #[test]
519    fn jpeg_extended_has_overshoot() {
520        for entry in JPEG_DECODE_EXTENDED {
521            assert!(
522                entry.can_overshoot,
523                "JPEG extended f32 decode should have overshoot"
524            );
525        }
526    }
527
528    #[test]
529    fn codec_count() {
530        assert_eq!(ALL_CODECS.len(), 9);
531    }
532}