1use crate::{ColorPrimaries, TransferFunction};
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24#[non_exhaustive]
25pub struct Cicp {
26 pub color_primaries: u8,
29 pub transfer_characteristics: u8,
32 pub matrix_coefficients: u8,
35 pub full_range: bool,
38}
39
40impl Cicp {
41 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 pub const SRGB: Self = Self {
58 color_primaries: 1,
59 transfer_characteristics: 13,
60 matrix_coefficients: 0,
61 full_range: true,
62 };
63
64 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 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 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 pub fn color_primaries_enum(&self) -> ColorPrimaries {
93 ColorPrimaries::from_cicp(self.color_primaries).unwrap_or(ColorPrimaries::Unknown)
94 }
95
96 pub fn transfer_function_enum(&self) -> TransferFunction {
101 TransferFunction::from_cicp(self.transfer_characteristics)
102 .unwrap_or(TransferFunction::Unknown)
103 }
104
105 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, full_range,
118 })
119 }
120
121 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 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 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 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 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}