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}