1use rayon::prelude::*;
27use thiserror::Error;
28
29#[derive(Debug, Clone, PartialEq, Error)]
33pub enum ScaleError {
34 #[error("Buffer size mismatch: expected {expected}, got {actual}")]
36 BufferSizeMismatch { expected: usize, actual: usize },
37 #[error("Invalid dimensions: {width}x{height}")]
39 InvalidDimensions { width: u32, height: u32 },
40 #[error("Pixel count overflow")]
42 PixelCountOverflow,
43 #[error("Planar plane size mismatch: plane '{plane}' has {actual} bytes, expected {expected}")]
45 PlanarMismatch {
46 plane: &'static str,
47 expected: usize,
48 actual: usize,
49 },
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
56pub enum ScaleFilter {
57 Nearest,
59 Bilinear,
61 Bicubic,
63 Area,
65}
66
67impl ScaleFilter {
68 #[must_use]
70 pub fn label(self) -> &'static str {
71 match self {
72 Self::Nearest => "nearest",
73 Self::Bilinear => "bilinear",
74 Self::Bicubic => "bicubic",
75 Self::Area => "area",
76 }
77 }
78}
79
80#[derive(Debug, Clone, Default)]
84pub struct ScaleStats {
85 pub dst_pixels: u64,
87 pub src_pixels_read: u64,
89 pub filter: Option<ScaleFilter>,
91}
92
93#[derive(Debug, Clone, Default)]
99pub struct ScaleKernel;
100
101impl ScaleKernel {
102 fn validate_rgba(buf: &[u8], width: u32, height: u32) -> Result<usize, ScaleError> {
104 if width == 0 || height == 0 {
105 return Err(ScaleError::InvalidDimensions { width, height });
106 }
107 let pixels = (width as usize)
108 .checked_mul(height as usize)
109 .ok_or(ScaleError::PixelCountOverflow)?;
110 let expected = pixels * 4;
111 if buf.len() != expected {
112 return Err(ScaleError::BufferSizeMismatch {
113 expected,
114 actual: buf.len(),
115 });
116 }
117 Ok(pixels)
118 }
119
120 fn validate_plane(
122 buf: &[u8],
123 width: u32,
124 height: u32,
125 name: &'static str,
126 ) -> Result<usize, ScaleError> {
127 if width == 0 || height == 0 {
128 return Err(ScaleError::InvalidDimensions { width, height });
129 }
130 let expected = (width as usize)
131 .checked_mul(height as usize)
132 .ok_or(ScaleError::PixelCountOverflow)?;
133 if buf.len() != expected {
134 return Err(ScaleError::PlanarMismatch {
135 plane: name,
136 expected,
137 actual: buf.len(),
138 });
139 }
140 Ok(expected)
141 }
142
143 pub fn scale_plane(
151 src: &[u8],
152 src_w: u32,
153 src_h: u32,
154 dst: &mut [u8],
155 dst_w: u32,
156 dst_h: u32,
157 filter: ScaleFilter,
158 ) -> Result<ScaleStats, ScaleError> {
159 Self::validate_plane(src, src_w, src_h, "src")?;
160 let dst_pixels = Self::validate_plane(dst, dst_w, dst_h, "dst")?;
161
162 let scale_x = src_w as f32 / dst_w as f32;
163 let scale_y = src_h as f32 / dst_h as f32;
164
165 dst.par_iter_mut().enumerate().for_each(|(i, out)| {
166 let dy = (i / dst_w as usize) as f32;
167 let dx = (i % dst_w as usize) as f32;
168 *out = match filter {
169 ScaleFilter::Nearest => {
170 sample_nearest_plane(src, src_w, src_h, dx, dy, scale_x, scale_y)
171 }
172 ScaleFilter::Bilinear => {
173 sample_bilinear_plane(src, src_w, src_h, dx, dy, scale_x, scale_y)
174 }
175 ScaleFilter::Bicubic => {
176 sample_bicubic_plane(src, src_w, src_h, dx, dy, scale_x, scale_y)
177 }
178 ScaleFilter::Area => sample_area_plane(src, src_w, src_h, dx, dy, scale_x, scale_y),
179 };
180 });
181
182 Ok(ScaleStats {
183 dst_pixels: dst_pixels as u64,
184 src_pixels_read: (src_w * src_h) as u64,
185 filter: Some(filter),
186 })
187 }
188
189 pub fn scale_rgba(
195 src: &[u8],
196 src_w: u32,
197 src_h: u32,
198 dst: &mut [u8],
199 dst_w: u32,
200 dst_h: u32,
201 filter: ScaleFilter,
202 ) -> Result<ScaleStats, ScaleError> {
203 Self::validate_rgba(src, src_w, src_h)?;
204 let dst_pixels = Self::validate_rgba(dst, dst_w, dst_h)?;
205
206 let scale_x = src_w as f32 / dst_w as f32;
207 let scale_y = src_h as f32 / dst_h as f32;
208
209 dst.par_chunks_mut(4).enumerate().for_each(|(i, out_px)| {
210 let dy = (i / dst_w as usize) as f32;
211 let dx = (i % dst_w as usize) as f32;
212
213 match filter {
214 ScaleFilter::Nearest => {
215 sample_nearest_rgba(src, src_w, src_h, dx, dy, scale_x, scale_y, out_px);
216 }
217 ScaleFilter::Bilinear => {
218 sample_bilinear_rgba(src, src_w, src_h, dx, dy, scale_x, scale_y, out_px);
219 }
220 ScaleFilter::Bicubic => {
221 sample_bicubic_rgba(src, src_w, src_h, dx, dy, scale_x, scale_y, out_px);
222 }
223 ScaleFilter::Area => {
224 sample_area_rgba(src, src_w, src_h, dx, dy, scale_x, scale_y, out_px);
225 }
226 }
227 });
228
229 Ok(ScaleStats {
230 dst_pixels: dst_pixels as u64,
231 src_pixels_read: (src_w * src_h) as u64,
232 filter: Some(filter),
233 })
234 }
235
236 pub fn scale_yuv420(
245 y_src: &[u8],
246 cb_src: &[u8],
247 cr_src: &[u8],
248 src_w: u32,
249 src_h: u32,
250 dst_w: u32,
251 dst_h: u32,
252 filter: ScaleFilter,
253 ) -> Result<(Vec<u8>, Vec<u8>, Vec<u8>), ScaleError> {
254 Self::validate_plane(y_src, src_w, src_h, "Y_src")?;
256 let chroma_src_w = (src_w + 1) / 2;
258 let chroma_src_h = (src_h + 1) / 2;
259 Self::validate_plane(cb_src, chroma_src_w, chroma_src_h, "Cb_src")?;
260 Self::validate_plane(cr_src, chroma_src_w, chroma_src_h, "Cr_src")?;
261
262 let chroma_dst_w = (dst_w + 1) / 2;
263 let chroma_dst_h = (dst_h + 1) / 2;
264
265 let mut y_dst = vec![0u8; (dst_w * dst_h) as usize];
267 Self::scale_plane(y_src, src_w, src_h, &mut y_dst, dst_w, dst_h, filter)?;
268
269 let mut cb_dst = vec![0u8; (chroma_dst_w * chroma_dst_h) as usize];
271 Self::scale_plane(
272 cb_src,
273 chroma_src_w,
274 chroma_src_h,
275 &mut cb_dst,
276 chroma_dst_w,
277 chroma_dst_h,
278 filter,
279 )?;
280
281 let mut cr_dst = vec![0u8; (chroma_dst_w * chroma_dst_h) as usize];
283 Self::scale_plane(
284 cr_src,
285 chroma_src_w,
286 chroma_src_h,
287 &mut cr_dst,
288 chroma_dst_w,
289 chroma_dst_h,
290 filter,
291 )?;
292
293 Ok((y_dst, cb_dst, cr_dst))
294 }
295
296 pub fn scale_yuv422(
305 y_src: &[u8],
306 cb_src: &[u8],
307 cr_src: &[u8],
308 src_w: u32,
309 src_h: u32,
310 dst_w: u32,
311 dst_h: u32,
312 filter: ScaleFilter,
313 ) -> Result<(Vec<u8>, Vec<u8>, Vec<u8>), ScaleError> {
314 Self::validate_plane(y_src, src_w, src_h, "Y_src")?;
315 let chroma_src_w = (src_w + 1) / 2;
316 Self::validate_plane(cb_src, chroma_src_w, src_h, "Cb_src")?;
317 Self::validate_plane(cr_src, chroma_src_w, src_h, "Cr_src")?;
318
319 let chroma_dst_w = (dst_w + 1) / 2;
320
321 let mut y_dst = vec![0u8; (dst_w * dst_h) as usize];
322 Self::scale_plane(y_src, src_w, src_h, &mut y_dst, dst_w, dst_h, filter)?;
323
324 let mut cb_dst = vec![0u8; (chroma_dst_w * dst_h) as usize];
325 Self::scale_plane(
326 cb_src,
327 chroma_src_w,
328 src_h,
329 &mut cb_dst,
330 chroma_dst_w,
331 dst_h,
332 filter,
333 )?;
334
335 let mut cr_dst = vec![0u8; (chroma_dst_w * dst_h) as usize];
336 Self::scale_plane(
337 cr_src,
338 chroma_src_w,
339 src_h,
340 &mut cr_dst,
341 chroma_dst_w,
342 dst_h,
343 filter,
344 )?;
345
346 Ok((y_dst, cb_dst, cr_dst))
347 }
348}
349
350#[inline]
354fn clamp_coord(v: i32, max: u32) -> usize {
355 v.clamp(0, max as i32 - 1) as usize
356}
357
358#[inline]
360fn read_plane(src: &[u8], w: u32, _h: u32, x: i32, y: i32) -> f32 {
361 let xi = x.clamp(0, w as i32 - 1) as usize;
362 let yi = y.clamp(0, _h as i32 - 1) as usize;
363 src[yi * w as usize + xi] as f32
364}
365
366fn sample_nearest_plane(
368 src: &[u8],
369 src_w: u32,
370 src_h: u32,
371 dx: f32,
372 dy: f32,
373 scale_x: f32,
374 scale_y: f32,
375) -> u8 {
376 let sx = ((dx + 0.5) * scale_x - 0.5).round() as i32;
377 let sy = ((dy + 0.5) * scale_y - 0.5).round() as i32;
378 let xi = clamp_coord(sx, src_w);
379 let yi = clamp_coord(sy, src_h);
380 src[yi * src_w as usize + xi]
381}
382
383fn sample_bilinear_plane(
385 src: &[u8],
386 src_w: u32,
387 src_h: u32,
388 dx: f32,
389 dy: f32,
390 scale_x: f32,
391 scale_y: f32,
392) -> u8 {
393 let sx = (dx + 0.5) * scale_x - 0.5;
394 let sy = (dy + 0.5) * scale_y - 0.5;
395 let x0 = sx.floor() as i32;
396 let y0 = sy.floor() as i32;
397 let wx = sx - x0 as f32;
398 let wy = sy - y0 as f32;
399
400 let v00 = read_plane(src, src_w, src_h, x0, y0);
401 let v10 = read_plane(src, src_w, src_h, x0 + 1, y0);
402 let v01 = read_plane(src, src_w, src_h, x0, y0 + 1);
403 let v11 = read_plane(src, src_w, src_h, x0 + 1, y0 + 1);
404
405 let result = v00 * (1.0 - wx) * (1.0 - wy)
406 + v10 * wx * (1.0 - wy)
407 + v01 * (1.0 - wx) * wy
408 + v11 * wx * wy;
409 result.round().clamp(0.0, 255.0) as u8
410}
411
412#[inline]
414fn bicubic_weight(t: f32) -> f32 {
415 let t = t.abs();
416 let a = -0.5_f32;
417 if t <= 1.0 {
418 (a + 2.0) * t * t * t - (a + 3.0) * t * t + 1.0
419 } else if t < 2.0 {
420 a * t * t * t - 5.0 * a * t * t + 8.0 * a * t - 4.0 * a
421 } else {
422 0.0
423 }
424}
425
426fn sample_bicubic_plane(
428 src: &[u8],
429 src_w: u32,
430 src_h: u32,
431 dx: f32,
432 dy: f32,
433 scale_x: f32,
434 scale_y: f32,
435) -> u8 {
436 let sx = (dx + 0.5) * scale_x - 0.5;
437 let sy = (dy + 0.5) * scale_y - 0.5;
438 let x0 = sx.floor() as i32;
439 let y0 = sy.floor() as i32;
440
441 let mut sum = 0.0_f32;
442 let mut weight_sum = 0.0_f32;
443 for ky in -1_i32..=2 {
444 let wy = bicubic_weight(sy - (y0 + ky) as f32);
445 for kx in -1_i32..=2 {
446 let wx = bicubic_weight(sx - (x0 + kx) as f32);
447 let w = wx * wy;
448 sum += read_plane(src, src_w, src_h, x0 + kx, y0 + ky) * w;
449 weight_sum += w;
450 }
451 }
452
453 if weight_sum.abs() < 1e-9 {
454 return 0;
455 }
456 (sum / weight_sum).round().clamp(0.0, 255.0) as u8
457}
458
459fn sample_area_plane(
461 src: &[u8],
462 src_w: u32,
463 src_h: u32,
464 dx: f32,
465 dy: f32,
466 scale_x: f32,
467 scale_y: f32,
468) -> u8 {
469 let sx0 = dx * scale_x;
471 let sy0 = dy * scale_y;
472 let sx1 = (dx + 1.0) * scale_x;
473 let sy1 = (dy + 1.0) * scale_y;
474
475 let xi0 = sx0.floor() as i32;
476 let yi0 = sy0.floor() as i32;
477 let xi1 = (sx1.ceil() as i32).min(src_w as i32);
478 let yi1 = (sy1.ceil() as i32).min(src_h as i32);
479
480 if xi1 <= xi0 + 1 && yi1 <= yi0 + 1 {
482 return sample_bilinear_plane(src, src_w, src_h, dx, dy, scale_x, scale_y);
483 }
484
485 let mut sum = 0.0_f32;
486 let mut total_weight = 0.0_f32;
487 for sy in yi0..yi1 {
488 let wy = partial_coverage(sy as f32, sy0, sy1);
489 for sx in xi0..xi1 {
490 let wx = partial_coverage(sx as f32, sx0, sx1);
491 let w = wx * wy;
492 sum += read_plane(src, src_w, src_h, sx, sy) * w;
493 total_weight += w;
494 }
495 }
496 if total_weight < 1e-9 {
497 return 0;
498 }
499 (sum / total_weight).round().clamp(0.0, 255.0) as u8
500}
501
502#[inline]
504fn partial_coverage(p: f32, start: f32, end: f32) -> f32 {
505 let lo = p.max(start);
506 let hi = (p + 1.0).min(end);
507 (hi - lo).max(0.0)
508}
509
510#[inline]
514fn read_rgba(src: &[u8], w: u32, h: u32, x: i32, y: i32) -> [f32; 4] {
515 let xi = x.clamp(0, w as i32 - 1) as usize;
516 let yi = y.clamp(0, h as i32 - 1) as usize;
517 let base = (yi * w as usize + xi) * 4;
518 [
519 src[base] as f32,
520 src[base + 1] as f32,
521 src[base + 2] as f32,
522 src[base + 3] as f32,
523 ]
524}
525
526fn sample_nearest_rgba(
527 src: &[u8],
528 src_w: u32,
529 src_h: u32,
530 dx: f32,
531 dy: f32,
532 scale_x: f32,
533 scale_y: f32,
534 out: &mut [u8],
535) {
536 let sx = ((dx + 0.5) * scale_x - 0.5).round() as i32;
537 let sy = ((dy + 0.5) * scale_y - 0.5).round() as i32;
538 let px = read_rgba(src, src_w, src_h, sx, sy);
539 for (o, &v) in out.iter_mut().zip(px.iter()) {
540 *o = v.round().clamp(0.0, 255.0) as u8;
541 }
542}
543
544fn sample_bilinear_rgba(
545 src: &[u8],
546 src_w: u32,
547 src_h: u32,
548 dx: f32,
549 dy: f32,
550 scale_x: f32,
551 scale_y: f32,
552 out: &mut [u8],
553) {
554 let sx = (dx + 0.5) * scale_x - 0.5;
555 let sy = (dy + 0.5) * scale_y - 0.5;
556 let x0 = sx.floor() as i32;
557 let y0 = sy.floor() as i32;
558 let wx = sx - x0 as f32;
559 let wy = sy - y0 as f32;
560
561 let v00 = read_rgba(src, src_w, src_h, x0, y0);
562 let v10 = read_rgba(src, src_w, src_h, x0 + 1, y0);
563 let v01 = read_rgba(src, src_w, src_h, x0, y0 + 1);
564 let v11 = read_rgba(src, src_w, src_h, x0 + 1, y0 + 1);
565
566 for c in 0..4 {
567 let r = v00[c] * (1.0 - wx) * (1.0 - wy)
568 + v10[c] * wx * (1.0 - wy)
569 + v01[c] * (1.0 - wx) * wy
570 + v11[c] * wx * wy;
571 out[c] = r.round().clamp(0.0, 255.0) as u8;
572 }
573}
574
575fn sample_bicubic_rgba(
576 src: &[u8],
577 src_w: u32,
578 src_h: u32,
579 dx: f32,
580 dy: f32,
581 scale_x: f32,
582 scale_y: f32,
583 out: &mut [u8],
584) {
585 let sx = (dx + 0.5) * scale_x - 0.5;
586 let sy = (dy + 0.5) * scale_y - 0.5;
587 let x0 = sx.floor() as i32;
588 let y0 = sy.floor() as i32;
589
590 let mut sum = [0.0_f32; 4];
591 let mut weight_sum = 0.0_f32;
592 for ky in -1_i32..=2 {
593 let wy = bicubic_weight(sy - (y0 + ky) as f32);
594 for kx in -1_i32..=2 {
595 let wx = bicubic_weight(sx - (x0 + kx) as f32);
596 let w = wx * wy;
597 let px = read_rgba(src, src_w, src_h, x0 + kx, y0 + ky);
598 for c in 0..4 {
599 sum[c] += px[c] * w;
600 }
601 weight_sum += w;
602 }
603 }
604 for c in 0..4 {
605 let v = if weight_sum.abs() > 1e-9 {
606 sum[c] / weight_sum
607 } else {
608 0.0
609 };
610 out[c] = v.round().clamp(0.0, 255.0) as u8;
611 }
612}
613
614fn sample_area_rgba(
615 src: &[u8],
616 src_w: u32,
617 src_h: u32,
618 dx: f32,
619 dy: f32,
620 scale_x: f32,
621 scale_y: f32,
622 out: &mut [u8],
623) {
624 let sx0 = dx * scale_x;
625 let sy0 = dy * scale_y;
626 let sx1 = (dx + 1.0) * scale_x;
627 let sy1 = (dy + 1.0) * scale_y;
628
629 let xi0 = sx0.floor() as i32;
630 let yi0 = sy0.floor() as i32;
631 let xi1 = (sx1.ceil() as i32).min(src_w as i32);
632 let yi1 = (sy1.ceil() as i32).min(src_h as i32);
633
634 if xi1 <= xi0 + 1 && yi1 <= yi0 + 1 {
635 sample_bilinear_rgba(src, src_w, src_h, dx, dy, scale_x, scale_y, out);
636 return;
637 }
638
639 let mut sum = [0.0_f32; 4];
640 let mut total_weight = 0.0_f32;
641 for sy in yi0..yi1 {
642 let wy = partial_coverage(sy as f32, sy0, sy1);
643 for sx in xi0..xi1 {
644 let wx = partial_coverage(sx as f32, sx0, sx1);
645 let w = wx * wy;
646 let px = read_rgba(src, src_w, src_h, sx, sy);
647 for c in 0..4 {
648 sum[c] += px[c] * w;
649 }
650 total_weight += w;
651 }
652 }
653 for c in 0..4 {
654 let v = if total_weight > 1e-9 {
655 sum[c] / total_weight
656 } else {
657 0.0
658 };
659 out[c] = v.round().clamp(0.0, 255.0) as u8;
660 }
661}
662
663#[cfg(test)]
666mod tests {
667 use super::*;
668
669 #[test]
672 fn test_scale_filter_labels() {
673 assert_eq!(ScaleFilter::Nearest.label(), "nearest");
674 assert_eq!(ScaleFilter::Bilinear.label(), "bilinear");
675 assert_eq!(ScaleFilter::Bicubic.label(), "bicubic");
676 assert_eq!(ScaleFilter::Area.label(), "area");
677 }
678
679 #[test]
682 fn test_scale_rgba_invalid_src_dims() {
683 let src = vec![0u8; 4];
684 let mut dst = vec![0u8; 4];
685 let err = ScaleKernel::scale_rgba(&src, 0, 1, &mut dst, 1, 1, ScaleFilter::Nearest);
686 assert!(matches!(err, Err(ScaleError::InvalidDimensions { .. })));
687 }
688
689 #[test]
690 fn test_scale_rgba_buffer_mismatch() {
691 let src = vec![0u8; 8]; let mut dst = vec![0u8; 4];
693 let err = ScaleKernel::scale_rgba(&src, 1, 1, &mut dst, 1, 1, ScaleFilter::Bilinear);
694 assert!(matches!(err, Err(ScaleError::BufferSizeMismatch { .. })));
695 }
696
697 #[test]
698 fn test_scale_plane_invalid_dims() {
699 let src = vec![0u8; 1];
700 let mut dst = vec![0u8; 4];
701 let err = ScaleKernel::scale_plane(&src, 0, 0, &mut dst, 2, 2, ScaleFilter::Nearest);
702 assert!(matches!(err, Err(ScaleError::InvalidDimensions { .. })));
703 }
704
705 fn identity_scale(filter: ScaleFilter) {
708 let src: Vec<u8> = (0..4 * 4 * 4).map(|i| (i * 7 % 256) as u8).collect();
710 let mut dst = vec![0u8; 4 * 4 * 4];
711 ScaleKernel::scale_rgba(&src, 4, 4, &mut dst, 4, 4, filter).unwrap();
712 for (i, (&s, &d)) in src.iter().zip(dst.iter()).enumerate() {
714 let diff = (s as i16 - d as i16).abs();
715 assert!(
716 diff <= 1,
717 "filter={}: pixel {i}: src={s} dst={d}",
718 filter.label()
719 );
720 }
721 }
722
723 #[test]
724 fn test_identity_nearest() {
725 identity_scale(ScaleFilter::Nearest);
726 }
727
728 #[test]
729 fn test_identity_bilinear() {
730 identity_scale(ScaleFilter::Bilinear);
731 }
732
733 #[test]
734 fn test_identity_bicubic() {
735 identity_scale(ScaleFilter::Bicubic);
736 }
737
738 #[test]
741 fn test_downscale_2x_preserves_constant_color() {
742 let src: Vec<u8> = (0..8 * 8).flat_map(|_| [255u8, 0, 0, 255]).collect();
744 let mut dst = vec![0u8; 4 * 4 * 4];
745 ScaleKernel::scale_rgba(&src, 8, 8, &mut dst, 4, 4, ScaleFilter::Bilinear).unwrap();
746 for px in dst.chunks(4) {
747 assert_eq!(px[0], 255, "R should be 255");
748 assert_eq!(px[1], 0, "G should be 0");
749 assert_eq!(px[2], 0, "B should be 0");
750 assert_eq!(px[3], 255, "A should be 255");
751 }
752 }
753
754 #[test]
755 fn test_downscale_output_size() {
756 let src = vec![128u8; 64 * 64 * 4];
757 let mut dst = vec![0u8; 32 * 32 * 4];
758 let stats =
759 ScaleKernel::scale_rgba(&src, 64, 64, &mut dst, 32, 32, ScaleFilter::Area).unwrap();
760 assert_eq!(stats.dst_pixels, 32 * 32);
761 }
762
763 #[test]
766 fn test_upscale_output_size() {
767 let src = vec![64u8; 8 * 8 * 4];
768 let mut dst = vec![0u8; 16 * 16 * 4];
769 let stats =
770 ScaleKernel::scale_rgba(&src, 8, 8, &mut dst, 16, 16, ScaleFilter::Bicubic).unwrap();
771 assert_eq!(stats.dst_pixels, 16 * 16);
772 }
773
774 #[test]
777 fn test_scale_plane_constant_value() {
778 let src = vec![200u8; 16 * 16];
779 let mut dst = vec![0u8; 8 * 8];
780 ScaleKernel::scale_plane(&src, 16, 16, &mut dst, 8, 8, ScaleFilter::Bilinear).unwrap();
781 assert!(
782 dst.iter().all(|&v| v == 200),
783 "constant plane should stay constant"
784 );
785 }
786
787 #[test]
790 fn test_scale_yuv420_output_sizes() {
791 let y_src = vec![128u8; 8 * 8];
792 let cb_src = vec![128u8; 4 * 4];
793 let cr_src = vec![128u8; 4 * 4];
794 let (y_dst, cb_dst, cr_dst) =
795 ScaleKernel::scale_yuv420(&y_src, &cb_src, &cr_src, 8, 8, 4, 4, ScaleFilter::Bilinear)
796 .unwrap();
797 assert_eq!(y_dst.len(), 4 * 4);
798 assert_eq!(cb_dst.len(), 2 * 2);
799 assert_eq!(cr_dst.len(), 2 * 2);
800 }
801
802 #[test]
803 fn test_scale_yuv420_constant_neutral() {
804 let y_src = vec![128u8; 8 * 8];
805 let cb_src = vec![128u8; 4 * 4];
806 let cr_src = vec![128u8; 4 * 4];
807 let (y_dst, cb_dst, cr_dst) =
808 ScaleKernel::scale_yuv420(&y_src, &cb_src, &cr_src, 8, 8, 16, 16, ScaleFilter::Nearest)
809 .unwrap();
810 assert_eq!(y_dst.len(), 16 * 16);
811 assert!(y_dst.iter().all(|&v| v == 128));
812 assert!(cb_dst.iter().all(|&v| v == 128));
813 assert!(cr_dst.iter().all(|&v| v == 128));
814 }
815
816 #[test]
819 fn test_scale_yuv422_output_sizes() {
820 let y_src = vec![128u8; 8 * 4]; let cb_src = vec![128u8; 4 * 4]; let cr_src = vec![128u8; 4 * 4]; let (y_dst, cb_dst, cr_dst) =
824 ScaleKernel::scale_yuv422(&y_src, &cb_src, &cr_src, 8, 4, 4, 2, ScaleFilter::Bilinear)
825 .unwrap();
826 assert_eq!(y_dst.len(), 4 * 2);
827 assert_eq!(cb_dst.len(), 2 * 2);
828 assert_eq!(cr_dst.len(), 2 * 2);
829 }
830
831 #[test]
834 fn test_bicubic_weight_at_zero_is_one() {
835 assert!((bicubic_weight(0.0) - 1.0).abs() < 1e-6);
836 }
837
838 #[test]
839 fn test_bicubic_weight_at_two_is_zero() {
840 assert!(bicubic_weight(2.0).abs() < 1e-6);
841 }
842
843 #[test]
846 fn test_scale_stats_filter_recorded() {
847 let src = vec![0u8; 4 * 4 * 4];
848 let mut dst = vec![0u8; 2 * 2 * 4];
849 let stats = ScaleKernel::scale_rgba(&src, 4, 4, &mut dst, 2, 2, ScaleFilter::Area).unwrap();
850 assert_eq!(stats.filter, Some(ScaleFilter::Area));
851 assert_eq!(stats.dst_pixels, 4);
852 }
853}