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}
120
121impl ChannelLayout {
122    /// Number of channels in this layout.
123    #[inline]
124    #[allow(unreachable_patterns)]
125    pub const fn channels(self) -> usize {
126        match self {
127            Self::Gray => 1,
128            Self::GrayAlpha => 2,
129            Self::Rgb | Self::Oklab => 3,
130            Self::Rgba | Self::Bgra | Self::OklabA => 4,
131            _ => 0,
132        }
133    }
134
135    /// Whether this layout includes an alpha channel.
136    #[inline]
137    #[allow(unreachable_patterns)]
138    pub const fn has_alpha(self) -> bool {
139        matches!(
140            self,
141            Self::GrayAlpha | Self::Rgba | Self::Bgra | Self::OklabA
142        )
143    }
144}
145
146impl fmt::Display for ChannelLayout {
147    #[allow(unreachable_patterns)]
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        match self {
150            Self::Gray => f.write_str("Gray"),
151            Self::GrayAlpha => f.write_str("GrayAlpha"),
152            Self::Rgb => f.write_str("RGB"),
153            Self::Rgba => f.write_str("RGBA"),
154            Self::Bgra => f.write_str("BGRA"),
155            Self::Oklab => f.write_str("Oklab"),
156            Self::OklabA => f.write_str("OklabA"),
157            _ => write!(f, "ChannelLayout({})", *self as u8),
158        }
159    }
160}
161
162// ---------------------------------------------------------------------------
163// Alpha mode
164// ---------------------------------------------------------------------------
165
166/// Alpha channel interpretation.
167///
168/// Wrapped in `Option<AlphaMode>` on [`PixelDescriptor`]: `None` means no
169/// alpha channel exists, while `Some(AlphaMode::Straight)` etc. describe
170/// the semantics of a present alpha channel.
171#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
172#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
173#[non_exhaustive]
174#[repr(u8)]
175pub enum AlphaMode {
176    /// Alpha bytes exist but values are undefined padding (RGBX, BGRX).
177    Undefined = 1,
178    /// Straight (unassociated) alpha.
179    Straight = 2,
180    /// Premultiplied (associated) alpha.
181    Premultiplied = 3,
182    /// Alpha channel present, all values fully opaque.
183    Opaque = 4,
184}
185
186impl AlphaMode {
187    /// Whether this mode represents a real alpha channel (not Undefined padding).
188    #[inline]
189    pub const fn has_alpha(self) -> bool {
190        matches!(self, Self::Straight | Self::Premultiplied | Self::Opaque)
191    }
192}
193
194impl fmt::Display for AlphaMode {
195    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196        match self {
197            Self::Undefined => f.write_str("undefined"),
198            Self::Straight => f.write_str("straight"),
199            Self::Premultiplied => f.write_str("premultiplied"),
200            Self::Opaque => f.write_str("opaque"),
201        }
202    }
203}
204
205// ---------------------------------------------------------------------------
206// Transfer function
207// ---------------------------------------------------------------------------
208
209/// Electro-optical transfer function.
210#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
211#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
212#[non_exhaustive]
213#[repr(u8)]
214pub enum TransferFunction {
215    /// Linear light (gamma 1.0).
216    Linear = 0,
217    /// sRGB transfer curve (IEC 61966-2-1).
218    Srgb = 1,
219    /// BT.709 transfer curve.
220    Bt709 = 2,
221    /// Perceptual Quantizer (SMPTE ST 2084, HDR10).
222    Pq = 3,
223    /// Hybrid Log-Gamma (ARIB STD-B67, HLG).
224    Hlg = 4,
225    /// Transfer function is not known.
226    Unknown = 255,
227}
228
229impl TransferFunction {
230    /// Map CICP `transfer_characteristics` code to a [`TransferFunction`].
231    #[inline]
232    pub const fn from_cicp(tc: u8) -> Option<Self> {
233        match tc {
234            1 => Some(Self::Bt709),
235            8 => Some(Self::Linear),
236            13 => Some(Self::Srgb),
237            16 => Some(Self::Pq),
238            18 => Some(Self::Hlg),
239            _ => None,
240        }
241    }
242
243    /// Convert to the CICP `transfer_characteristics` code.
244    #[allow(unreachable_patterns)]
245    #[inline]
246    pub const fn to_cicp(self) -> Option<u8> {
247        match self {
248            Self::Bt709 => Some(1),
249            Self::Linear => Some(8),
250            Self::Srgb => Some(13),
251            Self::Pq => Some(16),
252            Self::Hlg => Some(18),
253            Self::Unknown => None,
254            _ => None,
255        }
256    }
257
258    /// Reference white luminance in nits.
259    ///
260    /// - SDR (sRGB, BT.709, Linear, Unknown): `1.0` (relative/scene-referred)
261    /// - PQ: `203.0` (ITU-R BT.2408 reference white)
262    /// - HLG: `1.0` (scene-referred)
263    #[allow(unreachable_patterns)]
264    pub fn reference_white_nits(&self) -> f32 {
265        match self {
266            Self::Pq => 203.0,
267            _ => 1.0,
268        }
269    }
270}
271
272impl fmt::Display for TransferFunction {
273    #[allow(unreachable_patterns)]
274    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
275        match self {
276            Self::Linear => f.write_str("linear"),
277            Self::Srgb => f.write_str("sRGB"),
278            Self::Bt709 => f.write_str("BT.709"),
279            Self::Pq => f.write_str("PQ"),
280            Self::Hlg => f.write_str("HLG"),
281            Self::Unknown => f.write_str("unknown"),
282            _ => write!(f, "TransferFunction({})", *self as u8),
283        }
284    }
285}
286
287// ---------------------------------------------------------------------------
288// Color primaries
289// ---------------------------------------------------------------------------
290
291/// Color primaries (CIE xy chromaticities of R, G, B).
292///
293/// Discriminant values match CICP `ColorPrimaries` codes (ITU-T H.273).
294#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
295#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
296#[non_exhaustive]
297#[repr(u8)]
298pub enum ColorPrimaries {
299    /// BT.709 / sRGB (CICP 1).
300    #[default]
301    Bt709 = 1,
302    /// BT.2020 / BT.2100 (CICP 9). Wide gamut for HDR.
303    Bt2020 = 9,
304    /// Display P3 (CICP 12). Apple ecosystem, wide gamut SDR.
305    DisplayP3 = 12,
306    /// Primaries not known.
307    Unknown = 255,
308}
309
310impl ColorPrimaries {
311    /// Map a CICP `color_primaries` code to a [`ColorPrimaries`].
312    #[inline]
313    pub const fn from_cicp(code: u8) -> Option<Self> {
314        match code {
315            1 => Some(Self::Bt709),
316            9 => Some(Self::Bt2020),
317            12 => Some(Self::DisplayP3),
318            _ => None,
319        }
320    }
321
322    /// Convert to the CICP `color_primaries` code.
323    #[allow(unreachable_patterns)]
324    #[inline]
325    pub const fn to_cicp(self) -> Option<u8> {
326        match self {
327            Self::Bt709 => Some(1),
328            Self::Bt2020 => Some(9),
329            Self::DisplayP3 => Some(12),
330            Self::Unknown => None,
331            _ => None,
332        }
333    }
334
335    /// Whether `self` fully contains the gamut of `other`.
336    ///
337    /// Gamut hierarchy: BT.2020 > Display P3 > BT.709.
338    #[inline]
339    pub const fn contains(self, other: Self) -> bool {
340        self.gamut_width() >= other.gamut_width()
341            && !matches!(self, Self::Unknown)
342            && !matches!(other, Self::Unknown)
343    }
344
345    #[allow(unreachable_patterns)]
346    const fn gamut_width(self) -> u8 {
347        match self {
348            Self::Bt709 => 1,
349            Self::DisplayP3 => 2,
350            Self::Bt2020 => 3,
351            Self::Unknown => 0,
352            _ => 0,
353        }
354    }
355}
356
357impl fmt::Display for ColorPrimaries {
358    #[allow(unreachable_patterns)]
359    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
360        match self {
361            Self::Bt709 => f.write_str("BT.709"),
362            Self::Bt2020 => f.write_str("BT.2020"),
363            Self::DisplayP3 => f.write_str("Display P3"),
364            Self::Unknown => f.write_str("unknown"),
365            _ => write!(f, "ColorPrimaries({})", *self as u8),
366        }
367    }
368}
369
370// ---------------------------------------------------------------------------
371// Signal range
372// ---------------------------------------------------------------------------
373
374/// Signal range for pixel values.
375#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
376#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
377#[non_exhaustive]
378#[repr(u8)]
379pub enum SignalRange {
380    /// Full range: 0-2^N-1 (e.g. 0-255 for 8-bit).
381    #[default]
382    Full = 0,
383    /// Narrow (limited/studio) range: 16-235 luma, 16-240 chroma (for 8-bit).
384    Narrow = 1,
385}
386
387impl fmt::Display for SignalRange {
388    #[allow(unreachable_patterns)]
389    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390        match self {
391            Self::Full => f.write_str("full"),
392            Self::Narrow => f.write_str("narrow"),
393            _ => write!(f, "SignalRange({})", *self as u8),
394        }
395    }
396}
397
398// ---------------------------------------------------------------------------
399// PixelDescriptor
400// ---------------------------------------------------------------------------
401
402/// Compact pixel format descriptor.
403///
404/// Combines a [`PixelFormat`] (physical pixel layout) with transfer function,
405/// alpha mode, color primaries, and signal range.
406#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
407#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
408#[non_exhaustive]
409pub struct PixelDescriptor {
410    /// Physical pixel format (channel type + layout as a flat enum).
411    pub format: PixelFormat,
412    /// Electro-optical transfer function.
413    pub transfer: TransferFunction,
414    /// Alpha interpretation. `None` = no alpha channel.
415    pub alpha: Option<AlphaMode>,
416    /// Color primaries (gamut). Defaults to BT.709/sRGB.
417    pub primaries: ColorPrimaries,
418    /// Signal range (full vs narrow/limited).
419    pub signal_range: SignalRange,
420}
421
422impl PixelDescriptor {
423    // -- Forwarding accessors -------------------------------------------------
424
425    /// The pixel format variant (layout + depth, no transfer or alpha semantics).
426    #[inline]
427    pub const fn pixel_format(&self) -> PixelFormat {
428        self.format
429    }
430
431    /// Channel storage type.
432    #[inline]
433    pub const fn channel_type(&self) -> ChannelType {
434        self.format.channel_type()
435    }
436
437    /// Alpha interpretation. `None` = no alpha channel.
438    #[inline]
439    pub const fn alpha(&self) -> Option<AlphaMode> {
440        self.alpha
441    }
442
443    /// Transfer function.
444    #[inline]
445    pub const fn transfer(&self) -> TransferFunction {
446        self.transfer
447    }
448
449    /// Byte order.
450    #[inline]
451    pub const fn byte_order(&self) -> ByteOrder {
452        self.format.byte_order()
453    }
454
455    /// Color model.
456    #[inline]
457    pub const fn color_model(&self) -> ColorModel {
458        self.format.color_model()
459    }
460
461    /// Channel layout (derived from the [`PixelFormat`] variant).
462    #[inline]
463    pub const fn layout(&self) -> ChannelLayout {
464        self.format.layout()
465    }
466
467    // -- Constructors ---------------------------------------------------------
468
469    /// Create a descriptor with default primaries (BT.709) and full range.
470    ///
471    /// # Panics
472    ///
473    /// Panics if the `(channel_type, layout, alpha)` combination has no
474    /// corresponding [`PixelFormat`] variant (e.g. `(U16, Bgra, _)`).
475    #[inline]
476    pub const fn new(
477        channel_type: ChannelType,
478        layout: ChannelLayout,
479        alpha: Option<AlphaMode>,
480        transfer: TransferFunction,
481    ) -> Self {
482        let format = match PixelFormat::from_parts(channel_type, layout, alpha) {
483            Some(f) => f,
484            None => panic!("unsupported PixelFormat combination"),
485        };
486        Self {
487            format,
488            transfer,
489            alpha,
490            primaries: ColorPrimaries::Bt709,
491            signal_range: SignalRange::Full,
492        }
493    }
494
495    /// Create a descriptor with explicit primaries.
496    ///
497    /// # Panics
498    ///
499    /// Panics if the `(channel_type, layout, alpha)` combination has no
500    /// corresponding [`PixelFormat`] variant.
501    #[inline]
502    pub const fn new_full(
503        channel_type: ChannelType,
504        layout: ChannelLayout,
505        alpha: Option<AlphaMode>,
506        transfer: TransferFunction,
507        primaries: ColorPrimaries,
508    ) -> Self {
509        let format = match PixelFormat::from_parts(channel_type, layout, alpha) {
510            Some(f) => f,
511            None => panic!("unsupported PixelFormat combination"),
512        };
513        Self {
514            format,
515            transfer,
516            alpha,
517            primaries,
518            signal_range: SignalRange::Full,
519        }
520    }
521
522    /// Create from a [`PixelFormat`] with default alpha, unknown transfer,
523    /// BT.709 primaries, and full range.
524    #[inline]
525    pub const fn from_pixel_format(format: PixelFormat) -> Self {
526        Self {
527            format,
528            transfer: TransferFunction::Unknown,
529            alpha: format.default_alpha(),
530            primaries: ColorPrimaries::Bt709,
531            signal_range: SignalRange::Full,
532        }
533    }
534
535    // -- sRGB constants -------------------------------------------------------
536
537    /// 8-bit sRGB RGB.
538    pub const RGB8_SRGB: Self = Self::new(
539        ChannelType::U8,
540        ChannelLayout::Rgb,
541        None,
542        TransferFunction::Srgb,
543    );
544    /// 8-bit sRGB RGBA with straight alpha.
545    pub const RGBA8_SRGB: Self = Self::new(
546        ChannelType::U8,
547        ChannelLayout::Rgba,
548        Some(AlphaMode::Straight),
549        TransferFunction::Srgb,
550    );
551    /// 16-bit sRGB RGB.
552    pub const RGB16_SRGB: Self = Self::new(
553        ChannelType::U16,
554        ChannelLayout::Rgb,
555        None,
556        TransferFunction::Srgb,
557    );
558    /// 16-bit sRGB RGBA with straight alpha.
559    pub const RGBA16_SRGB: Self = Self::new(
560        ChannelType::U16,
561        ChannelLayout::Rgba,
562        Some(AlphaMode::Straight),
563        TransferFunction::Srgb,
564    );
565    /// Linear-light f32 RGB.
566    pub const RGBF32_LINEAR: Self = Self::new(
567        ChannelType::F32,
568        ChannelLayout::Rgb,
569        None,
570        TransferFunction::Linear,
571    );
572    /// Linear-light f32 RGBA with straight alpha.
573    pub const RGBAF32_LINEAR: Self = Self::new(
574        ChannelType::F32,
575        ChannelLayout::Rgba,
576        Some(AlphaMode::Straight),
577        TransferFunction::Linear,
578    );
579    /// 8-bit sRGB grayscale.
580    pub const GRAY8_SRGB: Self = Self::new(
581        ChannelType::U8,
582        ChannelLayout::Gray,
583        None,
584        TransferFunction::Srgb,
585    );
586    /// 16-bit sRGB grayscale.
587    pub const GRAY16_SRGB: Self = Self::new(
588        ChannelType::U16,
589        ChannelLayout::Gray,
590        None,
591        TransferFunction::Srgb,
592    );
593    /// Linear-light f32 grayscale.
594    pub const GRAYF32_LINEAR: Self = Self::new(
595        ChannelType::F32,
596        ChannelLayout::Gray,
597        None,
598        TransferFunction::Linear,
599    );
600    /// 8-bit sRGB grayscale with straight alpha.
601    pub const GRAYA8_SRGB: Self = Self::new(
602        ChannelType::U8,
603        ChannelLayout::GrayAlpha,
604        Some(AlphaMode::Straight),
605        TransferFunction::Srgb,
606    );
607    /// 16-bit sRGB grayscale with straight alpha.
608    pub const GRAYA16_SRGB: Self = Self::new(
609        ChannelType::U16,
610        ChannelLayout::GrayAlpha,
611        Some(AlphaMode::Straight),
612        TransferFunction::Srgb,
613    );
614    /// Linear-light f32 grayscale with straight alpha.
615    pub const GRAYAF32_LINEAR: Self = Self::new(
616        ChannelType::F32,
617        ChannelLayout::GrayAlpha,
618        Some(AlphaMode::Straight),
619        TransferFunction::Linear,
620    );
621    /// 8-bit sRGB BGRA with straight alpha.
622    pub const BGRA8_SRGB: Self = Self::new(
623        ChannelType::U8,
624        ChannelLayout::Bgra,
625        Some(AlphaMode::Straight),
626        TransferFunction::Srgb,
627    );
628    /// 8-bit sRGB RGBX (padding byte, not alpha).
629    pub const RGBX8_SRGB: Self = Self::new(
630        ChannelType::U8,
631        ChannelLayout::Rgba,
632        Some(AlphaMode::Undefined),
633        TransferFunction::Srgb,
634    );
635    /// 8-bit sRGB BGRX (padding byte, not alpha).
636    pub const BGRX8_SRGB: Self = Self::new(
637        ChannelType::U8,
638        ChannelLayout::Bgra,
639        Some(AlphaMode::Undefined),
640        TransferFunction::Srgb,
641    );
642
643    // -- Transfer-agnostic constants ------------------------------------------
644
645    /// 8-bit RGB, transfer unknown.
646    pub const RGB8: Self = Self::new(
647        ChannelType::U8,
648        ChannelLayout::Rgb,
649        None,
650        TransferFunction::Unknown,
651    );
652    /// 8-bit RGBA, transfer unknown.
653    pub const RGBA8: Self = Self::new(
654        ChannelType::U8,
655        ChannelLayout::Rgba,
656        Some(AlphaMode::Straight),
657        TransferFunction::Unknown,
658    );
659    /// 16-bit RGB, transfer unknown.
660    pub const RGB16: Self = Self::new(
661        ChannelType::U16,
662        ChannelLayout::Rgb,
663        None,
664        TransferFunction::Unknown,
665    );
666    /// 16-bit RGBA, transfer unknown.
667    pub const RGBA16: Self = Self::new(
668        ChannelType::U16,
669        ChannelLayout::Rgba,
670        Some(AlphaMode::Straight),
671        TransferFunction::Unknown,
672    );
673    /// f32 RGB, transfer unknown.
674    pub const RGBF32: Self = Self::new(
675        ChannelType::F32,
676        ChannelLayout::Rgb,
677        None,
678        TransferFunction::Unknown,
679    );
680    /// f32 RGBA, transfer unknown.
681    pub const RGBAF32: Self = Self::new(
682        ChannelType::F32,
683        ChannelLayout::Rgba,
684        Some(AlphaMode::Straight),
685        TransferFunction::Unknown,
686    );
687    /// 8-bit grayscale, transfer unknown.
688    pub const GRAY8: Self = Self::new(
689        ChannelType::U8,
690        ChannelLayout::Gray,
691        None,
692        TransferFunction::Unknown,
693    );
694    /// 16-bit grayscale, transfer unknown.
695    pub const GRAY16: Self = Self::new(
696        ChannelType::U16,
697        ChannelLayout::Gray,
698        None,
699        TransferFunction::Unknown,
700    );
701    /// f32 grayscale, transfer unknown.
702    pub const GRAYF32: Self = Self::new(
703        ChannelType::F32,
704        ChannelLayout::Gray,
705        None,
706        TransferFunction::Unknown,
707    );
708    /// 8-bit grayscale with alpha, transfer unknown.
709    pub const GRAYA8: Self = Self::new(
710        ChannelType::U8,
711        ChannelLayout::GrayAlpha,
712        Some(AlphaMode::Straight),
713        TransferFunction::Unknown,
714    );
715    /// 16-bit grayscale with alpha, transfer unknown.
716    pub const GRAYA16: Self = Self::new(
717        ChannelType::U16,
718        ChannelLayout::GrayAlpha,
719        Some(AlphaMode::Straight),
720        TransferFunction::Unknown,
721    );
722    /// f32 grayscale with alpha, transfer unknown.
723    pub const GRAYAF32: Self = Self::new(
724        ChannelType::F32,
725        ChannelLayout::GrayAlpha,
726        Some(AlphaMode::Straight),
727        TransferFunction::Unknown,
728    );
729    /// 8-bit BGRA, transfer unknown.
730    pub const BGRA8: Self = Self::new(
731        ChannelType::U8,
732        ChannelLayout::Bgra,
733        Some(AlphaMode::Straight),
734        TransferFunction::Unknown,
735    );
736    /// 8-bit RGBX, transfer unknown.
737    pub const RGBX8: Self = Self::new(
738        ChannelType::U8,
739        ChannelLayout::Rgba,
740        Some(AlphaMode::Undefined),
741        TransferFunction::Unknown,
742    );
743    /// 8-bit BGRX, transfer unknown.
744    pub const BGRX8: Self = Self::new(
745        ChannelType::U8,
746        ChannelLayout::Bgra,
747        Some(AlphaMode::Undefined),
748        TransferFunction::Unknown,
749    );
750
751    // -- Oklab constants ------------------------------------------------------
752
753    /// Oklab f32 (L, a, b), transfer unknown.
754    pub const OKLABF32: Self = Self {
755        format: PixelFormat::OklabF32,
756        transfer: TransferFunction::Unknown,
757        alpha: None,
758        primaries: ColorPrimaries::Bt709,
759        signal_range: SignalRange::Full,
760    };
761    /// Oklab+alpha f32 (L, a, b, alpha), transfer unknown.
762    pub const OKLABAF32: Self = Self {
763        format: PixelFormat::OklabaF32,
764        transfer: TransferFunction::Unknown,
765        alpha: Some(AlphaMode::Straight),
766        primaries: ColorPrimaries::Bt709,
767        signal_range: SignalRange::Full,
768    };
769
770    // -- Methods --------------------------------------------------------------
771
772    /// Number of channels.
773    #[inline]
774    pub const fn channels(self) -> usize {
775        self.format.channels()
776    }
777
778    /// Bytes per pixel.
779    #[inline]
780    pub const fn bytes_per_pixel(self) -> usize {
781        self.format.bytes_per_pixel()
782    }
783
784    /// Whether this descriptor has meaningful alpha data.
785    #[inline]
786    pub const fn has_alpha(self) -> bool {
787        matches!(
788            self.alpha,
789            Some(AlphaMode::Straight) | Some(AlphaMode::Premultiplied) | Some(AlphaMode::Opaque)
790        )
791    }
792
793    /// Whether this descriptor is grayscale.
794    #[inline]
795    pub const fn is_grayscale(self) -> bool {
796        self.format.is_grayscale()
797    }
798
799    /// Whether this descriptor uses BGR byte order.
800    #[inline]
801    pub const fn is_bgr(self) -> bool {
802        matches!(self.format.byte_order(), ByteOrder::Bgr)
803    }
804
805    /// Return a copy with a different transfer function.
806    #[inline]
807    #[must_use]
808    pub const fn with_transfer(self, transfer: TransferFunction) -> Self {
809        Self { transfer, ..self }
810    }
811
812    /// Return a copy with different primaries.
813    #[inline]
814    #[must_use]
815    pub const fn with_primaries(self, primaries: ColorPrimaries) -> Self {
816        Self { primaries, ..self }
817    }
818
819    /// Return a copy with a different alpha mode.
820    #[inline]
821    #[must_use]
822    pub const fn with_alpha(self, alpha: Option<AlphaMode>) -> Self {
823        Self { alpha, ..self }
824    }
825
826    /// Alias for [`with_alpha`](Self::with_alpha).
827    #[inline]
828    #[must_use]
829    pub const fn with_alpha_mode(self, alpha: Option<AlphaMode>) -> Self {
830        self.with_alpha(alpha)
831    }
832
833    /// Return a copy with a different signal range.
834    #[inline]
835    #[must_use]
836    pub const fn with_signal_range(self, signal_range: SignalRange) -> Self {
837        Self {
838            signal_range,
839            ..self
840        }
841    }
842
843    /// Whether this format is fully opaque (no transparency possible).
844    ///
845    /// Returns `true` when there is no alpha channel (`None`), the alpha
846    /// bytes are undefined padding (`Undefined`), or alpha is all-255 (`Opaque`).
847    #[inline]
848    pub const fn is_opaque(self) -> bool {
849        matches!(
850            self.alpha,
851            None | Some(AlphaMode::Undefined | AlphaMode::Opaque)
852        )
853    }
854
855    /// Whether this format may contain transparent pixels.
856    ///
857    /// Returns `true` for [`Straight`](AlphaMode::Straight) and
858    /// [`Premultiplied`](AlphaMode::Premultiplied).
859    #[inline]
860    #[allow(unreachable_patterns)]
861    pub const fn may_have_transparency(self) -> bool {
862        matches!(
863            self.alpha,
864            Some(AlphaMode::Straight | AlphaMode::Premultiplied)
865        )
866    }
867
868    /// Whether the transfer function is [`Linear`](TransferFunction::Linear).
869    #[inline]
870    pub const fn is_linear(self) -> bool {
871        matches!(self.transfer, TransferFunction::Linear)
872    }
873
874    /// Whether the transfer function is [`Unknown`](TransferFunction::Unknown).
875    #[inline]
876    pub const fn is_unknown_transfer(self) -> bool {
877        matches!(self.transfer, TransferFunction::Unknown)
878    }
879
880    /// Minimum byte alignment required for the channel type (1, 2, or 4).
881    #[inline]
882    pub const fn min_alignment(self) -> usize {
883        self.format.channel_type().byte_size()
884    }
885
886    /// Tightly-packed byte stride for a given width.
887    #[inline]
888    pub const fn aligned_stride(self, width: u32) -> usize {
889        width as usize * self.bytes_per_pixel()
890    }
891
892    /// SIMD-friendly byte stride for a given width.
893    ///
894    /// The stride is a multiple of `lcm(bytes_per_pixel, simd_align)`,
895    /// ensuring every row start is both pixel-aligned and SIMD-aligned.
896    /// `simd_align` must be a power of 2.
897    #[inline]
898    pub const fn simd_aligned_stride(self, width: u32, simd_align: usize) -> usize {
899        let bpp = self.bytes_per_pixel();
900        let raw = width as usize * bpp;
901        let align = lcm(bpp, simd_align);
902        align_up_general(raw, align)
903    }
904
905    /// Whether this descriptor's channel type and layout are compatible with `other`.
906    ///
907    /// "Compatible" means the raw bytes can be reinterpreted as `other`
908    /// without any pixel transformation — same channel type, same layout.
909    #[inline]
910    pub const fn layout_compatible(self, other: Self) -> bool {
911        self.format.channel_type() as u8 == other.format.channel_type() as u8
912            && self.layout() as u8 == other.layout() as u8
913    }
914}
915
916// Alignment helpers.
917
918const fn gcd(mut a: usize, mut b: usize) -> usize {
919    while b != 0 {
920        let t = b;
921        b = a % b;
922        a = t;
923    }
924    a
925}
926
927const fn lcm(a: usize, b: usize) -> usize {
928    if a == 0 || b == 0 {
929        0
930    } else {
931        a / gcd(a, b) * b
932    }
933}
934
935const fn align_up_general(value: usize, align: usize) -> usize {
936    if align == 0 {
937        return value;
938    }
939    let rem = value % align;
940    if rem == 0 { value } else { value + align - rem }
941}
942
943impl fmt::Display for PixelDescriptor {
944    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
945        write!(
946            f,
947            "{} {} {}",
948            self.format,
949            self.format.channel_type(),
950            self.transfer
951        )?;
952        if let Some(alpha) = self.alpha
953            && alpha.has_alpha()
954        {
955            write!(f, " alpha={alpha}")?;
956        }
957        Ok(())
958    }
959}
960
961// ---------------------------------------------------------------------------
962// Color model — what the channels represent
963// ---------------------------------------------------------------------------
964
965/// What the channels represent, independent of channel count or byte order.
966#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
967#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
968#[non_exhaustive]
969#[repr(u8)]
970pub enum ColorModel {
971    /// Single grayscale channel.
972    Gray = 0,
973    /// Red, green, blue (or BGR when [`ByteOrder::Bgr`]).
974    Rgb = 1,
975    /// Luma + chroma (Y, Cb, Cr).
976    YCbCr = 2,
977    /// Oklab perceptual color space (L, a, b).
978    Oklab = 3,
979}
980
981impl ColorModel {
982    /// Number of color channels (excluding alpha).
983    #[inline]
984    #[allow(unreachable_patterns)]
985    pub const fn color_channels(self) -> u8 {
986        match self {
987            Self::Gray => 1,
988            _ => 3,
989        }
990    }
991}
992
993impl fmt::Display for ColorModel {
994    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
995        match self {
996            Self::Gray => f.write_str("Gray"),
997            Self::Rgb => f.write_str("RGB"),
998            Self::YCbCr => f.write_str("YCbCr"),
999            Self::Oklab => f.write_str("Oklab"),
1000        }
1001    }
1002}
1003
1004// ---------------------------------------------------------------------------
1005// Byte order
1006// ---------------------------------------------------------------------------
1007
1008/// RGB-family byte order. Only meaningful when color model is RGB.
1009#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1010#[non_exhaustive]
1011#[repr(u8)]
1012pub enum ByteOrder {
1013    /// Standard order: R, G, B (+ A if present).
1014    #[default]
1015    Native = 0,
1016    /// Windows/DirectX order: B, G, R (+ A if present).
1017    Bgr = 1,
1018}
1019
1020impl fmt::Display for ByteOrder {
1021    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1022        match self {
1023            Self::Native => f.write_str("native"),
1024            Self::Bgr => f.write_str("BGR"),
1025        }
1026    }
1027}
1028
1029// ---------------------------------------------------------------------------
1030// PixelFormat — flat enum for physical pixel layout
1031// ---------------------------------------------------------------------------
1032
1033/// Physical pixel layout for match-based format dispatch.
1034///
1035/// Each variant encodes the channel type (U8/U16/F32) and layout (RGB/RGBA/
1036/// Gray/etc.) in one discriminant. Transfer function and alpha mode live on
1037/// [`PixelDescriptor`], not here.
1038///
1039/// Use this enum when you need exhaustive `match` dispatch over known
1040/// pixel layouts.
1041#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1042#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1043#[non_exhaustive]
1044#[repr(u8)]
1045pub enum PixelFormat {
1046    Rgb8 = 1,
1047    Rgba8 = 2,
1048    Rgb16 = 3,
1049    Rgba16 = 4,
1050    RgbF32 = 5,
1051    RgbaF32 = 6,
1052    Gray8 = 7,
1053    Gray16 = 8,
1054    GrayF32 = 9,
1055    GrayA8 = 10,
1056    GrayA16 = 11,
1057    GrayAF32 = 12,
1058    Bgra8 = 13,
1059    Rgbx8 = 14,
1060    Bgrx8 = 15,
1061    OklabF32 = 16,
1062    OklabaF32 = 17,
1063}
1064
1065impl PixelFormat {
1066    /// Channel storage type.
1067    #[inline]
1068    #[allow(unreachable_patterns)]
1069    pub const fn channel_type(self) -> ChannelType {
1070        match self {
1071            Self::Rgb8
1072            | Self::Rgba8
1073            | Self::Gray8
1074            | Self::GrayA8
1075            | Self::Bgra8
1076            | Self::Rgbx8
1077            | Self::Bgrx8 => ChannelType::U8,
1078            Self::Rgb16 | Self::Rgba16 | Self::Gray16 | Self::GrayA16 => ChannelType::U16,
1079            Self::RgbF32
1080            | Self::RgbaF32
1081            | Self::GrayF32
1082            | Self::GrayAF32
1083            | Self::OklabF32
1084            | Self::OklabaF32 => ChannelType::F32,
1085            _ => ChannelType::U8,
1086        }
1087    }
1088
1089    /// Channel layout.
1090    #[inline]
1091    #[allow(unreachable_patterns)]
1092    pub const fn layout(self) -> ChannelLayout {
1093        match self {
1094            Self::Rgb8 | Self::Rgb16 | Self::RgbF32 => ChannelLayout::Rgb,
1095            Self::Rgba8 | Self::Rgba16 | Self::RgbaF32 | Self::Rgbx8 => ChannelLayout::Rgba,
1096            Self::Gray8 | Self::Gray16 | Self::GrayF32 => ChannelLayout::Gray,
1097            Self::GrayA8 | Self::GrayA16 | Self::GrayAF32 => ChannelLayout::GrayAlpha,
1098            Self::Bgra8 | Self::Bgrx8 => ChannelLayout::Bgra,
1099            Self::OklabF32 => ChannelLayout::Oklab,
1100            Self::OklabaF32 => ChannelLayout::OklabA,
1101            _ => ChannelLayout::Rgb,
1102        }
1103    }
1104
1105    /// Color model (what the channels represent).
1106    #[inline]
1107    #[allow(unreachable_patterns)]
1108    pub const fn color_model(self) -> ColorModel {
1109        match self {
1110            Self::Gray8
1111            | Self::Gray16
1112            | Self::GrayF32
1113            | Self::GrayA8
1114            | Self::GrayA16
1115            | Self::GrayAF32 => ColorModel::Gray,
1116            Self::OklabF32 | Self::OklabaF32 => ColorModel::Oklab,
1117            _ => ColorModel::Rgb,
1118        }
1119    }
1120
1121    /// Byte order (Native or BGR).
1122    #[inline]
1123    #[allow(unreachable_patterns)]
1124    pub const fn byte_order(self) -> ByteOrder {
1125        match self {
1126            Self::Bgra8 | Self::Bgrx8 => ByteOrder::Bgr,
1127            _ => ByteOrder::Native,
1128        }
1129    }
1130
1131    /// Number of channels (including alpha/padding if present).
1132    #[inline]
1133    pub const fn channels(self) -> usize {
1134        self.layout().channels()
1135    }
1136
1137    /// Bytes per pixel.
1138    #[inline]
1139    pub const fn bytes_per_pixel(self) -> usize {
1140        self.channels() * self.channel_type().byte_size()
1141    }
1142
1143    /// Whether this format has alpha or padding bytes (4th channel).
1144    #[inline]
1145    pub const fn has_alpha_bytes(self) -> bool {
1146        self.layout().has_alpha()
1147    }
1148
1149    /// Whether this format is grayscale.
1150    #[inline]
1151    pub const fn is_grayscale(self) -> bool {
1152        matches!(self.color_model(), ColorModel::Gray)
1153    }
1154
1155    /// Default alpha mode for this format.
1156    ///
1157    /// - Formats with no alpha bytes → `None`
1158    /// - Formats with padding (Rgbx8, Bgrx8) → `Some(AlphaMode::Undefined)`
1159    /// - Formats with alpha → `Some(AlphaMode::Straight)`
1160    #[allow(unreachable_patterns)]
1161    #[inline]
1162    pub const fn default_alpha(self) -> Option<AlphaMode> {
1163        match self {
1164            Self::Rgb8
1165            | Self::Rgb16
1166            | Self::RgbF32
1167            | Self::Gray8
1168            | Self::Gray16
1169            | Self::GrayF32
1170            | Self::OklabF32 => None,
1171            Self::Rgbx8 | Self::Bgrx8 => Some(AlphaMode::Undefined),
1172            _ => Some(AlphaMode::Straight),
1173        }
1174    }
1175
1176    /// Short human-readable name.
1177    #[allow(unreachable_patterns)]
1178    #[inline]
1179    pub const fn name(self) -> &'static str {
1180        match self {
1181            Self::Rgb8 => "RGB8",
1182            Self::Rgba8 => "RGBA8",
1183            Self::Rgb16 => "RGB16",
1184            Self::Rgba16 => "RGBA16",
1185            Self::RgbF32 => "RgbF32",
1186            Self::RgbaF32 => "RgbaF32",
1187            Self::Gray8 => "Gray8",
1188            Self::Gray16 => "Gray16",
1189            Self::GrayF32 => "GrayF32",
1190            Self::GrayA8 => "GrayA8",
1191            Self::GrayA16 => "GrayA16",
1192            Self::GrayAF32 => "GrayAF32",
1193            Self::Bgra8 => "BGRA8",
1194            Self::Rgbx8 => "RGBX8",
1195            Self::Bgrx8 => "BGRX8",
1196            Self::OklabF32 => "OklabF32",
1197            Self::OklabaF32 => "OklabaF32",
1198            _ => "Unknown",
1199        }
1200    }
1201
1202    /// Resolve a format from channel type, layout, and alpha presence.
1203    ///
1204    /// Returns `None` for combinations that have no `PixelFormat` variant
1205    /// (e.g. `(U16, Bgra, _)`).
1206    #[inline]
1207    pub const fn from_parts(
1208        channel_type: ChannelType,
1209        layout: ChannelLayout,
1210        alpha: Option<AlphaMode>,
1211    ) -> Option<Self> {
1212        let is_padding = matches!(alpha, Some(AlphaMode::Undefined));
1213        match (channel_type, layout, is_padding) {
1214            (ChannelType::U8, ChannelLayout::Rgb, _) => Some(Self::Rgb8),
1215            (ChannelType::U16, ChannelLayout::Rgb, _) => Some(Self::Rgb16),
1216            (ChannelType::F32, ChannelLayout::Rgb, _) => Some(Self::RgbF32),
1217
1218            (ChannelType::U8, ChannelLayout::Rgba, true) => Some(Self::Rgbx8),
1219            (ChannelType::U8, ChannelLayout::Rgba, false) => Some(Self::Rgba8),
1220            (ChannelType::U16, ChannelLayout::Rgba, _) => Some(Self::Rgba16),
1221            (ChannelType::F32, ChannelLayout::Rgba, _) => Some(Self::RgbaF32),
1222
1223            (ChannelType::U8, ChannelLayout::Gray, _) => Some(Self::Gray8),
1224            (ChannelType::U16, ChannelLayout::Gray, _) => Some(Self::Gray16),
1225            (ChannelType::F32, ChannelLayout::Gray, _) => Some(Self::GrayF32),
1226
1227            (ChannelType::U8, ChannelLayout::GrayAlpha, _) => Some(Self::GrayA8),
1228            (ChannelType::U16, ChannelLayout::GrayAlpha, _) => Some(Self::GrayA16),
1229            (ChannelType::F32, ChannelLayout::GrayAlpha, _) => Some(Self::GrayAF32),
1230
1231            (ChannelType::U8, ChannelLayout::Bgra, true) => Some(Self::Bgrx8),
1232            (ChannelType::U8, ChannelLayout::Bgra, false) => Some(Self::Bgra8),
1233
1234            (ChannelType::F32, ChannelLayout::Oklab, _) => Some(Self::OklabF32),
1235            (ChannelType::F32, ChannelLayout::OklabA, _) => Some(Self::OklabaF32),
1236
1237            _ => None,
1238        }
1239    }
1240
1241    /// Base descriptor with `Unknown` transfer and BT.709 primaries.
1242    #[allow(unreachable_patterns)]
1243    #[inline]
1244    pub const fn descriptor(self) -> PixelDescriptor {
1245        match self {
1246            Self::Rgb8 => PixelDescriptor::RGB8,
1247            Self::Rgba8 => PixelDescriptor::RGBA8,
1248            Self::Rgb16 => PixelDescriptor::RGB16,
1249            Self::Rgba16 => PixelDescriptor::RGBA16,
1250            Self::RgbF32 => PixelDescriptor::RGBF32,
1251            Self::RgbaF32 => PixelDescriptor::RGBAF32,
1252            Self::Gray8 => PixelDescriptor::GRAY8,
1253            Self::Gray16 => PixelDescriptor::GRAY16,
1254            Self::GrayF32 => PixelDescriptor::GRAYF32,
1255            Self::GrayA8 => PixelDescriptor::GRAYA8,
1256            Self::GrayA16 => PixelDescriptor::GRAYA16,
1257            Self::GrayAF32 => PixelDescriptor::GRAYAF32,
1258            Self::Bgra8 => PixelDescriptor::BGRA8,
1259            Self::Rgbx8 => PixelDescriptor::RGBX8,
1260            Self::Bgrx8 => PixelDescriptor::BGRX8,
1261            Self::OklabF32 => PixelDescriptor::OKLABF32,
1262            Self::OklabaF32 => PixelDescriptor::OKLABAF32,
1263            _ => PixelDescriptor::RGB8,
1264        }
1265    }
1266}
1267
1268impl fmt::Display for PixelFormat {
1269    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1270        f.write_str(self.name())
1271    }
1272}
1273
1274// ---------------------------------------------------------------------------
1275// Tests
1276// ---------------------------------------------------------------------------
1277
1278#[cfg(test)]
1279mod tests {
1280    use alloc::format;
1281    use core::mem::size_of;
1282
1283    use super::*;
1284
1285    #[test]
1286    fn channel_type_byte_size() {
1287        assert_eq!(ChannelType::U8.byte_size(), 1);
1288        assert_eq!(ChannelType::U16.byte_size(), 2);
1289        assert_eq!(ChannelType::F16.byte_size(), 2);
1290        assert_eq!(ChannelType::F32.byte_size(), 4);
1291    }
1292
1293    #[test]
1294    fn descriptor_bytes_per_pixel() {
1295        assert_eq!(PixelDescriptor::RGB8.bytes_per_pixel(), 3);
1296        assert_eq!(PixelDescriptor::RGBA8.bytes_per_pixel(), 4);
1297        assert_eq!(PixelDescriptor::GRAY8.bytes_per_pixel(), 1);
1298        assert_eq!(PixelDescriptor::RGBAF32.bytes_per_pixel(), 16);
1299        assert_eq!(PixelDescriptor::GRAYA8.bytes_per_pixel(), 2);
1300    }
1301
1302    #[test]
1303    fn descriptor_has_alpha() {
1304        assert!(!PixelDescriptor::RGB8.has_alpha());
1305        assert!(PixelDescriptor::RGBA8.has_alpha());
1306        assert!(!PixelDescriptor::RGBX8.has_alpha());
1307        assert!(PixelDescriptor::GRAYA8.has_alpha());
1308    }
1309
1310    #[test]
1311    fn descriptor_is_grayscale() {
1312        assert!(PixelDescriptor::GRAY8.is_grayscale());
1313        assert!(PixelDescriptor::GRAYA8.is_grayscale());
1314        assert!(!PixelDescriptor::RGB8.is_grayscale());
1315    }
1316
1317    #[test]
1318    fn layout_compatible() {
1319        assert!(PixelDescriptor::RGB8_SRGB.layout_compatible(PixelDescriptor::RGB8));
1320        assert!(!PixelDescriptor::RGB8.layout_compatible(PixelDescriptor::RGBA8));
1321    }
1322
1323    #[test]
1324    fn pixel_format_descriptor_roundtrip() {
1325        let desc = PixelFormat::Rgba8.descriptor();
1326        assert_eq!(desc.layout(), ChannelLayout::Rgba);
1327        assert_eq!(desc.channel_type(), ChannelType::U8);
1328    }
1329
1330    #[test]
1331    fn pixel_format_enum_basics() {
1332        assert_eq!(PixelFormat::Rgb8.channels(), 3);
1333        assert_eq!(PixelFormat::Rgba8.channels(), 4);
1334        assert!(PixelFormat::Rgba8.has_alpha_bytes());
1335        assert!(!PixelFormat::Rgb8.has_alpha_bytes());
1336        assert_eq!(PixelFormat::RgbF32.bytes_per_pixel(), 12);
1337        assert_eq!(PixelFormat::RgbaF32.bytes_per_pixel(), 16);
1338        assert_eq!(PixelFormat::Gray8.channels(), 1);
1339        assert!(PixelFormat::Gray8.is_grayscale());
1340        assert!(!PixelFormat::Rgb8.is_grayscale());
1341        assert_eq!(PixelFormat::Bgra8.byte_order(), ByteOrder::Bgr);
1342        assert_eq!(PixelFormat::Rgb8.byte_order(), ByteOrder::Native);
1343    }
1344
1345    #[test]
1346    fn pixel_format_enum_size() {
1347        // Single-byte discriminant — much smaller than old 5-field struct.
1348        assert!(size_of::<PixelFormat>() <= 2);
1349    }
1350
1351    #[test]
1352    fn pixel_format_from_parts_roundtrip() {
1353        let fmt = PixelFormat::Rgba8;
1354        let rebuilt =
1355            PixelFormat::from_parts(fmt.channel_type(), fmt.layout(), fmt.default_alpha());
1356        assert_eq!(rebuilt, Some(fmt));
1357
1358        let fmt2 = PixelFormat::Bgra8;
1359        let rebuilt2 =
1360            PixelFormat::from_parts(fmt2.channel_type(), fmt2.layout(), fmt2.default_alpha());
1361        assert_eq!(rebuilt2, Some(fmt2));
1362
1363        let fmt3 = PixelFormat::Gray8;
1364        let rebuilt3 =
1365            PixelFormat::from_parts(fmt3.channel_type(), fmt3.layout(), fmt3.default_alpha());
1366        assert_eq!(rebuilt3, Some(fmt3));
1367    }
1368
1369    #[test]
1370    fn alpha_mode_semantics() {
1371        // None (Option) = no alpha channel
1372        assert!(!PixelDescriptor::RGB8.has_alpha());
1373        // Undefined = padding bytes, not real alpha
1374        assert!(!AlphaMode::Undefined.has_alpha());
1375        // Straight and Premultiplied = real alpha
1376        assert!(AlphaMode::Straight.has_alpha());
1377        assert!(AlphaMode::Premultiplied.has_alpha());
1378        assert!(AlphaMode::Opaque.has_alpha());
1379    }
1380
1381    #[test]
1382    fn color_primaries_containment() {
1383        assert!(ColorPrimaries::Bt2020.contains(ColorPrimaries::DisplayP3));
1384        assert!(ColorPrimaries::Bt2020.contains(ColorPrimaries::Bt709));
1385        assert!(ColorPrimaries::DisplayP3.contains(ColorPrimaries::Bt709));
1386        assert!(!ColorPrimaries::Bt709.contains(ColorPrimaries::DisplayP3));
1387        assert!(!ColorPrimaries::Unknown.contains(ColorPrimaries::Bt709));
1388    }
1389
1390    #[test]
1391    fn descriptor_size() {
1392        // PixelFormat (1 byte enum) + transfer (1) + alpha (2) + primaries (1) + signal_range (1) = ~6
1393        assert!(size_of::<PixelDescriptor>() <= 8);
1394    }
1395
1396    #[test]
1397    fn color_model_channels() {
1398        assert_eq!(ColorModel::Gray.color_channels(), 1);
1399        assert_eq!(ColorModel::Rgb.color_channels(), 3);
1400        assert_eq!(ColorModel::YCbCr.color_channels(), 3);
1401        assert_eq!(ColorModel::Oklab.color_channels(), 3);
1402    }
1403
1404    // --- PlaneSemantic tests ---
1405
1406    // --- PlaneDescriptor tests ---
1407
1408    // --- PlaneMask tests ---
1409
1410    // --- PlaneLayout tests ---
1411
1412    // --- MultiPlaneImage tests ---
1413
1414    // --- PlaneRelationship tests ---
1415
1416    #[test]
1417    fn reference_white_nits_values() {
1418        assert_eq!(TransferFunction::Pq.reference_white_nits(), 203.0);
1419        assert_eq!(TransferFunction::Srgb.reference_white_nits(), 1.0);
1420        assert_eq!(TransferFunction::Hlg.reference_white_nits(), 1.0);
1421        assert_eq!(TransferFunction::Linear.reference_white_nits(), 1.0);
1422        assert_eq!(TransferFunction::Unknown.reference_white_nits(), 1.0);
1423    }
1424
1425    // --- PlaneLayout mask tests ---
1426
1427    // --- Display impl tests ---
1428
1429    #[test]
1430    fn channel_type_display() {
1431        assert_eq!(format!("{}", ChannelType::U8), "U8");
1432        assert_eq!(format!("{}", ChannelType::U16), "U16");
1433        assert_eq!(format!("{}", ChannelType::F32), "F32");
1434        assert_eq!(format!("{}", ChannelType::F16), "F16");
1435    }
1436
1437    #[test]
1438    fn channel_layout_display() {
1439        assert_eq!(format!("{}", ChannelLayout::Gray), "Gray");
1440        assert_eq!(format!("{}", ChannelLayout::GrayAlpha), "GrayAlpha");
1441        assert_eq!(format!("{}", ChannelLayout::Rgb), "RGB");
1442        assert_eq!(format!("{}", ChannelLayout::Rgba), "RGBA");
1443        assert_eq!(format!("{}", ChannelLayout::Bgra), "BGRA");
1444        assert_eq!(format!("{}", ChannelLayout::Oklab), "Oklab");
1445        assert_eq!(format!("{}", ChannelLayout::OklabA), "OklabA");
1446    }
1447
1448    #[test]
1449    fn alpha_mode_display() {
1450        assert_eq!(format!("{}", AlphaMode::Undefined), "undefined");
1451        assert_eq!(format!("{}", AlphaMode::Straight), "straight");
1452        assert_eq!(format!("{}", AlphaMode::Premultiplied), "premultiplied");
1453        assert_eq!(format!("{}", AlphaMode::Opaque), "opaque");
1454    }
1455
1456    #[test]
1457    fn transfer_function_display() {
1458        assert_eq!(format!("{}", TransferFunction::Linear), "linear");
1459        assert_eq!(format!("{}", TransferFunction::Srgb), "sRGB");
1460        assert_eq!(format!("{}", TransferFunction::Bt709), "BT.709");
1461        assert_eq!(format!("{}", TransferFunction::Pq), "PQ");
1462        assert_eq!(format!("{}", TransferFunction::Hlg), "HLG");
1463        assert_eq!(format!("{}", TransferFunction::Unknown), "unknown");
1464    }
1465
1466    #[test]
1467    fn color_primaries_display() {
1468        assert_eq!(format!("{}", ColorPrimaries::Bt709), "BT.709");
1469        assert_eq!(format!("{}", ColorPrimaries::Bt2020), "BT.2020");
1470        assert_eq!(format!("{}", ColorPrimaries::DisplayP3), "Display P3");
1471        assert_eq!(format!("{}", ColorPrimaries::Unknown), "unknown");
1472    }
1473
1474    #[test]
1475    fn signal_range_display() {
1476        assert_eq!(format!("{}", SignalRange::Full), "full");
1477        assert_eq!(format!("{}", SignalRange::Narrow), "narrow");
1478    }
1479
1480    #[test]
1481    fn pixel_descriptor_display() {
1482        let s = format!("{}", PixelDescriptor::RGB8_SRGB);
1483        assert!(s.contains("U8"), "expected U8 in: {s}");
1484        assert!(s.contains("sRGB"), "expected sRGB in: {s}");
1485
1486        let s = format!("{}", PixelDescriptor::RGBA8_SRGB);
1487        assert!(s.contains("alpha=straight"), "expected alpha in: {s}");
1488    }
1489
1490    #[test]
1491    fn pixel_format_display() {
1492        let s = format!("{}", PixelFormat::Rgb8);
1493        assert!(s.contains("RGB8"));
1494        let s = format!("{}", PixelFormat::Bgra8);
1495        assert!(s.contains("BGRA8"));
1496    }
1497
1498    // --- from_cicp / to_cicp tests ---
1499
1500    #[test]
1501    fn transfer_function_from_cicp() {
1502        assert_eq!(
1503            TransferFunction::from_cicp(1),
1504            Some(TransferFunction::Bt709)
1505        );
1506        assert_eq!(
1507            TransferFunction::from_cicp(8),
1508            Some(TransferFunction::Linear)
1509        );
1510        assert_eq!(
1511            TransferFunction::from_cicp(13),
1512            Some(TransferFunction::Srgb)
1513        );
1514        assert_eq!(TransferFunction::from_cicp(16), Some(TransferFunction::Pq));
1515        assert_eq!(TransferFunction::from_cicp(18), Some(TransferFunction::Hlg));
1516        assert_eq!(TransferFunction::from_cicp(99), None);
1517    }
1518
1519    #[test]
1520    fn transfer_function_to_cicp() {
1521        assert_eq!(TransferFunction::Bt709.to_cicp(), Some(1));
1522        assert_eq!(TransferFunction::Linear.to_cicp(), Some(8));
1523        assert_eq!(TransferFunction::Srgb.to_cicp(), Some(13));
1524        assert_eq!(TransferFunction::Pq.to_cicp(), Some(16));
1525        assert_eq!(TransferFunction::Hlg.to_cicp(), Some(18));
1526        assert_eq!(TransferFunction::Unknown.to_cicp(), None);
1527    }
1528
1529    #[test]
1530    fn transfer_function_cicp_roundtrip() {
1531        for tf in [
1532            TransferFunction::Bt709,
1533            TransferFunction::Linear,
1534            TransferFunction::Srgb,
1535            TransferFunction::Pq,
1536            TransferFunction::Hlg,
1537        ] {
1538            let code = tf.to_cicp().unwrap();
1539            assert_eq!(TransferFunction::from_cicp(code), Some(tf));
1540        }
1541    }
1542
1543    #[test]
1544    fn color_primaries_from_cicp() {
1545        assert_eq!(ColorPrimaries::from_cicp(1), Some(ColorPrimaries::Bt709));
1546        assert_eq!(ColorPrimaries::from_cicp(9), Some(ColorPrimaries::Bt2020));
1547        assert_eq!(
1548            ColorPrimaries::from_cicp(12),
1549            Some(ColorPrimaries::DisplayP3)
1550        );
1551        assert_eq!(ColorPrimaries::from_cicp(99), None);
1552    }
1553
1554    #[test]
1555    fn color_primaries_to_cicp() {
1556        assert_eq!(ColorPrimaries::Bt709.to_cicp(), Some(1));
1557        assert_eq!(ColorPrimaries::Bt2020.to_cicp(), Some(9));
1558        assert_eq!(ColorPrimaries::DisplayP3.to_cicp(), Some(12));
1559        assert_eq!(ColorPrimaries::Unknown.to_cicp(), None);
1560    }
1561
1562    // --- ChannelType helpers ---
1563
1564    #[test]
1565    fn channel_type_helpers() {
1566        assert!(ChannelType::U8.is_u8());
1567        assert!(!ChannelType::U8.is_u16());
1568        assert!(ChannelType::U16.is_u16());
1569        assert!(ChannelType::F32.is_f32());
1570        assert!(ChannelType::F16.is_f16());
1571        assert!(ChannelType::U8.is_integer());
1572        assert!(ChannelType::U16.is_integer());
1573        assert!(!ChannelType::F32.is_integer());
1574        assert!(ChannelType::F32.is_float());
1575        assert!(ChannelType::F16.is_float());
1576        assert!(!ChannelType::U8.is_float());
1577    }
1578
1579    // --- ChannelLayout helpers ---
1580
1581    #[test]
1582    fn channel_layout_channels() {
1583        assert_eq!(ChannelLayout::Gray.channels(), 1);
1584        assert_eq!(ChannelLayout::GrayAlpha.channels(), 2);
1585        assert_eq!(ChannelLayout::Rgb.channels(), 3);
1586        assert_eq!(ChannelLayout::Rgba.channels(), 4);
1587        assert_eq!(ChannelLayout::Bgra.channels(), 4);
1588        assert_eq!(ChannelLayout::Oklab.channels(), 3);
1589        assert_eq!(ChannelLayout::OklabA.channels(), 4);
1590    }
1591
1592    #[test]
1593    fn channel_layout_has_alpha() {
1594        assert!(!ChannelLayout::Gray.has_alpha());
1595        assert!(ChannelLayout::GrayAlpha.has_alpha());
1596        assert!(!ChannelLayout::Rgb.has_alpha());
1597        assert!(ChannelLayout::Rgba.has_alpha());
1598        assert!(ChannelLayout::Bgra.has_alpha());
1599        assert!(!ChannelLayout::Oklab.has_alpha());
1600        assert!(ChannelLayout::OklabA.has_alpha());
1601    }
1602
1603    // --- PixelDescriptor builder methods ---
1604
1605    #[test]
1606    fn with_transfer() {
1607        let desc = PixelDescriptor::RGB8_SRGB.with_transfer(TransferFunction::Linear);
1608        assert_eq!(desc.transfer(), TransferFunction::Linear);
1609        assert_eq!(desc.layout(), ChannelLayout::Rgb);
1610    }
1611
1612    #[test]
1613    fn with_primaries() {
1614        let desc = PixelDescriptor::RGB8_SRGB.with_primaries(ColorPrimaries::DisplayP3);
1615        assert_eq!(desc.primaries, ColorPrimaries::DisplayP3);
1616    }
1617
1618    #[test]
1619    fn with_signal_range() {
1620        let desc = PixelDescriptor::RGB8_SRGB.with_signal_range(SignalRange::Narrow);
1621        assert_eq!(desc.signal_range, SignalRange::Narrow);
1622    }
1623
1624    #[test]
1625    fn with_alpha_mode() {
1626        let desc = PixelDescriptor::RGBA8_SRGB.with_alpha(Some(AlphaMode::Premultiplied));
1627        assert_eq!(desc.alpha(), Some(AlphaMode::Premultiplied));
1628    }
1629
1630    // --- PixelDescriptor predicates ---
1631
1632    #[test]
1633    fn is_opaque_and_may_have_transparency() {
1634        assert!(PixelDescriptor::RGB8_SRGB.is_opaque());
1635        assert!(!PixelDescriptor::RGB8_SRGB.may_have_transparency());
1636        assert!(!PixelDescriptor::RGBA8_SRGB.is_opaque());
1637        assert!(PixelDescriptor::RGBA8_SRGB.may_have_transparency());
1638
1639        let rgbx = PixelDescriptor::new(
1640            ChannelType::U8,
1641            ChannelLayout::Rgba,
1642            Some(AlphaMode::Undefined),
1643            TransferFunction::Srgb,
1644        );
1645        assert!(rgbx.is_opaque());
1646        assert!(!rgbx.may_have_transparency());
1647    }
1648
1649    #[test]
1650    fn is_linear_and_is_unknown_transfer() {
1651        assert!(!PixelDescriptor::RGB8_SRGB.is_linear());
1652        assert!(PixelDescriptor::RGBF32_LINEAR.is_linear());
1653        assert!(!PixelDescriptor::RGB8_SRGB.is_unknown_transfer());
1654        let desc = PixelDescriptor::RGB8_SRGB.with_transfer(TransferFunction::Unknown);
1655        assert!(desc.is_unknown_transfer());
1656    }
1657
1658    #[test]
1659    fn min_alignment() {
1660        assert_eq!(PixelDescriptor::RGB8_SRGB.min_alignment(), 1);
1661        assert_eq!(PixelDescriptor::RGBF32_LINEAR.min_alignment(), 4);
1662    }
1663
1664    #[test]
1665    fn aligned_stride() {
1666        assert_eq!(PixelDescriptor::RGB8_SRGB.aligned_stride(100), 300);
1667        assert_eq!(PixelDescriptor::RGBA8_SRGB.aligned_stride(100), 400);
1668        assert_eq!(PixelDescriptor::RGBF32_LINEAR.aligned_stride(10), 120);
1669    }
1670
1671    #[test]
1672    fn simd_aligned_stride() {
1673        let stride = PixelDescriptor::RGB8_SRGB.simd_aligned_stride(100, 16);
1674        assert!(stride >= 300);
1675        assert_eq!(stride % 16, 0);
1676        assert_eq!(stride % 3, 0); // pixel-aligned
1677    }
1678
1679    // --- new_full and from_pixel_format ---
1680
1681    #[test]
1682    fn new_full_constructor() {
1683        let desc = PixelDescriptor::new_full(
1684            ChannelType::U8,
1685            ChannelLayout::Rgb,
1686            None,
1687            TransferFunction::Srgb,
1688            ColorPrimaries::DisplayP3,
1689        );
1690        assert_eq!(desc.primaries, ColorPrimaries::DisplayP3);
1691        assert_eq!(desc.transfer(), TransferFunction::Srgb);
1692    }
1693
1694    #[test]
1695    fn from_pixel_format_constructor() {
1696        let desc = PixelDescriptor::from_pixel_format(PixelFormat::Rgba8);
1697        assert_eq!(desc.layout(), ChannelLayout::Rgba);
1698        assert_eq!(desc.transfer(), TransferFunction::Unknown);
1699        assert_eq!(desc.primaries, ColorPrimaries::Bt709);
1700        assert_eq!(desc.signal_range, SignalRange::Full);
1701    }
1702
1703    // --- PixelFormat::name ---
1704
1705    #[test]
1706    fn pixel_format_name() {
1707        assert_eq!(PixelFormat::Rgb8.name(), "RGB8");
1708        assert_eq!(PixelFormat::Bgra8.name(), "BGRA8");
1709        assert_eq!(PixelFormat::Gray8.name(), "Gray8");
1710    }
1711
1712    // --- ColorModel ---
1713
1714    #[test]
1715    fn color_model_display() {
1716        assert_eq!(format!("{}", ColorModel::Gray), "Gray");
1717        assert_eq!(format!("{}", ColorModel::Rgb), "RGB");
1718        assert_eq!(format!("{}", ColorModel::YCbCr), "YCbCr");
1719        assert_eq!(format!("{}", ColorModel::Oklab), "Oklab");
1720    }
1721
1722    // --- SignalRange default ---
1723
1724    #[test]
1725    fn signal_range_default() {
1726        assert_eq!(SignalRange::default(), SignalRange::Full);
1727    }
1728
1729    // --- ColorPrimaries default ---
1730
1731    #[test]
1732    fn color_primaries_default() {
1733        assert_eq!(ColorPrimaries::default(), ColorPrimaries::Bt709);
1734    }
1735}