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    /// # See also
675    ///
676    /// * [`new_full`](Self::new_full) — same constructor with an explicit
677    ///   [`ColorPrimaries`] argument, for anything that isn't BT.709/sRGB
678    ///   gamut (BT.2020 HDR, Display P3, …).
679    /// * The HDR presets [`RGB16_BT2100_PQ`](Self::RGB16_BT2100_PQ) /
680    ///   [`RGB16_BT2100_HLG`](Self::RGB16_BT2100_HLG) — common BT.2100
681    ///   combinations without spelling out the enums.
682    ///
683    /// # Panics
684    ///
685    /// Panics if the `(channel_type, layout, alpha)` combination has no
686    /// corresponding [`PixelFormat`] variant (e.g. `(U16, Bgra, _)`).
687    #[inline]
688    pub const fn new(
689        channel_type: ChannelType,
690        layout: ChannelLayout,
691        alpha: Option<AlphaMode>,
692        transfer: TransferFunction,
693    ) -> Self {
694        let format = match PixelFormat::from_parts(channel_type, layout, alpha) {
695            Some(f) => f,
696            None => panic!("unsupported PixelFormat combination"),
697        };
698        Self {
699            format,
700            transfer,
701            alpha,
702            primaries: ColorPrimaries::Bt709,
703            signal_range: SignalRange::Full,
704        }
705    }
706
707    /// Create a descriptor with explicit primaries.
708    ///
709    /// # See also
710    ///
711    /// * [`new`](Self::new) — the four-argument form when BT.709/sRGB
712    ///   primaries (the default) are what you want.
713    /// * [`RGB16_BT2100_PQ`](Self::RGB16_BT2100_PQ) /
714    ///   [`RGB16_BT2100_HLG`](Self::RGB16_BT2100_HLG) — ready-made BT.2100
715    ///   HDR presets built on this constructor.
716    ///
717    /// # Panics
718    ///
719    /// Panics if the `(channel_type, layout, alpha)` combination has no
720    /// corresponding [`PixelFormat`] variant.
721    #[inline]
722    pub const fn new_full(
723        channel_type: ChannelType,
724        layout: ChannelLayout,
725        alpha: Option<AlphaMode>,
726        transfer: TransferFunction,
727        primaries: ColorPrimaries,
728    ) -> Self {
729        let format = match PixelFormat::from_parts(channel_type, layout, alpha) {
730            Some(f) => f,
731            None => panic!("unsupported PixelFormat combination"),
732        };
733        Self {
734            format,
735            transfer,
736            alpha,
737            primaries,
738            signal_range: SignalRange::Full,
739        }
740    }
741
742    /// Create from a [`PixelFormat`] with default alpha, unknown transfer,
743    /// BT.709 primaries, and full range.
744    #[inline]
745    pub const fn from_pixel_format(format: PixelFormat) -> Self {
746        Self {
747            format,
748            transfer: TransferFunction::Unknown,
749            alpha: format.default_alpha(),
750            primaries: ColorPrimaries::Bt709,
751            signal_range: SignalRange::Full,
752        }
753    }
754
755    // -- sRGB constants -------------------------------------------------------
756
757    /// 8-bit sRGB RGB.
758    pub const RGB8_SRGB: Self = Self::new(
759        ChannelType::U8,
760        ChannelLayout::Rgb,
761        None,
762        TransferFunction::Srgb,
763    );
764    /// 8-bit sRGB RGBA with straight alpha.
765    pub const RGBA8_SRGB: Self = Self::new(
766        ChannelType::U8,
767        ChannelLayout::Rgba,
768        Some(AlphaMode::Straight),
769        TransferFunction::Srgb,
770    );
771    /// 16-bit sRGB RGB.
772    pub const RGB16_SRGB: Self = Self::new(
773        ChannelType::U16,
774        ChannelLayout::Rgb,
775        None,
776        TransferFunction::Srgb,
777    );
778    /// 16-bit sRGB RGBA with straight alpha.
779    pub const RGBA16_SRGB: Self = Self::new(
780        ChannelType::U16,
781        ChannelLayout::Rgba,
782        Some(AlphaMode::Straight),
783        TransferFunction::Srgb,
784    );
785    /// Linear-light f32 RGB.
786    pub const RGBF32_LINEAR: Self = Self::new(
787        ChannelType::F32,
788        ChannelLayout::Rgb,
789        None,
790        TransferFunction::Linear,
791    );
792    /// Linear-light f32 RGBA with straight alpha.
793    pub const RGBAF32_LINEAR: Self = Self::new(
794        ChannelType::F32,
795        ChannelLayout::Rgba,
796        Some(AlphaMode::Straight),
797        TransferFunction::Linear,
798    );
799    /// 8-bit sRGB grayscale.
800    pub const GRAY8_SRGB: Self = Self::new(
801        ChannelType::U8,
802        ChannelLayout::Gray,
803        None,
804        TransferFunction::Srgb,
805    );
806    /// 16-bit sRGB grayscale.
807    pub const GRAY16_SRGB: Self = Self::new(
808        ChannelType::U16,
809        ChannelLayout::Gray,
810        None,
811        TransferFunction::Srgb,
812    );
813    /// Linear-light f32 grayscale.
814    pub const GRAYF32_LINEAR: Self = Self::new(
815        ChannelType::F32,
816        ChannelLayout::Gray,
817        None,
818        TransferFunction::Linear,
819    );
820    /// 8-bit sRGB grayscale with straight alpha.
821    pub const GRAYA8_SRGB: Self = Self::new(
822        ChannelType::U8,
823        ChannelLayout::GrayAlpha,
824        Some(AlphaMode::Straight),
825        TransferFunction::Srgb,
826    );
827    /// 16-bit sRGB grayscale with straight alpha.
828    pub const GRAYA16_SRGB: Self = Self::new(
829        ChannelType::U16,
830        ChannelLayout::GrayAlpha,
831        Some(AlphaMode::Straight),
832        TransferFunction::Srgb,
833    );
834    /// Linear-light f32 grayscale with straight alpha.
835    pub const GRAYAF32_LINEAR: Self = Self::new(
836        ChannelType::F32,
837        ChannelLayout::GrayAlpha,
838        Some(AlphaMode::Straight),
839        TransferFunction::Linear,
840    );
841    /// 8-bit sRGB BGRA with straight alpha.
842    pub const BGRA8_SRGB: Self = Self::new(
843        ChannelType::U8,
844        ChannelLayout::Bgra,
845        Some(AlphaMode::Straight),
846        TransferFunction::Srgb,
847    );
848    /// 8-bit sRGB RGBX (padding byte, not alpha).
849    pub const RGBX8_SRGB: Self = Self::new(
850        ChannelType::U8,
851        ChannelLayout::Rgba,
852        Some(AlphaMode::Undefined),
853        TransferFunction::Srgb,
854    );
855    /// 8-bit sRGB BGRX (padding byte, not alpha).
856    pub const BGRX8_SRGB: Self = Self::new(
857        ChannelType::U8,
858        ChannelLayout::Bgra,
859        Some(AlphaMode::Undefined),
860        TransferFunction::Srgb,
861    );
862
863    // -- HDR (BT.2100) constants ------------------------------------------------
864
865    /// 16-bit BT.2100 PQ RGB (BT.2020 primaries, PQ transfer, full range) —
866    /// CICP `(9, 16, 0, full)`. The common interchange form for HDR10-style
867    /// stills decoded to u16.
868    ///
869    /// Use [`new_full`](Self::new_full) for other channel types or layouts in
870    /// this color space.
871    pub const RGB16_BT2100_PQ: Self = Self::new_full(
872        ChannelType::U16,
873        ChannelLayout::Rgb,
874        None,
875        TransferFunction::Pq,
876        ColorPrimaries::Bt2020,
877    );
878    /// 16-bit BT.2100 HLG RGB (BT.2020 primaries, HLG transfer, full range) —
879    /// CICP `(9, 18, 0, full)`.
880    ///
881    /// Use [`new_full`](Self::new_full) for other channel types or layouts in
882    /// this color space.
883    pub const RGB16_BT2100_HLG: Self = Self::new_full(
884        ChannelType::U16,
885        ChannelLayout::Rgb,
886        None,
887        TransferFunction::Hlg,
888        ColorPrimaries::Bt2020,
889    );
890
891    // -- Transfer-agnostic constants ------------------------------------------
892
893    /// 8-bit RGB, transfer unknown.
894    pub const RGB8: Self = Self::new(
895        ChannelType::U8,
896        ChannelLayout::Rgb,
897        None,
898        TransferFunction::Unknown,
899    );
900    /// 8-bit RGBA, transfer unknown.
901    pub const RGBA8: Self = Self::new(
902        ChannelType::U8,
903        ChannelLayout::Rgba,
904        Some(AlphaMode::Straight),
905        TransferFunction::Unknown,
906    );
907    /// 16-bit RGB, transfer unknown.
908    pub const RGB16: Self = Self::new(
909        ChannelType::U16,
910        ChannelLayout::Rgb,
911        None,
912        TransferFunction::Unknown,
913    );
914    /// 16-bit RGBA, transfer unknown.
915    pub const RGBA16: Self = Self::new(
916        ChannelType::U16,
917        ChannelLayout::Rgba,
918        Some(AlphaMode::Straight),
919        TransferFunction::Unknown,
920    );
921    /// f32 RGB, transfer unknown.
922    pub const RGBF32: Self = Self::new(
923        ChannelType::F32,
924        ChannelLayout::Rgb,
925        None,
926        TransferFunction::Unknown,
927    );
928    /// f32 RGBA, transfer unknown.
929    pub const RGBAF32: Self = Self::new(
930        ChannelType::F32,
931        ChannelLayout::Rgba,
932        Some(AlphaMode::Straight),
933        TransferFunction::Unknown,
934    );
935    /// 8-bit grayscale, transfer unknown.
936    pub const GRAY8: Self = Self::new(
937        ChannelType::U8,
938        ChannelLayout::Gray,
939        None,
940        TransferFunction::Unknown,
941    );
942    /// 16-bit grayscale, transfer unknown.
943    pub const GRAY16: Self = Self::new(
944        ChannelType::U16,
945        ChannelLayout::Gray,
946        None,
947        TransferFunction::Unknown,
948    );
949    /// f32 grayscale, transfer unknown.
950    pub const GRAYF32: Self = Self::new(
951        ChannelType::F32,
952        ChannelLayout::Gray,
953        None,
954        TransferFunction::Unknown,
955    );
956    /// 8-bit grayscale with alpha, transfer unknown.
957    pub const GRAYA8: Self = Self::new(
958        ChannelType::U8,
959        ChannelLayout::GrayAlpha,
960        Some(AlphaMode::Straight),
961        TransferFunction::Unknown,
962    );
963    /// 16-bit grayscale with alpha, transfer unknown.
964    pub const GRAYA16: Self = Self::new(
965        ChannelType::U16,
966        ChannelLayout::GrayAlpha,
967        Some(AlphaMode::Straight),
968        TransferFunction::Unknown,
969    );
970    /// f32 grayscale with alpha, transfer unknown.
971    pub const GRAYAF32: Self = Self::new(
972        ChannelType::F32,
973        ChannelLayout::GrayAlpha,
974        Some(AlphaMode::Straight),
975        TransferFunction::Unknown,
976    );
977
978    // -- F16 constants (pub(crate) — internal use by PixelFormat::descriptor()
979    //    mapping. Tests construct via PixelDescriptor::new(). Linear-TF
980    //    F16 variants would be added if an external codec consumer needs
981    //    them — none today.) -----------------------------------------------
982
983    pub(crate) const RGBF16: Self = Self::new(
984        ChannelType::F16,
985        ChannelLayout::Rgb,
986        None,
987        TransferFunction::Unknown,
988    );
989    pub(crate) const RGBAF16: Self = Self::new(
990        ChannelType::F16,
991        ChannelLayout::Rgba,
992        Some(AlphaMode::Straight),
993        TransferFunction::Unknown,
994    );
995    pub(crate) const GRAYF16: Self = Self::new(
996        ChannelType::F16,
997        ChannelLayout::Gray,
998        None,
999        TransferFunction::Unknown,
1000    );
1001    pub(crate) const GRAYAF16: Self = Self::new(
1002        ChannelType::F16,
1003        ChannelLayout::GrayAlpha,
1004        Some(AlphaMode::Straight),
1005        TransferFunction::Unknown,
1006    );
1007    /// 8-bit BGRA, transfer unknown.
1008    pub const BGRA8: Self = Self::new(
1009        ChannelType::U8,
1010        ChannelLayout::Bgra,
1011        Some(AlphaMode::Straight),
1012        TransferFunction::Unknown,
1013    );
1014    /// 8-bit RGBX, transfer unknown.
1015    pub const RGBX8: Self = Self::new(
1016        ChannelType::U8,
1017        ChannelLayout::Rgba,
1018        Some(AlphaMode::Undefined),
1019        TransferFunction::Unknown,
1020    );
1021    /// 8-bit BGRX, transfer unknown.
1022    pub const BGRX8: Self = Self::new(
1023        ChannelType::U8,
1024        ChannelLayout::Bgra,
1025        Some(AlphaMode::Undefined),
1026        TransferFunction::Unknown,
1027    );
1028
1029    // -- Oklab constants ------------------------------------------------------
1030
1031    /// Oklab f32 (L, a, b), transfer unknown.
1032    pub const OKLABF32: Self = Self {
1033        format: PixelFormat::OklabF32,
1034        transfer: TransferFunction::Unknown,
1035        alpha: None,
1036        primaries: ColorPrimaries::Bt709,
1037        signal_range: SignalRange::Full,
1038    };
1039    /// Oklab+alpha f32 (L, a, b, alpha), transfer unknown.
1040    pub const OKLABAF32: Self = Self {
1041        format: PixelFormat::OklabaF32,
1042        transfer: TransferFunction::Unknown,
1043        alpha: Some(AlphaMode::Straight),
1044        primaries: ColorPrimaries::Bt709,
1045        signal_range: SignalRange::Full,
1046    };
1047
1048    // -- CMYK constants --------------------------------------------------------
1049
1050    /// 8-bit CMYK, no transfer function or primaries.
1051    pub const CMYK8: Self = Self::from_pixel_format(PixelFormat::Cmyk8);
1052
1053    // -- Methods --------------------------------------------------------------
1054
1055    /// Number of channels.
1056    #[inline]
1057    pub const fn channels(self) -> usize {
1058        self.format.channels()
1059    }
1060
1061    /// Bytes per pixel.
1062    #[inline]
1063    pub const fn bytes_per_pixel(self) -> usize {
1064        self.format.bytes_per_pixel()
1065    }
1066
1067    /// Bytes per channel (1 for U8, 2 for U16/F16, 4 for F32).
1068    ///
1069    /// Shorthand for `self.channel_type().byte_size()` — the per-sample
1070    /// width, as opposed to [`bytes_per_pixel`](Self::bytes_per_pixel)
1071    /// (all channels of one pixel). The usual "do I need a
1072    /// high-bit-depth path?" check is `bytes_per_channel() > 1`.
1073    #[inline]
1074    pub const fn bytes_per_channel(self) -> usize {
1075        self.channel_type().byte_size()
1076    }
1077
1078    /// Whether this descriptor has meaningful alpha data.
1079    #[inline]
1080    pub const fn has_alpha(self) -> bool {
1081        matches!(
1082            self.alpha,
1083            Some(AlphaMode::Straight) | Some(AlphaMode::Premultiplied) | Some(AlphaMode::Opaque)
1084        )
1085    }
1086
1087    /// Whether this descriptor is grayscale.
1088    #[inline]
1089    pub const fn is_grayscale(self) -> bool {
1090        self.format.is_grayscale()
1091    }
1092
1093    /// Whether this descriptor uses BGR byte order.
1094    #[inline]
1095    pub const fn is_bgr(self) -> bool {
1096        matches!(self.format.byte_order(), ByteOrder::Bgr)
1097    }
1098
1099    /// Return a copy of this descriptor **relabeled** with a different
1100    /// transfer function. Does not touch any pixel bytes.
1101    ///
1102    /// Use when the source data was mistagged (e.g., a codec decoded a
1103    /// profile-less image and you know its true TF from another source)
1104    /// or when you want to assert a particular TF for downstream planning
1105    /// without round-tripping through EOTF/OETF kernels.
1106    ///
1107    /// **This is metadata-only.** To actually re-encode pixel bytes from
1108    /// one transfer function into another, pass a source buffer and a
1109    /// destination descriptor with the new TF into [`RowConverter::new`]
1110    /// — that builds a plan with the appropriate EOTF/OETF kernels.
1111    ///
1112    /// [`RowConverter::new`]: https://docs.rs/zenpixels-convert/latest/zenpixels_convert/struct.RowConverter.html#method.new
1113    #[inline]
1114    #[must_use]
1115    pub const fn with_transfer(self, transfer: TransferFunction) -> Self {
1116        Self { transfer, ..self }
1117    }
1118
1119    /// Return a copy of this descriptor **relabeled** with different
1120    /// primaries. Does not touch any pixel bytes or apply a gamut matrix.
1121    ///
1122    /// Use when you know the true primaries of the source data independent
1123    /// of whatever upstream tagging said, or to assert a particular primary
1124    /// set for downstream planning.
1125    ///
1126    /// **This is metadata-only.** To actually apply a gamut conversion
1127    /// (e.g., BT.709 → Display P3), pass the new descriptor as the
1128    /// destination to [`RowConverter::new`] — that inserts the appropriate
1129    /// gamut matrix in linear light.
1130    ///
1131    /// [`RowConverter::new`]: https://docs.rs/zenpixels-convert/latest/zenpixels_convert/struct.RowConverter.html#method.new
1132    #[inline]
1133    #[must_use]
1134    pub const fn with_primaries(self, primaries: ColorPrimaries) -> Self {
1135        Self { primaries, ..self }
1136    }
1137
1138    /// Return a copy of this descriptor **relabeled** with a different
1139    /// alpha mode. Does not touch any pixel bytes or apply (un)premultiply.
1140    ///
1141    /// Use when the source alpha mode was mistagged or when you want to
1142    /// assert straight/premultiplied without round-tripping through the
1143    /// premul kernels.
1144    ///
1145    /// **This is metadata-only.** To actually premultiply or un-premultiply
1146    /// pixel values, pass the new descriptor as the destination to
1147    /// [`RowConverter::new`] — that inserts a `StraightToPremul` or
1148    /// `PremulToStraight` step. (Note: the built-in premul kernels operate
1149    /// in the source byte space — "encoded premul", per Canvas 2D
1150    /// semantics — not in linear light.)
1151    ///
1152    /// [`RowConverter::new`]: https://docs.rs/zenpixels-convert/latest/zenpixels_convert/struct.RowConverter.html#method.new
1153    #[inline]
1154    #[must_use]
1155    pub const fn with_alpha(self, alpha: Option<AlphaMode>) -> Self {
1156        Self { alpha, ..self }
1157    }
1158
1159    /// Alias for [`with_alpha`](Self::with_alpha).
1160    #[inline]
1161    #[must_use]
1162    pub const fn with_alpha_mode(self, alpha: Option<AlphaMode>) -> Self {
1163        self.with_alpha(alpha)
1164    }
1165
1166    /// Return a copy with a different signal range.
1167    #[inline]
1168    #[must_use]
1169    pub const fn with_signal_range(self, signal_range: SignalRange) -> Self {
1170        Self {
1171            signal_range,
1172            ..self
1173        }
1174    }
1175
1176    /// Whether this format is fully opaque (no transparency possible).
1177    ///
1178    /// Returns `true` when there is no alpha channel (`None`), the alpha
1179    /// bytes are undefined padding (`Undefined`), or alpha is all-255 (`Opaque`).
1180    #[inline]
1181    pub const fn is_opaque(self) -> bool {
1182        matches!(
1183            self.alpha,
1184            None | Some(AlphaMode::Undefined | AlphaMode::Opaque)
1185        )
1186    }
1187
1188    /// Whether this format may contain transparent pixels.
1189    ///
1190    /// Returns `true` for [`Straight`](AlphaMode::Straight) and
1191    /// [`Premultiplied`](AlphaMode::Premultiplied).
1192    #[inline]
1193    #[allow(unreachable_patterns)]
1194    pub const fn may_have_transparency(self) -> bool {
1195        matches!(
1196            self.alpha,
1197            Some(AlphaMode::Straight | AlphaMode::Premultiplied)
1198        )
1199    }
1200
1201    /// Whether the transfer function is [`Linear`](TransferFunction::Linear).
1202    #[inline]
1203    pub const fn is_linear(self) -> bool {
1204        matches!(self.transfer, TransferFunction::Linear)
1205    }
1206
1207    /// Whether the transfer function is [`Unknown`](TransferFunction::Unknown).
1208    #[inline]
1209    pub const fn is_unknown_transfer(self) -> bool {
1210        matches!(self.transfer, TransferFunction::Unknown)
1211    }
1212
1213    /// Minimum byte alignment required for the channel type (1, 2, or 4).
1214    #[inline]
1215    pub const fn min_alignment(self) -> usize {
1216        self.format.channel_type().byte_size()
1217    }
1218
1219    /// Tightly-packed byte stride for a given width.
1220    #[inline]
1221    pub const fn aligned_stride(self, width: u32) -> usize {
1222        width as usize * self.bytes_per_pixel()
1223    }
1224
1225    /// SIMD-friendly byte stride for a given width.
1226    ///
1227    /// The stride is a multiple of `lcm(bytes_per_pixel, simd_align)`,
1228    /// ensuring every row start is both pixel-aligned and SIMD-aligned.
1229    /// `simd_align` must be a power of 2.
1230    #[inline]
1231    pub const fn simd_aligned_stride(self, width: u32, simd_align: usize) -> usize {
1232        let bpp = self.bytes_per_pixel();
1233        let raw = width as usize * bpp;
1234        let align = lcm(bpp, simd_align);
1235        align_up_general(raw, align)
1236    }
1237
1238    /// Whether this descriptor's channel type and layout are compatible with `other`.
1239    ///
1240    /// "Compatible" means the raw bytes can be reinterpreted as `other`
1241    /// without any pixel transformation — same channel type, same layout.
1242    #[inline]
1243    pub const fn layout_compatible(self, other: Self) -> bool {
1244        self.format.channel_type() as u8 == other.format.channel_type() as u8
1245            && self.layout() as u8 == other.layout() as u8
1246    }
1247}
1248
1249// Alignment helpers.
1250
1251const fn gcd(mut a: usize, mut b: usize) -> usize {
1252    while b != 0 {
1253        let t = b;
1254        b = a % b;
1255        a = t;
1256    }
1257    a
1258}
1259
1260const fn lcm(a: usize, b: usize) -> usize {
1261    if a == 0 || b == 0 {
1262        0
1263    } else {
1264        a / gcd(a, b) * b
1265    }
1266}
1267
1268const fn align_up_general(value: usize, align: usize) -> usize {
1269    if align == 0 {
1270        return value;
1271    }
1272    let rem = value % align;
1273    if rem == 0 { value } else { value + align - rem }
1274}
1275
1276impl fmt::Display for PixelDescriptor {
1277    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1278        write!(
1279            f,
1280            "{} {} {}",
1281            self.format,
1282            self.format.channel_type(),
1283            self.transfer
1284        )?;
1285        if let Some(alpha) = self.alpha {
1286            if alpha.has_alpha() {
1287                write!(f, " alpha={alpha}")?;
1288            }
1289        }
1290        Ok(())
1291    }
1292}
1293
1294// ---------------------------------------------------------------------------
1295// Color model — what the channels represent
1296// ---------------------------------------------------------------------------
1297
1298/// What the channels represent, independent of channel count or byte order.
1299#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1300#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1301#[non_exhaustive]
1302#[repr(u8)]
1303pub enum ColorModel {
1304    /// Single grayscale channel.
1305    Gray = 0,
1306    /// Red, green, blue (or BGR when [`ByteOrder::Bgr`]).
1307    Rgb = 1,
1308    /// Luma + chroma (Y, Cb, Cr).
1309    YCbCr = 2,
1310    /// Oklab perceptual color space (L, a, b).
1311    Oklab = 3,
1312    /// Cyan, magenta, yellow, key (black). Device-dependent printing color space.
1313    /// RGB primaries and transfer functions do not apply to CMYK data.
1314    /// Use a CMS with an ICC profile for CMYK↔RGB conversion.
1315    Cmyk = 4,
1316}
1317
1318impl ColorModel {
1319    /// Number of color channels (excluding alpha).
1320    #[inline]
1321    #[allow(unreachable_patterns)]
1322    pub const fn color_channels(self) -> u8 {
1323        match self {
1324            Self::Gray => 1,
1325            Self::Cmyk => 4,
1326            _ => 3,
1327        }
1328    }
1329}
1330
1331impl fmt::Display for ColorModel {
1332    #[allow(unreachable_patterns)]
1333    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1334        match self {
1335            Self::Gray => f.write_str("Gray"),
1336            Self::Rgb => f.write_str("RGB"),
1337            Self::YCbCr => f.write_str("YCbCr"),
1338            Self::Oklab => f.write_str("Oklab"),
1339            Self::Cmyk => f.write_str("CMYK"),
1340            _ => write!(f, "ColorModel({})", *self as u8),
1341        }
1342    }
1343}
1344
1345// ---------------------------------------------------------------------------
1346// Byte order
1347// ---------------------------------------------------------------------------
1348
1349/// RGB-family byte order. Only meaningful when color model is RGB.
1350#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1351#[non_exhaustive]
1352#[repr(u8)]
1353pub enum ByteOrder {
1354    /// Standard order: R, G, B (+ A if present).
1355    #[default]
1356    Native = 0,
1357    /// Windows/DirectX order: B, G, R (+ A if present).
1358    Bgr = 1,
1359}
1360
1361impl fmt::Display for ByteOrder {
1362    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1363        match self {
1364            Self::Native => f.write_str("native"),
1365            Self::Bgr => f.write_str("BGR"),
1366        }
1367    }
1368}
1369
1370// ---------------------------------------------------------------------------
1371// PixelFormat — flat enum for physical pixel layout
1372// ---------------------------------------------------------------------------
1373
1374/// Physical pixel layout for match-based format dispatch.
1375///
1376/// Each variant encodes the channel type (U8/U16/F32) and layout (RGB/RGBA/
1377/// Gray/etc.) in one discriminant. Transfer function and alpha mode live on
1378/// [`PixelDescriptor`], not here.
1379///
1380/// Use this enum when you need exhaustive `match` dispatch over known
1381/// pixel layouts.
1382#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1383#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1384#[non_exhaustive]
1385#[repr(u8)]
1386pub enum PixelFormat {
1387    Rgb8 = 1,
1388    Rgba8 = 2,
1389    Rgb16 = 3,
1390    Rgba16 = 4,
1391    RgbF32 = 5,
1392    RgbaF32 = 6,
1393    Gray8 = 7,
1394    Gray16 = 8,
1395    GrayF32 = 9,
1396    GrayA8 = 10,
1397    GrayA16 = 11,
1398    GrayAF32 = 12,
1399    Bgra8 = 13,
1400    Rgbx8 = 14,
1401    Bgrx8 = 15,
1402    OklabF32 = 16,
1403    OklabaF32 = 17,
1404    /// 8-bit CMYK (4 bytes per pixel, no transfer function).
1405    Cmyk8 = 18,
1406    /// f16 (IEEE 754 half-precision) RGB.
1407    RgbF16 = 19,
1408    /// f16 (IEEE 754 half-precision) RGBA.
1409    RgbaF16 = 20,
1410    /// f16 (IEEE 754 half-precision) grayscale.
1411    GrayF16 = 21,
1412    /// f16 (IEEE 754 half-precision) grayscale with alpha.
1413    GrayAF16 = 22,
1414}
1415
1416impl PixelFormat {
1417    /// Channel storage type.
1418    #[inline]
1419    #[allow(unreachable_patterns)]
1420    pub const fn channel_type(self) -> ChannelType {
1421        match self {
1422            Self::Rgb8
1423            | Self::Rgba8
1424            | Self::Gray8
1425            | Self::GrayA8
1426            | Self::Bgra8
1427            | Self::Rgbx8
1428            | Self::Bgrx8
1429            | Self::Cmyk8 => ChannelType::U8,
1430            Self::Rgb16 | Self::Rgba16 | Self::Gray16 | Self::GrayA16 => ChannelType::U16,
1431            Self::RgbF32
1432            | Self::RgbaF32
1433            | Self::GrayF32
1434            | Self::GrayAF32
1435            | Self::OklabF32
1436            | Self::OklabaF32 => ChannelType::F32,
1437            Self::RgbF16 | Self::RgbaF16 | Self::GrayF16 | Self::GrayAF16 => ChannelType::F16,
1438            _ => ChannelType::U8,
1439        }
1440    }
1441
1442    /// Channel layout.
1443    #[inline]
1444    #[allow(unreachable_patterns)]
1445    pub const fn layout(self) -> ChannelLayout {
1446        match self {
1447            Self::Rgb8 | Self::Rgb16 | Self::RgbF32 | Self::RgbF16 => ChannelLayout::Rgb,
1448            Self::Rgba8 | Self::Rgba16 | Self::RgbaF32 | Self::RgbaF16 | Self::Rgbx8 => {
1449                ChannelLayout::Rgba
1450            }
1451            Self::Gray8 | Self::Gray16 | Self::GrayF32 | Self::GrayF16 => ChannelLayout::Gray,
1452            Self::GrayA8 | Self::GrayA16 | Self::GrayAF32 | Self::GrayAF16 => {
1453                ChannelLayout::GrayAlpha
1454            }
1455            Self::Bgra8 | Self::Bgrx8 => ChannelLayout::Bgra,
1456            Self::OklabF32 => ChannelLayout::Oklab,
1457            Self::OklabaF32 => ChannelLayout::OklabA,
1458            Self::Cmyk8 => ChannelLayout::Cmyk,
1459            _ => ChannelLayout::Rgb,
1460        }
1461    }
1462
1463    /// Color model (what the channels represent).
1464    #[inline]
1465    #[allow(unreachable_patterns)]
1466    pub const fn color_model(self) -> ColorModel {
1467        match self {
1468            Self::Gray8
1469            | Self::Gray16
1470            | Self::GrayF32
1471            | Self::GrayF16
1472            | Self::GrayA8
1473            | Self::GrayA16
1474            | Self::GrayAF32
1475            | Self::GrayAF16 => ColorModel::Gray,
1476            Self::OklabF32 | Self::OklabaF32 => ColorModel::Oklab,
1477            Self::Cmyk8 => ColorModel::Cmyk,
1478            _ => ColorModel::Rgb,
1479        }
1480    }
1481
1482    /// Byte order (Native or BGR).
1483    #[inline]
1484    #[allow(unreachable_patterns)]
1485    pub const fn byte_order(self) -> ByteOrder {
1486        match self {
1487            Self::Bgra8 | Self::Bgrx8 => ByteOrder::Bgr,
1488            _ => ByteOrder::Native,
1489        }
1490    }
1491
1492    /// Number of channels (including alpha/padding if present).
1493    #[inline]
1494    pub const fn channels(self) -> usize {
1495        self.layout().channels()
1496    }
1497
1498    /// Bytes per pixel.
1499    #[inline]
1500    pub const fn bytes_per_pixel(self) -> usize {
1501        self.channels() * self.channel_type().byte_size()
1502    }
1503
1504    /// Whether this format has alpha or padding bytes (4th channel).
1505    #[inline]
1506    pub const fn has_alpha_bytes(self) -> bool {
1507        self.layout().has_alpha()
1508    }
1509
1510    /// Whether this format is grayscale.
1511    #[inline]
1512    pub const fn is_grayscale(self) -> bool {
1513        matches!(self.color_model(), ColorModel::Gray)
1514    }
1515
1516    /// Default alpha mode for this format.
1517    ///
1518    /// - Formats with no alpha bytes → `None`
1519    /// - Formats with padding (Rgbx8, Bgrx8) → `Some(AlphaMode::Undefined)`
1520    /// - Formats with alpha → `Some(AlphaMode::Straight)`
1521    #[allow(unreachable_patterns)]
1522    #[inline]
1523    pub const fn default_alpha(self) -> Option<AlphaMode> {
1524        match self {
1525            Self::Rgb8
1526            | Self::Rgb16
1527            | Self::RgbF32
1528            | Self::RgbF16
1529            | Self::Gray8
1530            | Self::Gray16
1531            | Self::GrayF32
1532            | Self::GrayF16
1533            | Self::OklabF32
1534            | Self::Cmyk8 => None,
1535            Self::Rgbx8 | Self::Bgrx8 => Some(AlphaMode::Undefined),
1536            _ => Some(AlphaMode::Straight),
1537        }
1538    }
1539
1540    /// Short human-readable name.
1541    #[allow(unreachable_patterns)]
1542    #[inline]
1543    pub const fn name(self) -> &'static str {
1544        match self {
1545            Self::Rgb8 => "RGB8",
1546            Self::Rgba8 => "RGBA8",
1547            Self::Rgb16 => "RGB16",
1548            Self::Rgba16 => "RGBA16",
1549            Self::RgbF32 => "RgbF32",
1550            Self::RgbaF32 => "RgbaF32",
1551            Self::Gray8 => "Gray8",
1552            Self::Gray16 => "Gray16",
1553            Self::GrayF32 => "GrayF32",
1554            Self::GrayA8 => "GrayA8",
1555            Self::GrayA16 => "GrayA16",
1556            Self::GrayAF32 => "GrayAF32",
1557            Self::Bgra8 => "BGRA8",
1558            Self::Rgbx8 => "RGBX8",
1559            Self::Bgrx8 => "BGRX8",
1560            Self::OklabF32 => "OklabF32",
1561            Self::OklabaF32 => "OklabaF32",
1562            Self::Cmyk8 => "CMYK8",
1563            Self::RgbF16 => "RgbF16",
1564            Self::RgbaF16 => "RgbaF16",
1565            Self::GrayF16 => "GrayF16",
1566            Self::GrayAF16 => "GrayAF16",
1567            _ => "Unknown",
1568        }
1569    }
1570
1571    /// Resolve a format from channel type, layout, and alpha presence.
1572    ///
1573    /// Returns `None` for combinations that have no `PixelFormat` variant
1574    /// (e.g. `(U16, Bgra, _)`).
1575    #[inline]
1576    pub const fn from_parts(
1577        channel_type: ChannelType,
1578        layout: ChannelLayout,
1579        alpha: Option<AlphaMode>,
1580    ) -> Option<Self> {
1581        let is_padding = matches!(alpha, Some(AlphaMode::Undefined));
1582        match (channel_type, layout, is_padding) {
1583            (ChannelType::U8, ChannelLayout::Rgb, _) => Some(Self::Rgb8),
1584            (ChannelType::U16, ChannelLayout::Rgb, _) => Some(Self::Rgb16),
1585            (ChannelType::F32, ChannelLayout::Rgb, _) => Some(Self::RgbF32),
1586            (ChannelType::F16, ChannelLayout::Rgb, _) => Some(Self::RgbF16),
1587
1588            (ChannelType::U8, ChannelLayout::Rgba, true) => Some(Self::Rgbx8),
1589            (ChannelType::U8, ChannelLayout::Rgba, false) => Some(Self::Rgba8),
1590            (ChannelType::U16, ChannelLayout::Rgba, _) => Some(Self::Rgba16),
1591            (ChannelType::F32, ChannelLayout::Rgba, _) => Some(Self::RgbaF32),
1592            (ChannelType::F16, ChannelLayout::Rgba, _) => Some(Self::RgbaF16),
1593
1594            (ChannelType::U8, ChannelLayout::Gray, _) => Some(Self::Gray8),
1595            (ChannelType::U16, ChannelLayout::Gray, _) => Some(Self::Gray16),
1596            (ChannelType::F32, ChannelLayout::Gray, _) => Some(Self::GrayF32),
1597            (ChannelType::F16, ChannelLayout::Gray, _) => Some(Self::GrayF16),
1598
1599            (ChannelType::U8, ChannelLayout::GrayAlpha, _) => Some(Self::GrayA8),
1600            (ChannelType::U16, ChannelLayout::GrayAlpha, _) => Some(Self::GrayA16),
1601            (ChannelType::F32, ChannelLayout::GrayAlpha, _) => Some(Self::GrayAF32),
1602            (ChannelType::F16, ChannelLayout::GrayAlpha, _) => Some(Self::GrayAF16),
1603
1604            (ChannelType::U8, ChannelLayout::Bgra, true) => Some(Self::Bgrx8),
1605            (ChannelType::U8, ChannelLayout::Bgra, false) => Some(Self::Bgra8),
1606
1607            (ChannelType::F32, ChannelLayout::Oklab, _) => Some(Self::OklabF32),
1608            (ChannelType::F32, ChannelLayout::OklabA, _) => Some(Self::OklabaF32),
1609
1610            (ChannelType::U8, ChannelLayout::Cmyk, _) => Some(Self::Cmyk8),
1611
1612            _ => None,
1613        }
1614    }
1615
1616    /// Base descriptor with `Unknown` transfer and BT.709 primaries.
1617    #[allow(unreachable_patterns)]
1618    #[inline]
1619    pub const fn descriptor(self) -> PixelDescriptor {
1620        match self {
1621            Self::Rgb8 => PixelDescriptor::RGB8,
1622            Self::Rgba8 => PixelDescriptor::RGBA8,
1623            Self::Rgb16 => PixelDescriptor::RGB16,
1624            Self::Rgba16 => PixelDescriptor::RGBA16,
1625            Self::RgbF32 => PixelDescriptor::RGBF32,
1626            Self::RgbaF32 => PixelDescriptor::RGBAF32,
1627            Self::Gray8 => PixelDescriptor::GRAY8,
1628            Self::Gray16 => PixelDescriptor::GRAY16,
1629            Self::GrayF32 => PixelDescriptor::GRAYF32,
1630            Self::GrayA8 => PixelDescriptor::GRAYA8,
1631            Self::GrayA16 => PixelDescriptor::GRAYA16,
1632            Self::GrayAF32 => PixelDescriptor::GRAYAF32,
1633            Self::Bgra8 => PixelDescriptor::BGRA8,
1634            Self::Rgbx8 => PixelDescriptor::RGBX8,
1635            Self::Bgrx8 => PixelDescriptor::BGRX8,
1636            Self::OklabF32 => PixelDescriptor::OKLABF32,
1637            Self::OklabaF32 => PixelDescriptor::OKLABAF32,
1638            Self::Cmyk8 => PixelDescriptor::CMYK8,
1639            Self::RgbF16 => PixelDescriptor::RGBF16,
1640            Self::RgbaF16 => PixelDescriptor::RGBAF16,
1641            Self::GrayF16 => PixelDescriptor::GRAYF16,
1642            Self::GrayAF16 => PixelDescriptor::GRAYAF16,
1643            _ => PixelDescriptor::RGB8,
1644        }
1645    }
1646}
1647
1648impl fmt::Display for PixelFormat {
1649    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1650        f.write_str(self.name())
1651    }
1652}
1653
1654// ---------------------------------------------------------------------------
1655// Tests
1656// ---------------------------------------------------------------------------
1657
1658#[cfg(test)]
1659mod tests {
1660    use alloc::format;
1661    use core::mem::size_of;
1662
1663    use super::*;
1664
1665    #[test]
1666    fn channel_type_byte_size() {
1667        assert_eq!(ChannelType::U8.byte_size(), 1);
1668        assert_eq!(ChannelType::U16.byte_size(), 2);
1669        assert_eq!(ChannelType::F16.byte_size(), 2);
1670        assert_eq!(ChannelType::F32.byte_size(), 4);
1671    }
1672
1673    #[test]
1674    fn descriptor_bytes_per_pixel() {
1675        assert_eq!(PixelDescriptor::RGB8.bytes_per_pixel(), 3);
1676        assert_eq!(PixelDescriptor::RGBA8.bytes_per_pixel(), 4);
1677        assert_eq!(PixelDescriptor::GRAY8.bytes_per_pixel(), 1);
1678        assert_eq!(PixelDescriptor::RGBAF32.bytes_per_pixel(), 16);
1679        assert_eq!(PixelDescriptor::GRAYA8.bytes_per_pixel(), 2);
1680    }
1681
1682    #[test]
1683    fn descriptor_bytes_per_channel() {
1684        assert_eq!(PixelDescriptor::RGB8.bytes_per_channel(), 1);
1685        assert_eq!(PixelDescriptor::RGB16_SRGB.bytes_per_channel(), 2);
1686        assert_eq!(PixelDescriptor::RGBAF32.bytes_per_channel(), 4);
1687        // The distinction this accessor exists for: per-sample width vs
1688        // all channels of a pixel.
1689        assert_ne!(
1690            PixelDescriptor::RGBA8.bytes_per_channel(),
1691            PixelDescriptor::RGBA8.bytes_per_pixel()
1692        );
1693    }
1694
1695    #[test]
1696    fn bt2100_presets_carry_hdr_signaling() {
1697        let pq = PixelDescriptor::RGB16_BT2100_PQ;
1698        assert_eq!(pq.format, PixelFormat::Rgb16);
1699        assert_eq!(pq.transfer, TransferFunction::Pq);
1700        assert_eq!(pq.primaries, ColorPrimaries::Bt2020);
1701        assert_eq!(pq.signal_range, SignalRange::Full);
1702
1703        let hlg = PixelDescriptor::RGB16_BT2100_HLG;
1704        assert_eq!(hlg.format, PixelFormat::Rgb16);
1705        assert_eq!(hlg.transfer, TransferFunction::Hlg);
1706        assert_eq!(hlg.primaries, ColorPrimaries::Bt2020);
1707        assert_eq!(hlg.signal_range, SignalRange::Full);
1708
1709        // The presets must agree with spelling it out via new_full.
1710        assert_eq!(
1711            pq,
1712            PixelDescriptor::new_full(
1713                ChannelType::U16,
1714                ChannelLayout::Rgb,
1715                None,
1716                TransferFunction::Pq,
1717                ColorPrimaries::Bt2020,
1718            )
1719        );
1720    }
1721
1722    #[test]
1723    fn descriptor_has_alpha() {
1724        assert!(!PixelDescriptor::RGB8.has_alpha());
1725        assert!(PixelDescriptor::RGBA8.has_alpha());
1726        assert!(!PixelDescriptor::RGBX8.has_alpha());
1727        assert!(PixelDescriptor::GRAYA8.has_alpha());
1728    }
1729
1730    #[test]
1731    fn descriptor_is_grayscale() {
1732        assert!(PixelDescriptor::GRAY8.is_grayscale());
1733        assert!(PixelDescriptor::GRAYA8.is_grayscale());
1734        assert!(!PixelDescriptor::RGB8.is_grayscale());
1735    }
1736
1737    #[test]
1738    fn layout_compatible() {
1739        assert!(PixelDescriptor::RGB8_SRGB.layout_compatible(PixelDescriptor::RGB8));
1740        assert!(!PixelDescriptor::RGB8.layout_compatible(PixelDescriptor::RGBA8));
1741    }
1742
1743    #[test]
1744    fn pixel_format_descriptor_roundtrip() {
1745        let desc = PixelFormat::Rgba8.descriptor();
1746        assert_eq!(desc.layout(), ChannelLayout::Rgba);
1747        assert_eq!(desc.channel_type(), ChannelType::U8);
1748    }
1749
1750    #[test]
1751    fn pixel_format_enum_basics() {
1752        assert_eq!(PixelFormat::Rgb8.channels(), 3);
1753        assert_eq!(PixelFormat::Rgba8.channels(), 4);
1754        assert!(PixelFormat::Rgba8.has_alpha_bytes());
1755        assert!(!PixelFormat::Rgb8.has_alpha_bytes());
1756        assert_eq!(PixelFormat::RgbF32.bytes_per_pixel(), 12);
1757        assert_eq!(PixelFormat::RgbaF32.bytes_per_pixel(), 16);
1758        assert_eq!(PixelFormat::Gray8.channels(), 1);
1759        assert!(PixelFormat::Gray8.is_grayscale());
1760        assert!(!PixelFormat::Rgb8.is_grayscale());
1761        assert_eq!(PixelFormat::Bgra8.byte_order(), ByteOrder::Bgr);
1762        assert_eq!(PixelFormat::Rgb8.byte_order(), ByteOrder::Native);
1763    }
1764
1765    #[test]
1766    fn pixel_format_enum_size() {
1767        // Single-byte discriminant — much smaller than old 5-field struct.
1768        assert!(size_of::<PixelFormat>() <= 2);
1769    }
1770
1771    #[test]
1772    fn pixel_format_from_parts_roundtrip() {
1773        let fmt = PixelFormat::Rgba8;
1774        let rebuilt =
1775            PixelFormat::from_parts(fmt.channel_type(), fmt.layout(), fmt.default_alpha());
1776        assert_eq!(rebuilt, Some(fmt));
1777
1778        let fmt2 = PixelFormat::Bgra8;
1779        let rebuilt2 =
1780            PixelFormat::from_parts(fmt2.channel_type(), fmt2.layout(), fmt2.default_alpha());
1781        assert_eq!(rebuilt2, Some(fmt2));
1782
1783        let fmt3 = PixelFormat::Gray8;
1784        let rebuilt3 =
1785            PixelFormat::from_parts(fmt3.channel_type(), fmt3.layout(), fmt3.default_alpha());
1786        assert_eq!(rebuilt3, Some(fmt3));
1787    }
1788
1789    #[test]
1790    fn alpha_mode_semantics() {
1791        // None (Option) = no alpha channel
1792        assert!(!PixelDescriptor::RGB8.has_alpha());
1793        // Undefined = padding bytes, not real alpha
1794        assert!(!AlphaMode::Undefined.has_alpha());
1795        // Straight and Premultiplied = real alpha
1796        assert!(AlphaMode::Straight.has_alpha());
1797        assert!(AlphaMode::Premultiplied.has_alpha());
1798        assert!(AlphaMode::Opaque.has_alpha());
1799    }
1800
1801    #[test]
1802    fn color_primaries_containment() {
1803        assert!(ColorPrimaries::Bt2020.contains(ColorPrimaries::DisplayP3));
1804        assert!(ColorPrimaries::Bt2020.contains(ColorPrimaries::Bt709));
1805        assert!(ColorPrimaries::DisplayP3.contains(ColorPrimaries::Bt709));
1806        assert!(!ColorPrimaries::Bt709.contains(ColorPrimaries::DisplayP3));
1807        assert!(!ColorPrimaries::Unknown.contains(ColorPrimaries::Bt709));
1808    }
1809
1810    #[test]
1811    fn descriptor_size() {
1812        // PixelFormat (1 byte enum) + transfer (1) + alpha (2) + primaries (1) + signal_range (1) = ~6
1813        assert!(size_of::<PixelDescriptor>() <= 8);
1814    }
1815
1816    #[test]
1817    fn color_model_channels() {
1818        assert_eq!(ColorModel::Gray.color_channels(), 1);
1819        assert_eq!(ColorModel::Rgb.color_channels(), 3);
1820        assert_eq!(ColorModel::YCbCr.color_channels(), 3);
1821        assert_eq!(ColorModel::Oklab.color_channels(), 3);
1822        assert_eq!(ColorModel::Cmyk.color_channels(), 4);
1823    }
1824
1825    // --- PlaneSemantic tests ---
1826
1827    // --- PlaneDescriptor tests ---
1828
1829    // --- PlaneMask tests ---
1830
1831    // --- PlaneLayout tests ---
1832
1833    // --- MultiPlaneImage tests ---
1834
1835    // --- PlaneRelationship tests ---
1836
1837    #[test]
1838    fn reference_white_nits_values() {
1839        assert_eq!(TransferFunction::Pq.reference_white_nits(), 203.0);
1840        assert_eq!(TransferFunction::Srgb.reference_white_nits(), 1.0);
1841        assert_eq!(TransferFunction::Linear.reference_white_nits(), 1.0);
1842        assert_eq!(TransferFunction::Unknown.reference_white_nits(), 1.0);
1843    }
1844
1845    // --- PlaneLayout mask tests ---
1846
1847    // --- Display impl tests ---
1848
1849    #[test]
1850    fn channel_type_display() {
1851        assert_eq!(format!("{}", ChannelType::U8), "U8");
1852        assert_eq!(format!("{}", ChannelType::U16), "U16");
1853        assert_eq!(format!("{}", ChannelType::F32), "F32");
1854        assert_eq!(format!("{}", ChannelType::F16), "F16");
1855    }
1856
1857    #[test]
1858    fn channel_layout_display() {
1859        assert_eq!(format!("{}", ChannelLayout::Gray), "Gray");
1860        assert_eq!(format!("{}", ChannelLayout::GrayAlpha), "GrayAlpha");
1861        assert_eq!(format!("{}", ChannelLayout::Rgb), "RGB");
1862        assert_eq!(format!("{}", ChannelLayout::Rgba), "RGBA");
1863        assert_eq!(format!("{}", ChannelLayout::Bgra), "BGRA");
1864        assert_eq!(format!("{}", ChannelLayout::Oklab), "Oklab");
1865        assert_eq!(format!("{}", ChannelLayout::OklabA), "OklabA");
1866    }
1867
1868    #[test]
1869    fn alpha_mode_display() {
1870        assert_eq!(format!("{}", AlphaMode::Undefined), "undefined");
1871        assert_eq!(format!("{}", AlphaMode::Straight), "straight");
1872        assert_eq!(format!("{}", AlphaMode::Premultiplied), "premultiplied");
1873        assert_eq!(format!("{}", AlphaMode::Opaque), "opaque");
1874    }
1875
1876    #[test]
1877    fn transfer_function_display() {
1878        assert_eq!(format!("{}", TransferFunction::Linear), "linear");
1879        assert_eq!(format!("{}", TransferFunction::Srgb), "sRGB");
1880        assert_eq!(format!("{}", TransferFunction::Bt709), "BT.709");
1881        assert_eq!(format!("{}", TransferFunction::Pq), "PQ");
1882        assert_eq!(format!("{}", TransferFunction::Unknown), "unknown");
1883    }
1884
1885    #[test]
1886    fn color_primaries_display() {
1887        assert_eq!(format!("{}", ColorPrimaries::Bt709), "BT.709");
1888        assert_eq!(format!("{}", ColorPrimaries::Bt2020), "BT.2020");
1889        assert_eq!(format!("{}", ColorPrimaries::DisplayP3), "Display P3");
1890        assert_eq!(format!("{}", ColorPrimaries::Unknown), "unknown");
1891    }
1892
1893    #[test]
1894    fn signal_range_display() {
1895        assert_eq!(format!("{}", SignalRange::Full), "full");
1896        assert_eq!(format!("{}", SignalRange::Narrow), "narrow");
1897    }
1898
1899    #[test]
1900    fn pixel_descriptor_display() {
1901        let s = format!("{}", PixelDescriptor::RGB8_SRGB);
1902        assert!(s.contains("U8"), "expected U8 in: {s}");
1903        assert!(s.contains("sRGB"), "expected sRGB in: {s}");
1904
1905        let s = format!("{}", PixelDescriptor::RGBA8_SRGB);
1906        assert!(s.contains("alpha=straight"), "expected alpha in: {s}");
1907    }
1908
1909    #[test]
1910    fn pixel_format_display() {
1911        let s = format!("{}", PixelFormat::Rgb8);
1912        assert!(s.contains("RGB8"));
1913        let s = format!("{}", PixelFormat::Bgra8);
1914        assert!(s.contains("BGRA8"));
1915    }
1916
1917    // --- from_cicp / to_cicp tests ---
1918
1919    #[test]
1920    fn transfer_function_from_cicp() {
1921        assert_eq!(
1922            TransferFunction::from_cicp(1),
1923            Some(TransferFunction::Bt709)
1924        );
1925        assert_eq!(
1926            TransferFunction::from_cicp(8),
1927            Some(TransferFunction::Linear)
1928        );
1929        assert_eq!(
1930            TransferFunction::from_cicp(13),
1931            Some(TransferFunction::Srgb)
1932        );
1933        assert_eq!(TransferFunction::from_cicp(16), Some(TransferFunction::Pq));
1934        assert_eq!(TransferFunction::from_cicp(18), Some(TransferFunction::Hlg));
1935        assert_eq!(TransferFunction::from_cicp(99), None);
1936    }
1937
1938    #[test]
1939    fn transfer_function_to_cicp() {
1940        assert_eq!(TransferFunction::Bt709.to_cicp(), Some(1));
1941        assert_eq!(TransferFunction::Linear.to_cicp(), Some(8));
1942        assert_eq!(TransferFunction::Srgb.to_cicp(), Some(13));
1943        assert_eq!(TransferFunction::Pq.to_cicp(), Some(16));
1944        assert_eq!(TransferFunction::Hlg.to_cicp(), Some(18));
1945        assert_eq!(TransferFunction::Unknown.to_cicp(), None);
1946    }
1947
1948    #[test]
1949    fn transfer_function_cicp_roundtrip() {
1950        for tf in [
1951            TransferFunction::Bt709,
1952            TransferFunction::Linear,
1953            TransferFunction::Srgb,
1954            TransferFunction::Pq,
1955        ] {
1956            let code = tf.to_cicp().unwrap();
1957            assert_eq!(TransferFunction::from_cicp(code), Some(tf));
1958        }
1959    }
1960
1961    #[test]
1962    fn color_primaries_from_cicp() {
1963        assert_eq!(ColorPrimaries::from_cicp(1), Some(ColorPrimaries::Bt709));
1964        assert_eq!(ColorPrimaries::from_cicp(9), Some(ColorPrimaries::Bt2020));
1965        assert_eq!(
1966            ColorPrimaries::from_cicp(12),
1967            Some(ColorPrimaries::DisplayP3)
1968        );
1969        assert_eq!(ColorPrimaries::from_cicp(99), None);
1970    }
1971
1972    #[test]
1973    fn color_primaries_to_cicp() {
1974        assert_eq!(ColorPrimaries::Bt709.to_cicp(), Some(1));
1975        assert_eq!(ColorPrimaries::Bt2020.to_cicp(), Some(9));
1976        assert_eq!(ColorPrimaries::DisplayP3.to_cicp(), Some(12));
1977        assert_eq!(ColorPrimaries::Unknown.to_cicp(), None);
1978    }
1979
1980    // --- ChannelType helpers ---
1981
1982    #[test]
1983    fn channel_type_helpers() {
1984        assert!(ChannelType::U8.is_u8());
1985        assert!(!ChannelType::U8.is_u16());
1986        assert!(ChannelType::U16.is_u16());
1987        assert!(ChannelType::F32.is_f32());
1988        assert!(ChannelType::F16.is_f16());
1989        assert!(ChannelType::U8.is_integer());
1990        assert!(ChannelType::U16.is_integer());
1991        assert!(!ChannelType::F32.is_integer());
1992        assert!(ChannelType::F32.is_float());
1993        assert!(ChannelType::F16.is_float());
1994        assert!(!ChannelType::U8.is_float());
1995    }
1996
1997    // --- ChannelLayout helpers ---
1998
1999    #[test]
2000    fn channel_layout_channels() {
2001        assert_eq!(ChannelLayout::Gray.channels(), 1);
2002        assert_eq!(ChannelLayout::GrayAlpha.channels(), 2);
2003        assert_eq!(ChannelLayout::Rgb.channels(), 3);
2004        assert_eq!(ChannelLayout::Rgba.channels(), 4);
2005        assert_eq!(ChannelLayout::Bgra.channels(), 4);
2006        assert_eq!(ChannelLayout::Oklab.channels(), 3);
2007        assert_eq!(ChannelLayout::OklabA.channels(), 4);
2008    }
2009
2010    #[test]
2011    fn channel_layout_has_alpha() {
2012        assert!(!ChannelLayout::Gray.has_alpha());
2013        assert!(ChannelLayout::GrayAlpha.has_alpha());
2014        assert!(!ChannelLayout::Rgb.has_alpha());
2015        assert!(ChannelLayout::Rgba.has_alpha());
2016        assert!(ChannelLayout::Bgra.has_alpha());
2017        assert!(!ChannelLayout::Oklab.has_alpha());
2018        assert!(ChannelLayout::OklabA.has_alpha());
2019    }
2020
2021    // --- PixelDescriptor builder methods ---
2022
2023    #[test]
2024    fn with_transfer() {
2025        let desc = PixelDescriptor::RGB8_SRGB.with_transfer(TransferFunction::Linear);
2026        assert_eq!(desc.transfer(), TransferFunction::Linear);
2027        assert_eq!(desc.layout(), ChannelLayout::Rgb);
2028    }
2029
2030    #[test]
2031    fn with_primaries() {
2032        let desc = PixelDescriptor::RGB8_SRGB.with_primaries(ColorPrimaries::DisplayP3);
2033        assert_eq!(desc.primaries, ColorPrimaries::DisplayP3);
2034    }
2035
2036    #[test]
2037    fn with_signal_range() {
2038        let desc = PixelDescriptor::RGB8_SRGB.with_signal_range(SignalRange::Narrow);
2039        assert_eq!(desc.signal_range, SignalRange::Narrow);
2040    }
2041
2042    #[test]
2043    fn with_alpha_mode() {
2044        let desc = PixelDescriptor::RGBA8_SRGB.with_alpha(Some(AlphaMode::Premultiplied));
2045        assert_eq!(desc.alpha(), Some(AlphaMode::Premultiplied));
2046    }
2047
2048    // --- PixelDescriptor predicates ---
2049
2050    #[test]
2051    fn is_opaque_and_may_have_transparency() {
2052        assert!(PixelDescriptor::RGB8_SRGB.is_opaque());
2053        assert!(!PixelDescriptor::RGB8_SRGB.may_have_transparency());
2054        assert!(!PixelDescriptor::RGBA8_SRGB.is_opaque());
2055        assert!(PixelDescriptor::RGBA8_SRGB.may_have_transparency());
2056
2057        let rgbx = PixelDescriptor::new(
2058            ChannelType::U8,
2059            ChannelLayout::Rgba,
2060            Some(AlphaMode::Undefined),
2061            TransferFunction::Srgb,
2062        );
2063        assert!(rgbx.is_opaque());
2064        assert!(!rgbx.may_have_transparency());
2065    }
2066
2067    #[test]
2068    fn is_linear_and_is_unknown_transfer() {
2069        assert!(!PixelDescriptor::RGB8_SRGB.is_linear());
2070        assert!(PixelDescriptor::RGBF32_LINEAR.is_linear());
2071        assert!(!PixelDescriptor::RGB8_SRGB.is_unknown_transfer());
2072        let desc = PixelDescriptor::RGB8_SRGB.with_transfer(TransferFunction::Unknown);
2073        assert!(desc.is_unknown_transfer());
2074    }
2075
2076    #[test]
2077    fn min_alignment() {
2078        assert_eq!(PixelDescriptor::RGB8_SRGB.min_alignment(), 1);
2079        assert_eq!(PixelDescriptor::RGBF32_LINEAR.min_alignment(), 4);
2080    }
2081
2082    #[test]
2083    fn aligned_stride() {
2084        assert_eq!(PixelDescriptor::RGB8_SRGB.aligned_stride(100), 300);
2085        assert_eq!(PixelDescriptor::RGBA8_SRGB.aligned_stride(100), 400);
2086        assert_eq!(PixelDescriptor::RGBF32_LINEAR.aligned_stride(10), 120);
2087    }
2088
2089    #[test]
2090    fn simd_aligned_stride() {
2091        let stride = PixelDescriptor::RGB8_SRGB.simd_aligned_stride(100, 16);
2092        assert!(stride >= 300);
2093        assert_eq!(stride % 16, 0);
2094        assert_eq!(stride % 3, 0); // pixel-aligned
2095    }
2096
2097    // --- new_full and from_pixel_format ---
2098
2099    #[test]
2100    fn new_full_constructor() {
2101        let desc = PixelDescriptor::new_full(
2102            ChannelType::U8,
2103            ChannelLayout::Rgb,
2104            None,
2105            TransferFunction::Srgb,
2106            ColorPrimaries::DisplayP3,
2107        );
2108        assert_eq!(desc.primaries, ColorPrimaries::DisplayP3);
2109        assert_eq!(desc.transfer(), TransferFunction::Srgb);
2110    }
2111
2112    #[test]
2113    fn from_pixel_format_constructor() {
2114        let desc = PixelDescriptor::from_pixel_format(PixelFormat::Rgba8);
2115        assert_eq!(desc.layout(), ChannelLayout::Rgba);
2116        assert_eq!(desc.transfer(), TransferFunction::Unknown);
2117        assert_eq!(desc.primaries, ColorPrimaries::Bt709);
2118        assert_eq!(desc.signal_range, SignalRange::Full);
2119    }
2120
2121    // --- PixelFormat::name ---
2122
2123    #[test]
2124    fn pixel_format_name() {
2125        assert_eq!(PixelFormat::Rgb8.name(), "RGB8");
2126        assert_eq!(PixelFormat::Bgra8.name(), "BGRA8");
2127        assert_eq!(PixelFormat::Gray8.name(), "Gray8");
2128    }
2129
2130    // --- ColorModel ---
2131
2132    #[test]
2133    fn color_model_display() {
2134        assert_eq!(format!("{}", ColorModel::Gray), "Gray");
2135        assert_eq!(format!("{}", ColorModel::Rgb), "RGB");
2136        assert_eq!(format!("{}", ColorModel::YCbCr), "YCbCr");
2137        assert_eq!(format!("{}", ColorModel::Oklab), "Oklab");
2138        assert_eq!(format!("{}", ColorModel::Cmyk), "CMYK");
2139    }
2140
2141    // --- SignalRange default ---
2142
2143    #[test]
2144    fn signal_range_default() {
2145        assert_eq!(SignalRange::default(), SignalRange::Full);
2146    }
2147
2148    // --- ColorPrimaries default ---
2149
2150    #[test]
2151    fn color_primaries_default() {
2152        assert_eq!(ColorPrimaries::default(), ColorPrimaries::Bt709);
2153    }
2154
2155    // --- CMYK tests ---
2156
2157    #[test]
2158    fn cmyk8_descriptor() {
2159        let d = PixelDescriptor::CMYK8;
2160        assert_eq!(d.color_model(), ColorModel::Cmyk);
2161        assert_eq!(d.channels(), 4);
2162        assert_eq!(d.bytes_per_pixel(), 4);
2163        assert_eq!(d.layout(), ChannelLayout::Cmyk);
2164        assert_eq!(d.channel_type(), ChannelType::U8);
2165        assert_eq!(d.transfer(), TransferFunction::Unknown);
2166        assert_eq!(d.primaries, ColorPrimaries::Bt709);
2167        assert!(!d.has_alpha());
2168        assert!(d.is_opaque());
2169    }
2170
2171    #[test]
2172    fn cmyk8_pixel_format() {
2173        let fmt = PixelFormat::Cmyk8;
2174        assert_eq!(fmt.channels(), 4);
2175        assert_eq!(fmt.bytes_per_pixel(), 4);
2176        assert_eq!(fmt.channel_type(), ChannelType::U8);
2177        assert_eq!(fmt.layout(), ChannelLayout::Cmyk);
2178        assert_eq!(fmt.color_model(), ColorModel::Cmyk);
2179        assert!(!fmt.has_alpha_bytes());
2180        assert!(!fmt.is_grayscale());
2181        assert_eq!(fmt.name(), "CMYK8");
2182        assert_eq!(fmt.default_alpha(), None);
2183    }
2184
2185    #[test]
2186    fn cmyk8_from_parts_roundtrip() {
2187        let fmt = PixelFormat::Cmyk8;
2188        let rebuilt =
2189            PixelFormat::from_parts(fmt.channel_type(), fmt.layout(), fmt.default_alpha());
2190        assert_eq!(rebuilt, Some(fmt));
2191    }
2192
2193    #[test]
2194    fn cmyk8_descriptor_roundtrip() {
2195        let desc = PixelFormat::Cmyk8.descriptor();
2196        assert_eq!(desc, PixelDescriptor::CMYK8);
2197    }
2198
2199    #[test]
2200    fn cmyk_channel_layout_display() {
2201        assert_eq!(format!("{}", ChannelLayout::Cmyk), "CMYK");
2202    }
2203
2204    #[test]
2205    fn cmyk_channel_layout_no_alpha() {
2206        assert!(!ChannelLayout::Cmyk.has_alpha());
2207    }
2208}