1#![allow(dead_code)]
2#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
15pub enum ColorRange {
16 Limited,
18 Full,
20}
21
22impl Default for ColorRange {
23 fn default() -> Self {
24 Self::Limited
25 }
26}
27
28impl ColorRange {
29 pub fn is_limited(&self) -> bool {
31 *self == Self::Limited
32 }
33
34 pub fn is_full(&self) -> bool {
36 *self == Self::Full
37 }
38}
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
42pub enum BitDepth {
43 Eight,
45 Ten,
47 Twelve,
49}
50
51impl BitDepth {
52 #[allow(clippy::cast_precision_loss)]
54 pub fn max_value(&self) -> u16 {
55 match self {
56 Self::Eight => 255,
57 Self::Ten => 1023,
58 Self::Twelve => 4095,
59 }
60 }
61
62 pub fn bits(&self) -> u8 {
64 match self {
65 Self::Eight => 8,
66 Self::Ten => 10,
67 Self::Twelve => 12,
68 }
69 }
70}
71
72impl Default for BitDepth {
73 fn default() -> Self {
74 Self::Eight
75 }
76}
77
78#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80pub struct LevelRange {
81 pub luma_min: u16,
83 pub luma_max: u16,
85 pub chroma_min: u16,
87 pub chroma_max: u16,
89}
90
91impl LevelRange {
92 #[allow(clippy::cast_precision_loss)]
94 pub fn new(range: ColorRange, depth: BitDepth) -> Self {
95 let shift = depth.bits() - 8;
96 match range {
97 ColorRange::Limited => Self {
98 luma_min: 16 << shift,
99 luma_max: 235 << shift,
100 chroma_min: 16 << shift,
101 chroma_max: 240 << shift,
102 },
103 ColorRange::Full => Self {
104 luma_min: 0,
105 luma_max: depth.max_value(),
106 chroma_min: 0,
107 chroma_max: depth.max_value(),
108 },
109 }
110 }
111
112 pub fn luma_span(&self) -> u16 {
114 self.luma_max - self.luma_min
115 }
116
117 pub fn chroma_span(&self) -> u16 {
119 self.chroma_max - self.chroma_min
120 }
121}
122
123pub fn clamp_luma(value: u16, levels: &LevelRange) -> u16 {
125 value.clamp(levels.luma_min, levels.luma_max)
126}
127
128pub fn clamp_chroma(value: u16, levels: &LevelRange) -> u16 {
130 value.clamp(levels.chroma_min, levels.chroma_max)
131}
132
133#[allow(clippy::cast_precision_loss)]
135pub fn limited_to_full_luma(value: u16, depth: BitDepth) -> u16 {
136 let limited = LevelRange::new(ColorRange::Limited, depth);
137 let max = depth.max_value();
138 if value <= limited.luma_min {
139 return 0;
140 }
141 if value >= limited.luma_max {
142 return max;
143 }
144 let span = limited.luma_span() as f64;
145 let scaled = (value - limited.luma_min) as f64 / span * max as f64;
146 (scaled.round() as u16).min(max)
147}
148
149#[allow(clippy::cast_precision_loss)]
151pub fn full_to_limited_luma(value: u16, depth: BitDepth) -> u16 {
152 let limited = LevelRange::new(ColorRange::Limited, depth);
153 let max = depth.max_value();
154 let span = limited.luma_span() as f64;
155 let scaled = value as f64 / max as f64 * span + limited.luma_min as f64;
156 (scaled.round() as u16).clamp(limited.luma_min, limited.luma_max)
157}
158
159#[allow(clippy::cast_precision_loss)]
161pub fn limited_to_full_chroma(value: u16, depth: BitDepth) -> u16 {
162 let limited = LevelRange::new(ColorRange::Limited, depth);
163 let max = depth.max_value();
164 if value <= limited.chroma_min {
165 return 0;
166 }
167 if value >= limited.chroma_max {
168 return max;
169 }
170 let span = limited.chroma_span() as f64;
171 let scaled = (value - limited.chroma_min) as f64 / span * max as f64;
172 (scaled.round() as u16).min(max)
173}
174
175#[allow(clippy::cast_precision_loss)]
177pub fn full_to_limited_chroma(value: u16, depth: BitDepth) -> u16 {
178 let limited = LevelRange::new(ColorRange::Limited, depth);
179 let max = depth.max_value();
180 let span = limited.chroma_span() as f64;
181 let scaled = value as f64 / max as f64 * span + limited.chroma_min as f64;
182 (scaled.round() as u16).clamp(limited.chroma_min, limited.chroma_max)
183}
184
185#[derive(Clone, Debug, PartialEq, Eq)]
187pub struct ComplianceReport {
188 pub luma_violations: usize,
190 pub chroma_violations: usize,
192 pub total_samples: usize,
194}
195
196impl ComplianceReport {
197 pub fn is_compliant(&self) -> bool {
199 self.luma_violations == 0 && self.chroma_violations == 0
200 }
201
202 #[allow(clippy::cast_precision_loss)]
204 pub fn violation_ratio(&self) -> f64 {
205 if self.total_samples == 0 {
206 return 0.0;
207 }
208 (self.luma_violations + self.chroma_violations) as f64 / self.total_samples as f64
209 }
210}
211
212pub fn check_luma_compliance(samples: &[u16], levels: &LevelRange) -> usize {
214 samples
215 .iter()
216 .filter(|&&v| v < levels.luma_min || v > levels.luma_max)
217 .count()
218}
219
220pub fn check_chroma_compliance(samples: &[u16], levels: &LevelRange) -> usize {
222 samples
223 .iter()
224 .filter(|&&v| v < levels.chroma_min || v > levels.chroma_max)
225 .count()
226}
227
228#[allow(clippy::cast_precision_loss)]
230pub fn convert_luma_buffer(
231 src: &[u16],
232 src_range: ColorRange,
233 dst_range: ColorRange,
234 depth: BitDepth,
235) -> Vec<u16> {
236 if src_range == dst_range {
237 return src.to_vec();
238 }
239 match (src_range, dst_range) {
240 (ColorRange::Limited, ColorRange::Full) => src
241 .iter()
242 .map(|&v| limited_to_full_luma(v, depth))
243 .collect(),
244 (ColorRange::Full, ColorRange::Limited) => src
245 .iter()
246 .map(|&v| full_to_limited_luma(v, depth))
247 .collect(),
248 _ => src.to_vec(),
249 }
250}
251
252#[allow(clippy::cast_precision_loss)]
254pub fn convert_chroma_buffer(
255 src: &[u16],
256 src_range: ColorRange,
257 dst_range: ColorRange,
258 depth: BitDepth,
259) -> Vec<u16> {
260 if src_range == dst_range {
261 return src.to_vec();
262 }
263 match (src_range, dst_range) {
264 (ColorRange::Limited, ColorRange::Full) => src
265 .iter()
266 .map(|&v| limited_to_full_chroma(v, depth))
267 .collect(),
268 (ColorRange::Full, ColorRange::Limited) => src
269 .iter()
270 .map(|&v| full_to_limited_chroma(v, depth))
271 .collect(),
272 _ => src.to_vec(),
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 #[test]
281 fn test_color_range_default() {
282 let range = ColorRange::default();
283 assert_eq!(range, ColorRange::Limited);
284 }
285
286 #[test]
287 fn test_color_range_predicates() {
288 assert!(ColorRange::Limited.is_limited());
289 assert!(!ColorRange::Limited.is_full());
290 assert!(ColorRange::Full.is_full());
291 assert!(!ColorRange::Full.is_limited());
292 }
293
294 #[test]
295 fn test_bit_depth_max_value() {
296 assert_eq!(BitDepth::Eight.max_value(), 255);
297 assert_eq!(BitDepth::Ten.max_value(), 1023);
298 assert_eq!(BitDepth::Twelve.max_value(), 4095);
299 }
300
301 #[test]
302 fn test_bit_depth_bits() {
303 assert_eq!(BitDepth::Eight.bits(), 8);
304 assert_eq!(BitDepth::Ten.bits(), 10);
305 assert_eq!(BitDepth::Twelve.bits(), 12);
306 }
307
308 #[test]
309 fn test_level_range_limited_8bit() {
310 let levels = LevelRange::new(ColorRange::Limited, BitDepth::Eight);
311 assert_eq!(levels.luma_min, 16);
312 assert_eq!(levels.luma_max, 235);
313 assert_eq!(levels.chroma_min, 16);
314 assert_eq!(levels.chroma_max, 240);
315 }
316
317 #[test]
318 fn test_level_range_full_8bit() {
319 let levels = LevelRange::new(ColorRange::Full, BitDepth::Eight);
320 assert_eq!(levels.luma_min, 0);
321 assert_eq!(levels.luma_max, 255);
322 assert_eq!(levels.chroma_min, 0);
323 assert_eq!(levels.chroma_max, 255);
324 }
325
326 #[test]
327 fn test_level_range_limited_10bit() {
328 let levels = LevelRange::new(ColorRange::Limited, BitDepth::Ten);
329 assert_eq!(levels.luma_min, 64);
330 assert_eq!(levels.luma_max, 940);
331 assert_eq!(levels.chroma_min, 64);
332 assert_eq!(levels.chroma_max, 960);
333 }
334
335 #[test]
336 fn test_clamp_luma() {
337 let levels = LevelRange::new(ColorRange::Limited, BitDepth::Eight);
338 assert_eq!(clamp_luma(0, &levels), 16);
339 assert_eq!(clamp_luma(128, &levels), 128);
340 assert_eq!(clamp_luma(255, &levels), 235);
341 }
342
343 #[test]
344 fn test_limited_to_full_luma_8bit() {
345 let depth = BitDepth::Eight;
346 assert_eq!(limited_to_full_luma(16, depth), 0);
347 assert_eq!(limited_to_full_luma(235, depth), 255);
348 let mid = limited_to_full_luma(126, depth);
350 assert!(mid > 100 && mid < 160);
351 }
352
353 #[test]
354 fn test_full_to_limited_luma_8bit() {
355 let depth = BitDepth::Eight;
356 assert_eq!(full_to_limited_luma(0, depth), 16);
357 assert_eq!(full_to_limited_luma(255, depth), 235);
358 }
359
360 #[test]
361 fn test_roundtrip_luma() {
362 let depth = BitDepth::Eight;
363 for v in (16..=235).step_by(10) {
364 let full = limited_to_full_luma(v, depth);
365 let back = full_to_limited_luma(full, depth);
366 assert!(
367 (back as i32 - v as i32).unsigned_abs() <= 1,
368 "roundtrip failed for {v}"
369 );
370 }
371 }
372
373 #[test]
374 fn test_compliance_report() {
375 let report = ComplianceReport {
376 luma_violations: 0,
377 chroma_violations: 0,
378 total_samples: 100,
379 };
380 assert!(report.is_compliant());
381 assert!((report.violation_ratio() - 0.0).abs() < f64::EPSILON);
382
383 let bad = ComplianceReport {
384 luma_violations: 5,
385 chroma_violations: 3,
386 total_samples: 100,
387 };
388 assert!(!bad.is_compliant());
389 assert!((bad.violation_ratio() - 0.08).abs() < f64::EPSILON);
390 }
391
392 #[test]
393 fn test_check_luma_compliance() {
394 let levels = LevelRange::new(ColorRange::Limited, BitDepth::Eight);
395 let samples = vec![0, 16, 128, 235, 255];
396 let violations = check_luma_compliance(&samples, &levels);
397 assert_eq!(violations, 2); }
399
400 #[test]
401 fn test_convert_luma_buffer_same_range() {
402 let buf = vec![16, 128, 235];
403 let result = convert_luma_buffer(
404 &buf,
405 ColorRange::Limited,
406 ColorRange::Limited,
407 BitDepth::Eight,
408 );
409 assert_eq!(result, buf);
410 }
411
412 #[test]
413 fn test_convert_chroma_buffer() {
414 let buf = vec![16, 128, 240];
415 let result =
416 convert_chroma_buffer(&buf, ColorRange::Limited, ColorRange::Full, BitDepth::Eight);
417 assert_eq!(result[0], 0);
418 assert_eq!(result[2], 255);
419 }
420}