Skip to main content

oximedia_dolbyvision/
metadata.rs

1//! Dolby Vision metadata levels.
2//!
3//! This module defines metadata structures for different Dolby Vision levels.
4
5/// Level 1 metadata: Frame-level metadata.
6#[derive(Debug, Clone, Default)]
7#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
8pub struct Level1Metadata {
9    /// Minimum PQ value in frame
10    pub min_pq: u16,
11
12    /// Maximum PQ value in frame
13    pub max_pq: u16,
14
15    /// Average PQ value in frame
16    pub avg_pq: u16,
17}
18
19/// Level 2 metadata: Trim passes for target display adaptation.
20#[derive(Debug, Clone, Default)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22pub struct Level2Metadata {
23    /// Target display index
24    pub target_display_index: u8,
25
26    /// Trim slope
27    pub trim_slope: i16,
28
29    /// Trim offset
30    pub trim_offset: i16,
31
32    /// Trim power
33    pub trim_power: i16,
34
35    /// Trim chroma weight
36    pub trim_chroma_weight: i16,
37
38    /// Trim saturation gain
39    pub trim_saturation_gain: i16,
40
41    /// MS weight (mastering display weight)
42    pub ms_weight: i16,
43
44    /// Target mid contrast
45    pub target_mid_contrast: u16,
46
47    /// Clip trim
48    pub clip_trim: u16,
49
50    /// Saturation vector field
51    pub saturation_vector_field: Vec<SaturationVector>,
52
53    /// Hue vector field
54    pub hue_vector_field: Vec<HueVector>,
55}
56
57/// Saturation adjustment vector.
58#[derive(Debug, Clone, Copy, Default)]
59#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
60pub struct SaturationVector {
61    /// Saturation gain
62    pub saturation_gain: i16,
63}
64
65/// Hue adjustment vector.
66#[derive(Debug, Clone, Copy, Default)]
67#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
68pub struct HueVector {
69    /// Hue angle shift
70    pub hue_angle: i16,
71}
72
73/// Level 3 metadata: Reserved for future use.
74#[derive(Debug, Clone, Default)]
75#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
76pub struct Level3Metadata {
77    /// Reserved data
78    pub reserved: Vec<u8>,
79}
80
81/// Level 5 metadata: Active area (image area within frame).
82#[derive(Debug, Clone, Default)]
83#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
84pub struct Level5Metadata {
85    /// Active area left offset
86    pub active_area_left_offset: u16,
87
88    /// Active area right offset
89    pub active_area_right_offset: u16,
90
91    /// Active area top offset
92    pub active_area_top_offset: u16,
93
94    /// Active area bottom offset
95    pub active_area_bottom_offset: u16,
96}
97
98/// Level 6 metadata: Fallback metadata for non-Dolby Vision displays.
99#[derive(Debug, Clone, Default)]
100#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
101pub struct Level6Metadata {
102    /// Maximum content light level (nits)
103    pub max_cll: u16,
104
105    /// Maximum frame-average light level (nits)
106    pub max_fall: u16,
107
108    /// Master display minimum luminance (0.0001 nits units)
109    pub min_display_mastering_luminance: u32,
110
111    /// Master display maximum luminance (nits)
112    pub max_display_mastering_luminance: u32,
113
114    /// Master display primaries (x, y for R, G, B in 0.00002 units)
115    pub master_display_primaries: [[u16; 2]; 3],
116
117    /// Master display white point (x, y in 0.00002 units)
118    pub master_display_white_point: [u16; 2],
119}
120
121impl Level6Metadata {
122    /// Create Level 6 metadata for BT.2020 primaries.
123    #[must_use]
124    pub fn bt2020() -> Self {
125        Self {
126            max_cll: 1000,
127            max_fall: 400,
128            min_display_mastering_luminance: 50, // 0.005 nits
129            max_display_mastering_luminance: 1000,
130            master_display_primaries: [
131                [34000, 16000], // Red: (0.680, 0.320)
132                [13250, 34500], // Green: (0.265, 0.690)
133                [7500, 3000],   // Blue: (0.150, 0.060)
134            ],
135            master_display_white_point: [15635, 16450], // D65: (0.3127, 0.3290)
136        }
137    }
138
139    /// Create Level 6 metadata for DCI-P3 primaries.
140    #[must_use]
141    pub fn dci_p3() -> Self {
142        Self {
143            max_cll: 1000,
144            max_fall: 400,
145            min_display_mastering_luminance: 50,
146            max_display_mastering_luminance: 1000,
147            master_display_primaries: [
148                [34000, 15500], // Red: (0.680, 0.310)
149                [16500, 35000], // Green: (0.330, 0.700)
150                [7500, 3000],   // Blue: (0.150, 0.060)
151            ],
152            master_display_white_point: [15700, 17850], // DCI white: (0.314, 0.357)
153        }
154    }
155}
156
157/// Level 8 metadata: Target display characteristics.
158#[derive(Debug, Clone, Default)]
159#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
160pub struct Level8Metadata {
161    /// Target display index
162    pub target_display_index: u8,
163
164    /// Target maximum luminance (nits)
165    pub target_max_pq: u16,
166
167    /// Target minimum luminance (PQ code)
168    pub target_min_pq: u16,
169
170    /// Target primary index (0 = BT.2020, 1 = DCI-P3, 2 = BT.709)
171    pub target_primary_index: u8,
172
173    /// Target EOTF (0 = BT.1886, 1 = PQ, 2 = HLG)
174    pub target_eotf: u8,
175
176    /// Diagonal size in inches
177    pub diagonal_size: u16,
178
179    /// Peak luminance (nits)
180    pub peak_luminance: u16,
181
182    /// Diffuse white luminance (nits)
183    pub diffuse_white_luminance: u16,
184
185    /// Ambient luminance (nits)
186    pub ambient_luminance: u16,
187
188    /// Surround reflection
189    pub surround_reflection: u16,
190}
191
192impl Level8Metadata {
193    /// Create Level 8 metadata for a standard HDR display (1000 nits).
194    #[must_use]
195    pub fn hdr_1000() -> Self {
196        Self {
197            target_display_index: 0,
198            target_max_pq: 3696, // 1000 nits in PQ
199            target_min_pq: 62,   // 0.005 nits in PQ
200            target_primary_index: 0,
201            target_eotf: 1, // PQ
202            diagonal_size: 65,
203            peak_luminance: 1000,
204            diffuse_white_luminance: 200,
205            ambient_luminance: 5,
206            surround_reflection: 10,
207        }
208    }
209
210    /// Create Level 8 metadata for a high-end HDR display (4000 nits).
211    #[must_use]
212    pub fn hdr_4000() -> Self {
213        Self {
214            target_display_index: 1,
215            target_max_pq: 4079, // 4000 nits in PQ
216            target_min_pq: 62,
217            target_primary_index: 0,
218            target_eotf: 1, // PQ
219            diagonal_size: 65,
220            peak_luminance: 4000,
221            diffuse_white_luminance: 200,
222            ambient_luminance: 5,
223            surround_reflection: 10,
224        }
225    }
226
227    /// Create Level 8 metadata for an HLG display.
228    #[must_use]
229    pub fn hlg() -> Self {
230        Self {
231            target_display_index: 2,
232            target_max_pq: 2081, // ~100 nits nominal
233            target_min_pq: 0,
234            target_primary_index: 0,
235            target_eotf: 2, // HLG
236            diagonal_size: 55,
237            peak_luminance: 1000,
238            diffuse_white_luminance: 100,
239            ambient_luminance: 5,
240            surround_reflection: 10,
241        }
242    }
243}
244
245/// Level 9 metadata: Source display characteristics.
246#[derive(Debug, Clone, Default)]
247#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
248pub struct Level9Metadata {
249    /// Source primary index (0 = BT.2020, 1 = DCI-P3, 2 = BT.709)
250    pub source_primary_index: u8,
251
252    /// Source maximum PQ
253    pub source_max_pq: u16,
254
255    /// Source minimum PQ
256    pub source_min_pq: u16,
257
258    /// Source diagonal size in inches
259    pub source_diagonal: u16,
260}
261
262impl Level9Metadata {
263    /// Create Level 9 metadata for BT.2020 mastering.
264    #[must_use]
265    pub fn bt2020_mastering() -> Self {
266        Self {
267            source_primary_index: 0,
268            source_max_pq: 3696, // 1000 nits
269            source_min_pq: 62,   // 0.005 nits
270            source_diagonal: 65,
271        }
272    }
273
274    /// Create Level 9 metadata for DCI-P3 mastering.
275    #[must_use]
276    pub fn dci_p3_mastering() -> Self {
277        Self {
278            source_primary_index: 1,
279            source_max_pq: 3696,
280            source_min_pq: 62,
281            source_diagonal: 65,
282        }
283    }
284}
285
286/// Level 4 metadata: Global dimming data.
287///
288/// Describes the anchor luminance for backward-compatible (HDR10) rendering
289/// when a Dolby Vision display management pipeline is not available.
290/// The anchor PQ code represents the reference level at which the base layer
291/// was graded, enabling global dimming compensation.
292#[derive(Debug, Clone, Default, PartialEq)]
293#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
294pub struct Level4Metadata {
295    /// Anchor PQ code value (0-4095) representing the dimming anchor luminance.
296    pub anchor_pq: u16,
297    /// Anchor power gain (fixed-point, scaled by 2^12, default 4096 = 1.0).
298    pub anchor_power: u16,
299    /// Active area flag: when true, dimming only applies to the active area.
300    pub active_area_flag: bool,
301}
302
303impl Level4Metadata {
304    /// Create Level 4 metadata with a typical SDR-like anchor (100 nits).
305    #[must_use]
306    pub fn sdr_anchor() -> Self {
307        Self {
308            anchor_pq: nits_to_pq(100),
309            anchor_power: 1 << 12, // 1.0 in fixed-point
310            active_area_flag: false,
311        }
312    }
313
314    /// Create Level 4 metadata for a high-brightness mastering anchor (1000 nits).
315    #[must_use]
316    pub fn hdr_anchor() -> Self {
317        Self {
318            anchor_pq: nits_to_pq(1000),
319            anchor_power: 1 << 12,
320            active_area_flag: false,
321        }
322    }
323
324    /// Create Level 4 metadata for a custom anchor luminance in nits.
325    #[must_use]
326    pub fn custom_anchor(nits: u16, power_gain: f32) -> Self {
327        let anchor_power = (power_gain * (1 << 12) as f32).clamp(0.0, 65535.0) as u16;
328        Self {
329            anchor_pq: nits_to_pq(nits),
330            anchor_power,
331            active_area_flag: false,
332        }
333    }
334
335    /// Return the anchor power as a floating-point multiplier.
336    #[must_use]
337    pub fn anchor_power_f32(&self) -> f32 {
338        f32::from(self.anchor_power) / (1 << 12) as f32
339    }
340
341    /// Return the anchor luminance in nits.
342    #[must_use]
343    pub fn anchor_nits(&self) -> u16 {
344        pq_to_nits(self.anchor_pq)
345    }
346
347    /// Validate that the metadata fields are within specification ranges.
348    ///
349    /// Returns a list of error descriptions; empty means valid.
350    #[must_use]
351    pub fn validate(&self) -> Vec<String> {
352        let mut errors = Vec::new();
353        if self.anchor_pq > 4095 {
354            errors.push(format!("anchor_pq {} exceeds maximum 4095", self.anchor_pq));
355        }
356        if self.anchor_power == 0 {
357            errors.push("anchor_power must be > 0".to_string());
358        }
359        errors
360    }
361}
362
363/// Level 7 metadata: Source display color volume.
364///
365/// Describes the color volume (gamut and luminance range) of the mastering display
366/// used for content creation. This allows receivers to understand the intended
367/// color volume and perform appropriate gamut mapping.
368#[derive(Debug, Clone, Default, PartialEq)]
369#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
370pub struct Level7Metadata {
371    /// Source display minimum PQ code value (0-4095).
372    pub source_min_pq: u16,
373    /// Source display maximum PQ code value (0-4095).
374    pub source_max_pq: u16,
375    /// Source display color primaries index (0 = BT.2020, 1 = DCI-P3, 2 = BT.709).
376    pub source_primary_index: u8,
377    /// Source display primaries: red (x, y) in 0.00002 units.
378    pub red_primary: [u16; 2],
379    /// Source display primaries: green (x, y) in 0.00002 units.
380    pub green_primary: [u16; 2],
381    /// Source display primaries: blue (x, y) in 0.00002 units.
382    pub blue_primary: [u16; 2],
383    /// Source display white point (x, y) in 0.00002 units.
384    pub white_point: [u16; 2],
385}
386
387impl Level7Metadata {
388    /// Create Level 7 metadata for BT.2020 primaries with 1000-nit peak.
389    #[must_use]
390    pub fn bt2020_1000nits() -> Self {
391        Self {
392            source_min_pq: 62, // ~0.005 nits
393            source_max_pq: nits_to_pq(1000),
394            source_primary_index: 0,
395            red_primary: [34000, 16000],   // (0.680, 0.320)
396            green_primary: [13250, 34500], // (0.265, 0.690)
397            blue_primary: [7500, 3000],    // (0.150, 0.060)
398            white_point: [15635, 16450],   // D65
399        }
400    }
401
402    /// Create Level 7 metadata for BT.2020 primaries with 4000-nit peak.
403    #[must_use]
404    pub fn bt2020_4000nits() -> Self {
405        Self {
406            source_min_pq: 62,
407            source_max_pq: nits_to_pq(4000),
408            source_primary_index: 0,
409            red_primary: [34000, 16000],
410            green_primary: [13250, 34500],
411            blue_primary: [7500, 3000],
412            white_point: [15635, 16450],
413        }
414    }
415
416    /// Create Level 7 metadata for DCI-P3 primaries with 1000-nit peak.
417    #[must_use]
418    pub fn dci_p3_1000nits() -> Self {
419        Self {
420            source_min_pq: 62,
421            source_max_pq: nits_to_pq(1000),
422            source_primary_index: 1,
423            red_primary: [34000, 15500],   // (0.680, 0.310)
424            green_primary: [16500, 35000], // (0.330, 0.700)
425            blue_primary: [7500, 3000],    // (0.150, 0.060)
426            white_point: [15635, 16450],   // D65
427        }
428    }
429
430    /// Create Level 7 metadata for BT.709 primaries with 100-nit peak (SDR mastering).
431    #[must_use]
432    pub fn bt709_100nits() -> Self {
433        Self {
434            source_min_pq: 62,
435            source_max_pq: nits_to_pq(100),
436            source_primary_index: 2,
437            red_primary: [32000, 16500],   // (0.640, 0.330)
438            green_primary: [15000, 30000], // (0.300, 0.600)
439            blue_primary: [7500, 3000],    // (0.150, 0.060)
440            white_point: [15635, 16450],   // D65
441        }
442    }
443
444    /// Return the source display minimum luminance in nits.
445    #[must_use]
446    pub fn source_min_nits(&self) -> u16 {
447        pq_to_nits(self.source_min_pq)
448    }
449
450    /// Return the source display maximum luminance in nits.
451    #[must_use]
452    pub fn source_max_nits(&self) -> u16 {
453        pq_to_nits(self.source_max_pq)
454    }
455
456    /// Return the primary name as a string.
457    #[must_use]
458    pub fn primary_name(&self) -> &str {
459        match self.source_primary_index {
460            0 => "BT.2020",
461            1 => "DCI-P3",
462            2 => "BT.709",
463            _ => "Unknown",
464        }
465    }
466
467    /// Validate that the metadata fields are within specification ranges.
468    #[must_use]
469    pub fn validate(&self) -> Vec<String> {
470        let mut errors = Vec::new();
471        if self.source_min_pq > 4095 {
472            errors.push(format!(
473                "source_min_pq {} exceeds maximum 4095",
474                self.source_min_pq
475            ));
476        }
477        if self.source_max_pq > 4095 {
478            errors.push(format!(
479                "source_max_pq {} exceeds maximum 4095",
480                self.source_max_pq
481            ));
482        }
483        if self.source_min_pq >= self.source_max_pq {
484            errors.push(format!(
485                "source_min_pq ({}) >= source_max_pq ({})",
486                self.source_min_pq, self.source_max_pq
487            ));
488        }
489        if self.source_primary_index > 2 {
490            errors.push(format!(
491                "source_primary_index {} is out of range (0-2)",
492                self.source_primary_index
493            ));
494        }
495        errors
496    }
497}
498
499/// Level 10 metadata: Reserved for future use.
500#[derive(Debug, Clone, Default)]
501#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
502pub struct Level10Metadata {
503    /// Reserved data
504    pub reserved: Vec<u8>,
505}
506
507/// Level 11 metadata: Content type and description.
508#[derive(Debug, Clone, Default)]
509#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
510pub struct Level11Metadata {
511    /// Content type
512    pub content_type: ContentType,
513
514    /// White point
515    pub whitepoint: u8,
516
517    /// Reference mode flag
518    pub reference_mode_flag: bool,
519
520    /// Sharpness
521    pub sharpness: u8,
522
523    /// Noise reduction
524    pub noise_reduction: u8,
525
526    /// MPEG noise reduction
527    pub mpeg_noise_reduction: u8,
528
529    /// Frame rate
530    pub frame_rate: u8,
531
532    /// Temporal filter strength
533    pub temporal_filter_strength: u8,
534}
535
536/// Content type classification.
537#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
538#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
539pub enum ContentType {
540    /// Unknown content type
541    #[default]
542    Unknown = 0,
543
544    /// Movie content
545    Movie = 1,
546
547    /// TV content
548    Tv = 2,
549
550    /// Sports content
551    Sports = 3,
552
553    /// Gaming content
554    Gaming = 4,
555
556    /// Animation content
557    Animation = 5,
558}
559
560impl ContentType {
561    /// Create from numeric value.
562    #[must_use]
563    pub const fn from_u8(value: u8) -> Self {
564        match value {
565            1 => Self::Movie,
566            2 => Self::Tv,
567            3 => Self::Sports,
568            4 => Self::Gaming,
569            5 => Self::Animation,
570            _ => Self::Unknown,
571        }
572    }
573}
574
575/// CMD (Content Metadata Descriptor) for extended content information.
576#[derive(Debug, Clone, Default)]
577#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
578pub struct ContentMetadataDescriptor {
579    /// Content title
580    pub title: Option<String>,
581
582    /// Content description
583    pub description: Option<String>,
584
585    /// Content language (ISO 639-2)
586    pub language: Option<String>,
587
588    /// Content creation date (ISO 8601)
589    pub creation_date: Option<String>,
590
591    /// Content creator
592    pub creator: Option<String>,
593
594    /// Content copyright
595    pub copyright: Option<String>,
596
597    /// Additional metadata key-value pairs
598    pub additional_metadata: Vec<(String, String)>,
599}
600
601impl ContentMetadataDescriptor {
602    /// Create a new empty CMD.
603    #[must_use]
604    pub const fn new() -> Self {
605        Self {
606            title: None,
607            description: None,
608            language: None,
609            creation_date: None,
610            creator: None,
611            copyright: None,
612            additional_metadata: Vec::new(),
613        }
614    }
615
616    /// Add a custom metadata field.
617    pub fn add_metadata(&mut self, key: String, value: String) {
618        self.additional_metadata.push((key, value));
619    }
620}
621
622/// Trim pass for display adaptation.
623#[derive(Debug, Clone, Default)]
624#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
625pub struct TrimPass {
626    /// Target max PQ
627    pub target_max_pq: u16,
628
629    /// Target min PQ
630    pub target_min_pq: u16,
631
632    /// Trim slope
633    pub trim_slope: i16,
634
635    /// Trim offset
636    pub trim_offset: i16,
637
638    /// Trim power
639    pub trim_power: i16,
640
641    /// Trim chroma weight
642    pub trim_chroma_weight: i16,
643
644    /// Trim saturation gain
645    pub trim_saturation_gain: i16,
646
647    /// MS weight
648    pub ms_weight: i16,
649}
650
651impl TrimPass {
652    /// Create identity trim pass (no modification).
653    #[must_use]
654    pub fn identity() -> Self {
655        Self {
656            target_max_pq: 4095,
657            target_min_pq: 0,
658            trim_slope: 1 << 12,
659            trim_offset: 0,
660            trim_power: 1 << 12,
661            trim_chroma_weight: 1 << 12,
662            trim_saturation_gain: 1 << 12,
663            ms_weight: 1 << 12,
664        }
665    }
666
667    /// Create trim pass for specific target peak brightness.
668    #[must_use]
669    pub fn for_peak_brightness(target_nits: u16) -> Self {
670        // Convert nits to PQ code value (simplified)
671        let target_max_pq = nits_to_pq(target_nits);
672
673        Self {
674            target_max_pq,
675            target_min_pq: 62, // 0.005 nits
676            trim_slope: 1 << 12,
677            trim_offset: 0,
678            trim_power: 1 << 12,
679            trim_chroma_weight: 1 << 12,
680            trim_saturation_gain: 1 << 12,
681            ms_weight: 1 << 12,
682        }
683    }
684}
685
686/// Convert nits to PQ code value (0-4095).
687#[must_use]
688pub fn nits_to_pq(nits: u16) -> u16 {
689    const M1: f64 = 0.159_301_758_113_479_8;
690    const M2: f64 = 78.843_750;
691    const C1: f64 = 0.835_937_5;
692    const C2: f64 = 18.851_562_5;
693    const C3: f64 = 18.6875;
694
695    let y = f64::from(nits) / 10_000.0;
696    let y_m1 = y.powf(M1);
697    let pq = ((C1 + C2 * y_m1) / (1.0 + C3 * y_m1)).powf(M2);
698
699    (pq * 4095.0).min(4095.0) as u16
700}
701
702/// Convert PQ code value (0-4095) to nits.
703#[must_use]
704#[allow(dead_code)]
705pub fn pq_to_nits(pq: u16) -> u16 {
706    const M1_INV: f64 = 1.0 / 0.159_301_758_113_479_8;
707    const M2_INV: f64 = 1.0 / 78.843_750;
708    const C1: f64 = 0.835_937_5;
709    const C2: f64 = 18.851_562_5;
710    const C3: f64 = 18.6875;
711
712    let pq_norm = f64::from(pq) / 4095.0;
713    let v = pq_norm.powf(M2_INV);
714    let y = ((v - C1).max(0.0) / (C2 - C3 * v)).powf(M1_INV);
715
716    (y * 10_000.0).min(10_000.0) as u16
717}
718
719/// Metadata block for generic extension.
720#[derive(Debug, Clone, Default)]
721#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
722pub struct MetadataBlock {
723    /// Block ID
724    pub block_id: u8,
725
726    /// Block length
727    pub block_length: u16,
728
729    /// Block data
730    pub block_data: Vec<u8>,
731}
732
733/// Color volume transform parameters.
734#[derive(Debug, Clone, Default)]
735#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
736pub struct ColorVolumeTransform {
737    /// 3D LUT size per dimension
738    pub lut_size: u8,
739
740    /// 3D LUT data (flattened RGB cube)
741    pub lut_data: Vec<[u16; 3]>,
742}
743
744impl ColorVolumeTransform {
745    /// Create identity transform.
746    #[must_use]
747    pub fn identity(size: u8) -> Self {
748        let total_points = usize::from(size) * usize::from(size) * usize::from(size);
749        let mut lut_data = Vec::with_capacity(total_points);
750
751        for r in 0..size {
752            for g in 0..size {
753                for b in 0..size {
754                    let scale = 4095_u16 / u16::from(size - 1);
755                    lut_data.push([
756                        u16::from(r) * scale,
757                        u16::from(g) * scale,
758                        u16::from(b) * scale,
759                    ]);
760                }
761            }
762        }
763
764        Self {
765            lut_size: size,
766            lut_data,
767        }
768    }
769}
770
771#[cfg(test)]
772mod tests {
773    use super::*;
774
775    #[test]
776    fn test_content_type() {
777        assert_eq!(ContentType::from_u8(0), ContentType::Unknown);
778        assert_eq!(ContentType::from_u8(1), ContentType::Movie);
779        assert_eq!(ContentType::from_u8(2), ContentType::Tv);
780        assert_eq!(ContentType::from_u8(3), ContentType::Sports);
781        assert_eq!(ContentType::from_u8(99), ContentType::Unknown);
782    }
783
784    #[test]
785    fn test_level6_presets() {
786        let bt2020 = Level6Metadata::bt2020();
787        assert_eq!(bt2020.max_cll, 1000);
788        assert_eq!(bt2020.master_display_primaries[0][0], 34000);
789
790        let dci_p3 = Level6Metadata::dci_p3();
791        assert_eq!(dci_p3.max_cll, 1000);
792    }
793
794    #[test]
795    fn test_level8_presets() {
796        let hdr1000 = Level8Metadata::hdr_1000();
797        assert_eq!(hdr1000.peak_luminance, 1000);
798        assert_eq!(hdr1000.target_eotf, 1);
799
800        let hdr4000 = Level8Metadata::hdr_4000();
801        assert_eq!(hdr4000.peak_luminance, 4000);
802
803        let hlg = Level8Metadata::hlg();
804        assert_eq!(hlg.target_eotf, 2);
805    }
806
807    #[test]
808    fn test_level9_presets() {
809        let bt2020 = Level9Metadata::bt2020_mastering();
810        assert_eq!(bt2020.source_primary_index, 0);
811
812        let p3 = Level9Metadata::dci_p3_mastering();
813        assert_eq!(p3.source_primary_index, 1);
814    }
815
816    #[test]
817    fn test_nits_pq_conversion() {
818        let pq_100 = nits_to_pq(100);
819        let nits_100 = pq_to_nits(pq_100);
820        assert!((nits_100 as i32 - 100).abs() <= 5);
821
822        let pq_1000 = nits_to_pq(1000);
823        let nits_1000 = pq_to_nits(pq_1000);
824        assert!((nits_1000 as i32 - 1000).abs() <= 50);
825
826        let pq_10000 = nits_to_pq(10000);
827        assert_eq!(pq_10000, 4095);
828    }
829
830    #[test]
831    fn test_trim_pass() {
832        let identity = TrimPass::identity();
833        assert_eq!(identity.target_max_pq, 4095);
834        assert_eq!(identity.trim_slope, 1 << 12);
835
836        let trim_1000 = TrimPass::for_peak_brightness(1000);
837        assert!(trim_1000.target_max_pq > 0);
838        assert!(trim_1000.target_max_pq < 4095);
839    }
840
841    #[test]
842    fn test_cmd() {
843        let mut cmd = ContentMetadataDescriptor::new();
844        cmd.add_metadata("key1".to_string(), "value1".to_string());
845        assert_eq!(cmd.additional_metadata.len(), 1);
846        assert_eq!(cmd.additional_metadata[0].0, "key1");
847    }
848
849    #[test]
850    fn test_color_volume_transform() {
851        let cvt = ColorVolumeTransform::identity(5);
852        assert_eq!(cvt.lut_size, 5);
853        assert_eq!(cvt.lut_data.len(), 125);
854    }
855}