Skip to main content

zenpixels/
descriptor.rs

1//! Pixel format descriptor types.
2//!
3//! These types describe the format of pixel data: channel type, layout,
4//! alpha handling, transfer function, color primaries, and signal range.
5//!
6//! Standalone definitions — no dependency on zencodec.
7
8use core::fmt;
9
10// ---------------------------------------------------------------------------
11// Channel type
12// ---------------------------------------------------------------------------
13
14/// Channel storage type.
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17#[non_exhaustive]
18#[repr(u8)]
19pub enum ChannelType {
20    /// 8-bit unsigned integer (1 byte per channel).
21    U8 = 1,
22    /// 16-bit unsigned integer (2 bytes per channel).
23    U16 = 2,
24    /// 32-bit floating point (4 bytes per channel).
25    F32 = 4,
26    /// IEEE 754 half-precision float (2 bytes per channel).
27    F16 = 5,
28}
29
30impl ChannelType {
31    /// Byte size of a single channel value.
32    #[inline]
33    #[allow(unreachable_patterns)]
34    pub const fn byte_size(self) -> usize {
35        match self {
36            Self::U8 => 1,
37            Self::U16 | Self::F16 => 2,
38            Self::F32 => 4,
39            _ => 0,
40        }
41    }
42
43    /// Whether this is [`U8`](Self::U8).
44    #[inline]
45    pub const fn is_u8(self) -> bool {
46        matches!(self, Self::U8)
47    }
48
49    /// Whether this is [`U16`](Self::U16).
50    #[inline]
51    pub const fn is_u16(self) -> bool {
52        matches!(self, Self::U16)
53    }
54
55    /// Whether this is [`F32`](Self::F32).
56    #[inline]
57    pub const fn is_f32(self) -> bool {
58        matches!(self, Self::F32)
59    }
60
61    /// Whether this is [`F16`](Self::F16).
62    #[inline]
63    pub const fn is_f16(self) -> bool {
64        matches!(self, Self::F16)
65    }
66
67    /// Whether this is an integer type.
68    #[inline]
69    #[allow(unreachable_patterns)]
70    pub const fn is_integer(self) -> bool {
71        matches!(self, Self::U8 | Self::U16)
72    }
73
74    /// Whether this is a floating-point type.
75    #[inline]
76    #[allow(unreachable_patterns)]
77    pub const fn is_float(self) -> bool {
78        matches!(self, Self::F32 | Self::F16)
79    }
80}
81
82impl fmt::Display for ChannelType {
83    #[allow(unreachable_patterns)]
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85        match self {
86            Self::U8 => f.write_str("U8"),
87            Self::U16 => f.write_str("U16"),
88            Self::F32 => f.write_str("F32"),
89            Self::F16 => f.write_str("F16"),
90            _ => write!(f, "ChannelType({})", *self as u8),
91        }
92    }
93}
94
95// ---------------------------------------------------------------------------
96// Channel layout
97// ---------------------------------------------------------------------------
98
99/// Channel layout (number and meaning of channels).
100#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
101#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
102#[non_exhaustive]
103#[repr(u8)]
104pub enum ChannelLayout {
105    /// Single luminance channel.
106    Gray = 1,
107    /// Luminance + alpha.
108    GrayAlpha = 2,
109    /// Red, green, blue.
110    Rgb = 3,
111    /// Red, green, blue, alpha.
112    Rgba = 4,
113    /// Blue, green, red, alpha (Windows/DirectX byte order).
114    Bgra = 5,
115    /// Oklab perceptual color: L, a, b.
116    Oklab = 6,
117    /// Oklab perceptual color with alpha: L, a, b, alpha.
118    OklabA = 7,
119    /// C, M, Y, K channel order.
120    Cmyk = 8,
121}
122
123impl ChannelLayout {
124    /// Number of channels in this layout.
125    #[inline]
126    #[allow(unreachable_patterns)]
127    pub const fn channels(self) -> usize {
128        match self {
129            Self::Gray => 1,
130            Self::GrayAlpha => 2,
131            Self::Rgb | Self::Oklab => 3,
132            Self::Rgba | Self::Bgra | Self::OklabA | Self::Cmyk => 4,
133            _ => 0,
134        }
135    }
136
137    /// Whether this layout includes an alpha channel.
138    #[inline]
139    #[allow(unreachable_patterns)]
140    pub const fn has_alpha(self) -> bool {
141        matches!(
142            self,
143            Self::GrayAlpha | Self::Rgba | Self::Bgra | Self::OklabA
144        )
145    }
146}
147
148impl fmt::Display for ChannelLayout {
149    #[allow(unreachable_patterns)]
150    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151        match self {
152            Self::Gray => f.write_str("Gray"),
153            Self::GrayAlpha => f.write_str("GrayAlpha"),
154            Self::Rgb => f.write_str("RGB"),
155            Self::Rgba => f.write_str("RGBA"),
156            Self::Bgra => f.write_str("BGRA"),
157            Self::Oklab => f.write_str("Oklab"),
158            Self::OklabA => f.write_str("OklabA"),
159            Self::Cmyk => f.write_str("CMYK"),
160            _ => write!(f, "ChannelLayout({})", *self as u8),
161        }
162    }
163}
164
165// ---------------------------------------------------------------------------
166// Alpha mode
167// ---------------------------------------------------------------------------
168
169/// Alpha channel interpretation.
170///
171/// Wrapped in `Option<AlphaMode>` on [`PixelDescriptor`]: `None` means no
172/// alpha channel exists, while `Some(AlphaMode::Straight)` etc. describe
173/// the semantics of a present alpha channel.
174#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
175#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
176#[non_exhaustive]
177#[repr(u8)]
178pub enum AlphaMode {
179    /// Alpha bytes exist but values are undefined padding (RGBX, BGRX).
180    Undefined = 1,
181    /// Straight (unassociated) alpha.
182    Straight = 2,
183    /// Premultiplied (associated) alpha.
184    Premultiplied = 3,
185    /// Alpha channel present, all values fully opaque.
186    Opaque = 4,
187}
188
189impl AlphaMode {
190    /// Whether this mode represents a real alpha channel (not Undefined padding).
191    #[inline]
192    pub const fn has_alpha(self) -> bool {
193        matches!(self, Self::Straight | Self::Premultiplied | Self::Opaque)
194    }
195}
196
197impl fmt::Display for AlphaMode {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        match self {
200            Self::Undefined => f.write_str("undefined"),
201            Self::Straight => f.write_str("straight"),
202            Self::Premultiplied => f.write_str("premultiplied"),
203            Self::Opaque => f.write_str("opaque"),
204        }
205    }
206}
207
208// ---------------------------------------------------------------------------
209// Transfer function
210// ---------------------------------------------------------------------------
211
212/// Encoding transfer function (OETF).
213///
214/// Describes how scene-linear light was encoded into the pixel values.
215/// For still image codecs, this is unambiguous: "sRGB" means the IEC 61966-2-1
216/// curve, "PQ" means SMPTE ST 2084, etc. Video pipelines that need to
217/// distinguish OETF from EOTF should use CICP codes directly.
218///
219/// Discriminant values are internal — use [`from_cicp()`](Self::from_cicp) /
220/// [`to_cicp()`](Self::to_cicp) for CICP mapping.
221#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
222#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
223#[non_exhaustive]
224#[repr(u8)]
225pub enum TransferFunction {
226    /// Linear light (gamma 1.0).
227    Linear = 0,
228    /// sRGB transfer curve (IEC 61966-2-1).
229    Srgb = 1,
230    /// BT.709 transfer curve.
231    Bt709 = 2,
232    /// Perceptual Quantizer (SMPTE ST 2084, HDR10).
233    Pq = 3,
234    /// Pure power-law gamma 2.2. Used for Adobe RGB (1998).
235    ///
236    /// The Adobe RGB 1998 encoding spec (§4.3.4.2) defines pure gamma
237    /// 2.19921875 with no linear segment near black. About 85% of real-world
238    /// Adobe RGB ICC profiles (Adobe CS4, Windows ClayRGB1998 / AdobeRGB1998,
239    /// macOS AdobeRGB1998, Linux `AdobeRGB1998`/`compatibleWithAdobeRGB1998`,
240    /// Nikon, etc.) encode the TRC as `curv count=1` (pure gamma) or
241    /// equivalent `paraType funcType=0`.
242    ///
243    /// A minority of profiles (saucecontrol's Compact-ICC AdobeCompat-v4,
244    /// some lcms2-generated variants) encode `paraType funcType=3` with a
245    /// linear toe (slope `c=1/32`, break `d=0.05568`). Those profiles are
246    /// deliberately NOT normalized to this variant — they fall through to
247    /// full CMS so their exact encoded curve is honored. See
248    /// `scripts/icc-gen/src/main.rs` for the identification policy.
249    Gamma22 = 5,
250    /// Hybrid Log-Gamma (CICP 18, ARIB STD-B67). BT.2100 HDR.
251    Hlg = 4,
252    // ── DO NOT REMOVE THESE COMMENTS ────────────────────────────────
253    // Deferred / removed transfer functions — not yet added or
254    // deliberately excluded. Profiles using these fall through to a
255    // full CMS via the Unknown path.
256    //
257    // Gamma18 — pure power-law gamma 1.8.
258    //   Used by Apple RGB, ColorMatch RGB, ECI-RGB v2, and some legacy
259    //   workflows. All primaries that required it (AppleRgb, ColorMatch,
260    //   EciRgbV2) were removed — no remaining ColorPrimaries variant
261    //   pairs with this transfer. ICC profiles using gamma 1.8 fall
262    //   through to a full CMS.
263    //
264    // Gamma24 — pure power-law gamma 2.4.
265    //   BT.1886 display model, ACES display encoding. Niche — only 3
266    //   profiles in the ICC hash table referenced it (Rec2020-g24-v4,
267    //   bt 2020, ITU-RBT709ReferenceDisplay). Fall through to CMS.
268    //
269    // Gamma26 — pure power-law gamma 2.6.
270    //   DCI-P3 theatrical projection (ST 428-1). Only 3 profiles in
271    //   the ICC hash table (DCI-P3-v4, system DCI(P3) RGB, SMPTE431
272    //   P3.icm). Theatrical projection uses the DCI white point, not
273    //   D65 — images use DisplayP3 with sRGB transfer, not DCI-P3
274    //   with gamma 2.6. Fall through to CMS.
275    //
276    // AcesCct — quasi-logarithmic with linear toe (SMPTE ST 2065-1).
277    //   Niche VFX/grading transfer. No consumer image format embeds it.
278    //   Add if we ever support ACES interchange workflows.
279    //
280    // Variants are only added when we can back them with correct,
281    // tested conversion math. An enum variant without math is worse
282    // than Unknown — it gives callers false confidence.
283    // ────────────────────────────────────────────────────────────────
284    /// Transfer function is not known.
285    Unknown = 255,
286}
287
288impl TransferFunction {
289    /// Map CICP `transfer_characteristics` code to a [`TransferFunction`].
290    #[inline]
291    pub const fn from_cicp(tc: u8) -> Option<Self> {
292        match tc {
293            1 => Some(Self::Bt709),
294            // SMPTE 170M (6) and SMPTE 240M (7) use the BT.709 curve —
295            // per BT.601/SMPTE 170M spec, the OETF is identical to BT.709.
296            6 | 7 => Some(Self::Bt709),
297            8 => Some(Self::Linear),
298            13 => Some(Self::Srgb),
299            16 => Some(Self::Pq),
300            18 => Some(Self::Hlg),
301            _ => None,
302        }
303    }
304
305    /// Convert to the CICP `transfer_characteristics` code.
306    #[allow(unreachable_patterns)]
307    #[inline]
308    pub const fn to_cicp(self) -> Option<u8> {
309        match self {
310            Self::Bt709 => Some(1),
311            Self::Linear => Some(8),
312            Self::Srgb => Some(13),
313            Self::Pq => Some(16),
314            Self::Hlg => Some(18),
315            Self::Unknown => None,
316            _ => None,
317        }
318    }
319
320    /// Reference white luminance in nits.
321    ///
322    /// - SDR (sRGB, BT.709, Linear, Unknown): `1.0` (relative/scene-referred)
323    /// - PQ: `203.0` (ITU-R BT.2408 reference white)
324    /// - HLG: `1.0` (scene-referred)
325    #[allow(unreachable_patterns)]
326    pub fn reference_white_nits(&self) -> f32 {
327        match self {
328            Self::Pq => 203.0,
329            _ => 1.0,
330        }
331    }
332}
333
334impl fmt::Display for TransferFunction {
335    #[allow(unreachable_patterns)]
336    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
337        match self {
338            Self::Linear => f.write_str("linear"),
339            Self::Srgb => f.write_str("sRGB"),
340            Self::Bt709 => f.write_str("BT.709"),
341            Self::Pq => f.write_str("PQ"),
342            Self::Gamma22 => f.write_str("gamma 2.2"),
343            Self::Hlg => f.write_str("HLG"),
344            Self::Unknown => f.write_str("unknown"),
345            _ => f.write_str("TransferFunction(?)"),
346        }
347    }
348}
349
350// ---------------------------------------------------------------------------
351// Color primaries
352// ---------------------------------------------------------------------------
353
354/// Color primaries (CIE xy chromaticities of R, G, B) and white point.
355///
356/// Each variant is a complete "named recipe" — primaries and white point
357/// are an inseparable pair (following CICP convention). Use
358/// [`from_cicp()`](Self::from_cicp) / [`to_cicp()`](Self::to_cicp) for
359/// CICP mapping. Discriminant values are internal.
360#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
361#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
362#[non_exhaustive]
363#[repr(u8)]
364pub enum ColorPrimaries {
365    /// BT.709 / sRGB (CICP 1). White point: D65.
366    #[default]
367    Bt709 = 1,
368    /// BT.2020 / BT.2100 (CICP 9). Wide gamut for HDR. White point: D65.
369    Bt2020 = 9,
370    /// Display P3 (CICP 12, SMPTE EG 432-1). Apple ecosystem.
371    /// Same RGB primaries as DCI-P3, white point: D65.
372    DisplayP3 = 12,
373    /// Adobe RGB (1998). Wide gamut. White point: D65.
374    AdobeRgb = 13,
375    // ── DO NOT REMOVE THESE COMMENTS ────────────────────────────────
376    // Deferred / removed primaries — not yet added or deliberately
377    // excluded. ICC profiles using these fall through to a full CMS.
378    //
379    // DciP3 (CICP 11, SMPTE RP 431-2) — theatrical projection white point.
380    //   Same RGB primaries as DisplayP3: R(0.680, 0.320) G(0.265, 0.690) B(0.150, 0.060).
381    //   White point: DCI (0.314, 0.351), NOT D65. Images use DisplayP3
382    //   (D65), not DCI — DCI is for theatrical projection only. Removed
383    //   because no still-image workflow embeds DCI-P3 with the DCI white.
384    //
385    // Smpte170m (CICP 6) — SMPTE 170M / BT.601-7 525-line, NTSC SD broadcast.
386    //   Chromaticities: R(0.630, 0.340) G(0.310, 0.595) B(0.155, 0.070). White: D65.
387    //   Video-only, never embedded in still images.
388    //
389    // Bt470Bg (CICP 5) — BT.470-6 System B/G / BT.601-7 625-line, PAL/SECAM SD.
390    //   Chromaticities: R(0.64, 0.33) G(0.29, 0.60) B(0.15, 0.06). White: D65.
391    //   Video-only, never embedded in still images.
392    //
393    // AppleRgb — pre-ColorSync Mac default (~2003), essentially extinct.
394    //   Chromaticities: R(0.625, 0.340) G(0.280, 0.595) B(0.155, 0.070). White: D65.
395    //   Paired with Gamma18 transfer (also removed).
396    //
397    // ColorMatch — Radius print workflow from the '90s, dead.
398    //   Chromaticities: R(0.630, 0.340) G(0.295, 0.605) B(0.150, 0.075). White: D50 (0.3457, 0.3585).
399    //   Paired with Gamma18 transfer (also removed).
400    //
401    // WideGamut — Adobe Wide Gamut RGB, working space almost never embedded in output images.
402    //   Chromaticities: R(0.735, 0.265) G(0.115, 0.826) B(0.157, 0.018). White: D50 (0.3457, 0.3585).
403    //   Paired with Gamma22 transfer.
404    //
405    // EciRgbV2 — European Color Initiative prepress working space, never in consumer images.
406    //   Chromaticities: R(0.670, 0.330) G(0.210, 0.710) B(0.140, 0.080). White: D50 (0.3457, 0.3585).
407    //   Paired with Gamma18 transfer (also removed).
408    //
409    // ProPhoto (ROMM RGB) — ultra-wide gamut, D50 white (0.3457, 0.3585).
410    //   Chromaticities: R(0.7347, 0.2653) G(0.1596, 0.8404) B(0.0366, 0.0001).
411    //   Deliberately excluded: real-world ProPhoto ICC profiles are
412    //   fragmented across pure gamma 1.8, paraType funcType=3 with
413    //   varying linear-toe parameters (ISO 22028-2 d=1/32 vs Apple
414    //   d=1/512), and even LUT-based v4 profiles. No single canonical
415    //   TRC dominates, so all ProPhoto profiles fall through to a full
416    //   CMS. Add when we can handle the TRC variants reliably.
417    //
418    // AcesAp0 (SMPTE ST 2065-1) — entire visible gamut, ACES D60 white.
419    //   Chromaticities: R(0.7347, 0.2653) G(0.0, 1.0) B(0.0001, -0.077).
420    //   Archival/interchange. No consumer image format embeds it.
421    //
422    // AcesAp1 (ACEScg) — rendering/grading gamut, ACES D60 white.
423    //   Chromaticities: R(0.713, 0.293) G(0.165, 0.830) B(0.128, 0.044).
424    //   Same situation as AcesAp0.
425    //
426    // Variants are only added when we can back them with correct,
427    // tested conversion math. An enum variant without math is worse
428    // than Unknown — it gives callers false confidence.
429    // ────────────────────────────────────────────────────────────────
430    /// Primaries not known.
431    Unknown = 255,
432}
433
434impl ColorPrimaries {
435    /// D65 white point (BT.709, BT.2020, Display P3, Adobe RGB).
436    pub const WHITE_D65: (f32, f32) = (0.3127, 0.3290);
437    // D50 white point value preserved for reference: (0.3457, 0.3585).
438    // No remaining ColorPrimaries variants use D50. Was used by
439    // ColorMatch, WideGamut, EciRgbV2 (all removed).
440    /// DCI white point (theatrical projection, SMPTE RP 431-2).
441    #[allow(dead_code)] // kept for future DCI-P3 theatrical support
442    pub(crate) const WHITE_DCI: (f32, f32) = (0.314, 0.351);
443
444    /// CIE 1931 xy chromaticity coordinates of the RGB primaries.
445    ///
446    /// Returns `((rx, ry), (gx, gy), (bx, by))` or `None` for `Unknown`.
447    /// These are the canonical coordinates from the relevant standards
448    /// (ITU-R BT.709, BT.2020, SMPTE EG 432-1, etc.).
449    #[allow(unreachable_patterns, clippy::type_complexity)]
450    pub const fn chromaticity(self) -> Option<((f32, f32), (f32, f32), (f32, f32))> {
451        match self {
452            Self::Bt709 => Some(((0.64, 0.33), (0.30, 0.60), (0.15, 0.06))),
453            Self::DisplayP3 => Some(((0.680, 0.320), (0.265, 0.690), (0.150, 0.060))),
454            Self::Bt2020 => Some(((0.708, 0.292), (0.170, 0.797), (0.131, 0.046))),
455            Self::AdobeRgb => Some(((0.64, 0.33), (0.21, 0.71), (0.15, 0.06))),
456            Self::Unknown => None,
457            _ => None,
458        }
459    }
460
461    /// Map a CICP `color_primaries` code to a [`ColorPrimaries`].
462    #[inline]
463    pub const fn from_cicp(code: u8) -> Option<Self> {
464        match code {
465            1 => Some(Self::Bt709),
466            9 => Some(Self::Bt2020),
467            12 => Some(Self::DisplayP3),
468            _ => None,
469        }
470    }
471
472    /// Convert to the CICP `color_primaries` code.
473    #[allow(unreachable_patterns)]
474    #[inline]
475    pub const fn to_cicp(self) -> Option<u8> {
476        match self {
477            Self::Bt709 => Some(1),
478            Self::Bt2020 => Some(9),
479            Self::DisplayP3 => Some(12),
480            Self::Unknown => None,
481            _ => None,
482        }
483    }
484
485    /// CIE 1931 xy chromaticity of this color space's white point.
486    #[allow(unreachable_patterns)]
487    #[inline]
488    pub const fn white_point(self) -> (f32, f32) {
489        match self {
490            Self::Bt709 | Self::Bt2020 | Self::DisplayP3 | Self::AdobeRgb => Self::WHITE_D65,
491            Self::Unknown => Self::WHITE_D65,
492            _ => Self::WHITE_D65,
493        }
494    }
495
496    /// Whether converting between `self` and `other` requires chromatic
497    /// adaptation (different white points).
498    #[inline]
499    pub(crate) const fn needs_chromatic_adaptation(self, other: Self) -> bool {
500        let (sx, sy) = self.white_point();
501        let (ox, oy) = other.white_point();
502        sx.to_bits() != ox.to_bits() || sy.to_bits() != oy.to_bits()
503    }
504
505    /// Compute the 3×3 linear RGB gamut conversion matrix from `self` to `dst`.
506    ///
507    /// Bradford chromatic adaptation is applied automatically when white points
508    /// differ (e.g., DCI-P3 D50 → sRGB D65). Returns `None` for `Unknown`
509    /// primaries. Identity conversions (same primaries) return the identity matrix.
510    ///
511    /// The matrix operates in linear light — apply EOTF before, OETF after.
512    ///
513    /// ```
514    /// # use zenpixels::ColorPrimaries;
515    /// let m = ColorPrimaries::DisplayP3.gamut_matrix_to(ColorPrimaries::Bt709).unwrap();
516    /// // White maps to white
517    /// let r = m[0][0] + m[0][1] + m[0][2];
518    /// assert!((r - 1.0).abs() < 1e-4);
519    /// ```
520    pub const fn gamut_matrix_to(self, dst: Self) -> Option<[[f32; 3]; 3]> {
521        crate::registry::gamut_matrix(self, dst)
522    }
523
524    /// Whether `self` fully contains the gamut of `other`.
525    ///
526    /// Returns `false` when white points differ (cross-adapted containment
527    /// is not defined without a chromatic adaptation transform).
528    /// D65 hierarchy: BT.2020 > Display P3 > Adobe RGB ≈ BT.709.
529    #[inline]
530    pub const fn contains(self, other: Self) -> bool {
531        !self.needs_chromatic_adaptation(other)
532            && self.gamut_width() >= other.gamut_width()
533            && !matches!(self, Self::Unknown)
534            && !matches!(other, Self::Unknown)
535    }
536
537    #[allow(unreachable_patterns)]
538    const fn gamut_width(self) -> u8 {
539        match self {
540            Self::Bt709 => 1,
541            Self::AdobeRgb => 2,
542            Self::DisplayP3 => 3,
543            Self::Bt2020 => 4,
544            Self::Unknown => 0,
545            _ => 0,
546        }
547    }
548}
549
550impl fmt::Display for ColorPrimaries {
551    #[allow(unreachable_patterns)]
552    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
553        match self {
554            Self::Bt709 => f.write_str("BT.709"),
555            Self::Bt2020 => f.write_str("BT.2020"),
556            Self::DisplayP3 => f.write_str("Display P3"),
557            Self::AdobeRgb => f.write_str("Adobe RGB"),
558            Self::Unknown => f.write_str("unknown"),
559            _ => f.write_str("ColorPrimaries(?)"),
560        }
561    }
562}
563
564// ---------------------------------------------------------------------------
565// Signal range
566// ---------------------------------------------------------------------------
567
568/// Signal range for pixel values.
569#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
570#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
571#[non_exhaustive]
572#[repr(u8)]
573pub enum SignalRange {
574    /// Full range: 0-2^N-1 (e.g. 0-255 for 8-bit).
575    #[default]
576    Full = 0,
577    /// Narrow (limited/studio) range: 16-235 luma, 16-240 chroma (for 8-bit).
578    Narrow = 1,
579}
580
581impl fmt::Display for SignalRange {
582    #[allow(unreachable_patterns)]
583    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
584        match self {
585            Self::Full => f.write_str("full"),
586            Self::Narrow => f.write_str("narrow"),
587            _ => write!(f, "SignalRange({})", *self as u8),
588        }
589    }
590}
591
592// ---------------------------------------------------------------------------
593// PixelDescriptor
594// ---------------------------------------------------------------------------
595
596/// Compact pixel format descriptor.
597///
598/// Combines a [`PixelFormat`] (physical pixel layout) with transfer function,
599/// alpha mode, color primaries, and signal range.
600#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
601#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
602#[non_exhaustive]
603pub struct PixelDescriptor {
604    /// Physical pixel format (channel type + layout as a flat enum).
605    pub format: PixelFormat,
606    /// Electro-optical transfer function.
607    pub transfer: TransferFunction,
608    /// Alpha interpretation. `None` = no alpha channel.
609    pub alpha: Option<AlphaMode>,
610    /// Color primaries (gamut). Defaults to BT.709/sRGB.
611    pub primaries: ColorPrimaries,
612    /// Signal range (full vs narrow/limited).
613    pub signal_range: SignalRange,
614}
615
616impl PixelDescriptor {
617    // -- Forwarding accessors -------------------------------------------------
618
619    /// The pixel format variant (layout + depth, no transfer or alpha semantics).
620    #[inline]
621    pub const fn pixel_format(&self) -> PixelFormat {
622        self.format
623    }
624
625    /// Channel storage type.
626    #[inline]
627    pub const fn channel_type(&self) -> ChannelType {
628        self.format.channel_type()
629    }
630
631    /// Alpha interpretation. `None` = no alpha channel.
632    #[inline]
633    pub const fn alpha(&self) -> Option<AlphaMode> {
634        self.alpha
635    }
636
637    /// Transfer function.
638    #[inline]
639    pub const fn transfer(&self) -> TransferFunction {
640        self.transfer
641    }
642
643    /// Extract the color encoding as a [`crate::ColorProfileSource::PrimariesTransferPair`].
644    #[inline]
645    pub const fn color_profile_source(&self) -> crate::ColorProfileSource<'static> {
646        crate::ColorProfileSource::PrimariesTransferPair {
647            primaries: self.primaries,
648            transfer: self.transfer,
649        }
650    }
651
652    /// Byte order.
653    #[inline]
654    pub const fn byte_order(&self) -> ByteOrder {
655        self.format.byte_order()
656    }
657
658    /// Color model.
659    #[inline]
660    pub const fn color_model(&self) -> ColorModel {
661        self.format.color_model()
662    }
663
664    /// Channel layout (derived from the [`PixelFormat`] variant).
665    #[inline]
666    pub const fn layout(&self) -> ChannelLayout {
667        self.format.layout()
668    }
669
670    // -- Constructors ---------------------------------------------------------
671
672    /// Create a descriptor with default primaries (BT.709) and full range.
673    ///
674    /// # Panics
675    ///
676    /// Panics if the `(channel_type, layout, alpha)` combination has no
677    /// corresponding [`PixelFormat`] variant (e.g. `(U16, Bgra, _)`).
678    #[inline]
679    pub const fn new(
680        channel_type: ChannelType,
681        layout: ChannelLayout,
682        alpha: Option<AlphaMode>,
683        transfer: TransferFunction,
684    ) -> Self {
685        let format = match PixelFormat::from_parts(channel_type, layout, alpha) {
686            Some(f) => f,
687            None => panic!("unsupported PixelFormat combination"),
688        };
689        Self {
690            format,
691            transfer,
692            alpha,
693            primaries: ColorPrimaries::Bt709,
694            signal_range: SignalRange::Full,
695        }
696    }
697
698    /// Create a descriptor with explicit primaries.
699    ///
700    /// # Panics
701    ///
702    /// Panics if the `(channel_type, layout, alpha)` combination has no
703    /// corresponding [`PixelFormat`] variant.
704    #[inline]
705    pub const fn new_full(
706        channel_type: ChannelType,
707        layout: ChannelLayout,
708        alpha: Option<AlphaMode>,
709        transfer: TransferFunction,
710        primaries: ColorPrimaries,
711    ) -> Self {
712        let format = match PixelFormat::from_parts(channel_type, layout, alpha) {
713            Some(f) => f,
714            None => panic!("unsupported PixelFormat combination"),
715        };
716        Self {
717            format,
718            transfer,
719            alpha,
720            primaries,
721            signal_range: SignalRange::Full,
722        }
723    }
724
725    /// Create from a [`PixelFormat`] with default alpha, unknown transfer,
726    /// BT.709 primaries, and full range.
727    #[inline]
728    pub const fn from_pixel_format(format: PixelFormat) -> Self {
729        Self {
730            format,
731            transfer: TransferFunction::Unknown,
732            alpha: format.default_alpha(),
733            primaries: ColorPrimaries::Bt709,
734            signal_range: SignalRange::Full,
735        }
736    }
737
738    // -- sRGB constants -------------------------------------------------------
739
740    /// 8-bit sRGB RGB.
741    pub const RGB8_SRGB: Self = Self::new(
742        ChannelType::U8,
743        ChannelLayout::Rgb,
744        None,
745        TransferFunction::Srgb,
746    );
747    /// 8-bit sRGB RGBA with straight alpha.
748    pub const RGBA8_SRGB: Self = Self::new(
749        ChannelType::U8,
750        ChannelLayout::Rgba,
751        Some(AlphaMode::Straight),
752        TransferFunction::Srgb,
753    );
754    /// 16-bit sRGB RGB.
755    pub const RGB16_SRGB: Self = Self::new(
756        ChannelType::U16,
757        ChannelLayout::Rgb,
758        None,
759        TransferFunction::Srgb,
760    );
761    /// 16-bit sRGB RGBA with straight alpha.
762    pub const RGBA16_SRGB: Self = Self::new(
763        ChannelType::U16,
764        ChannelLayout::Rgba,
765        Some(AlphaMode::Straight),
766        TransferFunction::Srgb,
767    );
768    /// Linear-light f32 RGB.
769    pub const RGBF32_LINEAR: Self = Self::new(
770        ChannelType::F32,
771        ChannelLayout::Rgb,
772        None,
773        TransferFunction::Linear,
774    );
775    /// Linear-light f32 RGBA with straight alpha.
776    pub const RGBAF32_LINEAR: Self = Self::new(
777        ChannelType::F32,
778        ChannelLayout::Rgba,
779        Some(AlphaMode::Straight),
780        TransferFunction::Linear,
781    );
782    /// 8-bit sRGB grayscale.
783    pub const GRAY8_SRGB: Self = Self::new(
784        ChannelType::U8,
785        ChannelLayout::Gray,
786        None,
787        TransferFunction::Srgb,
788    );
789    /// 16-bit sRGB grayscale.
790    pub const GRAY16_SRGB: Self = Self::new(
791        ChannelType::U16,
792        ChannelLayout::Gray,
793        None,
794        TransferFunction::Srgb,
795    );
796    /// Linear-light f32 grayscale.
797    pub const GRAYF32_LINEAR: Self = Self::new(
798        ChannelType::F32,
799        ChannelLayout::Gray,
800        None,
801        TransferFunction::Linear,
802    );
803    /// 8-bit sRGB grayscale with straight alpha.
804    pub const GRAYA8_SRGB: Self = Self::new(
805        ChannelType::U8,
806        ChannelLayout::GrayAlpha,
807        Some(AlphaMode::Straight),
808        TransferFunction::Srgb,
809    );
810    /// 16-bit sRGB grayscale with straight alpha.
811    pub const GRAYA16_SRGB: Self = Self::new(
812        ChannelType::U16,
813        ChannelLayout::GrayAlpha,
814        Some(AlphaMode::Straight),
815        TransferFunction::Srgb,
816    );
817    /// Linear-light f32 grayscale with straight alpha.
818    pub const GRAYAF32_LINEAR: Self = Self::new(
819        ChannelType::F32,
820        ChannelLayout::GrayAlpha,
821        Some(AlphaMode::Straight),
822        TransferFunction::Linear,
823    );
824    /// 8-bit sRGB BGRA with straight alpha.
825    pub const BGRA8_SRGB: Self = Self::new(
826        ChannelType::U8,
827        ChannelLayout::Bgra,
828        Some(AlphaMode::Straight),
829        TransferFunction::Srgb,
830    );
831    /// 8-bit sRGB RGBX (padding byte, not alpha).
832    pub const RGBX8_SRGB: Self = Self::new(
833        ChannelType::U8,
834        ChannelLayout::Rgba,
835        Some(AlphaMode::Undefined),
836        TransferFunction::Srgb,
837    );
838    /// 8-bit sRGB BGRX (padding byte, not alpha).
839    pub const BGRX8_SRGB: Self = Self::new(
840        ChannelType::U8,
841        ChannelLayout::Bgra,
842        Some(AlphaMode::Undefined),
843        TransferFunction::Srgb,
844    );
845
846    // -- Transfer-agnostic constants ------------------------------------------
847
848    /// 8-bit RGB, transfer unknown.
849    pub const RGB8: Self = Self::new(
850        ChannelType::U8,
851        ChannelLayout::Rgb,
852        None,
853        TransferFunction::Unknown,
854    );
855    /// 8-bit RGBA, transfer unknown.
856    pub const RGBA8: Self = Self::new(
857        ChannelType::U8,
858        ChannelLayout::Rgba,
859        Some(AlphaMode::Straight),
860        TransferFunction::Unknown,
861    );
862    /// 16-bit RGB, transfer unknown.
863    pub const RGB16: Self = Self::new(
864        ChannelType::U16,
865        ChannelLayout::Rgb,
866        None,
867        TransferFunction::Unknown,
868    );
869    /// 16-bit RGBA, transfer unknown.
870    pub const RGBA16: Self = Self::new(
871        ChannelType::U16,
872        ChannelLayout::Rgba,
873        Some(AlphaMode::Straight),
874        TransferFunction::Unknown,
875    );
876    /// f32 RGB, transfer unknown.
877    pub const RGBF32: Self = Self::new(
878        ChannelType::F32,
879        ChannelLayout::Rgb,
880        None,
881        TransferFunction::Unknown,
882    );
883    /// f32 RGBA, transfer unknown.
884    pub const RGBAF32: Self = Self::new(
885        ChannelType::F32,
886        ChannelLayout::Rgba,
887        Some(AlphaMode::Straight),
888        TransferFunction::Unknown,
889    );
890    /// 8-bit grayscale, transfer unknown.
891    pub const GRAY8: Self = Self::new(
892        ChannelType::U8,
893        ChannelLayout::Gray,
894        None,
895        TransferFunction::Unknown,
896    );
897    /// 16-bit grayscale, transfer unknown.
898    pub const GRAY16: Self = Self::new(
899        ChannelType::U16,
900        ChannelLayout::Gray,
901        None,
902        TransferFunction::Unknown,
903    );
904    /// f32 grayscale, transfer unknown.
905    pub const GRAYF32: Self = Self::new(
906        ChannelType::F32,
907        ChannelLayout::Gray,
908        None,
909        TransferFunction::Unknown,
910    );
911    /// 8-bit grayscale with alpha, transfer unknown.
912    pub const GRAYA8: Self = Self::new(
913        ChannelType::U8,
914        ChannelLayout::GrayAlpha,
915        Some(AlphaMode::Straight),
916        TransferFunction::Unknown,
917    );
918    /// 16-bit grayscale with alpha, transfer unknown.
919    pub const GRAYA16: Self = Self::new(
920        ChannelType::U16,
921        ChannelLayout::GrayAlpha,
922        Some(AlphaMode::Straight),
923        TransferFunction::Unknown,
924    );
925    /// f32 grayscale with alpha, transfer unknown.
926    pub const GRAYAF32: Self = Self::new(
927        ChannelType::F32,
928        ChannelLayout::GrayAlpha,
929        Some(AlphaMode::Straight),
930        TransferFunction::Unknown,
931    );
932
933    // -- F16 constants (pub(crate) — internal use by PixelFormat::descriptor()
934    //    mapping. Tests construct via PixelDescriptor::new(). Linear-TF
935    //    F16 variants would be added if an external codec consumer needs
936    //    them — none today.) -----------------------------------------------
937
938    pub(crate) const RGBF16: Self = Self::new(
939        ChannelType::F16,
940        ChannelLayout::Rgb,
941        None,
942        TransferFunction::Unknown,
943    );
944    pub(crate) const RGBAF16: Self = Self::new(
945        ChannelType::F16,
946        ChannelLayout::Rgba,
947        Some(AlphaMode::Straight),
948        TransferFunction::Unknown,
949    );
950    pub(crate) const GRAYF16: Self = Self::new(
951        ChannelType::F16,
952        ChannelLayout::Gray,
953        None,
954        TransferFunction::Unknown,
955    );
956    pub(crate) const GRAYAF16: Self = Self::new(
957        ChannelType::F16,
958        ChannelLayout::GrayAlpha,
959        Some(AlphaMode::Straight),
960        TransferFunction::Unknown,
961    );
962    /// 8-bit BGRA, transfer unknown.
963    pub const BGRA8: Self = Self::new(
964        ChannelType::U8,
965        ChannelLayout::Bgra,
966        Some(AlphaMode::Straight),
967        TransferFunction::Unknown,
968    );
969    /// 8-bit RGBX, transfer unknown.
970    pub const RGBX8: Self = Self::new(
971        ChannelType::U8,
972        ChannelLayout::Rgba,
973        Some(AlphaMode::Undefined),
974        TransferFunction::Unknown,
975    );
976    /// 8-bit BGRX, transfer unknown.
977    pub const BGRX8: Self = Self::new(
978        ChannelType::U8,
979        ChannelLayout::Bgra,
980        Some(AlphaMode::Undefined),
981        TransferFunction::Unknown,
982    );
983
984    // -- Oklab constants ------------------------------------------------------
985
986    /// Oklab f32 (L, a, b), transfer unknown.
987    pub const OKLABF32: Self = Self {
988        format: PixelFormat::OklabF32,
989        transfer: TransferFunction::Unknown,
990        alpha: None,
991        primaries: ColorPrimaries::Bt709,
992        signal_range: SignalRange::Full,
993    };
994    /// Oklab+alpha f32 (L, a, b, alpha), transfer unknown.
995    pub const OKLABAF32: Self = Self {
996        format: PixelFormat::OklabaF32,
997        transfer: TransferFunction::Unknown,
998        alpha: Some(AlphaMode::Straight),
999        primaries: ColorPrimaries::Bt709,
1000        signal_range: SignalRange::Full,
1001    };
1002
1003    // -- CMYK constants --------------------------------------------------------
1004
1005    /// 8-bit CMYK, no transfer function or primaries.
1006    pub const CMYK8: Self = Self::from_pixel_format(PixelFormat::Cmyk8);
1007
1008    // -- Methods --------------------------------------------------------------
1009
1010    /// Number of channels.
1011    #[inline]
1012    pub const fn channels(self) -> usize {
1013        self.format.channels()
1014    }
1015
1016    /// Bytes per pixel.
1017    #[inline]
1018    pub const fn bytes_per_pixel(self) -> usize {
1019        self.format.bytes_per_pixel()
1020    }
1021
1022    /// Whether this descriptor has meaningful alpha data.
1023    #[inline]
1024    pub const fn has_alpha(self) -> bool {
1025        matches!(
1026            self.alpha,
1027            Some(AlphaMode::Straight) | Some(AlphaMode::Premultiplied) | Some(AlphaMode::Opaque)
1028        )
1029    }
1030
1031    /// Whether this descriptor is grayscale.
1032    #[inline]
1033    pub const fn is_grayscale(self) -> bool {
1034        self.format.is_grayscale()
1035    }
1036
1037    /// Whether this descriptor uses BGR byte order.
1038    #[inline]
1039    pub const fn is_bgr(self) -> bool {
1040        matches!(self.format.byte_order(), ByteOrder::Bgr)
1041    }
1042
1043    /// Return a copy of this descriptor **relabeled** with a different
1044    /// transfer function. Does not touch any pixel bytes.
1045    ///
1046    /// Use when the source data was mistagged (e.g., a codec decoded a
1047    /// profile-less image and you know its true TF from another source)
1048    /// or when you want to assert a particular TF for downstream planning
1049    /// without round-tripping through EOTF/OETF kernels.
1050    ///
1051    /// **This is metadata-only.** To actually re-encode pixel bytes from
1052    /// one transfer function into another, pass a source buffer and a
1053    /// destination descriptor with the new TF into [`RowConverter::new`]
1054    /// — that builds a plan with the appropriate EOTF/OETF kernels.
1055    ///
1056    /// [`RowConverter::new`]: https://docs.rs/zenpixels-convert/latest/zenpixels_convert/struct.RowConverter.html#method.new
1057    #[inline]
1058    #[must_use]
1059    pub const fn with_transfer(self, transfer: TransferFunction) -> Self {
1060        Self { transfer, ..self }
1061    }
1062
1063    /// Return a copy of this descriptor **relabeled** with different
1064    /// primaries. Does not touch any pixel bytes or apply a gamut matrix.
1065    ///
1066    /// Use when you know the true primaries of the source data independent
1067    /// of whatever upstream tagging said, or to assert a particular primary
1068    /// set for downstream planning.
1069    ///
1070    /// **This is metadata-only.** To actually apply a gamut conversion
1071    /// (e.g., BT.709 → Display P3), pass the new descriptor as the
1072    /// destination to [`RowConverter::new`] — that inserts the appropriate
1073    /// gamut matrix in linear light.
1074    ///
1075    /// [`RowConverter::new`]: https://docs.rs/zenpixels-convert/latest/zenpixels_convert/struct.RowConverter.html#method.new
1076    #[inline]
1077    #[must_use]
1078    pub const fn with_primaries(self, primaries: ColorPrimaries) -> Self {
1079        Self { primaries, ..self }
1080    }
1081
1082    /// Return a copy of this descriptor **relabeled** with a different
1083    /// alpha mode. Does not touch any pixel bytes or apply (un)premultiply.
1084    ///
1085    /// Use when the source alpha mode was mistagged or when you want to
1086    /// assert straight/premultiplied without round-tripping through the
1087    /// premul kernels.
1088    ///
1089    /// **This is metadata-only.** To actually premultiply or un-premultiply
1090    /// pixel values, pass the new descriptor as the destination to
1091    /// [`RowConverter::new`] — that inserts a `StraightToPremul` or
1092    /// `PremulToStraight` step. (Note: the built-in premul kernels operate
1093    /// in the source byte space — "encoded premul", per Canvas 2D
1094    /// semantics — not in linear light.)
1095    ///
1096    /// [`RowConverter::new`]: https://docs.rs/zenpixels-convert/latest/zenpixels_convert/struct.RowConverter.html#method.new
1097    #[inline]
1098    #[must_use]
1099    pub const fn with_alpha(self, alpha: Option<AlphaMode>) -> Self {
1100        Self { alpha, ..self }
1101    }
1102
1103    /// Alias for [`with_alpha`](Self::with_alpha).
1104    #[inline]
1105    #[must_use]
1106    pub const fn with_alpha_mode(self, alpha: Option<AlphaMode>) -> Self {
1107        self.with_alpha(alpha)
1108    }
1109
1110    /// Return a copy with a different signal range.
1111    #[inline]
1112    #[must_use]
1113    pub const fn with_signal_range(self, signal_range: SignalRange) -> Self {
1114        Self {
1115            signal_range,
1116            ..self
1117        }
1118    }
1119
1120    /// Whether this format is fully opaque (no transparency possible).
1121    ///
1122    /// Returns `true` when there is no alpha channel (`None`), the alpha
1123    /// bytes are undefined padding (`Undefined`), or alpha is all-255 (`Opaque`).
1124    #[inline]
1125    pub const fn is_opaque(self) -> bool {
1126        matches!(
1127            self.alpha,
1128            None | Some(AlphaMode::Undefined | AlphaMode::Opaque)
1129        )
1130    }
1131
1132    /// Whether this format may contain transparent pixels.
1133    ///
1134    /// Returns `true` for [`Straight`](AlphaMode::Straight) and
1135    /// [`Premultiplied`](AlphaMode::Premultiplied).
1136    #[inline]
1137    #[allow(unreachable_patterns)]
1138    pub const fn may_have_transparency(self) -> bool {
1139        matches!(
1140            self.alpha,
1141            Some(AlphaMode::Straight | AlphaMode::Premultiplied)
1142        )
1143    }
1144
1145    /// Whether the transfer function is [`Linear`](TransferFunction::Linear).
1146    #[inline]
1147    pub const fn is_linear(self) -> bool {
1148        matches!(self.transfer, TransferFunction::Linear)
1149    }
1150
1151    /// Whether the transfer function is [`Unknown`](TransferFunction::Unknown).
1152    #[inline]
1153    pub const fn is_unknown_transfer(self) -> bool {
1154        matches!(self.transfer, TransferFunction::Unknown)
1155    }
1156
1157    /// Minimum byte alignment required for the channel type (1, 2, or 4).
1158    #[inline]
1159    pub const fn min_alignment(self) -> usize {
1160        self.format.channel_type().byte_size()
1161    }
1162
1163    /// Tightly-packed byte stride for a given width.
1164    #[inline]
1165    pub const fn aligned_stride(self, width: u32) -> usize {
1166        width as usize * self.bytes_per_pixel()
1167    }
1168
1169    /// SIMD-friendly byte stride for a given width.
1170    ///
1171    /// The stride is a multiple of `lcm(bytes_per_pixel, simd_align)`,
1172    /// ensuring every row start is both pixel-aligned and SIMD-aligned.
1173    /// `simd_align` must be a power of 2.
1174    #[inline]
1175    pub const fn simd_aligned_stride(self, width: u32, simd_align: usize) -> usize {
1176        let bpp = self.bytes_per_pixel();
1177        let raw = width as usize * bpp;
1178        let align = lcm(bpp, simd_align);
1179        align_up_general(raw, align)
1180    }
1181
1182    /// Whether this descriptor's channel type and layout are compatible with `other`.
1183    ///
1184    /// "Compatible" means the raw bytes can be reinterpreted as `other`
1185    /// without any pixel transformation — same channel type, same layout.
1186    #[inline]
1187    pub const fn layout_compatible(self, other: Self) -> bool {
1188        self.format.channel_type() as u8 == other.format.channel_type() as u8
1189            && self.layout() as u8 == other.layout() as u8
1190    }
1191}
1192
1193// Alignment helpers.
1194
1195const fn gcd(mut a: usize, mut b: usize) -> usize {
1196    while b != 0 {
1197        let t = b;
1198        b = a % b;
1199        a = t;
1200    }
1201    a
1202}
1203
1204const fn lcm(a: usize, b: usize) -> usize {
1205    if a == 0 || b == 0 {
1206        0
1207    } else {
1208        a / gcd(a, b) * b
1209    }
1210}
1211
1212const fn align_up_general(value: usize, align: usize) -> usize {
1213    if align == 0 {
1214        return value;
1215    }
1216    let rem = value % align;
1217    if rem == 0 { value } else { value + align - rem }
1218}
1219
1220impl fmt::Display for PixelDescriptor {
1221    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1222        write!(
1223            f,
1224            "{} {} {}",
1225            self.format,
1226            self.format.channel_type(),
1227            self.transfer
1228        )?;
1229        if let Some(alpha) = self.alpha {
1230            if alpha.has_alpha() {
1231                write!(f, " alpha={alpha}")?;
1232            }
1233        }
1234        Ok(())
1235    }
1236}
1237
1238// ---------------------------------------------------------------------------
1239// Color model — what the channels represent
1240// ---------------------------------------------------------------------------
1241
1242/// What the channels represent, independent of channel count or byte order.
1243#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1244#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1245#[non_exhaustive]
1246#[repr(u8)]
1247pub enum ColorModel {
1248    /// Single grayscale channel.
1249    Gray = 0,
1250    /// Red, green, blue (or BGR when [`ByteOrder::Bgr`]).
1251    Rgb = 1,
1252    /// Luma + chroma (Y, Cb, Cr).
1253    YCbCr = 2,
1254    /// Oklab perceptual color space (L, a, b).
1255    Oklab = 3,
1256    /// Cyan, magenta, yellow, key (black). Device-dependent printing color space.
1257    /// RGB primaries and transfer functions do not apply to CMYK data.
1258    /// Use a CMS with an ICC profile for CMYK↔RGB conversion.
1259    Cmyk = 4,
1260}
1261
1262impl ColorModel {
1263    /// Number of color channels (excluding alpha).
1264    #[inline]
1265    #[allow(unreachable_patterns)]
1266    pub const fn color_channels(self) -> u8 {
1267        match self {
1268            Self::Gray => 1,
1269            Self::Cmyk => 4,
1270            _ => 3,
1271        }
1272    }
1273}
1274
1275impl fmt::Display for ColorModel {
1276    #[allow(unreachable_patterns)]
1277    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1278        match self {
1279            Self::Gray => f.write_str("Gray"),
1280            Self::Rgb => f.write_str("RGB"),
1281            Self::YCbCr => f.write_str("YCbCr"),
1282            Self::Oklab => f.write_str("Oklab"),
1283            Self::Cmyk => f.write_str("CMYK"),
1284            _ => write!(f, "ColorModel({})", *self as u8),
1285        }
1286    }
1287}
1288
1289// ---------------------------------------------------------------------------
1290// Byte order
1291// ---------------------------------------------------------------------------
1292
1293/// RGB-family byte order. Only meaningful when color model is RGB.
1294#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1295#[non_exhaustive]
1296#[repr(u8)]
1297pub enum ByteOrder {
1298    /// Standard order: R, G, B (+ A if present).
1299    #[default]
1300    Native = 0,
1301    /// Windows/DirectX order: B, G, R (+ A if present).
1302    Bgr = 1,
1303}
1304
1305impl fmt::Display for ByteOrder {
1306    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1307        match self {
1308            Self::Native => f.write_str("native"),
1309            Self::Bgr => f.write_str("BGR"),
1310        }
1311    }
1312}
1313
1314// ---------------------------------------------------------------------------
1315// PixelFormat — flat enum for physical pixel layout
1316// ---------------------------------------------------------------------------
1317
1318/// Physical pixel layout for match-based format dispatch.
1319///
1320/// Each variant encodes the channel type (U8/U16/F32) and layout (RGB/RGBA/
1321/// Gray/etc.) in one discriminant. Transfer function and alpha mode live on
1322/// [`PixelDescriptor`], not here.
1323///
1324/// Use this enum when you need exhaustive `match` dispatch over known
1325/// pixel layouts.
1326#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1327#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1328#[non_exhaustive]
1329#[repr(u8)]
1330pub enum PixelFormat {
1331    Rgb8 = 1,
1332    Rgba8 = 2,
1333    Rgb16 = 3,
1334    Rgba16 = 4,
1335    RgbF32 = 5,
1336    RgbaF32 = 6,
1337    Gray8 = 7,
1338    Gray16 = 8,
1339    GrayF32 = 9,
1340    GrayA8 = 10,
1341    GrayA16 = 11,
1342    GrayAF32 = 12,
1343    Bgra8 = 13,
1344    Rgbx8 = 14,
1345    Bgrx8 = 15,
1346    OklabF32 = 16,
1347    OklabaF32 = 17,
1348    /// 8-bit CMYK (4 bytes per pixel, no transfer function).
1349    Cmyk8 = 18,
1350    /// f16 (IEEE 754 half-precision) RGB.
1351    RgbF16 = 19,
1352    /// f16 (IEEE 754 half-precision) RGBA.
1353    RgbaF16 = 20,
1354    /// f16 (IEEE 754 half-precision) grayscale.
1355    GrayF16 = 21,
1356    /// f16 (IEEE 754 half-precision) grayscale with alpha.
1357    GrayAF16 = 22,
1358}
1359
1360impl PixelFormat {
1361    /// Channel storage type.
1362    #[inline]
1363    #[allow(unreachable_patterns)]
1364    pub const fn channel_type(self) -> ChannelType {
1365        match self {
1366            Self::Rgb8
1367            | Self::Rgba8
1368            | Self::Gray8
1369            | Self::GrayA8
1370            | Self::Bgra8
1371            | Self::Rgbx8
1372            | Self::Bgrx8
1373            | Self::Cmyk8 => ChannelType::U8,
1374            Self::Rgb16 | Self::Rgba16 | Self::Gray16 | Self::GrayA16 => ChannelType::U16,
1375            Self::RgbF32
1376            | Self::RgbaF32
1377            | Self::GrayF32
1378            | Self::GrayAF32
1379            | Self::OklabF32
1380            | Self::OklabaF32 => ChannelType::F32,
1381            Self::RgbF16 | Self::RgbaF16 | Self::GrayF16 | Self::GrayAF16 => ChannelType::F16,
1382            _ => ChannelType::U8,
1383        }
1384    }
1385
1386    /// Channel layout.
1387    #[inline]
1388    #[allow(unreachable_patterns)]
1389    pub const fn layout(self) -> ChannelLayout {
1390        match self {
1391            Self::Rgb8 | Self::Rgb16 | Self::RgbF32 | Self::RgbF16 => ChannelLayout::Rgb,
1392            Self::Rgba8 | Self::Rgba16 | Self::RgbaF32 | Self::RgbaF16 | Self::Rgbx8 => {
1393                ChannelLayout::Rgba
1394            }
1395            Self::Gray8 | Self::Gray16 | Self::GrayF32 | Self::GrayF16 => ChannelLayout::Gray,
1396            Self::GrayA8 | Self::GrayA16 | Self::GrayAF32 | Self::GrayAF16 => {
1397                ChannelLayout::GrayAlpha
1398            }
1399            Self::Bgra8 | Self::Bgrx8 => ChannelLayout::Bgra,
1400            Self::OklabF32 => ChannelLayout::Oklab,
1401            Self::OklabaF32 => ChannelLayout::OklabA,
1402            Self::Cmyk8 => ChannelLayout::Cmyk,
1403            _ => ChannelLayout::Rgb,
1404        }
1405    }
1406
1407    /// Color model (what the channels represent).
1408    #[inline]
1409    #[allow(unreachable_patterns)]
1410    pub const fn color_model(self) -> ColorModel {
1411        match self {
1412            Self::Gray8
1413            | Self::Gray16
1414            | Self::GrayF32
1415            | Self::GrayF16
1416            | Self::GrayA8
1417            | Self::GrayA16
1418            | Self::GrayAF32
1419            | Self::GrayAF16 => ColorModel::Gray,
1420            Self::OklabF32 | Self::OklabaF32 => ColorModel::Oklab,
1421            Self::Cmyk8 => ColorModel::Cmyk,
1422            _ => ColorModel::Rgb,
1423        }
1424    }
1425
1426    /// Byte order (Native or BGR).
1427    #[inline]
1428    #[allow(unreachable_patterns)]
1429    pub const fn byte_order(self) -> ByteOrder {
1430        match self {
1431            Self::Bgra8 | Self::Bgrx8 => ByteOrder::Bgr,
1432            _ => ByteOrder::Native,
1433        }
1434    }
1435
1436    /// Number of channels (including alpha/padding if present).
1437    #[inline]
1438    pub const fn channels(self) -> usize {
1439        self.layout().channels()
1440    }
1441
1442    /// Bytes per pixel.
1443    #[inline]
1444    pub const fn bytes_per_pixel(self) -> usize {
1445        self.channels() * self.channel_type().byte_size()
1446    }
1447
1448    /// Whether this format has alpha or padding bytes (4th channel).
1449    #[inline]
1450    pub const fn has_alpha_bytes(self) -> bool {
1451        self.layout().has_alpha()
1452    }
1453
1454    /// Whether this format is grayscale.
1455    #[inline]
1456    pub const fn is_grayscale(self) -> bool {
1457        matches!(self.color_model(), ColorModel::Gray)
1458    }
1459
1460    /// Default alpha mode for this format.
1461    ///
1462    /// - Formats with no alpha bytes → `None`
1463    /// - Formats with padding (Rgbx8, Bgrx8) → `Some(AlphaMode::Undefined)`
1464    /// - Formats with alpha → `Some(AlphaMode::Straight)`
1465    #[allow(unreachable_patterns)]
1466    #[inline]
1467    pub const fn default_alpha(self) -> Option<AlphaMode> {
1468        match self {
1469            Self::Rgb8
1470            | Self::Rgb16
1471            | Self::RgbF32
1472            | Self::RgbF16
1473            | Self::Gray8
1474            | Self::Gray16
1475            | Self::GrayF32
1476            | Self::GrayF16
1477            | Self::OklabF32
1478            | Self::Cmyk8 => None,
1479            Self::Rgbx8 | Self::Bgrx8 => Some(AlphaMode::Undefined),
1480            _ => Some(AlphaMode::Straight),
1481        }
1482    }
1483
1484    /// Short human-readable name.
1485    #[allow(unreachable_patterns)]
1486    #[inline]
1487    pub const fn name(self) -> &'static str {
1488        match self {
1489            Self::Rgb8 => "RGB8",
1490            Self::Rgba8 => "RGBA8",
1491            Self::Rgb16 => "RGB16",
1492            Self::Rgba16 => "RGBA16",
1493            Self::RgbF32 => "RgbF32",
1494            Self::RgbaF32 => "RgbaF32",
1495            Self::Gray8 => "Gray8",
1496            Self::Gray16 => "Gray16",
1497            Self::GrayF32 => "GrayF32",
1498            Self::GrayA8 => "GrayA8",
1499            Self::GrayA16 => "GrayA16",
1500            Self::GrayAF32 => "GrayAF32",
1501            Self::Bgra8 => "BGRA8",
1502            Self::Rgbx8 => "RGBX8",
1503            Self::Bgrx8 => "BGRX8",
1504            Self::OklabF32 => "OklabF32",
1505            Self::OklabaF32 => "OklabaF32",
1506            Self::Cmyk8 => "CMYK8",
1507            Self::RgbF16 => "RgbF16",
1508            Self::RgbaF16 => "RgbaF16",
1509            Self::GrayF16 => "GrayF16",
1510            Self::GrayAF16 => "GrayAF16",
1511            _ => "Unknown",
1512        }
1513    }
1514
1515    /// Resolve a format from channel type, layout, and alpha presence.
1516    ///
1517    /// Returns `None` for combinations that have no `PixelFormat` variant
1518    /// (e.g. `(U16, Bgra, _)`).
1519    #[inline]
1520    pub const fn from_parts(
1521        channel_type: ChannelType,
1522        layout: ChannelLayout,
1523        alpha: Option<AlphaMode>,
1524    ) -> Option<Self> {
1525        let is_padding = matches!(alpha, Some(AlphaMode::Undefined));
1526        match (channel_type, layout, is_padding) {
1527            (ChannelType::U8, ChannelLayout::Rgb, _) => Some(Self::Rgb8),
1528            (ChannelType::U16, ChannelLayout::Rgb, _) => Some(Self::Rgb16),
1529            (ChannelType::F32, ChannelLayout::Rgb, _) => Some(Self::RgbF32),
1530            (ChannelType::F16, ChannelLayout::Rgb, _) => Some(Self::RgbF16),
1531
1532            (ChannelType::U8, ChannelLayout::Rgba, true) => Some(Self::Rgbx8),
1533            (ChannelType::U8, ChannelLayout::Rgba, false) => Some(Self::Rgba8),
1534            (ChannelType::U16, ChannelLayout::Rgba, _) => Some(Self::Rgba16),
1535            (ChannelType::F32, ChannelLayout::Rgba, _) => Some(Self::RgbaF32),
1536            (ChannelType::F16, ChannelLayout::Rgba, _) => Some(Self::RgbaF16),
1537
1538            (ChannelType::U8, ChannelLayout::Gray, _) => Some(Self::Gray8),
1539            (ChannelType::U16, ChannelLayout::Gray, _) => Some(Self::Gray16),
1540            (ChannelType::F32, ChannelLayout::Gray, _) => Some(Self::GrayF32),
1541            (ChannelType::F16, ChannelLayout::Gray, _) => Some(Self::GrayF16),
1542
1543            (ChannelType::U8, ChannelLayout::GrayAlpha, _) => Some(Self::GrayA8),
1544            (ChannelType::U16, ChannelLayout::GrayAlpha, _) => Some(Self::GrayA16),
1545            (ChannelType::F32, ChannelLayout::GrayAlpha, _) => Some(Self::GrayAF32),
1546            (ChannelType::F16, ChannelLayout::GrayAlpha, _) => Some(Self::GrayAF16),
1547
1548            (ChannelType::U8, ChannelLayout::Bgra, true) => Some(Self::Bgrx8),
1549            (ChannelType::U8, ChannelLayout::Bgra, false) => Some(Self::Bgra8),
1550
1551            (ChannelType::F32, ChannelLayout::Oklab, _) => Some(Self::OklabF32),
1552            (ChannelType::F32, ChannelLayout::OklabA, _) => Some(Self::OklabaF32),
1553
1554            (ChannelType::U8, ChannelLayout::Cmyk, _) => Some(Self::Cmyk8),
1555
1556            _ => None,
1557        }
1558    }
1559
1560    /// Base descriptor with `Unknown` transfer and BT.709 primaries.
1561    #[allow(unreachable_patterns)]
1562    #[inline]
1563    pub const fn descriptor(self) -> PixelDescriptor {
1564        match self {
1565            Self::Rgb8 => PixelDescriptor::RGB8,
1566            Self::Rgba8 => PixelDescriptor::RGBA8,
1567            Self::Rgb16 => PixelDescriptor::RGB16,
1568            Self::Rgba16 => PixelDescriptor::RGBA16,
1569            Self::RgbF32 => PixelDescriptor::RGBF32,
1570            Self::RgbaF32 => PixelDescriptor::RGBAF32,
1571            Self::Gray8 => PixelDescriptor::GRAY8,
1572            Self::Gray16 => PixelDescriptor::GRAY16,
1573            Self::GrayF32 => PixelDescriptor::GRAYF32,
1574            Self::GrayA8 => PixelDescriptor::GRAYA8,
1575            Self::GrayA16 => PixelDescriptor::GRAYA16,
1576            Self::GrayAF32 => PixelDescriptor::GRAYAF32,
1577            Self::Bgra8 => PixelDescriptor::BGRA8,
1578            Self::Rgbx8 => PixelDescriptor::RGBX8,
1579            Self::Bgrx8 => PixelDescriptor::BGRX8,
1580            Self::OklabF32 => PixelDescriptor::OKLABF32,
1581            Self::OklabaF32 => PixelDescriptor::OKLABAF32,
1582            Self::Cmyk8 => PixelDescriptor::CMYK8,
1583            Self::RgbF16 => PixelDescriptor::RGBF16,
1584            Self::RgbaF16 => PixelDescriptor::RGBAF16,
1585            Self::GrayF16 => PixelDescriptor::GRAYF16,
1586            Self::GrayAF16 => PixelDescriptor::GRAYAF16,
1587            _ => PixelDescriptor::RGB8,
1588        }
1589    }
1590}
1591
1592impl fmt::Display for PixelFormat {
1593    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1594        f.write_str(self.name())
1595    }
1596}
1597
1598// ---------------------------------------------------------------------------
1599// Tests
1600// ---------------------------------------------------------------------------
1601
1602#[cfg(test)]
1603mod tests {
1604    use alloc::format;
1605    use core::mem::size_of;
1606
1607    use super::*;
1608
1609    #[test]
1610    fn channel_type_byte_size() {
1611        assert_eq!(ChannelType::U8.byte_size(), 1);
1612        assert_eq!(ChannelType::U16.byte_size(), 2);
1613        assert_eq!(ChannelType::F16.byte_size(), 2);
1614        assert_eq!(ChannelType::F32.byte_size(), 4);
1615    }
1616
1617    #[test]
1618    fn descriptor_bytes_per_pixel() {
1619        assert_eq!(PixelDescriptor::RGB8.bytes_per_pixel(), 3);
1620        assert_eq!(PixelDescriptor::RGBA8.bytes_per_pixel(), 4);
1621        assert_eq!(PixelDescriptor::GRAY8.bytes_per_pixel(), 1);
1622        assert_eq!(PixelDescriptor::RGBAF32.bytes_per_pixel(), 16);
1623        assert_eq!(PixelDescriptor::GRAYA8.bytes_per_pixel(), 2);
1624    }
1625
1626    #[test]
1627    fn descriptor_has_alpha() {
1628        assert!(!PixelDescriptor::RGB8.has_alpha());
1629        assert!(PixelDescriptor::RGBA8.has_alpha());
1630        assert!(!PixelDescriptor::RGBX8.has_alpha());
1631        assert!(PixelDescriptor::GRAYA8.has_alpha());
1632    }
1633
1634    #[test]
1635    fn descriptor_is_grayscale() {
1636        assert!(PixelDescriptor::GRAY8.is_grayscale());
1637        assert!(PixelDescriptor::GRAYA8.is_grayscale());
1638        assert!(!PixelDescriptor::RGB8.is_grayscale());
1639    }
1640
1641    #[test]
1642    fn layout_compatible() {
1643        assert!(PixelDescriptor::RGB8_SRGB.layout_compatible(PixelDescriptor::RGB8));
1644        assert!(!PixelDescriptor::RGB8.layout_compatible(PixelDescriptor::RGBA8));
1645    }
1646
1647    #[test]
1648    fn pixel_format_descriptor_roundtrip() {
1649        let desc = PixelFormat::Rgba8.descriptor();
1650        assert_eq!(desc.layout(), ChannelLayout::Rgba);
1651        assert_eq!(desc.channel_type(), ChannelType::U8);
1652    }
1653
1654    #[test]
1655    fn pixel_format_enum_basics() {
1656        assert_eq!(PixelFormat::Rgb8.channels(), 3);
1657        assert_eq!(PixelFormat::Rgba8.channels(), 4);
1658        assert!(PixelFormat::Rgba8.has_alpha_bytes());
1659        assert!(!PixelFormat::Rgb8.has_alpha_bytes());
1660        assert_eq!(PixelFormat::RgbF32.bytes_per_pixel(), 12);
1661        assert_eq!(PixelFormat::RgbaF32.bytes_per_pixel(), 16);
1662        assert_eq!(PixelFormat::Gray8.channels(), 1);
1663        assert!(PixelFormat::Gray8.is_grayscale());
1664        assert!(!PixelFormat::Rgb8.is_grayscale());
1665        assert_eq!(PixelFormat::Bgra8.byte_order(), ByteOrder::Bgr);
1666        assert_eq!(PixelFormat::Rgb8.byte_order(), ByteOrder::Native);
1667    }
1668
1669    #[test]
1670    fn pixel_format_enum_size() {
1671        // Single-byte discriminant — much smaller than old 5-field struct.
1672        assert!(size_of::<PixelFormat>() <= 2);
1673    }
1674
1675    #[test]
1676    fn pixel_format_from_parts_roundtrip() {
1677        let fmt = PixelFormat::Rgba8;
1678        let rebuilt =
1679            PixelFormat::from_parts(fmt.channel_type(), fmt.layout(), fmt.default_alpha());
1680        assert_eq!(rebuilt, Some(fmt));
1681
1682        let fmt2 = PixelFormat::Bgra8;
1683        let rebuilt2 =
1684            PixelFormat::from_parts(fmt2.channel_type(), fmt2.layout(), fmt2.default_alpha());
1685        assert_eq!(rebuilt2, Some(fmt2));
1686
1687        let fmt3 = PixelFormat::Gray8;
1688        let rebuilt3 =
1689            PixelFormat::from_parts(fmt3.channel_type(), fmt3.layout(), fmt3.default_alpha());
1690        assert_eq!(rebuilt3, Some(fmt3));
1691    }
1692
1693    #[test]
1694    fn alpha_mode_semantics() {
1695        // None (Option) = no alpha channel
1696        assert!(!PixelDescriptor::RGB8.has_alpha());
1697        // Undefined = padding bytes, not real alpha
1698        assert!(!AlphaMode::Undefined.has_alpha());
1699        // Straight and Premultiplied = real alpha
1700        assert!(AlphaMode::Straight.has_alpha());
1701        assert!(AlphaMode::Premultiplied.has_alpha());
1702        assert!(AlphaMode::Opaque.has_alpha());
1703    }
1704
1705    #[test]
1706    fn color_primaries_containment() {
1707        assert!(ColorPrimaries::Bt2020.contains(ColorPrimaries::DisplayP3));
1708        assert!(ColorPrimaries::Bt2020.contains(ColorPrimaries::Bt709));
1709        assert!(ColorPrimaries::DisplayP3.contains(ColorPrimaries::Bt709));
1710        assert!(!ColorPrimaries::Bt709.contains(ColorPrimaries::DisplayP3));
1711        assert!(!ColorPrimaries::Unknown.contains(ColorPrimaries::Bt709));
1712    }
1713
1714    #[test]
1715    fn descriptor_size() {
1716        // PixelFormat (1 byte enum) + transfer (1) + alpha (2) + primaries (1) + signal_range (1) = ~6
1717        assert!(size_of::<PixelDescriptor>() <= 8);
1718    }
1719
1720    #[test]
1721    fn color_model_channels() {
1722        assert_eq!(ColorModel::Gray.color_channels(), 1);
1723        assert_eq!(ColorModel::Rgb.color_channels(), 3);
1724        assert_eq!(ColorModel::YCbCr.color_channels(), 3);
1725        assert_eq!(ColorModel::Oklab.color_channels(), 3);
1726        assert_eq!(ColorModel::Cmyk.color_channels(), 4);
1727    }
1728
1729    // --- PlaneSemantic tests ---
1730
1731    // --- PlaneDescriptor tests ---
1732
1733    // --- PlaneMask tests ---
1734
1735    // --- PlaneLayout tests ---
1736
1737    // --- MultiPlaneImage tests ---
1738
1739    // --- PlaneRelationship tests ---
1740
1741    #[test]
1742    fn reference_white_nits_values() {
1743        assert_eq!(TransferFunction::Pq.reference_white_nits(), 203.0);
1744        assert_eq!(TransferFunction::Srgb.reference_white_nits(), 1.0);
1745        assert_eq!(TransferFunction::Linear.reference_white_nits(), 1.0);
1746        assert_eq!(TransferFunction::Unknown.reference_white_nits(), 1.0);
1747    }
1748
1749    // --- PlaneLayout mask tests ---
1750
1751    // --- Display impl tests ---
1752
1753    #[test]
1754    fn channel_type_display() {
1755        assert_eq!(format!("{}", ChannelType::U8), "U8");
1756        assert_eq!(format!("{}", ChannelType::U16), "U16");
1757        assert_eq!(format!("{}", ChannelType::F32), "F32");
1758        assert_eq!(format!("{}", ChannelType::F16), "F16");
1759    }
1760
1761    #[test]
1762    fn channel_layout_display() {
1763        assert_eq!(format!("{}", ChannelLayout::Gray), "Gray");
1764        assert_eq!(format!("{}", ChannelLayout::GrayAlpha), "GrayAlpha");
1765        assert_eq!(format!("{}", ChannelLayout::Rgb), "RGB");
1766        assert_eq!(format!("{}", ChannelLayout::Rgba), "RGBA");
1767        assert_eq!(format!("{}", ChannelLayout::Bgra), "BGRA");
1768        assert_eq!(format!("{}", ChannelLayout::Oklab), "Oklab");
1769        assert_eq!(format!("{}", ChannelLayout::OklabA), "OklabA");
1770    }
1771
1772    #[test]
1773    fn alpha_mode_display() {
1774        assert_eq!(format!("{}", AlphaMode::Undefined), "undefined");
1775        assert_eq!(format!("{}", AlphaMode::Straight), "straight");
1776        assert_eq!(format!("{}", AlphaMode::Premultiplied), "premultiplied");
1777        assert_eq!(format!("{}", AlphaMode::Opaque), "opaque");
1778    }
1779
1780    #[test]
1781    fn transfer_function_display() {
1782        assert_eq!(format!("{}", TransferFunction::Linear), "linear");
1783        assert_eq!(format!("{}", TransferFunction::Srgb), "sRGB");
1784        assert_eq!(format!("{}", TransferFunction::Bt709), "BT.709");
1785        assert_eq!(format!("{}", TransferFunction::Pq), "PQ");
1786        assert_eq!(format!("{}", TransferFunction::Unknown), "unknown");
1787    }
1788
1789    #[test]
1790    fn color_primaries_display() {
1791        assert_eq!(format!("{}", ColorPrimaries::Bt709), "BT.709");
1792        assert_eq!(format!("{}", ColorPrimaries::Bt2020), "BT.2020");
1793        assert_eq!(format!("{}", ColorPrimaries::DisplayP3), "Display P3");
1794        assert_eq!(format!("{}", ColorPrimaries::Unknown), "unknown");
1795    }
1796
1797    #[test]
1798    fn signal_range_display() {
1799        assert_eq!(format!("{}", SignalRange::Full), "full");
1800        assert_eq!(format!("{}", SignalRange::Narrow), "narrow");
1801    }
1802
1803    #[test]
1804    fn pixel_descriptor_display() {
1805        let s = format!("{}", PixelDescriptor::RGB8_SRGB);
1806        assert!(s.contains("U8"), "expected U8 in: {s}");
1807        assert!(s.contains("sRGB"), "expected sRGB in: {s}");
1808
1809        let s = format!("{}", PixelDescriptor::RGBA8_SRGB);
1810        assert!(s.contains("alpha=straight"), "expected alpha in: {s}");
1811    }
1812
1813    #[test]
1814    fn pixel_format_display() {
1815        let s = format!("{}", PixelFormat::Rgb8);
1816        assert!(s.contains("RGB8"));
1817        let s = format!("{}", PixelFormat::Bgra8);
1818        assert!(s.contains("BGRA8"));
1819    }
1820
1821    // --- from_cicp / to_cicp tests ---
1822
1823    #[test]
1824    fn transfer_function_from_cicp() {
1825        assert_eq!(
1826            TransferFunction::from_cicp(1),
1827            Some(TransferFunction::Bt709)
1828        );
1829        assert_eq!(
1830            TransferFunction::from_cicp(8),
1831            Some(TransferFunction::Linear)
1832        );
1833        assert_eq!(
1834            TransferFunction::from_cicp(13),
1835            Some(TransferFunction::Srgb)
1836        );
1837        assert_eq!(TransferFunction::from_cicp(16), Some(TransferFunction::Pq));
1838        assert_eq!(TransferFunction::from_cicp(18), Some(TransferFunction::Hlg));
1839        assert_eq!(TransferFunction::from_cicp(99), None);
1840    }
1841
1842    #[test]
1843    fn transfer_function_to_cicp() {
1844        assert_eq!(TransferFunction::Bt709.to_cicp(), Some(1));
1845        assert_eq!(TransferFunction::Linear.to_cicp(), Some(8));
1846        assert_eq!(TransferFunction::Srgb.to_cicp(), Some(13));
1847        assert_eq!(TransferFunction::Pq.to_cicp(), Some(16));
1848        assert_eq!(TransferFunction::Hlg.to_cicp(), Some(18));
1849        assert_eq!(TransferFunction::Unknown.to_cicp(), None);
1850    }
1851
1852    #[test]
1853    fn transfer_function_cicp_roundtrip() {
1854        for tf in [
1855            TransferFunction::Bt709,
1856            TransferFunction::Linear,
1857            TransferFunction::Srgb,
1858            TransferFunction::Pq,
1859        ] {
1860            let code = tf.to_cicp().unwrap();
1861            assert_eq!(TransferFunction::from_cicp(code), Some(tf));
1862        }
1863    }
1864
1865    #[test]
1866    fn color_primaries_from_cicp() {
1867        assert_eq!(ColorPrimaries::from_cicp(1), Some(ColorPrimaries::Bt709));
1868        assert_eq!(ColorPrimaries::from_cicp(9), Some(ColorPrimaries::Bt2020));
1869        assert_eq!(
1870            ColorPrimaries::from_cicp(12),
1871            Some(ColorPrimaries::DisplayP3)
1872        );
1873        assert_eq!(ColorPrimaries::from_cicp(99), None);
1874    }
1875
1876    #[test]
1877    fn color_primaries_to_cicp() {
1878        assert_eq!(ColorPrimaries::Bt709.to_cicp(), Some(1));
1879        assert_eq!(ColorPrimaries::Bt2020.to_cicp(), Some(9));
1880        assert_eq!(ColorPrimaries::DisplayP3.to_cicp(), Some(12));
1881        assert_eq!(ColorPrimaries::Unknown.to_cicp(), None);
1882    }
1883
1884    // --- ChannelType helpers ---
1885
1886    #[test]
1887    fn channel_type_helpers() {
1888        assert!(ChannelType::U8.is_u8());
1889        assert!(!ChannelType::U8.is_u16());
1890        assert!(ChannelType::U16.is_u16());
1891        assert!(ChannelType::F32.is_f32());
1892        assert!(ChannelType::F16.is_f16());
1893        assert!(ChannelType::U8.is_integer());
1894        assert!(ChannelType::U16.is_integer());
1895        assert!(!ChannelType::F32.is_integer());
1896        assert!(ChannelType::F32.is_float());
1897        assert!(ChannelType::F16.is_float());
1898        assert!(!ChannelType::U8.is_float());
1899    }
1900
1901    // --- ChannelLayout helpers ---
1902
1903    #[test]
1904    fn channel_layout_channels() {
1905        assert_eq!(ChannelLayout::Gray.channels(), 1);
1906        assert_eq!(ChannelLayout::GrayAlpha.channels(), 2);
1907        assert_eq!(ChannelLayout::Rgb.channels(), 3);
1908        assert_eq!(ChannelLayout::Rgba.channels(), 4);
1909        assert_eq!(ChannelLayout::Bgra.channels(), 4);
1910        assert_eq!(ChannelLayout::Oklab.channels(), 3);
1911        assert_eq!(ChannelLayout::OklabA.channels(), 4);
1912    }
1913
1914    #[test]
1915    fn channel_layout_has_alpha() {
1916        assert!(!ChannelLayout::Gray.has_alpha());
1917        assert!(ChannelLayout::GrayAlpha.has_alpha());
1918        assert!(!ChannelLayout::Rgb.has_alpha());
1919        assert!(ChannelLayout::Rgba.has_alpha());
1920        assert!(ChannelLayout::Bgra.has_alpha());
1921        assert!(!ChannelLayout::Oklab.has_alpha());
1922        assert!(ChannelLayout::OklabA.has_alpha());
1923    }
1924
1925    // --- PixelDescriptor builder methods ---
1926
1927    #[test]
1928    fn with_transfer() {
1929        let desc = PixelDescriptor::RGB8_SRGB.with_transfer(TransferFunction::Linear);
1930        assert_eq!(desc.transfer(), TransferFunction::Linear);
1931        assert_eq!(desc.layout(), ChannelLayout::Rgb);
1932    }
1933
1934    #[test]
1935    fn with_primaries() {
1936        let desc = PixelDescriptor::RGB8_SRGB.with_primaries(ColorPrimaries::DisplayP3);
1937        assert_eq!(desc.primaries, ColorPrimaries::DisplayP3);
1938    }
1939
1940    #[test]
1941    fn with_signal_range() {
1942        let desc = PixelDescriptor::RGB8_SRGB.with_signal_range(SignalRange::Narrow);
1943        assert_eq!(desc.signal_range, SignalRange::Narrow);
1944    }
1945
1946    #[test]
1947    fn with_alpha_mode() {
1948        let desc = PixelDescriptor::RGBA8_SRGB.with_alpha(Some(AlphaMode::Premultiplied));
1949        assert_eq!(desc.alpha(), Some(AlphaMode::Premultiplied));
1950    }
1951
1952    // --- PixelDescriptor predicates ---
1953
1954    #[test]
1955    fn is_opaque_and_may_have_transparency() {
1956        assert!(PixelDescriptor::RGB8_SRGB.is_opaque());
1957        assert!(!PixelDescriptor::RGB8_SRGB.may_have_transparency());
1958        assert!(!PixelDescriptor::RGBA8_SRGB.is_opaque());
1959        assert!(PixelDescriptor::RGBA8_SRGB.may_have_transparency());
1960
1961        let rgbx = PixelDescriptor::new(
1962            ChannelType::U8,
1963            ChannelLayout::Rgba,
1964            Some(AlphaMode::Undefined),
1965            TransferFunction::Srgb,
1966        );
1967        assert!(rgbx.is_opaque());
1968        assert!(!rgbx.may_have_transparency());
1969    }
1970
1971    #[test]
1972    fn is_linear_and_is_unknown_transfer() {
1973        assert!(!PixelDescriptor::RGB8_SRGB.is_linear());
1974        assert!(PixelDescriptor::RGBF32_LINEAR.is_linear());
1975        assert!(!PixelDescriptor::RGB8_SRGB.is_unknown_transfer());
1976        let desc = PixelDescriptor::RGB8_SRGB.with_transfer(TransferFunction::Unknown);
1977        assert!(desc.is_unknown_transfer());
1978    }
1979
1980    #[test]
1981    fn min_alignment() {
1982        assert_eq!(PixelDescriptor::RGB8_SRGB.min_alignment(), 1);
1983        assert_eq!(PixelDescriptor::RGBF32_LINEAR.min_alignment(), 4);
1984    }
1985
1986    #[test]
1987    fn aligned_stride() {
1988        assert_eq!(PixelDescriptor::RGB8_SRGB.aligned_stride(100), 300);
1989        assert_eq!(PixelDescriptor::RGBA8_SRGB.aligned_stride(100), 400);
1990        assert_eq!(PixelDescriptor::RGBF32_LINEAR.aligned_stride(10), 120);
1991    }
1992
1993    #[test]
1994    fn simd_aligned_stride() {
1995        let stride = PixelDescriptor::RGB8_SRGB.simd_aligned_stride(100, 16);
1996        assert!(stride >= 300);
1997        assert_eq!(stride % 16, 0);
1998        assert_eq!(stride % 3, 0); // pixel-aligned
1999    }
2000
2001    // --- new_full and from_pixel_format ---
2002
2003    #[test]
2004    fn new_full_constructor() {
2005        let desc = PixelDescriptor::new_full(
2006            ChannelType::U8,
2007            ChannelLayout::Rgb,
2008            None,
2009            TransferFunction::Srgb,
2010            ColorPrimaries::DisplayP3,
2011        );
2012        assert_eq!(desc.primaries, ColorPrimaries::DisplayP3);
2013        assert_eq!(desc.transfer(), TransferFunction::Srgb);
2014    }
2015
2016    #[test]
2017    fn from_pixel_format_constructor() {
2018        let desc = PixelDescriptor::from_pixel_format(PixelFormat::Rgba8);
2019        assert_eq!(desc.layout(), ChannelLayout::Rgba);
2020        assert_eq!(desc.transfer(), TransferFunction::Unknown);
2021        assert_eq!(desc.primaries, ColorPrimaries::Bt709);
2022        assert_eq!(desc.signal_range, SignalRange::Full);
2023    }
2024
2025    // --- PixelFormat::name ---
2026
2027    #[test]
2028    fn pixel_format_name() {
2029        assert_eq!(PixelFormat::Rgb8.name(), "RGB8");
2030        assert_eq!(PixelFormat::Bgra8.name(), "BGRA8");
2031        assert_eq!(PixelFormat::Gray8.name(), "Gray8");
2032    }
2033
2034    // --- ColorModel ---
2035
2036    #[test]
2037    fn color_model_display() {
2038        assert_eq!(format!("{}", ColorModel::Gray), "Gray");
2039        assert_eq!(format!("{}", ColorModel::Rgb), "RGB");
2040        assert_eq!(format!("{}", ColorModel::YCbCr), "YCbCr");
2041        assert_eq!(format!("{}", ColorModel::Oklab), "Oklab");
2042        assert_eq!(format!("{}", ColorModel::Cmyk), "CMYK");
2043    }
2044
2045    // --- SignalRange default ---
2046
2047    #[test]
2048    fn signal_range_default() {
2049        assert_eq!(SignalRange::default(), SignalRange::Full);
2050    }
2051
2052    // --- ColorPrimaries default ---
2053
2054    #[test]
2055    fn color_primaries_default() {
2056        assert_eq!(ColorPrimaries::default(), ColorPrimaries::Bt709);
2057    }
2058
2059    // --- CMYK tests ---
2060
2061    #[test]
2062    fn cmyk8_descriptor() {
2063        let d = PixelDescriptor::CMYK8;
2064        assert_eq!(d.color_model(), ColorModel::Cmyk);
2065        assert_eq!(d.channels(), 4);
2066        assert_eq!(d.bytes_per_pixel(), 4);
2067        assert_eq!(d.layout(), ChannelLayout::Cmyk);
2068        assert_eq!(d.channel_type(), ChannelType::U8);
2069        assert_eq!(d.transfer(), TransferFunction::Unknown);
2070        assert_eq!(d.primaries, ColorPrimaries::Bt709);
2071        assert!(!d.has_alpha());
2072        assert!(d.is_opaque());
2073    }
2074
2075    #[test]
2076    fn cmyk8_pixel_format() {
2077        let fmt = PixelFormat::Cmyk8;
2078        assert_eq!(fmt.channels(), 4);
2079        assert_eq!(fmt.bytes_per_pixel(), 4);
2080        assert_eq!(fmt.channel_type(), ChannelType::U8);
2081        assert_eq!(fmt.layout(), ChannelLayout::Cmyk);
2082        assert_eq!(fmt.color_model(), ColorModel::Cmyk);
2083        assert!(!fmt.has_alpha_bytes());
2084        assert!(!fmt.is_grayscale());
2085        assert_eq!(fmt.name(), "CMYK8");
2086        assert_eq!(fmt.default_alpha(), None);
2087    }
2088
2089    #[test]
2090    fn cmyk8_from_parts_roundtrip() {
2091        let fmt = PixelFormat::Cmyk8;
2092        let rebuilt =
2093            PixelFormat::from_parts(fmt.channel_type(), fmt.layout(), fmt.default_alpha());
2094        assert_eq!(rebuilt, Some(fmt));
2095    }
2096
2097    #[test]
2098    fn cmyk8_descriptor_roundtrip() {
2099        let desc = PixelFormat::Cmyk8.descriptor();
2100        assert_eq!(desc, PixelDescriptor::CMYK8);
2101    }
2102
2103    #[test]
2104    fn cmyk_channel_layout_display() {
2105        assert_eq!(format!("{}", ChannelLayout::Cmyk), "CMYK");
2106    }
2107
2108    #[test]
2109    fn cmyk_channel_layout_no_alpha() {
2110        assert!(!ChannelLayout::Cmyk.has_alpha());
2111    }
2112}