1use crate::bit_writer::BitWriter;
8use crate::error::{Error, Result};
9
10#[derive(Debug, Clone, Copy, PartialEq)]
15pub struct CIExy {
16 pub x: f64,
18 pub y: f64,
20}
21
22impl CIExy {
23 pub fn new(x: f64, y: f64) -> Self {
25 Self { x, y }
26 }
27}
28
29#[derive(Debug, Clone, Copy, PartialEq)]
31pub struct CustomPrimaries {
32 pub red: CIExy,
34 pub green: CIExy,
36 pub blue: CIExy,
38}
39
40const CUSTOMXY_ROUGH_LIMIT: f64 = 4.0;
42
43const CUSTOMXY_MUL: u32 = 1_000_000;
45
46const CUSTOMXY_MIN: i32 = -0x200000;
48
49const CUSTOMXY_MAX: i32 = 0x1FFFFF;
51
52fn pack_signed(value: i32) -> u32 {
57 ((value as u32) << 1) ^ (((!(value as u32)) >> 31).wrapping_sub(1))
58}
59
60fn xy_to_fixed(value: f64, name: &str) -> Result<i32> {
64 if value.abs() >= CUSTOMXY_ROUGH_LIMIT {
65 return Err(Error::InvalidInput(format!(
66 "custom {name} coordinate {value} out of range (must be < {CUSTOMXY_ROUGH_LIMIT})"
67 )));
68 }
69 let fixed = (value * f64::from(CUSTOMXY_MUL)).round() as i32;
70 if !(CUSTOMXY_MIN..=CUSTOMXY_MAX).contains(&fixed) {
71 return Err(Error::InvalidInput(format!(
72 "custom {name} coordinate {value} (fixed-point {fixed}) out of range [{CUSTOMXY_MIN}, {CUSTOMXY_MAX}]"
73 )));
74 }
75 Ok(fixed)
76}
77
78fn write_customxy_value(writer: &mut BitWriter, value: i32, name: &str) -> Result<()> {
88 let _ = name; let packed = pack_signed(value);
90
91 if packed < 524288 {
92 crate::trace::debug_eprintln!(
94 "CENC [bit {}]: {name} = {value} (packed {packed}, selector 0, 19 bits)",
95 writer.bits_written()
96 );
97 writer.write(2, 0)?;
98 writer.write(19, packed as u64)?;
99 } else if packed < 1048576 {
100 crate::trace::debug_eprintln!(
102 "CENC [bit {}]: {name} = {value} (packed {packed}, selector 1, 19 bits + offset 524288)",
103 writer.bits_written()
104 );
105 writer.write(2, 1)?;
106 writer.write(19, (packed - 524288) as u64)?;
107 } else if packed < 2097152 {
108 crate::trace::debug_eprintln!(
110 "CENC [bit {}]: {name} = {value} (packed {packed}, selector 2, 20 bits + offset 1048576)",
111 writer.bits_written()
112 );
113 writer.write(2, 2)?;
114 writer.write(20, (packed - 1048576) as u64)?;
115 } else {
116 crate::trace::debug_eprintln!(
118 "CENC [bit {}]: {name} = {value} (packed {packed}, selector 3, 21 bits + offset 2097152)",
119 writer.bits_written()
120 );
121 writer.write(2, 3)?;
122 writer.write(21, (packed - 2097152) as u64)?;
123 }
124 Ok(())
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
129#[repr(u8)]
130pub enum ColorSpace {
131 #[default]
133 Rgb = 0,
134 Gray = 1,
136 Xyb = 2,
138 Unknown = 3,
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
144#[repr(u8)]
145pub enum WhitePoint {
146 #[default]
148 D65 = 1,
149 Custom = 2,
151 E = 10,
153 Dci = 11,
155}
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
159#[repr(u8)]
160pub enum Primaries {
161 #[default]
163 Srgb = 1,
164 Custom = 2,
166 Bt2100 = 9,
168 P3 = 11,
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
174#[repr(u8)]
175pub enum TransferFunction {
176 Bt709 = 1,
178 Unknown = 2,
180 Linear = 8,
182 #[default]
184 Srgb = 13,
185 Pq = 16,
187 Dci = 17,
189 Hlg = 18,
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
195#[repr(u8)]
196pub enum RenderingIntent {
197 #[default]
199 Perceptual = 0,
200 Relative = 1,
202 Saturation = 2,
204 Absolute = 3,
206}
207
208#[derive(Debug, Clone, Default)]
210pub struct ColorEncoding {
211 pub color_space: ColorSpace,
213 pub white_point: WhitePoint,
215 pub custom_white_point: Option<CIExy>,
217 pub primaries: Primaries,
219 pub custom_primaries: Option<CustomPrimaries>,
221 pub transfer_function: TransferFunction,
223 pub rendering_intent: RenderingIntent,
225 pub want_icc: bool,
227 pub gamma: Option<f32>,
230}
231
232impl ColorEncoding {
233 pub fn srgb() -> Self {
235 Self {
236 color_space: ColorSpace::Rgb,
237 white_point: WhitePoint::D65,
238 custom_white_point: None,
239 primaries: Primaries::Srgb,
240 custom_primaries: None,
241 transfer_function: TransferFunction::Srgb,
242 rendering_intent: RenderingIntent::Perceptual,
243 want_icc: false,
244 gamma: None,
245 }
246 }
247
248 pub fn linear_srgb() -> Self {
250 Self {
251 color_space: ColorSpace::Rgb,
252 white_point: WhitePoint::D65,
253 custom_white_point: None,
254 primaries: Primaries::Srgb,
255 custom_primaries: None,
256 transfer_function: TransferFunction::Linear,
257 rendering_intent: RenderingIntent::Perceptual,
258 want_icc: false,
259 gamma: None,
260 }
261 }
262
263 pub fn gray() -> Self {
265 Self {
266 color_space: ColorSpace::Gray,
267 white_point: WhitePoint::D65,
268 custom_white_point: None,
269 primaries: Primaries::Srgb,
270 custom_primaries: None,
271 transfer_function: TransferFunction::Srgb,
272 rendering_intent: RenderingIntent::Perceptual,
273 want_icc: false,
274 gamma: None,
275 }
276 }
277
278 pub fn display_p3() -> Self {
280 Self {
281 color_space: ColorSpace::Rgb,
282 white_point: WhitePoint::D65,
283 custom_white_point: None,
284 primaries: Primaries::P3,
285 custom_primaries: None,
286 transfer_function: TransferFunction::Srgb,
287 rendering_intent: RenderingIntent::Perceptual,
288 want_icc: false,
289 gamma: None,
290 }
291 }
292
293 pub fn with_gamma(gamma: f32) -> Self {
298 Self {
299 gamma: Some(gamma),
300 ..Self::srgb()
301 }
302 }
303
304 pub fn gray_with_gamma(gamma: f32) -> Self {
306 Self {
307 gamma: Some(gamma),
308 ..Self::gray()
309 }
310 }
311
312 pub fn bt2100_pq() -> Self {
314 Self {
315 color_space: ColorSpace::Rgb,
316 white_point: WhitePoint::D65,
317 custom_white_point: None,
318 primaries: Primaries::Bt2100,
319 custom_primaries: None,
320 transfer_function: TransferFunction::Pq,
321 rendering_intent: RenderingIntent::Perceptual,
322 want_icc: false,
323 gamma: None,
324 }
325 }
326
327 pub fn grayscale() -> Self {
329 Self {
330 color_space: ColorSpace::Gray,
331 white_point: WhitePoint::D65,
332 custom_white_point: None,
333 primaries: Primaries::Srgb,
334 custom_primaries: None,
335 transfer_function: TransferFunction::Srgb,
336 rendering_intent: RenderingIntent::Perceptual,
337 want_icc: false,
338 gamma: None,
339 }
340 }
341
342 pub fn with_custom_white_point(white_point: CIExy) -> Self {
347 Self {
348 white_point: WhitePoint::Custom,
349 custom_white_point: Some(white_point),
350 ..Self::srgb()
351 }
352 }
353
354 pub fn with_custom_primaries(primaries: CustomPrimaries) -> Self {
358 Self {
359 primaries: Primaries::Custom,
360 custom_primaries: Some(primaries),
361 ..Self::srgb()
362 }
363 }
364
365 pub fn with_custom_white_point_and_primaries(
367 white_point: CIExy,
368 primaries: CustomPrimaries,
369 ) -> Self {
370 Self {
371 white_point: WhitePoint::Custom,
372 custom_white_point: Some(white_point),
373 primaries: Primaries::Custom,
374 custom_primaries: Some(primaries),
375 ..Self::srgb()
376 }
377 }
378
379 pub fn is_srgb(&self) -> bool {
385 self.color_space == ColorSpace::Rgb
386 && self.white_point == WhitePoint::D65
387 && self.custom_white_point.is_none()
388 && self.primaries == Primaries::Srgb
389 && self.custom_primaries.is_none()
390 && self.transfer_function == TransferFunction::Srgb
391 && self.rendering_intent == RenderingIntent::Perceptual
392 && !self.want_icc
393 && self.gamma.is_none()
394 }
395
396 pub fn is_gray(&self) -> bool {
398 self.color_space == ColorSpace::Gray
399 }
400
401 pub fn write(&self, writer: &mut BitWriter) -> Result<()> {
403 let all_default = self.is_srgb();
405 crate::trace::debug_eprintln!(
406 "CENC [bit {}]: all_default = {}",
407 writer.bits_written(),
408 all_default
409 );
410 writer.write_bit(all_default)?;
411
412 if all_default {
413 return Ok(());
414 }
415
416 crate::trace::debug_eprintln!(
418 "CENC [bit {}]: want_icc = {}",
419 writer.bits_written(),
420 self.want_icc
421 );
422 writer.write_bit(self.want_icc)?;
423
424 crate::trace::debug_eprintln!(
426 "CENC [bit {}]: color_space = {:?} ({})",
427 writer.bits_written(),
428 self.color_space,
429 self.color_space as u8
430 );
431 writer.write(2, self.color_space as u64)?;
432
433 if self.want_icc {
434 return Ok(());
436 }
437
438 let wp = match self.white_point {
440 WhitePoint::D65 => 1,
441 WhitePoint::Custom => 2,
442 WhitePoint::E => 10,
443 WhitePoint::Dci => 11,
444 };
445 crate::trace::debug_eprintln!(
446 "CENC [bit {}]: white_point = {:?} ({})",
447 writer.bits_written(),
448 self.white_point,
449 wp
450 );
451 writer.write_enum_default(wp)?;
452 if self.white_point == WhitePoint::Custom {
453 let wp_xy = self.custom_white_point.ok_or_else(|| {
454 Error::InvalidInput(
455 "custom_white_point must be set when white_point is Custom".into(),
456 )
457 })?;
458 let wx = xy_to_fixed(wp_xy.x, "white_point.x")?;
459 let wy = xy_to_fixed(wp_xy.y, "white_point.y")?;
460 write_customxy_value(writer, wx, "white_point.x")?;
461 write_customxy_value(writer, wy, "white_point.y")?;
462 }
463
464 if self.color_space == ColorSpace::Rgb {
466 let prim = match self.primaries {
467 Primaries::Srgb => 1,
468 Primaries::Custom => 2,
469 Primaries::Bt2100 => 9,
470 Primaries::P3 => 11,
471 };
472 crate::trace::debug_eprintln!(
473 "CENC [bit {}]: primaries = {:?} ({})",
474 writer.bits_written(),
475 self.primaries,
476 prim
477 );
478 writer.write_enum_default(prim)?;
479 if self.primaries == Primaries::Custom {
480 let cp = self.custom_primaries.ok_or_else(|| {
481 Error::InvalidInput(
482 "custom_primaries must be set when primaries is Custom".into(),
483 )
484 })?;
485 let rx = xy_to_fixed(cp.red.x, "red.x")?;
487 let ry = xy_to_fixed(cp.red.y, "red.y")?;
488 write_customxy_value(writer, rx, "red.x")?;
489 write_customxy_value(writer, ry, "red.y")?;
490 let gx = xy_to_fixed(cp.green.x, "green.x")?;
492 let gy = xy_to_fixed(cp.green.y, "green.y")?;
493 write_customxy_value(writer, gx, "green.x")?;
494 write_customxy_value(writer, gy, "green.y")?;
495 let bx = xy_to_fixed(cp.blue.x, "blue.x")?;
497 let by = xy_to_fixed(cp.blue.y, "blue.y")?;
498 write_customxy_value(writer, bx, "blue.x")?;
499 write_customxy_value(writer, by, "blue.y")?;
500 }
501 } else {
502 crate::trace::debug_eprintln!(
503 "CENC [bit {}]: primaries skipped (not RGB)",
504 writer.bits_written()
505 );
506 }
507
508 let have_gamma = self.gamma.is_some();
510 crate::trace::debug_eprintln!(
511 "CENC [bit {}]: have_gamma = {}",
512 writer.bits_written(),
513 have_gamma
514 );
515 writer.write_bit(have_gamma)?;
516
517 if have_gamma {
518 let g = self.gamma.expect("gamma must be set when have_gamma=true");
519 let encoded = (g * 10_000_000.0).round() as u32;
521 crate::trace::debug_eprintln!(
522 "CENC [bit {}]: gamma = {} (encoded {})",
523 writer.bits_written(),
524 g,
525 encoded
526 );
527 writer.write(24, encoded as u64)?;
528 } else {
529 let tf = match self.transfer_function {
531 TransferFunction::Bt709 => 1,
532 TransferFunction::Unknown => 2,
533 TransferFunction::Linear => 8,
534 TransferFunction::Srgb => 13,
535 TransferFunction::Pq => 16,
536 TransferFunction::Dci => 17,
537 TransferFunction::Hlg => 18,
538 };
539 crate::trace::debug_eprintln!(
540 "CENC [bit {}]: transfer_function = {:?} ({})",
541 writer.bits_written(),
542 self.transfer_function,
543 tf
544 );
545 writer.write_enum_default(tf)?;
546 }
547
548 crate::trace::debug_eprintln!(
550 "CENC [bit {}]: rendering_intent = {:?} ({})",
551 writer.bits_written(),
552 self.rendering_intent,
553 self.rendering_intent as u8
554 );
555 writer.write(2, self.rendering_intent as u64)?;
556 crate::trace::debug_eprintln!("CENC [bit {}]: color_encoding done", writer.bits_written());
557
558 Ok(())
559 }
560}
561
562#[cfg(test)]
563mod tests {
564 use super::*;
565
566 #[test]
567 fn test_srgb_is_default() {
568 let enc = ColorEncoding::srgb();
569 assert!(enc.is_srgb());
572 }
573
574 #[test]
575 fn test_write_srgb() {
576 let enc = ColorEncoding::srgb();
577 let mut writer = BitWriter::new();
578 enc.write(&mut writer).unwrap();
579 writer.zero_pad_to_byte();
580
581 assert_eq!(writer.bits_written(), 8);
584 }
585
586 #[test]
587 fn test_write_non_default_srgb() {
588 let enc = ColorEncoding {
590 rendering_intent: RenderingIntent::Relative, ..ColorEncoding::srgb()
592 };
593 let mut writer = BitWriter::new();
594 enc.write(&mut writer).unwrap();
595 writer.zero_pad_to_byte();
596
597 assert_eq!(writer.bits_written(), 24);
604 }
605
606 #[test]
607 fn test_color_space_values() {
608 assert_eq!(ColorSpace::Rgb as u8, 0);
609 assert_eq!(ColorSpace::Gray as u8, 1);
610 assert_eq!(ColorSpace::Xyb as u8, 2);
611 assert_eq!(ColorSpace::Unknown as u8, 3);
612 }
613
614 #[test]
615 fn test_white_point_values() {
616 assert_eq!(WhitePoint::D65 as u8, 1);
617 assert_eq!(WhitePoint::Custom as u8, 2);
618 assert_eq!(WhitePoint::E as u8, 10);
619 assert_eq!(WhitePoint::Dci as u8, 11);
620 }
621
622 #[test]
623 fn test_primaries_values() {
624 assert_eq!(Primaries::Srgb as u8, 1);
625 assert_eq!(Primaries::Custom as u8, 2);
626 assert_eq!(Primaries::Bt2100 as u8, 9);
627 assert_eq!(Primaries::P3 as u8, 11);
628 }
629
630 #[test]
631 fn test_transfer_function_values() {
632 assert_eq!(TransferFunction::Bt709 as u8, 1);
633 assert_eq!(TransferFunction::Unknown as u8, 2);
634 assert_eq!(TransferFunction::Linear as u8, 8);
635 assert_eq!(TransferFunction::Srgb as u8, 13);
636 assert_eq!(TransferFunction::Pq as u8, 16);
637 assert_eq!(TransferFunction::Dci as u8, 17);
638 assert_eq!(TransferFunction::Hlg as u8, 18);
639 }
640
641 #[test]
642 fn test_rendering_intent_values() {
643 assert_eq!(RenderingIntent::Perceptual as u8, 0);
644 assert_eq!(RenderingIntent::Relative as u8, 1);
645 assert_eq!(RenderingIntent::Saturation as u8, 2);
646 assert_eq!(RenderingIntent::Absolute as u8, 3);
647 }
648
649 #[test]
650 fn test_write_linear_srgb() {
651 let enc = ColorEncoding::linear_srgb();
652 assert_eq!(enc.transfer_function, TransferFunction::Linear);
653
654 let mut writer = BitWriter::new();
655 enc.write(&mut writer).unwrap();
656 assert!(writer.bits_written() > 0);
657 }
658
659 #[test]
660 fn test_write_grayscale() {
661 let enc = ColorEncoding::grayscale();
662 assert!(enc.is_gray());
663 assert_eq!(enc.color_space, ColorSpace::Gray);
664
665 let mut writer = BitWriter::new();
666 enc.write(&mut writer).unwrap();
667 assert!(writer.bits_written() > 0);
669 }
670
671 #[test]
672 fn test_write_gray() {
673 let enc = ColorEncoding::gray();
674 assert!(enc.is_gray());
675
676 let mut writer = BitWriter::new();
677 enc.write(&mut writer).unwrap();
678 assert!(writer.bits_written() > 0);
679 }
680
681 #[test]
682 fn test_write_display_p3() {
683 let enc = ColorEncoding::display_p3();
684 assert_eq!(enc.primaries, Primaries::P3);
685
686 let mut writer = BitWriter::new();
687 enc.write(&mut writer).unwrap();
688 assert!(writer.bits_written() > 0);
689 }
690
691 #[test]
692 fn test_write_bt2100_pq() {
693 let enc = ColorEncoding::bt2100_pq();
694 assert_eq!(enc.primaries, Primaries::Bt2100);
695 assert_eq!(enc.transfer_function, TransferFunction::Pq);
696
697 let mut writer = BitWriter::new();
698 enc.write(&mut writer).unwrap();
699 assert!(writer.bits_written() > 0);
700 }
701
702 #[test]
703 fn test_write_with_want_icc() {
704 let mut enc = ColorEncoding::srgb();
705 enc.want_icc = true;
706
707 let mut writer = BitWriter::new();
708 enc.write(&mut writer).unwrap();
709 assert_eq!(writer.bits_written(), 4);
711 }
712
713 #[test]
714 fn test_write_bt709_transfer() {
715 let mut enc = ColorEncoding::srgb();
716 enc.transfer_function = TransferFunction::Bt709;
717
718 let mut writer = BitWriter::new();
719 enc.write(&mut writer).unwrap();
720 assert!(writer.bits_written() > 0);
721 }
722
723 #[test]
724 fn test_write_dci_transfer() {
725 let mut enc = ColorEncoding::srgb();
726 enc.transfer_function = TransferFunction::Dci;
727
728 let mut writer = BitWriter::new();
729 enc.write(&mut writer).unwrap();
730 assert!(writer.bits_written() > 0);
731 }
732
733 #[test]
734 fn test_write_hlg_transfer() {
735 let mut enc = ColorEncoding::srgb();
736 enc.transfer_function = TransferFunction::Hlg;
737
738 let mut writer = BitWriter::new();
739 enc.write(&mut writer).unwrap();
740 assert!(writer.bits_written() > 0);
741 }
742
743 #[test]
744 fn test_write_e_white_point() {
745 let mut enc = ColorEncoding::srgb();
746 enc.white_point = WhitePoint::E;
747
748 let mut writer = BitWriter::new();
749 enc.write(&mut writer).unwrap();
750 assert!(writer.bits_written() > 0);
751 }
752
753 #[test]
754 fn test_write_dci_white_point() {
755 let mut enc = ColorEncoding::srgb();
756 enc.white_point = WhitePoint::Dci;
757
758 let mut writer = BitWriter::new();
759 enc.write(&mut writer).unwrap();
760 assert!(writer.bits_written() > 0);
761 }
762
763 #[test]
764 fn test_rendering_intent_saturation() {
765 let mut enc = ColorEncoding::srgb();
766 enc.rendering_intent = RenderingIntent::Saturation;
767
768 let mut writer = BitWriter::new();
769 enc.write(&mut writer).unwrap();
770 assert!(writer.bits_written() > 0);
771 }
772
773 #[test]
774 fn test_rendering_intent_absolute() {
775 let mut enc = ColorEncoding::srgb();
776 enc.rendering_intent = RenderingIntent::Absolute;
777
778 let mut writer = BitWriter::new();
779 enc.write(&mut writer).unwrap();
780 assert!(writer.bits_written() > 0);
781 }
782
783 #[test]
784 fn test_xyb_color_space() {
785 let mut enc = ColorEncoding::srgb();
786 enc.color_space = ColorSpace::Xyb;
787
788 let mut writer = BitWriter::new();
789 enc.write(&mut writer).unwrap();
790 assert!(writer.bits_written() > 0);
792 }
793
794 #[test]
795 fn test_unknown_color_space() {
796 let mut enc = ColorEncoding::srgb();
797 enc.color_space = ColorSpace::Unknown;
798
799 let mut writer = BitWriter::new();
800 enc.write(&mut writer).unwrap();
801 assert!(writer.bits_written() > 0);
803 }
804
805 #[test]
806 fn test_default_encoding() {
807 let enc = ColorEncoding::default();
808 assert_eq!(enc.color_space, ColorSpace::Rgb);
809 assert_eq!(enc.white_point, WhitePoint::D65);
810 assert_eq!(enc.primaries, Primaries::Srgb);
811 assert_eq!(enc.transfer_function, TransferFunction::Srgb);
812 assert_eq!(enc.rendering_intent, RenderingIntent::Perceptual);
813 assert!(!enc.want_icc);
814 assert!(enc.gamma.is_none());
815 }
816
817 #[test]
818 fn test_gamma_encoding() {
819 let enc = ColorEncoding::with_gamma(0.45455);
821 assert!(!enc.is_srgb()); assert_eq!(enc.gamma, Some(0.45455));
823
824 let mut writer = BitWriter::new();
825 enc.write(&mut writer).unwrap();
826 writer.zero_pad_to_byte();
827
828 let encoded = (0.45455_f32 * 10_000_000.0).round() as u32;
830 assert_eq!(encoded, 4_545_500);
831
832 assert_eq!(writer.bits_written(), 40); }
837
838 #[test]
839 fn test_gray_with_gamma() {
840 let enc = ColorEncoding::gray_with_gamma(0.45455);
841 assert!(enc.is_gray());
842 assert_eq!(enc.gamma, Some(0.45455));
843 assert!(!enc.is_srgb());
844
845 let mut writer = BitWriter::new();
846 enc.write(&mut writer).unwrap();
847 assert!(writer.bits_written() > 0);
849 }
850
851 #[test]
854 fn test_pack_signed_zero() {
855 assert_eq!(pack_signed(0), 0);
856 }
857
858 #[test]
859 fn test_pack_signed_positive() {
860 assert_eq!(pack_signed(1), 2);
861 assert_eq!(pack_signed(2), 4);
862 assert_eq!(pack_signed(100), 200);
863 }
864
865 #[test]
866 fn test_pack_signed_negative() {
867 assert_eq!(pack_signed(-1), 1);
868 assert_eq!(pack_signed(-2), 3);
869 assert_eq!(pack_signed(-100), 199);
870 }
871
872 #[test]
873 fn test_pack_signed_roundtrip() {
874 for v in [-1000000, -1, 0, 1, 1000000, CUSTOMXY_MIN, CUSTOMXY_MAX] {
876 let packed = pack_signed(v);
877 let unpacked = (packed >> 1) as i32 ^ (((!packed) & 1).wrapping_sub(1)) as i32;
879 assert_eq!(unpacked, v, "pack_signed roundtrip failed for {v}");
880 }
881 }
882
883 #[test]
886 fn test_xy_to_fixed_d65() {
887 let x = xy_to_fixed(0.3127, "x").unwrap();
889 let y = xy_to_fixed(0.3290, "y").unwrap();
890 assert_eq!(x, 312700);
891 assert_eq!(y, 329000);
892 }
893
894 #[test]
895 fn test_xy_to_fixed_out_of_range() {
896 assert!(xy_to_fixed(4.0, "x").is_err());
898 assert!(xy_to_fixed(-4.0, "x").is_err());
899 assert!(xy_to_fixed(3.9, "x").is_err());
901 }
902
903 #[test]
904 fn test_xy_to_fixed_negative() {
905 let v = xy_to_fixed(-0.5, "x").unwrap();
906 assert_eq!(v, -500000);
907 }
908
909 #[test]
912 fn test_write_custom_white_point_d50() {
913 let enc = ColorEncoding::with_custom_white_point(CIExy::new(0.3457, 0.3585));
915 assert_eq!(enc.white_point, WhitePoint::Custom);
916 assert!(enc.custom_white_point.is_some());
917 assert!(!enc.is_srgb());
918
919 let mut writer = BitWriter::new();
920 enc.write(&mut writer).unwrap();
921 assert!(writer.bits_written() > 0);
923 }
926
927 #[test]
928 fn test_write_custom_white_point_missing_coordinates() {
929 let enc = ColorEncoding {
931 white_point: WhitePoint::Custom,
932 custom_white_point: None,
933 ..ColorEncoding::srgb()
934 };
935
936 let mut writer = BitWriter::new();
937 let result = enc.write(&mut writer);
938 assert!(result.is_err());
939 let err = result.unwrap_err();
940 assert!(
941 err.to_string().contains("custom_white_point must be set"),
942 "unexpected error: {err}"
943 );
944 }
945
946 #[test]
949 fn test_write_custom_primaries() {
950 let primaries = CustomPrimaries {
952 red: CIExy::new(0.6400, 0.3300),
953 green: CIExy::new(0.2100, 0.7100),
954 blue: CIExy::new(0.1500, 0.0600),
955 };
956 let enc = ColorEncoding::with_custom_primaries(primaries);
957 assert_eq!(enc.primaries, Primaries::Custom);
958 assert!(enc.custom_primaries.is_some());
959 assert!(!enc.is_srgb());
960
961 let mut writer = BitWriter::new();
962 enc.write(&mut writer).unwrap();
963 assert!(writer.bits_written() > 0);
965 }
966
967 #[test]
968 fn test_write_custom_primaries_missing_coordinates() {
969 let enc = ColorEncoding {
970 primaries: Primaries::Custom,
971 custom_primaries: None,
972 ..ColorEncoding::srgb()
973 };
974
975 let mut writer = BitWriter::new();
976 let result = enc.write(&mut writer);
977 assert!(result.is_err());
978 let err = result.unwrap_err();
979 assert!(
980 err.to_string().contains("custom_primaries must be set"),
981 "unexpected error: {err}"
982 );
983 }
984
985 #[test]
988 fn test_write_custom_white_point_and_primaries() {
989 let enc = ColorEncoding::with_custom_white_point_and_primaries(
990 CIExy::new(0.3457, 0.3585), CustomPrimaries {
992 red: CIExy::new(0.7347, 0.2653),
993 green: CIExy::new(0.1596, 0.8404),
994 blue: CIExy::new(0.0366, 0.0001),
995 },
996 );
997 assert_eq!(enc.white_point, WhitePoint::Custom);
998 assert_eq!(enc.primaries, Primaries::Custom);
999
1000 let mut writer = BitWriter::new();
1001 enc.write(&mut writer).unwrap();
1002 assert!(writer.bits_written() > 0);
1004 }
1005
1006 #[test]
1009 fn test_customxy_encoding_small_positive() {
1010 let mut writer = BitWriter::new();
1014 write_customxy_value(&mut writer, 312700, "test").unwrap();
1015 let packed = pack_signed(312700);
1016 assert_eq!(packed, 625400);
1017 assert_eq!(writer.bits_written(), 21);
1019 }
1020
1021 #[test]
1022 fn test_customxy_encoding_zero() {
1023 let mut writer = BitWriter::new();
1025 write_customxy_value(&mut writer, 0, "test").unwrap();
1026 assert_eq!(pack_signed(0), 0);
1027 assert_eq!(writer.bits_written(), 21);
1029 }
1030
1031 #[test]
1032 fn test_customxy_encoding_negative() {
1033 let mut writer = BitWriter::new();
1035 write_customxy_value(&mut writer, -1, "test").unwrap();
1036 assert_eq!(pack_signed(-1), 1);
1037 assert_eq!(writer.bits_written(), 21); }
1039
1040 #[test]
1041 fn test_customxy_encoding_all_selectors() {
1042 let mut w = BitWriter::new();
1046 write_customxy_value(&mut w, 262143, "test").unwrap();
1048 assert_eq!(w.bits_written(), 21); let mut w = BitWriter::new();
1052 write_customxy_value(&mut w, 262144, "test").unwrap();
1054 assert_eq!(w.bits_written(), 21); let mut w = BitWriter::new();
1058 write_customxy_value(&mut w, 524288, "test").unwrap();
1060 assert_eq!(w.bits_written(), 22); let mut w = BitWriter::new();
1064 write_customxy_value(&mut w, 1048576, "test").unwrap();
1066 assert_eq!(w.bits_written(), 23); }
1068
1069 #[test]
1070 fn test_write_custom_wp_bit_count_vs_standard() {
1071 let enc_d65 = ColorEncoding {
1073 rendering_intent: RenderingIntent::Relative,
1074 ..ColorEncoding::srgb()
1075 };
1076 let enc_custom = ColorEncoding {
1077 white_point: WhitePoint::Custom,
1078 custom_white_point: Some(CIExy::new(0.3127, 0.3290)),
1079 rendering_intent: RenderingIntent::Relative,
1080 ..ColorEncoding::srgb()
1081 };
1082
1083 let mut w_d65 = BitWriter::new();
1084 enc_d65.write(&mut w_d65).unwrap();
1085 let bits_d65 = w_d65.bits_written();
1086
1087 let mut w_custom = BitWriter::new();
1088 enc_custom.write(&mut w_custom).unwrap();
1089 let bits_custom = w_custom.bits_written();
1090
1091 assert!(
1092 bits_custom > bits_d65,
1093 "custom WP should use more bits: {bits_custom} vs {bits_d65}"
1094 );
1095 }
1096
1097 #[test]
1098 fn test_default_encoding_custom_fields() {
1099 let enc = ColorEncoding::default();
1100 assert!(enc.custom_white_point.is_none());
1101 assert!(enc.custom_primaries.is_none());
1102 }
1103
1104 #[test]
1107 fn test_write_grayscale_custom_white_point() {
1108 let enc = ColorEncoding {
1109 color_space: ColorSpace::Gray,
1110 white_point: WhitePoint::Custom,
1111 custom_white_point: Some(CIExy::new(0.3457, 0.3585)),
1112 ..ColorEncoding::gray()
1113 };
1114
1115 let mut writer = BitWriter::new();
1116 enc.write(&mut writer).unwrap();
1117 assert!(writer.bits_written() > 0);
1119 }
1120
1121 #[test]
1124 fn test_roundtrip_custom_white_point_d50() {
1125 let width = 16u32;
1127 let height = 16u32;
1128 let pixels: Vec<u8> = (0..width * height * 3).map(|i| (i % 256) as u8).collect();
1129
1130 let ce = ColorEncoding::with_custom_white_point(CIExy::new(0.3457, 0.3585));
1131
1132 let encoded = crate::LosslessConfig::new()
1133 .encode_request(width, height, crate::PixelLayout::Rgb8)
1134 .with_color_encoding(ce)
1135 .encode(&pixels)
1136 .expect("encoding with custom white point should succeed");
1137
1138 let decoded = crate::test_helpers::decode_with_jxl_rs(&encoded)
1140 .expect("jxl-rs should decode custom white point");
1141 assert_eq!(decoded.width, width as usize);
1142 assert_eq!(decoded.height, height as usize);
1143 }
1144
1145 #[test]
1146 fn test_roundtrip_custom_primaries() {
1147 let width = 16u32;
1149 let height = 16u32;
1150 let pixels: Vec<u8> = (0..width * height * 3)
1151 .map(|i| ((i * 7) % 256) as u8)
1152 .collect();
1153
1154 let ce = ColorEncoding::with_custom_primaries(CustomPrimaries {
1155 red: CIExy::new(0.6400, 0.3300),
1156 green: CIExy::new(0.2100, 0.7100),
1157 blue: CIExy::new(0.1500, 0.0600),
1158 });
1159
1160 let encoded = crate::LosslessConfig::new()
1161 .encode_request(width, height, crate::PixelLayout::Rgb8)
1162 .with_color_encoding(ce)
1163 .encode(&pixels)
1164 .expect("encoding with custom primaries should succeed");
1165
1166 let decoded = crate::test_helpers::decode_with_jxl_rs(&encoded)
1167 .expect("jxl-rs should decode custom primaries");
1168 assert_eq!(decoded.width, width as usize);
1169 assert_eq!(decoded.height, height as usize);
1170 }
1171
1172 #[test]
1173 fn test_roundtrip_custom_white_point_and_primaries() {
1174 let width = 16u32;
1176 let height = 16u32;
1177 let pixels: Vec<u8> = (0..width * height * 3)
1178 .map(|i| ((i * 13) % 256) as u8)
1179 .collect();
1180
1181 let ce = ColorEncoding::with_custom_white_point_and_primaries(
1182 CIExy::new(0.3457, 0.3585), CustomPrimaries {
1184 red: CIExy::new(0.7347, 0.2653),
1185 green: CIExy::new(0.1596, 0.8404),
1186 blue: CIExy::new(0.0366, 0.0001),
1187 },
1188 );
1189
1190 let encoded = crate::LosslessConfig::new()
1191 .encode_request(width, height, crate::PixelLayout::Rgb8)
1192 .with_color_encoding(ce)
1193 .encode(&pixels)
1194 .expect("encoding with custom WP + primaries should succeed");
1195
1196 let decoded = crate::test_helpers::decode_with_jxl_rs(&encoded)
1197 .expect("jxl-rs should decode custom WP + primaries");
1198 assert_eq!(decoded.width, width as usize);
1199 assert_eq!(decoded.height, height as usize);
1200 }
1201}