1use crate::TransferFunction;
9
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
15pub struct ContentLightLevel {
16 pub max_content_light_level: u16,
19 pub max_frame_average_light_level: u16,
22}
23
24impl ContentLightLevel {
25 pub const fn new(max_content_light_level: u16, max_frame_average_light_level: u16) -> Self {
27 Self {
28 max_content_light_level,
29 max_frame_average_light_level,
30 }
31 }
32}
33
34#[derive(Clone, Copy, Debug, Default, PartialEq)]
39pub struct MasteringDisplay {
40 pub primaries_xy: [[f32; 2]; 3],
43 pub white_point_xy: [f32; 2],
45 pub max_luminance: f32,
47 pub min_luminance: f32,
49}
50
51impl MasteringDisplay {
52 pub const fn new(
54 primaries_xy: [[f32; 2]; 3],
55 white_point_xy: [f32; 2],
56 max_luminance: f32,
57 min_luminance: f32,
58 ) -> Self {
59 Self {
60 primaries_xy,
61 white_point_xy,
62 max_luminance,
63 min_luminance,
64 }
65 }
66
67 pub const HDR10_REFERENCE: Self = Self {
69 primaries_xy: [[0.708, 0.292], [0.170, 0.797], [0.131, 0.046]],
70 white_point_xy: [0.3127, 0.3290],
71 max_luminance: 10000.0,
72 min_luminance: 0.0001,
73 };
74
75 pub const DISPLAY_P3_1000: Self = Self {
77 primaries_xy: [[0.680, 0.320], [0.265, 0.690], [0.150, 0.060]],
78 white_point_xy: [0.3127, 0.3290],
79 max_luminance: 1000.0,
80 min_luminance: 0.0001,
81 };
82}
83
84#[derive(Clone, Copy, Debug, PartialEq)]
89pub struct HdrMetadata {
90 pub transfer: TransferFunction,
92 pub content_light_level: Option<ContentLightLevel>,
94 pub mastering_display: Option<MasteringDisplay>,
96}
97
98impl HdrMetadata {
99 #[must_use]
101 pub fn is_hdr(&self) -> bool {
102 matches!(self.transfer, TransferFunction::Pq | TransferFunction::Hlg)
103 }
104
105 #[must_use]
107 pub fn is_sdr(&self) -> bool {
108 !self.is_hdr()
109 }
110
111 pub fn hdr10(cll: ContentLightLevel) -> Self {
113 Self {
114 transfer: TransferFunction::Pq,
115 content_light_level: Some(cll),
116 mastering_display: Some(MasteringDisplay::HDR10_REFERENCE),
117 }
118 }
119
120 pub fn hlg() -> Self {
122 Self {
123 transfer: TransferFunction::Hlg,
124 content_light_level: None,
125 mastering_display: None,
126 }
127 }
128}
129
130#[inline]
141#[must_use]
142pub fn reinhard_tonemap(v: f32) -> f32 {
143 v / (1.0 + v)
144}
145
146#[inline]
150#[must_use]
151pub fn reinhard_inverse(v: f32) -> f32 {
152 if v >= 1.0 {
153 return f32::MAX;
154 }
155 v / (1.0 - v)
156}
157
158#[cfg(feature = "std")]
165#[inline]
166#[must_use]
167pub fn exposure_tonemap(v: f32, exposure: f32) -> f32 {
168 (v * 2.0f32.powf(exposure)).clamp(0.0, 1.0)
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn reinhard_boundaries() {
177 assert_eq!(reinhard_tonemap(0.0), 0.0);
178 assert!((reinhard_tonemap(1.0) - 0.5).abs() < 1e-6);
179 assert!(reinhard_tonemap(1000.0) > 0.99);
180 assert!(reinhard_tonemap(1000.0) < 1.0);
181 }
182
183 #[test]
184 fn reinhard_roundtrip() {
185 for &v in &[0.0, 0.1, 0.5, 1.0, 2.0, 10.0, 100.0] {
186 let mapped = reinhard_tonemap(v);
187 let unmapped = reinhard_inverse(mapped);
188 assert!(
189 (unmapped - v).abs() < 1e-4,
190 "Reinhard roundtrip failed for {v}: got {unmapped}"
191 );
192 }
193 }
194
195 #[test]
196 fn hdr_metadata_is_hdr() {
197 assert!(HdrMetadata::hdr10(ContentLightLevel::default()).is_hdr());
198 assert!(HdrMetadata::hlg().is_hdr());
199 assert!(
200 HdrMetadata {
201 transfer: TransferFunction::Srgb,
202 content_light_level: None,
203 mastering_display: None,
204 }
205 .is_sdr()
206 );
207 }
208
209 #[test]
210 fn content_light_level_new() {
211 let cll = ContentLightLevel::new(1000, 500);
212 assert_eq!(cll.max_content_light_level, 1000);
213 assert_eq!(cll.max_frame_average_light_level, 500);
214 }
215
216 #[test]
217 fn content_light_level_default() {
218 let cll = ContentLightLevel::default();
219 assert_eq!(cll.max_content_light_level, 0);
220 assert_eq!(cll.max_frame_average_light_level, 0);
221 }
222
223 #[test]
224 fn mastering_display_new() {
225 let md = MasteringDisplay::new(
226 [[0.68, 0.32], [0.265, 0.69], [0.15, 0.06]],
227 [0.3127, 0.329],
228 1000.0,
229 0.001,
230 );
231 assert_eq!(md.max_luminance, 1000.0);
232 assert_eq!(md.min_luminance, 0.001);
233 }
234
235 #[test]
236 fn mastering_display_constants() {
237 assert_eq!(MasteringDisplay::HDR10_REFERENCE.max_luminance, 10000.0);
238 assert_eq!(MasteringDisplay::DISPLAY_P3_1000.max_luminance, 1000.0);
239 }
240
241 #[test]
242 fn hdr10_constructor() {
243 let cll = ContentLightLevel::new(4000, 1000);
244 let meta = HdrMetadata::hdr10(cll);
245 assert!(meta.is_hdr());
246 assert_eq!(meta.transfer, TransferFunction::Pq);
247 assert_eq!(meta.content_light_level, Some(cll));
248 assert!(meta.mastering_display.is_some());
249 }
250
251 #[test]
252 fn hlg_constructor() {
253 let meta = HdrMetadata::hlg();
254 assert!(meta.is_hdr());
255 assert_eq!(meta.transfer, TransferFunction::Hlg);
256 assert!(meta.content_light_level.is_none());
257 assert!(meta.mastering_display.is_none());
258 }
259
260 #[test]
261 #[cfg(feature = "std")]
262 fn exposure_tonemap_values() {
263 assert!((exposure_tonemap(0.5, 0.0) - 0.5).abs() < 1e-6);
265 assert!((exposure_tonemap(0.25, 1.0) - 0.5).abs() < 1e-5);
267 assert!((exposure_tonemap(0.5, -1.0) - 0.25).abs() < 1e-5);
269 assert_eq!(exposure_tonemap(0.8, 1.0), 1.0);
271 assert_eq!(exposure_tonemap(0.0, 5.0), 0.0);
272 }
273
274 #[test]
275 fn reinhard_inverse_at_one() {
276 assert_eq!(reinhard_inverse(1.0), f32::MAX);
277 }
278
279 #[test]
280 fn content_light_level_clone_eq() {
281 let a = ContentLightLevel::new(100, 50);
282 let b = a;
283 assert_eq!(a, b);
284 }
285
286 #[test]
287 #[cfg(feature = "std")]
288 fn content_light_level_hash() {
289 use core::hash::{Hash, Hasher};
290 let a = ContentLightLevel::new(100, 50);
291 let b = a;
292 let mut h1 = std::hash::DefaultHasher::new();
293 a.hash(&mut h1);
294 let mut h2 = std::hash::DefaultHasher::new();
295 b.hash(&mut h2);
296 assert_eq!(h1.finish(), h2.finish());
297 }
298
299 #[test]
300 fn hdr_metadata_clone_partial_eq() {
301 let a = HdrMetadata::hlg();
302 let b = a;
303 assert_eq!(a, b);
304 }
305}