Skip to main content

zenpixels/
cicp.rs

1//! CICP (Coding-Independent Code Points) color description.
2//!
3//! ITU-T H.273 / ISO 23091-2 defines code points for color primaries,
4//! transfer characteristics, and matrix coefficients. This struct
5//! carries the four fields needed by [`ColorContext`](crate::color::ColorContext).
6
7use crate::{ColorPrimaries, TransferFunction};
8
9/// CICP color description (ITU-T H.273).
10///
11/// Coding-Independent Code Points describe the color space of an image
12/// without requiring an ICC profile. Used by AVIF, HEIF, JPEG XL, and
13/// video codecs (H.264, H.265, AV1).
14///
15/// Common combinations for RGB content (matrix_coefficients = 0 = Identity):
16/// - sRGB: `(1, 13, 0, true)` — BT.709 primaries, sRGB transfer
17/// - Display P3: `(12, 13, 0, true)` — P3 primaries, sRGB transfer
18/// - BT.2100 PQ (HDR): `(9, 16, 0, true)` — BT.2020 primaries, PQ transfer
19/// - BT.2100 HLG (HDR): `(9, 18, 0, true)` — BT.2020 primaries, HLG transfer
20///
21/// Video/YCbCr content uses non-zero matrix_coefficients (e.g., 6=BT.601, 9=BT.2020).
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24#[non_exhaustive]
25pub struct Cicp {
26    /// Color primaries (ColourPrimaries). Common values:
27    /// 1 = BT.709/sRGB, 9 = BT.2020, 12 = Display P3.
28    pub color_primaries: u8,
29    /// Transfer characteristics (TransferCharacteristics). Common values:
30    /// 1 = BT.709, 13 = sRGB, 16 = PQ (HDR), 18 = HLG (HDR).
31    pub transfer_characteristics: u8,
32    /// Matrix coefficients (MatrixCoefficients). Common values:
33    /// 0 = Identity/RGB, 1 = BT.709, 6 = BT.601, 9 = BT.2020.
34    pub matrix_coefficients: u8,
35    /// Whether pixel values use the full range (0-255 for 8-bit)
36    /// or video/limited range (16-235 for 8-bit luma).
37    pub full_range: bool,
38}
39
40impl Cicp {
41    /// Create a CICP color description from raw code points.
42    pub const fn new(
43        color_primaries: u8,
44        transfer_characteristics: u8,
45        matrix_coefficients: u8,
46        full_range: bool,
47    ) -> Self {
48        Self {
49            color_primaries,
50            transfer_characteristics,
51            matrix_coefficients,
52            full_range,
53        }
54    }
55
56    /// sRGB color space: BT.709 primaries, sRGB transfer, Identity (RGB) matrix, full range.
57    pub const SRGB: Self = Self {
58        color_primaries: 1,
59        transfer_characteristics: 13,
60        matrix_coefficients: 0,
61        full_range: true,
62    };
63
64    /// BT.2100 PQ (HDR10): BT.2020 primaries, PQ transfer, BT.2020 matrix, full range.
65    pub const BT2100_PQ: Self = Self {
66        color_primaries: 9,
67        transfer_characteristics: 16,
68        matrix_coefficients: 9,
69        full_range: true,
70    };
71
72    /// BT.2100 HLG (HDR): BT.2020 primaries, HLG transfer, BT.2020 matrix, full range.
73    pub const BT2100_HLG: Self = Self {
74        color_primaries: 9,
75        transfer_characteristics: 18,
76        matrix_coefficients: 9,
77        full_range: true,
78    };
79
80    /// Display P3 with sRGB transfer: P3 primaries, sRGB transfer, Identity matrix, full range.
81    pub const DISPLAY_P3: Self = Self {
82        color_primaries: 12,
83        transfer_characteristics: 13,
84        matrix_coefficients: 0,
85        full_range: true,
86    };
87
88    /// Map the CICP `color_primaries` code to a [`ColorPrimaries`] enum.
89    ///
90    /// Returns [`Unknown`](ColorPrimaries::Unknown) for unrecognized codes.
91    /// This is a convenience wrapper around [`ColorPrimaries::from_cicp`].
92    pub fn color_primaries_enum(&self) -> ColorPrimaries {
93        ColorPrimaries::from_cicp(self.color_primaries).unwrap_or(ColorPrimaries::Unknown)
94    }
95
96    /// Map the CICP `transfer_characteristics` code to a [`TransferFunction`] enum.
97    ///
98    /// Returns [`Unknown`](TransferFunction::Unknown) for unrecognized codes.
99    /// This is a convenience wrapper around [`TransferFunction::from_cicp`].
100    pub fn transfer_function_enum(&self) -> TransferFunction {
101        TransferFunction::from_cicp(self.transfer_characteristics)
102            .unwrap_or(TransferFunction::Unknown)
103    }
104
105    /// Create a CICP from a [`PixelDescriptor`](crate::PixelDescriptor).
106    ///
107    /// Returns `None` if the descriptor's transfer function or color primaries
108    /// cannot be mapped to CICP code points (e.g., `Unknown` variants).
109    pub fn from_descriptor(desc: &crate::PixelDescriptor) -> Option<Self> {
110        let tc = desc.transfer.to_cicp()?;
111        let cp = desc.primaries.to_cicp()?;
112        let full_range = matches!(desc.signal_range, crate::SignalRange::Full);
113        Some(Self {
114            color_primaries: cp,
115            transfer_characteristics: tc,
116            matrix_coefficients: 0, // RGB content uses Identity
117            full_range,
118        })
119    }
120
121    /// Convert to a [`PixelDescriptor`](crate::PixelDescriptor) with the given
122    /// [`PixelFormat`](crate::PixelFormat).
123    ///
124    /// Maps the CICP code points to the corresponding enum variants.
125    /// Unmapped codes become `Unknown`.
126    pub fn to_descriptor(&self, format: crate::PixelFormat) -> crate::PixelDescriptor {
127        let transfer = self.transfer_function_enum();
128        let primaries = self.color_primaries_enum();
129        let signal_range = if self.full_range {
130            crate::SignalRange::Full
131        } else {
132            crate::SignalRange::Narrow
133        };
134        // Derive alpha from the pixel format's channel layout.
135        let alpha = if format.layout().has_alpha() {
136            Some(crate::AlphaMode::Straight)
137        } else {
138            None
139        };
140        crate::PixelDescriptor {
141            format,
142            transfer,
143            alpha,
144            primaries,
145            signal_range,
146        }
147    }
148
149    /// Human-readable name for the color primaries code (ITU-T H.273 Table 2).
150    pub fn color_primaries_name(code: u8) -> &'static str {
151        match code {
152            0 => "Reserved",
153            1 => "BT.709/sRGB",
154            2 => "Unspecified",
155            4 => "BT.470M",
156            5 => "BT.601 (625)",
157            6 => "BT.601 (525)",
158            7 => "SMPTE 240M",
159            8 => "Generic Film",
160            9 => "BT.2020",
161            10 => "XYZ",
162            11 => "SMPTE 431 (DCI-P3)",
163            12 => "Display P3",
164            22 => "EBU Tech 3213",
165            _ => "Unknown",
166        }
167    }
168
169    /// Human-readable name for the transfer characteristics code (ITU-T H.273 Table 3).
170    pub fn transfer_characteristics_name(code: u8) -> &'static str {
171        match code {
172            0 => "Reserved",
173            1 => "BT.709",
174            2 => "Unspecified",
175            4 => "BT.470M (Gamma 2.2)",
176            5 => "BT.470BG (Gamma 2.8)",
177            6 => "BT.601",
178            7 => "SMPTE 240M",
179            8 => "Linear",
180            9 => "Log 100:1",
181            10 => "Log 316:1",
182            11 => "IEC 61966-2-4",
183            12 => "BT.1361",
184            13 => "sRGB",
185            14 => "BT.2020 (10-bit)",
186            15 => "BT.2020 (12-bit)",
187            16 => "PQ (HDR)",
188            17 => "SMPTE 428",
189            18 => "HLG (HDR)",
190            _ => "Unknown",
191        }
192    }
193
194    /// Human-readable name for the matrix coefficients code (ITU-T H.273 Table 4).
195    pub fn matrix_coefficients_name(code: u8) -> &'static str {
196        match code {
197            0 => "Identity/RGB",
198            1 => "BT.709",
199            2 => "Unspecified",
200            4 => "FCC",
201            5 => "BT.470BG",
202            6 => "BT.601",
203            7 => "SMPTE 240M",
204            8 => "YCgCo",
205            9 => "BT.2020 NCL",
206            10 => "BT.2020 CL",
207            11 => "SMPTE 2085",
208            12 => "Chroma NCL",
209            13 => "Chroma CL",
210            14 => "ICtCp",
211            _ => "Unknown",
212        }
213    }
214}
215
216impl core::fmt::Display for Cicp {
217    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
218        write!(
219            f,
220            "{} / {} / {} ({})",
221            Self::color_primaries_name(self.color_primaries),
222            Self::transfer_characteristics_name(self.transfer_characteristics),
223            Self::matrix_coefficients_name(self.matrix_coefficients),
224            if self.full_range {
225                "full range"
226            } else {
227                "limited range"
228            },
229        )
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    use alloc::format;
237
238    #[test]
239    fn cicp_new() {
240        let c = Cicp::new(1, 13, 0, true);
241        assert_eq!(c, Cicp::SRGB);
242    }
243
244    #[test]
245    fn cicp_constants() {
246        assert_eq!(Cicp::SRGB.color_primaries, 1);
247        assert_eq!(Cicp::SRGB.transfer_characteristics, 13);
248        assert_eq!(Cicp::BT2100_PQ.transfer_characteristics, 16);
249        assert_eq!(Cicp::BT2100_HLG.transfer_characteristics, 18);
250        assert_eq!(Cicp::DISPLAY_P3.color_primaries, 12);
251    }
252
253    #[test]
254    fn color_primaries_enum() {
255        assert_eq!(Cicp::SRGB.color_primaries_enum(), ColorPrimaries::Bt709);
256        assert_eq!(
257            Cicp::BT2100_PQ.color_primaries_enum(),
258            ColorPrimaries::Bt2020
259        );
260        assert_eq!(
261            Cicp::DISPLAY_P3.color_primaries_enum(),
262            ColorPrimaries::DisplayP3
263        );
264        assert_eq!(
265            Cicp::new(255, 0, 0, true).color_primaries_enum(),
266            ColorPrimaries::Unknown
267        );
268    }
269
270    #[test]
271    fn transfer_function_enum() {
272        assert_eq!(Cicp::SRGB.transfer_function_enum(), TransferFunction::Srgb);
273        assert_eq!(
274            Cicp::BT2100_PQ.transfer_function_enum(),
275            TransferFunction::Pq
276        );
277        assert_eq!(
278            Cicp::BT2100_HLG.transfer_function_enum(),
279            TransferFunction::Hlg
280        );
281        assert_eq!(
282            Cicp::new(1, 255, 0, true).transfer_function_enum(),
283            TransferFunction::Unknown
284        );
285    }
286
287    #[test]
288    fn color_primaries_name_known() {
289        assert_eq!(Cicp::color_primaries_name(0), "Reserved");
290        assert_eq!(Cicp::color_primaries_name(1), "BT.709/sRGB");
291        assert_eq!(Cicp::color_primaries_name(9), "BT.2020");
292        assert_eq!(Cicp::color_primaries_name(12), "Display P3");
293        assert_eq!(Cicp::color_primaries_name(200), "Unknown");
294    }
295
296    #[test]
297    fn transfer_characteristics_name_known() {
298        assert_eq!(Cicp::transfer_characteristics_name(8), "Linear");
299        assert_eq!(Cicp::transfer_characteristics_name(13), "sRGB");
300        assert_eq!(Cicp::transfer_characteristics_name(16), "PQ (HDR)");
301        assert_eq!(Cicp::transfer_characteristics_name(18), "HLG (HDR)");
302        assert_eq!(Cicp::transfer_characteristics_name(200), "Unknown");
303    }
304
305    #[test]
306    fn matrix_coefficients_name_known() {
307        assert_eq!(Cicp::matrix_coefficients_name(0), "Identity/RGB");
308        assert_eq!(Cicp::matrix_coefficients_name(1), "BT.709");
309        assert_eq!(Cicp::matrix_coefficients_name(9), "BT.2020 NCL");
310        assert_eq!(Cicp::matrix_coefficients_name(200), "Unknown");
311    }
312
313    #[test]
314    fn display_srgb() {
315        let s = format!("{}", Cicp::SRGB);
316        assert!(s.contains("BT.709/sRGB"));
317        assert!(s.contains("sRGB"));
318        assert!(s.contains("full range"));
319    }
320
321    #[test]
322    fn display_limited_range() {
323        let c = Cicp::new(1, 1, 1, false);
324        let s = format!("{c}");
325        assert!(s.contains("limited range"));
326    }
327
328    #[test]
329    fn debug_and_clone() {
330        let c = Cicp::SRGB;
331        let _ = format!("{c:?}");
332        let c2 = c;
333        assert_eq!(c, c2);
334    }
335
336    #[test]
337    #[cfg(feature = "std")]
338    fn hash() {
339        use core::hash::{Hash, Hasher};
340        let mut h1 = std::hash::DefaultHasher::new();
341        Cicp::SRGB.hash(&mut h1);
342        let mut h2 = std::hash::DefaultHasher::new();
343        Cicp::SRGB.hash(&mut h2);
344        assert_eq!(h1.finish(), h2.finish());
345    }
346
347    #[test]
348    fn from_descriptor_srgb() {
349        use crate::{AlphaMode, PixelDescriptor, PixelFormat, SignalRange};
350        let desc = PixelDescriptor {
351            format: PixelFormat::Rgba8,
352            transfer: TransferFunction::Srgb,
353            alpha: Some(AlphaMode::Straight),
354            primaries: ColorPrimaries::Bt709,
355            signal_range: SignalRange::Full,
356        };
357        let cicp = Cicp::from_descriptor(&desc).unwrap();
358        assert_eq!(cicp, Cicp::SRGB);
359    }
360
361    #[test]
362    fn from_descriptor_unknown_returns_none() {
363        use crate::{PixelDescriptor, PixelFormat, SignalRange};
364        let desc = PixelDescriptor {
365            format: PixelFormat::Rgb8,
366            transfer: TransferFunction::Unknown,
367            alpha: None,
368            primaries: ColorPrimaries::Bt709,
369            signal_range: SignalRange::Full,
370        };
371        assert!(Cicp::from_descriptor(&desc).is_none());
372    }
373
374    #[test]
375    fn to_descriptor_srgb_rgba() {
376        use crate::{AlphaMode, PixelFormat, SignalRange};
377        let desc = Cicp::SRGB.to_descriptor(PixelFormat::Rgba8);
378        assert_eq!(desc.format, PixelFormat::Rgba8);
379        assert_eq!(desc.transfer, TransferFunction::Srgb);
380        assert_eq!(desc.primaries, ColorPrimaries::Bt709);
381        assert_eq!(desc.alpha, Some(AlphaMode::Straight));
382        assert_eq!(desc.signal_range, SignalRange::Full);
383    }
384
385    #[test]
386    fn to_descriptor_narrow_range() {
387        use crate::{PixelFormat, SignalRange};
388        let cicp = Cicp::new(1, 13, 0, false);
389        let desc = cicp.to_descriptor(PixelFormat::Rgb8);
390        assert_eq!(desc.signal_range, SignalRange::Narrow);
391        assert!(desc.alpha.is_none());
392    }
393
394    #[test]
395    fn descriptor_roundtrip() {
396        use crate::PixelFormat;
397        for cicp in [
398            Cicp::SRGB,
399            Cicp::BT2100_PQ,
400            Cicp::BT2100_HLG,
401            Cicp::DISPLAY_P3,
402        ] {
403            let desc = cicp.to_descriptor(PixelFormat::Rgb8);
404            let back = Cicp::from_descriptor(&desc).unwrap();
405            assert_eq!(back.color_primaries, cicp.color_primaries);
406            assert_eq!(back.transfer_characteristics, cicp.transfer_characteristics);
407            assert_eq!(back.full_range, cicp.full_range);
408        }
409    }
410}