1#![allow(clippy::cast_precision_loss)]
21#![allow(clippy::cast_possible_truncation)]
22
23use serde::{Deserialize, Serialize};
24use thiserror::Error;
25
26#[derive(Debug, Clone, Error)]
30pub enum HdrError {
31 #[error("Unsupported HDR conversion: {from:?} → {to:?}")]
33 UnsupportedConversion {
34 from: TransferFunction,
36 to: TransferFunction,
38 },
39
40 #[error("Missing HDR metadata field: {0}")]
42 MissingField(String),
43
44 #[error("HDR field '{field}' value {value} is out of range [{min}, {max}]")]
46 OutOfRange {
47 field: String,
49 value: f64,
51 min: f64,
53 max: f64,
55 },
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
62pub enum TransferFunction {
63 Bt709,
65 Pq,
67 Hlg,
69 Gamma22,
71 Linear,
73 Unspecified,
75}
76
77impl TransferFunction {
78 #[must_use]
80 pub fn h273_code(self) -> u8 {
81 match self {
82 Self::Bt709 => 1,
83 Self::Gamma22 => 4,
84 Self::Linear => 8,
85 Self::Pq => 16,
86 Self::Hlg => 18,
87 Self::Unspecified => 2,
88 }
89 }
90
91 #[must_use]
93 pub fn is_hdr(self) -> bool {
94 matches!(self, Self::Pq | Self::Hlg)
95 }
96
97 #[must_use]
99 pub fn is_wide_gamut(self) -> bool {
100 matches!(self, Self::Pq | Self::Hlg)
101 }
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
108pub enum ColourPrimaries {
109 Bt709,
111 Bt2020,
113 DisplayP3,
115 Unspecified,
117}
118
119impl ColourPrimaries {
120 #[must_use]
122 pub fn h273_code(self) -> u8 {
123 match self {
124 Self::Bt709 => 1,
125 Self::Unspecified => 2,
126 Self::DisplayP3 => 12,
127 Self::Bt2020 => 9,
128 }
129 }
130}
131
132#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
139pub struct MasteringDisplay {
140 pub red_x: f64,
142 pub red_y: f64,
144 pub green_x: f64,
146 pub green_y: f64,
148 pub blue_x: f64,
150 pub blue_y: f64,
152 pub white_x: f64,
154 pub white_y: f64,
156 pub max_luminance: f64,
158 pub min_luminance: f64,
160}
161
162impl MasteringDisplay {
163 #[must_use]
166 pub fn p3_d65_1000nit() -> Self {
167 Self {
168 red_x: 0.680,
169 red_y: 0.320,
170 green_x: 0.265,
171 green_y: 0.690,
172 blue_x: 0.150,
173 blue_y: 0.060,
174 white_x: 0.3127,
175 white_y: 0.3290,
176 max_luminance: 1000.0,
177 min_luminance: 0.0050,
178 }
179 }
180
181 #[must_use]
183 pub fn bt2020_4000nit() -> Self {
184 Self {
185 red_x: 0.708,
186 red_y: 0.292,
187 green_x: 0.170,
188 green_y: 0.797,
189 blue_x: 0.131,
190 blue_y: 0.046,
191 white_x: 0.3127,
192 white_y: 0.3290,
193 max_luminance: 4000.0,
194 min_luminance: 0.005,
195 }
196 }
197
198 pub fn validate(&self) -> Result<(), HdrError> {
204 let check_chroma = |name: &str, v: f64| {
205 if v < 0.0 || v > 1.0 {
206 Err(HdrError::OutOfRange {
207 field: name.to_string(),
208 value: v,
209 min: 0.0,
210 max: 1.0,
211 })
212 } else {
213 Ok(())
214 }
215 };
216 check_chroma("red_x", self.red_x)?;
217 check_chroma("red_y", self.red_y)?;
218 check_chroma("green_x", self.green_x)?;
219 check_chroma("green_y", self.green_y)?;
220 check_chroma("blue_x", self.blue_x)?;
221 check_chroma("blue_y", self.blue_y)?;
222 check_chroma("white_x", self.white_x)?;
223 check_chroma("white_y", self.white_y)?;
224
225 if self.max_luminance <= 0.0 {
226 return Err(HdrError::OutOfRange {
227 field: "max_luminance".to_string(),
228 value: self.max_luminance,
229 min: 0.001,
230 max: f64::MAX,
231 });
232 }
233 if self.min_luminance < 0.0 || self.min_luminance >= self.max_luminance {
234 return Err(HdrError::OutOfRange {
235 field: "min_luminance".to_string(),
236 value: self.min_luminance,
237 min: 0.0,
238 max: self.max_luminance,
239 });
240 }
241 Ok(())
242 }
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
249pub struct ContentLightLevel {
250 pub max_cll: u16,
252 pub max_fall: u16,
254}
255
256impl ContentLightLevel {
257 #[must_use]
259 pub fn new(max_cll: u16, max_fall: u16) -> Self {
260 Self { max_cll, max_fall }
261 }
262
263 #[must_use]
265 pub fn hdr10_default() -> Self {
266 Self {
267 max_cll: 1000,
268 max_fall: 400,
269 }
270 }
271
272 pub fn validate(&self) -> Result<(), HdrError> {
278 if self.max_fall > self.max_cll {
279 return Err(HdrError::OutOfRange {
280 field: "max_fall".to_string(),
281 value: f64::from(self.max_fall),
282 min: 0.0,
283 max: f64::from(self.max_cll),
284 });
285 }
286 Ok(())
287 }
288}
289
290#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
294pub enum DolbyVisionProfile {
295 Profile4,
297 Profile5,
299 Profile7,
301 Profile8,
303 Profile9,
305}
306
307impl DolbyVisionProfile {
308 #[must_use]
310 pub fn profile_number(self) -> u8 {
311 match self {
312 Self::Profile4 => 4,
313 Self::Profile5 => 5,
314 Self::Profile7 => 7,
315 Self::Profile8 => 8,
316 Self::Profile9 => 9,
317 }
318 }
319
320 #[must_use]
322 pub fn is_backward_compatible(self) -> bool {
323 matches!(self, Self::Profile4 | Self::Profile7 | Self::Profile8)
324 }
325}
326
327#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
329pub struct DolbyVisionMeta {
330 pub profile: DolbyVisionProfile,
332 pub level: u8,
334 pub has_rpu: bool,
336 pub has_el: bool,
338}
339
340impl DolbyVisionMeta {
341 #[must_use]
343 pub fn new(profile: DolbyVisionProfile, level: u8) -> Self {
344 Self {
345 profile,
346 level,
347 has_rpu: true,
348 has_el: false,
349 }
350 }
351
352 pub fn validate(&self) -> Result<(), HdrError> {
358 if self.level < 1 || self.level > 13 {
359 return Err(HdrError::OutOfRange {
360 field: "dv_level".to_string(),
361 value: f64::from(self.level),
362 min: 1.0,
363 max: 13.0,
364 });
365 }
366 Ok(())
367 }
368}
369
370#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
377pub struct HdrMetadata {
378 pub transfer_function: Option<TransferFunction>,
380 pub colour_primaries: Option<ColourPrimaries>,
382 pub mastering_display: Option<MasteringDisplay>,
384 pub content_light_level: Option<ContentLightLevel>,
386 pub dolby_vision: Option<DolbyVisionMeta>,
388}
389
390impl HdrMetadata {
391 #[must_use]
393 pub fn hdr10(mastering: MasteringDisplay, cll: ContentLightLevel) -> Self {
394 Self {
395 transfer_function: Some(TransferFunction::Pq),
396 colour_primaries: Some(ColourPrimaries::Bt2020),
397 mastering_display: Some(mastering),
398 content_light_level: Some(cll),
399 dolby_vision: None,
400 }
401 }
402
403 #[must_use]
405 pub fn hlg() -> Self {
406 Self {
407 transfer_function: Some(TransferFunction::Hlg),
408 colour_primaries: Some(ColourPrimaries::Bt2020),
409 mastering_display: None,
410 content_light_level: None,
411 dolby_vision: None,
412 }
413 }
414
415 #[must_use]
417 pub fn is_hdr(&self) -> bool {
418 self.transfer_function
419 .map(TransferFunction::is_hdr)
420 .unwrap_or(false)
421 || self.dolby_vision.is_some()
422 }
423
424 pub fn validate(&self) -> Result<(), HdrError> {
430 if let Some(md) = &self.mastering_display {
431 md.validate()?;
432 }
433 if let Some(cll) = &self.content_light_level {
434 cll.validate()?;
435 }
436 if let Some(dv) = &self.dolby_vision {
437 dv.validate()?;
438 }
439 Ok(())
440 }
441}
442
443#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
447pub enum HdrPassthroughMode {
448 Passthrough,
450 Strip,
452 Convert {
457 target_tf: TransferFunction,
459 target_primaries: ColourPrimaries,
461 },
462 Inject(HdrMetadata),
464}
465
466impl Default for HdrPassthroughMode {
467 fn default() -> Self {
468 Self::Passthrough
469 }
470}
471
472#[derive(Debug, Clone, Default)]
477pub struct HdrProcessor {
478 mode: HdrPassthroughMode,
479}
480
481impl HdrProcessor {
482 #[must_use]
484 pub fn new(mode: HdrPassthroughMode) -> Self {
485 Self { mode }
486 }
487
488 pub fn process(&self, source: Option<&HdrMetadata>) -> Result<Option<HdrMetadata>, HdrError> {
497 match &self.mode {
498 HdrPassthroughMode::Passthrough => Ok(source.cloned()),
499
500 HdrPassthroughMode::Strip => Ok(None),
501
502 HdrPassthroughMode::Inject(meta) => {
503 meta.validate()?;
504 Ok(Some(meta.clone()))
505 }
506
507 HdrPassthroughMode::Convert {
508 target_tf,
509 target_primaries,
510 } => {
511 let src_tf = source
512 .and_then(|m| m.transfer_function)
513 .unwrap_or(TransferFunction::Unspecified);
514
515 Self::check_conversion(src_tf, *target_tf)?;
517
518 let mut out = source.cloned().unwrap_or_default();
519 out.transfer_function = Some(*target_tf);
520 out.colour_primaries = Some(*target_primaries);
521
522 if !target_tf.is_hdr() {
524 out.mastering_display = None;
525 out.content_light_level = None;
526 out.dolby_vision = None;
527 }
528
529 if *target_tf == TransferFunction::Hlg {
531 out.mastering_display = None;
532 out.content_light_level = None;
533 out.dolby_vision = None;
534 }
535
536 Ok(Some(out))
537 }
538 }
539 }
540
541 fn check_conversion(from: TransferFunction, to: TransferFunction) -> Result<(), HdrError> {
543 use TransferFunction::{Bt709, Hlg, Pq, Unspecified};
544 let ok = matches!(
545 (from, to),
546 (Pq, Pq) | (Hlg, Hlg) | (Bt709, Bt709) |
548 (Pq, Bt709) | (Hlg, Bt709) |
550 (Pq, Hlg) | (Hlg, Pq) |
552 (Unspecified, _)
554 );
555 if ok {
556 Ok(())
557 } else {
558 Err(HdrError::UnsupportedConversion { from, to })
559 }
560 }
561
562 #[must_use]
564 pub fn mode(&self) -> &HdrPassthroughMode {
565 &self.mode
566 }
567}
568
569#[must_use]
581pub fn encode_mastering_display_sei(md: &MasteringDisplay) -> [u8; 24] {
582 let to_u16 = |v: f64| -> u16 { (v * 50_000.0).round() as u16 };
583 let to_u32 = |v: f64| -> u32 { (v * 10_000.0).round() as u32 };
584
585 let mut buf = [0u8; 24];
586 let pairs: [(f64, f64); 4] = [
587 (md.green_x, md.green_y),
588 (md.blue_x, md.blue_y),
589 (md.red_x, md.red_y),
590 (md.white_x, md.white_y),
591 ];
592 for (i, (x, y)) in pairs.iter().enumerate() {
593 let xv = to_u16(*x);
594 let yv = to_u16(*y);
595 buf[i * 4] = (xv >> 8) as u8;
596 buf[i * 4 + 1] = (xv & 0xFF) as u8;
597 buf[i * 4 + 2] = (yv >> 8) as u8;
598 buf[i * 4 + 3] = (yv & 0xFF) as u8;
599 }
600 let max_u32 = to_u32(md.max_luminance);
601 let min_u32 = to_u32(md.min_luminance);
602 buf[16] = (max_u32 >> 24) as u8;
603 buf[17] = (max_u32 >> 16) as u8;
604 buf[18] = (max_u32 >> 8) as u8;
605 buf[19] = (max_u32 & 0xFF) as u8;
606 buf[20] = (min_u32 >> 24) as u8;
607 buf[21] = (min_u32 >> 16) as u8;
608 buf[22] = (min_u32 >> 8) as u8;
609 buf[23] = (min_u32 & 0xFF) as u8;
610 buf
611}
612
613pub fn decode_mastering_display_sei(buf: &[u8]) -> Result<MasteringDisplay, HdrError> {
620 if buf.len() < 24 {
621 return Err(HdrError::MissingField(
622 "SEI payload too short (need 24 bytes)".to_string(),
623 ));
624 }
625 let read_u16 = |i: usize| -> f64 {
626 let v = (u16::from(buf[i]) << 8) | u16::from(buf[i + 1]);
627 f64::from(v) / 50_000.0
628 };
629 let read_u32 = |i: usize| -> f64 {
630 let v = (u32::from(buf[i]) << 24)
631 | (u32::from(buf[i + 1]) << 16)
632 | (u32::from(buf[i + 2]) << 8)
633 | u32::from(buf[i + 3]);
634 f64::from(v) / 10_000.0
635 };
636
637 let md = MasteringDisplay {
638 green_x: read_u16(0),
639 green_y: read_u16(2),
640 blue_x: read_u16(4),
641 blue_y: read_u16(6),
642 red_x: read_u16(8),
643 red_y: read_u16(10),
644 white_x: read_u16(12),
645 white_y: read_u16(14),
646 max_luminance: read_u32(16),
647 min_luminance: read_u32(20),
648 };
649 md.validate()?;
650 Ok(md)
651}
652
653#[must_use]
656pub fn encode_cll_sei(cll: &ContentLightLevel) -> [u8; 4] {
657 [
658 (cll.max_cll >> 8) as u8,
659 (cll.max_cll & 0xFF) as u8,
660 (cll.max_fall >> 8) as u8,
661 (cll.max_fall & 0xFF) as u8,
662 ]
663}
664
665pub fn decode_cll_sei(buf: &[u8]) -> Result<ContentLightLevel, HdrError> {
671 if buf.len() < 4 {
672 return Err(HdrError::MissingField(
673 "CLL SEI payload too short (need 4 bytes)".to_string(),
674 ));
675 }
676 let max_cll = (u16::from(buf[0]) << 8) | u16::from(buf[1]);
677 let max_fall = (u16::from(buf[2]) << 8) | u16::from(buf[3]);
678 let cll = ContentLightLevel { max_cll, max_fall };
679 cll.validate()?;
680 Ok(cll)
681}
682
683#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
690pub struct Hdr10PlusDynamicMeta {
691 pub application_version: u8,
693 pub targeted_system_display_max_luminance: u16,
695 pub average_maxrgb: f64,
697 pub maxrgb_percentiles: Vec<(u8, f64)>,
699 pub fraction_bright_pixels: f64,
701 pub knee_point: (f64, f64),
703 pub bezier_curve_anchors: Vec<f64>,
705}
706
707impl Hdr10PlusDynamicMeta {
708 #[must_use]
710 pub fn new(targeted_max_lum: u16) -> Self {
711 Self {
712 application_version: 1,
713 targeted_system_display_max_luminance: targeted_max_lum,
714 average_maxrgb: 0.5,
715 maxrgb_percentiles: vec![(1, 0.01), (50, 0.5), (99, 0.95)],
716 fraction_bright_pixels: 0.01,
717 knee_point: (0.5, 0.5),
718 bezier_curve_anchors: vec![0.25, 0.5, 0.75],
719 }
720 }
721
722 pub fn validate(&self) -> Result<(), HdrError> {
728 if self.application_version > 1 {
729 return Err(HdrError::OutOfRange {
730 field: "hdr10plus_application_version".to_string(),
731 value: f64::from(self.application_version),
732 min: 0.0,
733 max: 1.0,
734 });
735 }
736 if self.average_maxrgb < 0.0 || self.average_maxrgb > 1.0 {
737 return Err(HdrError::OutOfRange {
738 field: "average_maxrgb".to_string(),
739 value: self.average_maxrgb,
740 min: 0.0,
741 max: 1.0,
742 });
743 }
744 if self.fraction_bright_pixels < 0.0 || self.fraction_bright_pixels > 1.0 {
745 return Err(HdrError::OutOfRange {
746 field: "fraction_bright_pixels".to_string(),
747 value: self.fraction_bright_pixels,
748 min: 0.0,
749 max: 1.0,
750 });
751 }
752 let (kx, ky) = self.knee_point;
753 if kx < 0.0 || kx > 1.0 {
754 return Err(HdrError::OutOfRange {
755 field: "knee_point_x".to_string(),
756 value: kx,
757 min: 0.0,
758 max: 1.0,
759 });
760 }
761 if ky < 0.0 || ky > 1.0 {
762 return Err(HdrError::OutOfRange {
763 field: "knee_point_y".to_string(),
764 value: ky,
765 min: 0.0,
766 max: 1.0,
767 });
768 }
769 if self.bezier_curve_anchors.len() > 9 {
770 return Err(HdrError::OutOfRange {
771 field: "bezier_curve_anchors_count".to_string(),
772 value: self.bezier_curve_anchors.len() as f64,
773 min: 0.0,
774 max: 9.0,
775 });
776 }
777 for (i, &a) in self.bezier_curve_anchors.iter().enumerate() {
778 if a < 0.0 || a > 1.0 {
779 return Err(HdrError::OutOfRange {
780 field: format!("bezier_anchor_{i}"),
781 value: a,
782 min: 0.0,
783 max: 1.0,
784 });
785 }
786 }
787 Ok(())
788 }
789
790 #[must_use]
802 pub fn encode(&self) -> Vec<u8> {
803 let to_u16 = |v: f64| -> u16 { (v * 10_000.0).round() as u16 };
804 let mut buf = Vec::with_capacity(16 + self.bezier_curve_anchors.len() * 2);
805 buf.push(self.application_version);
806 buf.extend_from_slice(&self.targeted_system_display_max_luminance.to_be_bytes());
807 buf.extend_from_slice(&to_u16(self.average_maxrgb).to_be_bytes());
808 buf.extend_from_slice(&to_u16(self.fraction_bright_pixels).to_be_bytes());
809 buf.extend_from_slice(&to_u16(self.knee_point.0).to_be_bytes());
810 buf.extend_from_slice(&to_u16(self.knee_point.1).to_be_bytes());
811 let anchor_count = self.bezier_curve_anchors.len().min(9) as u8;
812 buf.push(anchor_count);
813 for &a in self.bezier_curve_anchors.iter().take(9) {
814 buf.extend_from_slice(&to_u16(a).to_be_bytes());
815 }
816 buf
817 }
818
819 pub fn decode(buf: &[u8]) -> Result<Self, HdrError> {
825 if buf.len() < 12 {
826 return Err(HdrError::MissingField(
827 "HDR10+ payload too short (need at least 12 bytes)".to_string(),
828 ));
829 }
830 let from_u16 = |i: usize| -> f64 {
831 let v = (u16::from(buf[i]) << 8) | u16::from(buf[i + 1]);
832 f64::from(v) / 10_000.0
833 };
834 let application_version = buf[0];
835 let targeted_max = (u16::from(buf[1]) << 8) | u16::from(buf[2]);
836 let average_maxrgb = from_u16(3);
837 let fraction_bright = from_u16(5);
838 let knee_x = from_u16(7);
839 let knee_y = from_u16(9);
840 let anchor_count = buf[11] as usize;
841 let needed = 12 + anchor_count * 2;
842 if buf.len() < needed {
843 return Err(HdrError::MissingField(format!(
844 "HDR10+ payload too short for {anchor_count} anchors (need {needed} bytes)"
845 )));
846 }
847 let mut anchors = Vec::with_capacity(anchor_count);
848 for i in 0..anchor_count {
849 let offset = 12 + i * 2;
850 let v = (u16::from(buf[offset]) << 8) | u16::from(buf[offset + 1]);
851 anchors.push(f64::from(v) / 10_000.0);
852 }
853 let meta = Self {
854 application_version,
855 targeted_system_display_max_luminance: targeted_max,
856 average_maxrgb,
857 maxrgb_percentiles: Vec::new(),
858 fraction_bright_pixels: fraction_bright,
859 knee_point: (knee_x, knee_y),
860 bezier_curve_anchors: anchors,
861 };
862 meta.validate()?;
863 Ok(meta)
864 }
865}
866
867#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
874pub struct DolbyVisionRpu {
875 pub payload: Vec<u8>,
877 pub rpu_profile: u8,
879 pub validated: bool,
881 pub frame_index: u64,
883}
884
885impl DolbyVisionRpu {
886 #[must_use]
888 pub fn new(payload: Vec<u8>, frame_index: u64) -> Self {
889 Self {
890 rpu_profile: Self::extract_profile(&payload),
891 payload,
892 validated: false,
893 frame_index,
894 }
895 }
896
897 fn extract_profile(payload: &[u8]) -> u8 {
902 if payload.len() < 2 {
906 return 0;
907 }
908 let rpu_type = payload[0] >> 2;
909 match rpu_type {
910 2 => 8, 0 => 5, 1 => 7, _ => 0,
914 }
915 }
916
917 pub fn validate(&mut self) -> Result<(), HdrError> {
923 if self.payload.is_empty() {
924 return Err(HdrError::MissingField(
925 "DV RPU payload is empty".to_string(),
926 ));
927 }
928 if self.payload.len() < 25 {
930 return Err(HdrError::MissingField(
931 "DV RPU payload too short (minimum 25 bytes)".to_string(),
932 ));
933 }
934 self.validated = true;
935 Ok(())
936 }
937
938 #[must_use]
940 pub fn size(&self) -> usize {
941 self.payload.len()
942 }
943}
944
945#[derive(Debug, Clone, Default)]
947pub struct DvRpuPassthrough {
948 rpus: Vec<DolbyVisionRpu>,
950 valid_count: usize,
952 invalid_count: usize,
954}
955
956impl DvRpuPassthrough {
957 #[must_use]
959 pub fn new() -> Self {
960 Self::default()
961 }
962
963 pub fn ingest(&mut self, payload: Vec<u8>, frame_index: u64) {
965 let mut rpu = DolbyVisionRpu::new(payload, frame_index);
966 if rpu.validate().is_ok() {
967 self.valid_count += 1;
968 } else {
969 self.invalid_count += 1;
970 }
971 self.rpus.push(rpu);
972 }
973
974 #[must_use]
976 pub fn get_rpu(&self, frame_index: u64) -> Option<&DolbyVisionRpu> {
977 self.rpus.iter().find(|r| r.frame_index == frame_index)
978 }
979
980 #[must_use]
982 pub fn count(&self) -> usize {
983 self.rpus.len()
984 }
985
986 #[must_use]
988 pub fn valid_count(&self) -> usize {
989 self.valid_count
990 }
991
992 #[must_use]
994 pub fn invalid_count(&self) -> usize {
995 self.invalid_count
996 }
997
998 pub fn drain_all(&mut self) -> Vec<DolbyVisionRpu> {
1000 self.valid_count = 0;
1001 self.invalid_count = 0;
1002 std::mem::take(&mut self.rpus)
1003 }
1004}
1005
1006#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1010pub enum TonemapCurve {
1011 Reinhard,
1013 Hable,
1015 Aces,
1017 Bt2390,
1019 Clip,
1021 Mobius,
1023}
1024
1025#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1027pub struct HdrToSdrConfig {
1028 pub curve: TonemapCurve,
1030 pub source_peak_nits: f64,
1032 pub target_peak_nits: f64,
1034 pub desat_strength: f64,
1036}
1037
1038impl HdrToSdrConfig {
1039 #[must_use]
1041 pub fn default_1000nit() -> Self {
1042 Self {
1043 curve: TonemapCurve::Bt2390,
1044 source_peak_nits: 1000.0,
1045 target_peak_nits: 100.0,
1046 desat_strength: 0.5,
1047 }
1048 }
1049
1050 pub fn validate(&self) -> Result<(), HdrError> {
1056 if self.source_peak_nits <= 0.0 {
1057 return Err(HdrError::OutOfRange {
1058 field: "source_peak_nits".to_string(),
1059 value: self.source_peak_nits,
1060 min: 0.001,
1061 max: f64::MAX,
1062 });
1063 }
1064 if self.target_peak_nits <= 0.0 {
1065 return Err(HdrError::OutOfRange {
1066 field: "target_peak_nits".to_string(),
1067 value: self.target_peak_nits,
1068 min: 0.001,
1069 max: f64::MAX,
1070 });
1071 }
1072 if self.desat_strength < 0.0 || self.desat_strength > 1.0 {
1073 return Err(HdrError::OutOfRange {
1074 field: "desat_strength".to_string(),
1075 value: self.desat_strength,
1076 min: 0.0,
1077 max: 1.0,
1078 });
1079 }
1080 Ok(())
1081 }
1082
1083 #[must_use]
1085 pub fn tonemap_reinhard(&self, l: f64) -> f64 {
1086 let normalised = l * self.source_peak_nits / self.target_peak_nits;
1087 let mapped = normalised / (1.0 + normalised);
1088 mapped * self.target_peak_nits
1089 }
1090
1091 #[must_use]
1093 pub fn tonemap_hable(&self, l: f64) -> f64 {
1094 let hable = |x: f64| -> f64 {
1095 let a = 0.15;
1096 let b = 0.50;
1097 let c = 0.10;
1098 let d = 0.20;
1099 let e = 0.02;
1100 let f = 0.30;
1101 ((x * (a * x + c * b) + d * e) / (x * (a * x + b) + d * f)) - e / f
1102 };
1103 let normalised = l * self.source_peak_nits / self.target_peak_nits;
1104 let white = self.source_peak_nits / self.target_peak_nits;
1105 (hable(normalised) / hable(white)) * self.target_peak_nits
1106 }
1107
1108 #[must_use]
1110 pub fn tonemap_aces(&self, l: f64) -> f64 {
1111 let normalised = l * self.source_peak_nits / self.target_peak_nits;
1112 let a = 2.51;
1113 let b = 0.03;
1114 let c = 2.43;
1115 let d = 0.59;
1116 let e = 0.14;
1117 let mapped = (normalised * (a * normalised + b)) / (normalised * (c * normalised + d) + e);
1118 mapped.clamp(0.0, 1.0) * self.target_peak_nits
1119 }
1120
1121 #[must_use]
1123 pub fn apply(&self, l: f64) -> f64 {
1124 match self.curve {
1125 TonemapCurve::Reinhard => self.tonemap_reinhard(l),
1126 TonemapCurve::Hable => self.tonemap_hable(l),
1127 TonemapCurve::Aces => self.tonemap_aces(l),
1128 TonemapCurve::Bt2390 | TonemapCurve::Mobius => {
1129 self.tonemap_reinhard(l)
1131 }
1132 TonemapCurve::Clip => (l * self.source_peak_nits).min(self.target_peak_nits),
1133 }
1134 }
1135}
1136
1137#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1139pub struct SdrToHdrConfig {
1140 pub target_peak_nits: f64,
1142 pub source_peak_nits: f64,
1144 pub highlight_gain: f64,
1146 pub midtone_boost: f64,
1148}
1149
1150impl SdrToHdrConfig {
1151 #[must_use]
1153 pub fn default_1000nit() -> Self {
1154 Self {
1155 target_peak_nits: 1000.0,
1156 source_peak_nits: 100.0,
1157 highlight_gain: 2.5,
1158 midtone_boost: 1.1,
1159 }
1160 }
1161
1162 pub fn validate(&self) -> Result<(), HdrError> {
1168 if self.target_peak_nits <= 0.0 {
1169 return Err(HdrError::OutOfRange {
1170 field: "target_peak_nits".to_string(),
1171 value: self.target_peak_nits,
1172 min: 0.001,
1173 max: f64::MAX,
1174 });
1175 }
1176 if self.source_peak_nits <= 0.0 {
1177 return Err(HdrError::OutOfRange {
1178 field: "source_peak_nits".to_string(),
1179 value: self.source_peak_nits,
1180 min: 0.001,
1181 max: f64::MAX,
1182 });
1183 }
1184 if self.highlight_gain < 1.0 {
1185 return Err(HdrError::OutOfRange {
1186 field: "highlight_gain".to_string(),
1187 value: self.highlight_gain,
1188 min: 1.0,
1189 max: f64::MAX,
1190 });
1191 }
1192 if self.midtone_boost < 0.5 || self.midtone_boost > 3.0 {
1193 return Err(HdrError::OutOfRange {
1194 field: "midtone_boost".to_string(),
1195 value: self.midtone_boost,
1196 min: 0.5,
1197 max: 3.0,
1198 });
1199 }
1200 Ok(())
1201 }
1202
1203 #[must_use]
1207 pub fn apply(&self, l_sdr: f64) -> f64 {
1208 if l_sdr <= 0.0 {
1209 return 0.0;
1210 }
1211 let normalised = (l_sdr / self.source_peak_nits).clamp(0.0, 1.0);
1212 let expanded = if normalised < 0.5 {
1214 normalised * self.midtone_boost
1215 } else {
1216 let t = (normalised - 0.5) * 2.0; let base = 0.5 * self.midtone_boost;
1218 base + t * 0.5 * self.highlight_gain
1219 };
1220 (expanded * self.target_peak_nits).min(self.target_peak_nits)
1221 }
1222}
1223
1224#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1228pub enum MetadataRepairAction {
1229 ClampChromaticity,
1231 FixLuminanceOrder,
1233 FixFallCll,
1235 InjectDefaultMastering,
1237 InjectDefaultCll,
1239}
1240
1241pub fn repair_hdr_metadata(meta: &mut HdrMetadata) -> Vec<MetadataRepairAction> {
1245 let mut repairs = Vec::new();
1246
1247 if let Some(md) = &mut meta.mastering_display {
1249 let mut clamped = false;
1250 let clamp_chroma = |v: &mut f64, changed: &mut bool| {
1251 if *v < 0.0 {
1252 *v = 0.0;
1253 *changed = true;
1254 }
1255 if *v > 1.0 {
1256 *v = 1.0;
1257 *changed = true;
1258 }
1259 };
1260 clamp_chroma(&mut md.red_x, &mut clamped);
1261 clamp_chroma(&mut md.red_y, &mut clamped);
1262 clamp_chroma(&mut md.green_x, &mut clamped);
1263 clamp_chroma(&mut md.green_y, &mut clamped);
1264 clamp_chroma(&mut md.blue_x, &mut clamped);
1265 clamp_chroma(&mut md.blue_y, &mut clamped);
1266 clamp_chroma(&mut md.white_x, &mut clamped);
1267 clamp_chroma(&mut md.white_y, &mut clamped);
1268 if clamped {
1269 repairs.push(MetadataRepairAction::ClampChromaticity);
1270 }
1271
1272 if md.min_luminance >= md.max_luminance && md.max_luminance > 0.0 {
1274 md.min_luminance = md.max_luminance * 0.001;
1275 repairs.push(MetadataRepairAction::FixLuminanceOrder);
1276 }
1277 if md.max_luminance <= 0.0 {
1278 md.max_luminance = 1000.0;
1279 md.min_luminance = 0.005;
1280 repairs.push(MetadataRepairAction::FixLuminanceOrder);
1281 }
1282 }
1283
1284 if let Some(cll) = &mut meta.content_light_level {
1286 if cll.max_fall > cll.max_cll {
1287 cll.max_fall = cll.max_cll;
1288 repairs.push(MetadataRepairAction::FixFallCll);
1289 }
1290 }
1291
1292 if let Some(tf) = meta.transfer_function {
1294 if tf == TransferFunction::Pq {
1295 if meta.mastering_display.is_none() {
1296 meta.mastering_display = Some(MasteringDisplay::p3_d65_1000nit());
1297 repairs.push(MetadataRepairAction::InjectDefaultMastering);
1298 }
1299 if meta.content_light_level.is_none() {
1300 meta.content_light_level = Some(ContentLightLevel::hdr10_default());
1301 repairs.push(MetadataRepairAction::InjectDefaultCll);
1302 }
1303 }
1304 }
1305
1306 repairs
1307}
1308
1309#[cfg(test)]
1312mod tests {
1313 use super::*;
1314
1315 #[test]
1318 fn test_transfer_function_h273_codes() {
1319 assert_eq!(TransferFunction::Bt709.h273_code(), 1);
1320 assert_eq!(TransferFunction::Pq.h273_code(), 16);
1321 assert_eq!(TransferFunction::Hlg.h273_code(), 18);
1322 assert_eq!(TransferFunction::Linear.h273_code(), 8);
1323 assert_eq!(TransferFunction::Unspecified.h273_code(), 2);
1324 }
1325
1326 #[test]
1327 fn test_transfer_function_is_hdr() {
1328 assert!(TransferFunction::Pq.is_hdr());
1329 assert!(TransferFunction::Hlg.is_hdr());
1330 assert!(!TransferFunction::Bt709.is_hdr());
1331 assert!(!TransferFunction::Linear.is_hdr());
1332 assert!(!TransferFunction::Unspecified.is_hdr());
1333 }
1334
1335 #[test]
1336 fn test_transfer_function_is_wide_gamut() {
1337 assert!(TransferFunction::Pq.is_wide_gamut());
1338 assert!(TransferFunction::Hlg.is_wide_gamut());
1339 assert!(!TransferFunction::Bt709.is_wide_gamut());
1340 }
1341
1342 #[test]
1345 fn test_colour_primaries_h273_codes() {
1346 assert_eq!(ColourPrimaries::Bt709.h273_code(), 1);
1347 assert_eq!(ColourPrimaries::Bt2020.h273_code(), 9);
1348 assert_eq!(ColourPrimaries::DisplayP3.h273_code(), 12);
1349 assert_eq!(ColourPrimaries::Unspecified.h273_code(), 2);
1350 }
1351
1352 #[test]
1355 fn test_mastering_display_p3_d65_1000nit_is_valid() {
1356 let md = MasteringDisplay::p3_d65_1000nit();
1357 assert!(md.validate().is_ok());
1358 }
1359
1360 #[test]
1361 fn test_mastering_display_bt2020_4000nit_is_valid() {
1362 let md = MasteringDisplay::bt2020_4000nit();
1363 assert!(md.validate().is_ok());
1364 }
1365
1366 #[test]
1367 fn test_mastering_display_bad_chromaticity() {
1368 let mut md = MasteringDisplay::p3_d65_1000nit();
1369 md.red_x = 1.5; assert!(matches!(
1371 md.validate(),
1372 Err(HdrError::OutOfRange { field, .. }) if field == "red_x"
1373 ));
1374 }
1375
1376 #[test]
1377 fn test_mastering_display_bad_luminance() {
1378 let mut md = MasteringDisplay::p3_d65_1000nit();
1379 md.min_luminance = md.max_luminance + 1.0;
1380 assert!(matches!(
1381 md.validate(),
1382 Err(HdrError::OutOfRange { field, .. }) if field == "min_luminance"
1383 ));
1384 }
1385
1386 #[test]
1389 fn test_cll_hdr10_default_valid() {
1390 let cll = ContentLightLevel::hdr10_default();
1391 assert!(cll.validate().is_ok());
1392 }
1393
1394 #[test]
1395 fn test_cll_invalid_fall_exceeds_cll() {
1396 let cll = ContentLightLevel::new(400, 1000);
1397 assert!(matches!(
1398 cll.validate(),
1399 Err(HdrError::OutOfRange { field, .. }) if field == "max_fall"
1400 ));
1401 }
1402
1403 #[test]
1406 fn test_dv_profile_numbers() {
1407 assert_eq!(DolbyVisionProfile::Profile4.profile_number(), 4);
1408 assert_eq!(DolbyVisionProfile::Profile8.profile_number(), 8);
1409 assert_eq!(DolbyVisionProfile::Profile9.profile_number(), 9);
1410 }
1411
1412 #[test]
1413 fn test_dv_backward_compatibility() {
1414 assert!(DolbyVisionProfile::Profile4.is_backward_compatible());
1415 assert!(DolbyVisionProfile::Profile8.is_backward_compatible());
1416 assert!(!DolbyVisionProfile::Profile5.is_backward_compatible());
1417 }
1418
1419 #[test]
1420 fn test_dv_level_validation() {
1421 let ok = DolbyVisionMeta::new(DolbyVisionProfile::Profile8, 6);
1422 assert!(ok.validate().is_ok());
1423
1424 let bad = DolbyVisionMeta::new(DolbyVisionProfile::Profile8, 14);
1425 assert!(bad.validate().is_err());
1426
1427 let bad_zero = DolbyVisionMeta::new(DolbyVisionProfile::Profile8, 0);
1428 assert!(bad_zero.validate().is_err());
1429 }
1430
1431 #[test]
1434 fn test_hdr_metadata_hdr10_is_hdr() {
1435 let meta = HdrMetadata::hdr10(
1436 MasteringDisplay::p3_d65_1000nit(),
1437 ContentLightLevel::hdr10_default(),
1438 );
1439 assert!(meta.is_hdr());
1440 assert!(meta.validate().is_ok());
1441 }
1442
1443 #[test]
1444 fn test_hdr_metadata_hlg_is_hdr() {
1445 let meta = HdrMetadata::hlg();
1446 assert!(meta.is_hdr());
1447 assert!(meta.validate().is_ok());
1448 }
1449
1450 #[test]
1451 fn test_hdr_metadata_default_not_hdr() {
1452 let meta = HdrMetadata::default();
1453 assert!(!meta.is_hdr());
1454 }
1455
1456 #[test]
1459 fn test_processor_passthrough_none() {
1460 let proc = HdrProcessor::new(HdrPassthroughMode::Passthrough);
1461 let result = proc.process(None).expect("passthrough None should succeed");
1462 assert!(result.is_none());
1463 }
1464
1465 #[test]
1466 fn test_processor_passthrough_some() {
1467 let proc = HdrProcessor::new(HdrPassthroughMode::Passthrough);
1468 let src = HdrMetadata::hlg();
1469 let result = proc
1470 .process(Some(&src))
1471 .expect("passthrough Some should succeed");
1472 assert!(result.is_some());
1473 assert_eq!(
1474 result.as_ref().and_then(|m| m.transfer_function),
1475 Some(TransferFunction::Hlg)
1476 );
1477 }
1478
1479 #[test]
1480 fn test_processor_strip() {
1481 let proc = HdrProcessor::new(HdrPassthroughMode::Strip);
1482 let src = HdrMetadata::hdr10(
1483 MasteringDisplay::p3_d65_1000nit(),
1484 ContentLightLevel::hdr10_default(),
1485 );
1486 let result = proc.process(Some(&src)).expect("strip should succeed");
1487 assert!(result.is_none());
1488 }
1489
1490 #[test]
1491 fn test_processor_inject() {
1492 let injected = HdrMetadata::hlg();
1493 let proc = HdrProcessor::new(HdrPassthroughMode::Inject(injected.clone()));
1494 let src = HdrMetadata::hdr10(
1495 MasteringDisplay::p3_d65_1000nit(),
1496 ContentLightLevel::hdr10_default(),
1497 );
1498 let result = proc
1499 .process(Some(&src))
1500 .expect("inject should succeed")
1501 .expect("inject should produce Some");
1502 assert_eq!(result.transfer_function, Some(TransferFunction::Hlg));
1503 }
1504
1505 #[test]
1506 fn test_processor_convert_pq_to_bt709() {
1507 let proc = HdrProcessor::new(HdrPassthroughMode::Convert {
1508 target_tf: TransferFunction::Bt709,
1509 target_primaries: ColourPrimaries::Bt709,
1510 });
1511 let src = HdrMetadata::hdr10(
1512 MasteringDisplay::p3_d65_1000nit(),
1513 ContentLightLevel::hdr10_default(),
1514 );
1515 let result = proc
1516 .process(Some(&src))
1517 .expect("conversion should succeed")
1518 .expect("conversion should produce Some");
1519 assert_eq!(result.transfer_function, Some(TransferFunction::Bt709));
1520 assert!(result.mastering_display.is_none());
1522 assert!(result.content_light_level.is_none());
1523 }
1524
1525 #[test]
1526 fn test_processor_convert_hlg_to_pq() {
1527 let proc = HdrProcessor::new(HdrPassthroughMode::Convert {
1528 target_tf: TransferFunction::Pq,
1529 target_primaries: ColourPrimaries::Bt2020,
1530 });
1531 let src = HdrMetadata::hlg();
1532 let result = proc
1533 .process(Some(&src))
1534 .expect("HLG→PQ should succeed")
1535 .expect("should produce Some");
1536 assert_eq!(result.transfer_function, Some(TransferFunction::Pq));
1537 }
1538
1539 #[test]
1540 fn test_processor_convert_sdr_to_pq_fails() {
1541 let proc = HdrProcessor::new(HdrPassthroughMode::Convert {
1542 target_tf: TransferFunction::Pq,
1543 target_primaries: ColourPrimaries::Bt2020,
1544 });
1545 let src = HdrMetadata {
1546 transfer_function: Some(TransferFunction::Bt709),
1547 ..HdrMetadata::default()
1548 };
1549 let result = proc.process(Some(&src));
1551 assert!(matches!(
1552 result,
1553 Err(HdrError::UnsupportedConversion { .. })
1554 ));
1555 }
1556
1557 #[test]
1560 fn test_mastering_display_sei_round_trip() {
1561 let original = MasteringDisplay::p3_d65_1000nit();
1562 let encoded = encode_mastering_display_sei(&original);
1563 let decoded = decode_mastering_display_sei(&encoded).expect("decode should succeed");
1564
1565 let eps = 0.002;
1567 assert!(
1568 (decoded.red_x - original.red_x).abs() < eps,
1569 "red_x mismatch"
1570 );
1571 assert!(
1572 (decoded.red_y - original.red_y).abs() < eps,
1573 "red_y mismatch"
1574 );
1575 assert!((decoded.green_x - original.green_x).abs() < eps);
1576 assert!((decoded.green_y - original.green_y).abs() < eps);
1577 assert!((decoded.blue_x - original.blue_x).abs() < eps);
1578 assert!((decoded.blue_y - original.blue_y).abs() < eps);
1579 assert!((decoded.white_x - original.white_x).abs() < eps);
1580 assert!((decoded.white_y - original.white_y).abs() < eps);
1581 assert!((decoded.max_luminance - original.max_luminance).abs() < 0.1);
1583 assert!((decoded.min_luminance - original.min_luminance).abs() < 0.001);
1584 }
1585
1586 #[test]
1587 fn test_mastering_display_sei_too_short() {
1588 let result = decode_mastering_display_sei(&[0u8; 12]);
1589 assert!(matches!(result, Err(HdrError::MissingField(_))));
1590 }
1591
1592 #[test]
1593 fn test_cll_sei_round_trip() {
1594 let original = ContentLightLevel::new(800, 300);
1595 let encoded = encode_cll_sei(&original);
1596 let decoded = decode_cll_sei(&encoded).expect("decode should succeed");
1597 assert_eq!(decoded.max_cll, original.max_cll);
1598 assert_eq!(decoded.max_fall, original.max_fall);
1599 }
1600
1601 #[test]
1602 fn test_cll_sei_too_short() {
1603 let result = decode_cll_sei(&[0u8; 2]);
1604 assert!(matches!(result, Err(HdrError::MissingField(_))));
1605 }
1606
1607 #[test]
1608 fn test_cll_sei_invalid_decoded_values() {
1609 let bad = ContentLightLevel {
1611 max_cll: 100,
1612 max_fall: 500,
1613 };
1614 let encoded = encode_cll_sei(&bad);
1615 let result = decode_cll_sei(&encoded);
1616 assert!(result.is_err());
1617 }
1618
1619 #[test]
1622 fn test_hdr10plus_new() {
1623 let meta = Hdr10PlusDynamicMeta::new(1000);
1624 assert_eq!(meta.application_version, 1);
1625 assert_eq!(meta.targeted_system_display_max_luminance, 1000);
1626 assert!(meta.validate().is_ok());
1627 }
1628
1629 #[test]
1630 fn test_hdr10plus_validate_bad_version() {
1631 let mut meta = Hdr10PlusDynamicMeta::new(1000);
1632 meta.application_version = 5;
1633 assert!(meta.validate().is_err());
1634 }
1635
1636 #[test]
1637 fn test_hdr10plus_validate_bad_avg_maxrgb() {
1638 let mut meta = Hdr10PlusDynamicMeta::new(1000);
1639 meta.average_maxrgb = 1.5;
1640 assert!(meta.validate().is_err());
1641 }
1642
1643 #[test]
1644 fn test_hdr10plus_validate_bad_knee_point() {
1645 let mut meta = Hdr10PlusDynamicMeta::new(1000);
1646 meta.knee_point = (-0.1, 0.5);
1647 assert!(meta.validate().is_err());
1648 }
1649
1650 #[test]
1651 fn test_hdr10plus_validate_too_many_anchors() {
1652 let mut meta = Hdr10PlusDynamicMeta::new(1000);
1653 meta.bezier_curve_anchors = vec![0.1; 10];
1654 assert!(meta.validate().is_err());
1655 }
1656
1657 #[test]
1658 fn test_hdr10plus_encode_decode_round_trip() {
1659 let original = Hdr10PlusDynamicMeta::new(1000);
1660 let encoded = original.encode();
1661 let decoded = Hdr10PlusDynamicMeta::decode(&encoded).expect("decode should succeed");
1662 assert_eq!(decoded.application_version, original.application_version);
1663 assert_eq!(
1664 decoded.targeted_system_display_max_luminance,
1665 original.targeted_system_display_max_luminance
1666 );
1667 assert!((decoded.average_maxrgb - original.average_maxrgb).abs() < 0.001);
1668 assert!((decoded.knee_point.0 - original.knee_point.0).abs() < 0.001);
1669 assert!((decoded.knee_point.1 - original.knee_point.1).abs() < 0.001);
1670 assert_eq!(
1671 decoded.bezier_curve_anchors.len(),
1672 original.bezier_curve_anchors.len()
1673 );
1674 }
1675
1676 #[test]
1677 fn test_hdr10plus_decode_too_short() {
1678 let result = Hdr10PlusDynamicMeta::decode(&[0u8; 5]);
1679 assert!(matches!(result, Err(HdrError::MissingField(_))));
1680 }
1681
1682 #[test]
1685 fn test_dv_rpu_new() {
1686 let payload = vec![0x08; 30]; let rpu = DolbyVisionRpu::new(payload, 0);
1688 assert_eq!(rpu.rpu_profile, 8);
1689 assert_eq!(rpu.frame_index, 0);
1690 assert!(!rpu.validated);
1691 }
1692
1693 #[test]
1694 fn test_dv_rpu_validate_empty() {
1695 let mut rpu = DolbyVisionRpu::new(Vec::new(), 0);
1696 assert!(rpu.validate().is_err());
1697 }
1698
1699 #[test]
1700 fn test_dv_rpu_validate_too_short() {
1701 let mut rpu = DolbyVisionRpu::new(vec![0x08; 10], 0);
1702 assert!(rpu.validate().is_err());
1703 }
1704
1705 #[test]
1706 fn test_dv_rpu_validate_ok() {
1707 let mut rpu = DolbyVisionRpu::new(vec![0x08; 30], 0);
1708 assert!(rpu.validate().is_ok());
1709 assert!(rpu.validated);
1710 }
1711
1712 #[test]
1713 fn test_dv_rpu_passthrough() {
1714 let mut pt = DvRpuPassthrough::new();
1715 assert_eq!(pt.count(), 0);
1716
1717 pt.ingest(vec![0x08; 30], 0);
1718 pt.ingest(vec![0x08; 30], 1);
1719 pt.ingest(vec![0x08; 5], 2); assert_eq!(pt.count(), 3);
1722 assert_eq!(pt.valid_count(), 2);
1723 assert_eq!(pt.invalid_count(), 1);
1724
1725 assert!(pt.get_rpu(0).is_some());
1726 assert!(pt.get_rpu(1).is_some());
1727 assert!(pt.get_rpu(99).is_none());
1728 }
1729
1730 #[test]
1731 fn test_dv_rpu_passthrough_drain() {
1732 let mut pt = DvRpuPassthrough::new();
1733 pt.ingest(vec![0x08; 30], 0);
1734 pt.ingest(vec![0x08; 30], 1);
1735
1736 let drained = pt.drain_all();
1737 assert_eq!(drained.len(), 2);
1738 assert_eq!(pt.count(), 0);
1739 assert_eq!(pt.valid_count(), 0);
1740 }
1741
1742 #[test]
1745 fn test_hdr_to_sdr_config_default() {
1746 let cfg = HdrToSdrConfig::default_1000nit();
1747 assert!(cfg.validate().is_ok());
1748 assert_eq!(cfg.source_peak_nits, 1000.0);
1749 assert_eq!(cfg.target_peak_nits, 100.0);
1750 }
1751
1752 #[test]
1753 fn test_hdr_to_sdr_validate_bad_source_peak() {
1754 let mut cfg = HdrToSdrConfig::default_1000nit();
1755 cfg.source_peak_nits = -1.0;
1756 assert!(cfg.validate().is_err());
1757 }
1758
1759 #[test]
1760 fn test_hdr_to_sdr_validate_bad_desat() {
1761 let mut cfg = HdrToSdrConfig::default_1000nit();
1762 cfg.desat_strength = 1.5;
1763 assert!(cfg.validate().is_err());
1764 }
1765
1766 #[test]
1767 fn test_tonemap_reinhard_zero() {
1768 let cfg = HdrToSdrConfig::default_1000nit();
1769 let result = cfg.tonemap_reinhard(0.0);
1770 assert!((result).abs() < 1e-6);
1771 }
1772
1773 #[test]
1774 fn test_tonemap_reinhard_monotonic() {
1775 let cfg = HdrToSdrConfig::default_1000nit();
1776 let a = cfg.tonemap_reinhard(0.1);
1777 let b = cfg.tonemap_reinhard(0.5);
1778 let c = cfg.tonemap_reinhard(1.0);
1779 assert!(a < b);
1780 assert!(b < c);
1781 }
1782
1783 #[test]
1784 fn test_tonemap_hable_positive() {
1785 let cfg = HdrToSdrConfig::default_1000nit();
1786 let result = cfg.tonemap_hable(0.5);
1787 assert!(result > 0.0);
1788 assert!(result < cfg.target_peak_nits);
1789 }
1790
1791 #[test]
1792 fn test_tonemap_aces_clamped() {
1793 let cfg = HdrToSdrConfig::default_1000nit();
1794 let result = cfg.tonemap_aces(100.0);
1795 assert!(result <= cfg.target_peak_nits);
1796 }
1797
1798 #[test]
1799 fn test_tonemap_apply_clip() {
1800 let cfg = HdrToSdrConfig {
1801 curve: TonemapCurve::Clip,
1802 source_peak_nits: 1000.0,
1803 target_peak_nits: 100.0,
1804 desat_strength: 0.0,
1805 };
1806 let result = cfg.apply(0.5);
1807 assert!((result - 100.0).abs() < 1e-6); }
1809
1810 #[test]
1813 fn test_sdr_to_hdr_default() {
1814 let cfg = SdrToHdrConfig::default_1000nit();
1815 assert!(cfg.validate().is_ok());
1816 assert_eq!(cfg.target_peak_nits, 1000.0);
1817 }
1818
1819 #[test]
1820 fn test_sdr_to_hdr_validate_bad_gain() {
1821 let mut cfg = SdrToHdrConfig::default_1000nit();
1822 cfg.highlight_gain = 0.5;
1823 assert!(cfg.validate().is_err());
1824 }
1825
1826 #[test]
1827 fn test_sdr_to_hdr_validate_bad_midtone() {
1828 let mut cfg = SdrToHdrConfig::default_1000nit();
1829 cfg.midtone_boost = 5.0;
1830 assert!(cfg.validate().is_err());
1831 }
1832
1833 #[test]
1834 fn test_sdr_to_hdr_apply_zero() {
1835 let cfg = SdrToHdrConfig::default_1000nit();
1836 assert!((cfg.apply(0.0)).abs() < 1e-6);
1837 }
1838
1839 #[test]
1840 fn test_sdr_to_hdr_apply_monotonic() {
1841 let cfg = SdrToHdrConfig::default_1000nit();
1842 let a = cfg.apply(10.0);
1843 let b = cfg.apply(50.0);
1844 let c = cfg.apply(100.0);
1845 assert!(a < b);
1846 assert!(b < c);
1847 assert!(c <= cfg.target_peak_nits);
1848 }
1849
1850 #[test]
1853 fn test_repair_clamp_chromaticity() {
1854 let mut meta = HdrMetadata::hdr10(
1855 MasteringDisplay {
1856 red_x: 1.5,
1857 red_y: -0.1,
1858 ..MasteringDisplay::p3_d65_1000nit()
1859 },
1860 ContentLightLevel::hdr10_default(),
1861 );
1862 let repairs = repair_hdr_metadata(&mut meta);
1863 assert!(repairs.contains(&MetadataRepairAction::ClampChromaticity));
1864 let md = meta
1865 .mastering_display
1866 .as_ref()
1867 .expect("should have mastering display");
1868 assert!((md.red_x - 1.0).abs() < 1e-6);
1869 assert!((md.red_y).abs() < 1e-6);
1870 }
1871
1872 #[test]
1873 fn test_repair_luminance_order() {
1874 let mut meta = HdrMetadata::hdr10(
1875 MasteringDisplay {
1876 max_luminance: 1000.0,
1877 min_luminance: 2000.0, ..MasteringDisplay::p3_d65_1000nit()
1879 },
1880 ContentLightLevel::hdr10_default(),
1881 );
1882 let repairs = repair_hdr_metadata(&mut meta);
1883 assert!(repairs.contains(&MetadataRepairAction::FixLuminanceOrder));
1884 let md = meta
1885 .mastering_display
1886 .as_ref()
1887 .expect("should have mastering display");
1888 assert!(md.min_luminance < md.max_luminance);
1889 }
1890
1891 #[test]
1892 fn test_repair_fall_cll() {
1893 let mut meta = HdrMetadata::hdr10(
1894 MasteringDisplay::p3_d65_1000nit(),
1895 ContentLightLevel {
1896 max_cll: 500,
1897 max_fall: 800,
1898 },
1899 );
1900 let repairs = repair_hdr_metadata(&mut meta);
1901 assert!(repairs.contains(&MetadataRepairAction::FixFallCll));
1902 let cll = meta.content_light_level.as_ref().expect("should have CLL");
1903 assert!(cll.max_fall <= cll.max_cll);
1904 }
1905
1906 #[test]
1907 fn test_repair_inject_defaults_for_pq() {
1908 let mut meta = HdrMetadata {
1909 transfer_function: Some(TransferFunction::Pq),
1910 colour_primaries: Some(ColourPrimaries::Bt2020),
1911 mastering_display: None,
1912 content_light_level: None,
1913 dolby_vision: None,
1914 };
1915 let repairs = repair_hdr_metadata(&mut meta);
1916 assert!(repairs.contains(&MetadataRepairAction::InjectDefaultMastering));
1917 assert!(repairs.contains(&MetadataRepairAction::InjectDefaultCll));
1918 assert!(meta.mastering_display.is_some());
1919 assert!(meta.content_light_level.is_some());
1920 }
1921
1922 #[test]
1923 fn test_repair_no_action_needed() {
1924 let mut meta = HdrMetadata::hdr10(
1925 MasteringDisplay::p3_d65_1000nit(),
1926 ContentLightLevel::hdr10_default(),
1927 );
1928 let repairs = repair_hdr_metadata(&mut meta);
1929 assert!(repairs.is_empty());
1930 }
1931
1932 #[test]
1933 fn test_processor_convert_with_sdr_to_pq_via_extended() {
1934 let proc = HdrProcessor::new(HdrPassthroughMode::Convert {
1936 target_tf: TransferFunction::Pq,
1937 target_primaries: ColourPrimaries::Bt2020,
1938 });
1939 let src = HdrMetadata {
1940 transfer_function: Some(TransferFunction::Bt709),
1941 ..HdrMetadata::default()
1942 };
1943 let result = proc.process(Some(&src));
1945 assert!(result.is_err());
1946 }
1947}