use fast_image_resize::images::{Image, ImageRef};
use fast_image_resize::{FilterType, PixelType, ResizeAlg, ResizeOptions, Resizer};
use crate::decode::{PixelLayout, YuvFrame, chroma_size};
const MIN_SHRINK: f64 = 0.9;
pub fn fit_within(width: u32, height: u32, max: (u32, u32)) -> Option<(u32, u32)> {
let (mw, mh) = max;
if width == 0 || height == 0 || mw == 0 || mh == 0 {
return None;
}
let scale = (mw as f64 / width as f64).min(mh as f64 / height as f64);
if scale >= MIN_SHRINK {
return None;
}
let even = |v: f64| ((v.round() as u32) & !1).max(2);
Some((even(width as f64 * scale), even(height as f64 * scale)))
}
#[derive(Default)]
pub struct FrameScaler {
resizer: Resizer,
}
impl FrameScaler {
pub fn downscale(&mut self, frame: &YuvFrame, max: (u32, u32)) -> Option<YuvFrame> {
let (tw, th) = fit_within(frame.width, frame.height, max)?;
let options = ResizeOptions::new().resize_alg(ResizeAlg::Convolution(FilterType::Bilinear));
let (cw, ch) = frame.chroma_size();
let (tcw, tch) = chroma_size(frame.layout, tw, th);
let dims = [(frame.width, frame.height, tw, th), (cw, ch, tcw, tch), (cw, ch, tcw, tch)];
let mut planes: [Vec<u8>; 3] = Default::default();
for (i, &(w, h, nw, nh)) in dims.iter().enumerate() {
if frame.layout == PixelLayout::I400 && i > 0 {
continue;
}
let packed = pack(&frame.planes[i], frame.strides[i], w as usize, h as usize);
let src = ImageRef::new(w, h, &packed, PixelType::U8).ok()?;
let mut dst = Image::new(nw, nh, PixelType::U8);
self.resizer.resize(&src, &mut dst, &options).ok()?;
planes[i] = dst.into_vec();
}
Some(YuvFrame {
width: tw,
height: th,
layout: frame.layout,
planes,
strides: [tw as usize, tcw as usize, tcw as usize],
matrix: frame.matrix,
full_range: frame.full_range,
pts: frame.pts,
})
}
}
fn pack(plane: &[u8], stride: usize, w: usize, h: usize) -> std::borrow::Cow<'_, [u8]> {
if stride == w {
return std::borrow::Cow::Borrowed(&plane[..w * h]);
}
std::borrow::Cow::Owned((0..h).flat_map(|y| &plane[y * stride..y * stride + w]).copied().collect())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::decode::ColorMatrix;
use std::time::Duration;
#[test]
fn fits_keeping_aspect_with_even_sizes() {
assert_eq!(fit_within(3840, 2160, (1920, 1200)), Some((1920, 1080)));
assert_eq!(fit_within(3840, 2160, (1000, 1000)), Some((1000, 562)));
assert_eq!(fit_within(1920, 1080, (1900, 1080)), None, "within 10 %: left alone");
assert_eq!(fit_within(1920, 1080, (3840, 2160)), None, "never upscaled");
assert_eq!(fit_within(1920, 1080, (0, 0)), None, "no limit");
}
#[test]
fn downscales_all_planes_and_keeps_content() {
let (w, h) = (64u32, 48u32);
let y: Vec<u8> = (0..h).flat_map(|_| (0..70u32).map(|x| (x * 4).min(255) as u8)).collect();
let frame = YuvFrame {
width: w,
height: h,
layout: PixelLayout::I420,
planes: [y, vec![90; 32 * 24], vec![200; 32 * 24]],
strides: [70, 32, 32],
matrix: ColorMatrix::Bt709,
full_range: false,
pts: Duration::from_millis(7),
};
let small = FrameScaler::default().downscale(&frame, (32, 32)).unwrap();
assert_eq!((small.width, small.height, small.layout, small.pts), (32, 24, PixelLayout::I420, frame.pts));
assert_eq!((small.planes[0].len(), small.planes[1].len(), small.planes[2].len()), (32 * 24, 16 * 12, 16 * 12));
assert!(small.planes[1].iter().all(|&v| v == 90) && small.planes[2].iter().all(|&v| v == 200));
let row = &small.planes[0][..32];
assert!(row[0] < 20 && row[31] > 230, "{row:?}");
assert!(row.windows(2).all(|p| p[0] <= p[1]), "{row:?}");
}
#[test]
fn fitting_frames_are_left_alone() {
let frame = YuvFrame {
width: 4,
height: 4,
layout: PixelLayout::I420,
planes: [vec![0; 16], vec![0; 4], vec![0; 4]],
strides: [4, 2, 2],
matrix: ColorMatrix::Bt709,
full_range: false,
pts: Duration::ZERO,
};
assert!(FrameScaler::default().downscale(&frame, (8, 8)).is_none());
}
}