1#![allow(dead_code)]
9#![allow(clippy::cast_precision_loss)]
10#![allow(clippy::cast_possible_truncation)]
11#![allow(clippy::cast_sign_loss)]
12
13use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17pub enum ChromaSubsampling {
18 Yuv444,
20 Yuv422,
22 Yuv420,
24 Yuv411,
26}
27
28impl ChromaSubsampling {
29 pub fn h_factor(&self) -> u32 {
31 match self {
32 Self::Yuv444 => 1,
33 Self::Yuv422 | Self::Yuv420 => 2,
34 Self::Yuv411 => 4,
35 }
36 }
37
38 pub fn v_factor(&self) -> u32 {
40 match self {
41 Self::Yuv444 | Self::Yuv422 | Self::Yuv411 => 1,
42 Self::Yuv420 => 2,
43 }
44 }
45
46 pub fn chroma_width(&self, luma_width: u32) -> u32 {
48 (luma_width + self.h_factor() - 1) / self.h_factor()
49 }
50
51 pub fn chroma_height(&self, luma_height: u32) -> u32 {
53 (luma_height + self.v_factor() - 1) / self.v_factor()
54 }
55}
56
57impl std::fmt::Display for ChromaSubsampling {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 match self {
60 Self::Yuv444 => write!(f, "4:4:4"),
61 Self::Yuv422 => write!(f, "4:2:2"),
62 Self::Yuv420 => write!(f, "4:2:0"),
63 Self::Yuv411 => write!(f, "4:1:1"),
64 }
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
70pub enum ChromaLocation {
71 Left,
73 Center,
75 TopLeft,
77}
78
79impl ChromaLocation {
80 pub fn h_offset(&self) -> f64 {
82 match self {
83 Self::Left | Self::TopLeft => 0.0,
84 Self::Center => 0.5,
85 }
86 }
87
88 pub fn v_offset(&self) -> f64 {
90 match self {
91 Self::Left | Self::Center => 0.5,
92 Self::TopLeft => 0.0,
93 }
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct ChromaScaleResult {
100 pub luma_width: u32,
102 pub luma_height: u32,
104 pub chroma_width: u32,
106 pub chroma_height: u32,
108 pub adjusted: bool,
110}
111
112#[derive(Debug, Clone, Copy)]
114pub struct ChromaScaler {
115 pub subsampling: ChromaSubsampling,
117 pub location: ChromaLocation,
119}
120
121impl ChromaScaler {
122 pub fn new(subsampling: ChromaSubsampling, location: ChromaLocation) -> Self {
124 Self {
125 subsampling,
126 location,
127 }
128 }
129
130 pub fn align_to_subsampling(&self, dim: u32, factor: u32) -> u32 {
132 if factor <= 1 {
133 return dim;
134 }
135 ((dim + factor - 1) / factor) * factor
136 }
137
138 pub fn compute_scaled_dims(
140 &self,
141 _src_w: u32,
142 _src_h: u32,
143 dst_w: u32,
144 dst_h: u32,
145 ) -> ChromaScaleResult {
146 let h_fac = self.subsampling.h_factor();
147 let v_fac = self.subsampling.v_factor();
148
149 let aligned_w = self.align_to_subsampling(dst_w, h_fac);
150 let aligned_h = self.align_to_subsampling(dst_h, v_fac);
151 let adjusted = aligned_w != dst_w || aligned_h != dst_h;
152
153 ChromaScaleResult {
154 luma_width: aligned_w,
155 luma_height: aligned_h,
156 chroma_width: self.subsampling.chroma_width(aligned_w),
157 chroma_height: self.subsampling.chroma_height(aligned_h),
158 adjusted,
159 }
160 }
161
162 pub fn total_samples(&self, luma_w: u32, luma_h: u32) -> u64 {
164 let luma = luma_w as u64 * luma_h as u64;
165 let cw = self.subsampling.chroma_width(luma_w) as u64;
166 let ch = self.subsampling.chroma_height(luma_h) as u64;
167 luma + 2 * cw * ch
168 }
169
170 pub fn chroma_ratio(&self) -> f64 {
172 let h = self.subsampling.h_factor() as f64;
173 let v = self.subsampling.v_factor() as f64;
174 2.0 / (h * v)
175 }
176}
177
178#[derive(Debug, Clone)]
186pub struct ChromaPlaneResampler {
187 pub subsampling: ChromaSubsampling,
189 pub src_location: ChromaLocation,
191 pub dst_location: ChromaLocation,
193}
194
195impl ChromaPlaneResampler {
196 pub fn same_siting(subsampling: ChromaSubsampling, location: ChromaLocation) -> Self {
198 Self {
199 subsampling,
200 src_location: location,
201 dst_location: location,
202 }
203 }
204
205 pub fn new(
207 subsampling: ChromaSubsampling,
208 src_location: ChromaLocation,
209 dst_location: ChromaLocation,
210 ) -> Self {
211 Self {
212 subsampling,
213 src_location,
214 dst_location,
215 }
216 }
217
218 pub fn resample_plane(
220 &self,
221 src: &[u8],
222 src_luma_w: u32,
223 src_luma_h: u32,
224 dst_luma_w: u32,
225 dst_luma_h: u32,
226 ) -> Vec<u8> {
227 let src_cw = self.subsampling.chroma_width(src_luma_w) as usize;
228 let src_ch = self.subsampling.chroma_height(src_luma_h) as usize;
229 let dst_cw = self.subsampling.chroma_width(dst_luma_w) as usize;
230 let dst_ch = self.subsampling.chroma_height(dst_luma_h) as usize;
231
232 if src.is_empty() || src_cw == 0 || src_ch == 0 || dst_cw == 0 || dst_ch == 0 {
233 return vec![0u8; dst_cw * dst_ch];
234 }
235
236 let scale_x = src_cw as f64 / dst_cw as f64;
237 let scale_y = src_ch as f64 / dst_ch as f64;
238
239 let hf = self.subsampling.h_factor() as f64;
240 let vf = self.subsampling.v_factor() as f64;
241
242 let src_ph = self.src_location.h_offset() / hf;
243 let src_pv = self.src_location.v_offset() / vf;
244 let dst_ph = self.dst_location.h_offset() / hf;
245 let dst_pv = self.dst_location.v_offset() / vf;
246
247 let mut dst = vec![0u8; dst_cw * dst_ch];
248
249 for cy in 0..dst_ch {
250 let sy_raw = (cy as f64 + dst_pv) * scale_y - src_pv;
251 let sy = sy_raw.clamp(0.0, (src_ch - 1) as f64);
252 let sy0 = sy.floor() as usize;
253 let sy1 = (sy0 + 1).min(src_ch - 1);
254 let fy = sy - sy.floor();
255
256 for cx in 0..dst_cw {
257 let sx_raw = (cx as f64 + dst_ph) * scale_x - src_ph;
258 let sx = sx_raw.clamp(0.0, (src_cw - 1) as f64);
259 let sx0 = sx.floor() as usize;
260 let sx1 = (sx0 + 1).min(src_cw - 1);
261 let fx = sx - sx.floor();
262
263 let p00 = src[sy0 * src_cw + sx0] as f64;
264 let p01 = src[sy0 * src_cw + sx1] as f64;
265 let p10 = src[sy1 * src_cw + sx0] as f64;
266 let p11 = src[sy1 * src_cw + sx1] as f64;
267
268 let val = p00 * (1.0 - fx) * (1.0 - fy)
269 + p01 * fx * (1.0 - fy)
270 + p10 * (1.0 - fx) * fy
271 + p11 * fx * fy;
272
273 dst[cy * dst_cw + cx] = val.round().clamp(0.0, 255.0) as u8;
274 }
275 }
276
277 dst
278 }
279
280 pub fn resample_both_planes(
282 &self,
283 cb_src: &[u8],
284 cr_src: &[u8],
285 src_luma_w: u32,
286 src_luma_h: u32,
287 dst_luma_w: u32,
288 dst_luma_h: u32,
289 ) -> (Vec<u8>, Vec<u8>) {
290 let cb = self.resample_plane(cb_src, src_luma_w, src_luma_h, dst_luma_w, dst_luma_h);
291 let cr = self.resample_plane(cr_src, src_luma_w, src_luma_h, dst_luma_w, dst_luma_h);
292 (cb, cr)
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 fn scaler_420() -> ChromaScaler {
301 ChromaScaler::new(ChromaSubsampling::Yuv420, ChromaLocation::Left)
302 }
303
304 fn scaler_422() -> ChromaScaler {
305 ChromaScaler::new(ChromaSubsampling::Yuv422, ChromaLocation::Left)
306 }
307
308 #[test]
309 fn test_chroma_width_420() {
310 assert_eq!(ChromaSubsampling::Yuv420.chroma_width(1920), 960);
311 }
312
313 #[test]
314 fn test_chroma_height_420() {
315 assert_eq!(ChromaSubsampling::Yuv420.chroma_height(1080), 540);
316 }
317
318 #[test]
319 fn test_chroma_width_422() {
320 assert_eq!(ChromaSubsampling::Yuv422.chroma_width(1920), 960);
321 }
322
323 #[test]
324 fn test_chroma_height_422_full() {
325 assert_eq!(ChromaSubsampling::Yuv422.chroma_height(1080), 1080);
326 }
327
328 #[test]
329 fn test_chroma_444_no_subsampling() {
330 assert_eq!(ChromaSubsampling::Yuv444.chroma_width(1920), 1920);
331 assert_eq!(ChromaSubsampling::Yuv444.chroma_height(1080), 1080);
332 }
333
334 #[test]
335 fn test_chroma_411_quarter_width() {
336 assert_eq!(ChromaSubsampling::Yuv411.chroma_width(1920), 480);
337 assert_eq!(ChromaSubsampling::Yuv411.chroma_height(1080), 1080);
338 }
339
340 #[test]
341 fn test_scaled_dims_420_aligned() {
342 let s = scaler_420();
343 let r = s.compute_scaled_dims(1920, 1080, 1280, 720);
344 assert_eq!(r.luma_width, 1280);
345 assert_eq!(r.luma_height, 720);
346 assert_eq!(r.chroma_width, 640);
347 assert_eq!(r.chroma_height, 360);
348 assert!(!r.adjusted);
349 }
350
351 #[test]
352 fn test_scaled_dims_420_needs_alignment() {
353 let s = scaler_420();
354 let r = s.compute_scaled_dims(1920, 1080, 1281, 721);
355 assert_eq!(r.luma_width, 1282);
356 assert_eq!(r.luma_height, 722);
357 assert!(r.adjusted);
358 }
359
360 #[test]
361 fn test_scaled_dims_444_no_alignment() {
362 let s = ChromaScaler::new(ChromaSubsampling::Yuv444, ChromaLocation::Center);
363 let r = s.compute_scaled_dims(1920, 1080, 1281, 721);
364 assert_eq!(r.luma_width, 1281);
365 assert_eq!(r.luma_height, 721);
366 assert!(!r.adjusted);
367 }
368
369 #[test]
370 fn test_total_samples_420() {
371 let s = scaler_420();
372 assert_eq!(s.total_samples(1920, 1080), 3_110_400);
373 }
374
375 #[test]
376 fn test_total_samples_444() {
377 let s = ChromaScaler::new(ChromaSubsampling::Yuv444, ChromaLocation::Left);
378 assert_eq!(s.total_samples(1920, 1080), 6_220_800);
379 }
380
381 #[test]
382 fn test_chroma_ratio_420() {
383 let s = scaler_420();
384 assert!((s.chroma_ratio() - 0.5).abs() < 1e-6);
385 }
386
387 #[test]
388 fn test_chroma_ratio_422() {
389 let s = scaler_422();
390 assert!((s.chroma_ratio() - 1.0).abs() < 1e-6);
391 }
392
393 #[test]
394 fn test_chroma_location_offsets() {
395 assert!((ChromaLocation::Left.h_offset() - 0.0).abs() < 1e-6);
396 assert!((ChromaLocation::Center.h_offset() - 0.5).abs() < 1e-6);
397 assert!((ChromaLocation::TopLeft.v_offset() - 0.0).abs() < 1e-6);
398 }
399
400 #[test]
401 fn test_subsampling_display() {
402 assert_eq!(ChromaSubsampling::Yuv420.to_string(), "4:2:0");
403 assert_eq!(ChromaSubsampling::Yuv422.to_string(), "4:2:2");
404 assert_eq!(ChromaSubsampling::Yuv444.to_string(), "4:4:4");
405 assert_eq!(ChromaSubsampling::Yuv411.to_string(), "4:1:1");
406 }
407
408 #[test]
409 fn test_align_to_subsampling() {
410 let s = scaler_420();
411 assert_eq!(s.align_to_subsampling(1920, 2), 1920);
412 assert_eq!(s.align_to_subsampling(1921, 2), 1922);
413 assert_eq!(s.align_to_subsampling(100, 4), 100);
414 assert_eq!(s.align_to_subsampling(101, 4), 104);
415 }
416
417 #[test]
418 fn test_chroma_odd_dimension_rounds_up() {
419 assert_eq!(ChromaSubsampling::Yuv420.chroma_width(1921), 961);
420 }
421
422 #[test]
425 fn test_resample_plane_420_downscale_output_size() {
426 let r = ChromaPlaneResampler::same_siting(ChromaSubsampling::Yuv420, ChromaLocation::Left);
427 let src = vec![128u8; 960 * 540];
428 let dst = r.resample_plane(&src, 1920, 1080, 960, 540);
429 assert_eq!(dst.len(), 480 * 270);
430 }
431
432 #[test]
433 fn test_resample_plane_420_flat_field_preserves_value() {
434 let r = ChromaPlaneResampler::same_siting(ChromaSubsampling::Yuv420, ChromaLocation::Left);
435 let src = vec![200u8; 8 * 4];
436 let dst = r.resample_plane(&src, 16, 8, 8, 4);
437 for &v in &dst {
438 assert_eq!(v, 200);
439 }
440 }
441
442 #[test]
443 fn test_resample_plane_444_identity_size() {
444 let r = ChromaPlaneResampler::same_siting(ChromaSubsampling::Yuv444, ChromaLocation::Left);
445 let src = vec![100u8; 16 * 8];
446 let dst = r.resample_plane(&src, 16, 8, 8, 4);
447 assert_eq!(dst.len(), 8 * 4);
448 }
449
450 #[test]
451 fn test_resample_plane_422_height_preserved() {
452 let r = ChromaPlaneResampler::same_siting(ChromaSubsampling::Yuv422, ChromaLocation::Left);
453 let src = vec![150u8; 8 * 8];
454 let dst = r.resample_plane(&src, 16, 8, 8, 4);
455 assert_eq!(dst.len(), 4 * 4);
456 }
457
458 #[test]
459 fn test_resample_plane_empty_source_returns_zeros() {
460 let r = ChromaPlaneResampler::same_siting(ChromaSubsampling::Yuv420, ChromaLocation::Left);
461 let dst = r.resample_plane(&[], 16, 8, 8, 4);
462 let dst_cw = ChromaSubsampling::Yuv420.chroma_width(8) as usize;
463 let dst_ch = ChromaSubsampling::Yuv420.chroma_height(4) as usize;
464 assert_eq!(dst.len(), dst_cw * dst_ch);
465 assert!(dst.iter().all(|&v| v == 0));
466 }
467
468 #[test]
469 fn test_resample_plane_center_vs_left_siting_differs() {
470 let r_left =
471 ChromaPlaneResampler::same_siting(ChromaSubsampling::Yuv420, ChromaLocation::Left);
472 let r_center =
473 ChromaPlaneResampler::same_siting(ChromaSubsampling::Yuv420, ChromaLocation::Center);
474
475 let src_cw = ChromaSubsampling::Yuv420.chroma_width(32) as usize;
476 let src_ch = ChromaSubsampling::Yuv420.chroma_height(16) as usize;
477 let src: Vec<u8> = (0..src_cw * src_ch)
478 .map(|i| ((i * 3) % 200) as u8)
479 .collect();
480
481 let dst_left = r_left.resample_plane(&src, 32, 16, 16, 8);
482 let dst_center = r_center.resample_plane(&src, 32, 16, 16, 8);
483
484 assert_eq!(dst_left.len(), dst_center.len());
485 let differs = dst_left.iter().zip(dst_center.iter()).any(|(a, b)| a != b);
486 assert!(differs, "different siting should produce different results");
487 }
488
489 #[test]
490 fn test_resample_both_planes_returns_correct_sizes() {
491 let r = ChromaPlaneResampler::same_siting(ChromaSubsampling::Yuv420, ChromaLocation::Left);
492 let src_cw = ChromaSubsampling::Yuv420.chroma_width(16) as usize;
493 let src_ch = ChromaSubsampling::Yuv420.chroma_height(8) as usize;
494 let cb_src = vec![100u8; src_cw * src_ch];
495 let cr_src = vec![150u8; src_cw * src_ch];
496 let (cb_dst, cr_dst) = r.resample_both_planes(&cb_src, &cr_src, 16, 8, 8, 4);
497 let dst_cw = ChromaSubsampling::Yuv420.chroma_width(8) as usize;
498 let dst_ch = ChromaSubsampling::Yuv420.chroma_height(4) as usize;
499 assert_eq!(cb_dst.len(), dst_cw * dst_ch);
500 assert_eq!(cr_dst.len(), dst_cw * dst_ch);
501 }
502
503 #[test]
504 fn test_resample_plane_420_upscale_output_size() {
505 let r = ChromaPlaneResampler::same_siting(ChromaSubsampling::Yuv420, ChromaLocation::Left);
506 let src_cw = ChromaSubsampling::Yuv420.chroma_width(16) as usize;
507 let src_ch = ChromaSubsampling::Yuv420.chroma_height(8) as usize;
508 let src = vec![64u8; src_cw * src_ch];
509 let dst = r.resample_plane(&src, 16, 8, 32, 16);
510 let exp_cw = ChromaSubsampling::Yuv420.chroma_width(32) as usize;
511 let exp_ch = ChromaSubsampling::Yuv420.chroma_height(16) as usize;
512 assert_eq!(dst.len(), exp_cw * exp_ch);
513 }
514
515 #[test]
516 fn test_resampler_cross_siting_constructs() {
517 let r = ChromaPlaneResampler::new(
518 ChromaSubsampling::Yuv420,
519 ChromaLocation::Left,
520 ChromaLocation::Center,
521 );
522 assert_eq!(r.subsampling, ChromaSubsampling::Yuv420);
523 }
524}