#![allow(dead_code)]
#![allow(clippy::cast_precision_loss)]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FitMode {
Contain,
Cover,
Stretch,
ContainNoUpscale,
}
impl std::fmt::Display for FitMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Contain => write!(f, "contain"),
Self::Cover => write!(f, "cover"),
Self::Stretch => write!(f, "stretch"),
Self::ContainNoUpscale => write!(f, "contain-no-upscale"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OutputGeometry {
pub scaled_width: u32,
pub scaled_height: u32,
pub offset_x: u32,
pub offset_y: u32,
pub frame_width: u32,
pub frame_height: u32,
}
impl OutputGeometry {
pub fn has_padding(&self) -> bool {
self.offset_x > 0 || self.offset_y > 0
}
pub fn pad_x(&self) -> u32 {
self.offset_x
}
pub fn pad_y(&self) -> u32 {
self.offset_y
}
}
#[derive(Debug, Clone, Copy)]
pub struct AspectPreserver {
pub mode: FitMode,
}
impl AspectPreserver {
pub fn new(mode: FitMode) -> Self {
Self { mode }
}
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
pub fn compute_output_dims(
&self,
src_w: u32,
src_h: u32,
tgt_w: u32,
tgt_h: u32,
) -> OutputGeometry {
if src_w == 0 || src_h == 0 {
return OutputGeometry {
scaled_width: 0,
scaled_height: 0,
offset_x: 0,
offset_y: 0,
frame_width: tgt_w,
frame_height: tgt_h,
};
}
match self.mode {
FitMode::Stretch => OutputGeometry {
scaled_width: tgt_w,
scaled_height: tgt_h,
offset_x: 0,
offset_y: 0,
frame_width: tgt_w,
frame_height: tgt_h,
},
FitMode::Contain | FitMode::ContainNoUpscale => {
let scale_x = tgt_w as f64 / src_w as f64;
let scale_y = tgt_h as f64 / src_h as f64;
let mut scale = scale_x.min(scale_y);
if self.mode == FitMode::ContainNoUpscale && scale > 1.0 {
scale = 1.0;
}
let sw = (src_w as f64 * scale).round() as u32;
let sh = (src_h as f64 * scale).round() as u32;
let ox = (tgt_w.saturating_sub(sw)) / 2;
let oy = (tgt_h.saturating_sub(sh)) / 2;
OutputGeometry {
scaled_width: sw,
scaled_height: sh,
offset_x: ox,
offset_y: oy,
frame_width: tgt_w,
frame_height: tgt_h,
}
}
FitMode::Cover => {
let scale_x = tgt_w as f64 / src_w as f64;
let scale_y = tgt_h as f64 / src_h as f64;
let scale = scale_x.max(scale_y);
let sw = (src_w as f64 * scale).round() as u32;
let sh = (src_h as f64 * scale).round() as u32;
let ox = 0u32;
let oy = 0u32;
OutputGeometry {
scaled_width: sw,
scaled_height: sh,
offset_x: ox,
offset_y: oy,
frame_width: tgt_w,
frame_height: tgt_h,
}
}
}
}
pub fn scaled_size(&self, src_w: u32, src_h: u32, tgt_w: u32, tgt_h: u32) -> (u32, u32) {
let g = self.compute_output_dims(src_w, src_h, tgt_w, tgt_h);
(g.scaled_width, g.scaled_height)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stretch_returns_exact_target() {
let ap = AspectPreserver::new(FitMode::Stretch);
let g = ap.compute_output_dims(640, 480, 1920, 1080);
assert_eq!(g.scaled_width, 1920);
assert_eq!(g.scaled_height, 1080);
assert_eq!(g.offset_x, 0);
assert_eq!(g.offset_y, 0);
}
#[test]
fn test_contain_wider_source_letterboxes() {
let ap = AspectPreserver::new(FitMode::Contain);
let g = ap.compute_output_dims(1920, 1080, 1024, 768);
assert_eq!(g.scaled_width, 1024);
assert!(g.offset_y > 0, "expected vertical padding for letterbox");
}
#[test]
fn test_contain_4x3_into_16x9_pillarbox() {
let ap = AspectPreserver::new(FitMode::Contain);
let g = ap.compute_output_dims(640, 480, 1920, 1080);
assert_eq!(g.scaled_height, 1080);
assert_eq!(g.scaled_width, 1440);
assert_eq!(g.offset_x, 240);
assert_eq!(g.offset_y, 0);
}
#[test]
fn test_contain_same_aspect_no_padding() {
let ap = AspectPreserver::new(FitMode::Contain);
let g = ap.compute_output_dims(1920, 1080, 1280, 720);
assert!(!g.has_padding());
}
#[test]
fn test_cover_fills_frame() {
let ap = AspectPreserver::new(FitMode::Cover);
let g = ap.compute_output_dims(640, 480, 1920, 1080);
assert!(g.scaled_width >= 1920 || g.scaled_height >= 1080);
}
#[test]
fn test_contain_no_upscale_small_source() {
let ap = AspectPreserver::new(FitMode::ContainNoUpscale);
let g = ap.compute_output_dims(320, 240, 1920, 1080);
assert_eq!(g.scaled_width, 320);
assert_eq!(g.scaled_height, 240);
}
#[test]
fn test_zero_source_returns_zero_scaled() {
let ap = AspectPreserver::new(FitMode::Contain);
let g = ap.compute_output_dims(0, 0, 1920, 1080);
assert_eq!(g.scaled_width, 0);
assert_eq!(g.scaled_height, 0);
}
#[test]
fn test_output_geometry_has_padding_true() {
let g = OutputGeometry {
scaled_width: 1440,
scaled_height: 1080,
offset_x: 240,
offset_y: 0,
frame_width: 1920,
frame_height: 1080,
};
assert!(g.has_padding());
}
#[test]
fn test_output_geometry_has_padding_false() {
let g = OutputGeometry {
scaled_width: 1920,
scaled_height: 1080,
offset_x: 0,
offset_y: 0,
frame_width: 1920,
frame_height: 1080,
};
assert!(!g.has_padding());
}
#[test]
fn test_pad_x_and_pad_y_accessors() {
let g = OutputGeometry {
scaled_width: 1440,
scaled_height: 810,
offset_x: 240,
offset_y: 135,
frame_width: 1920,
frame_height: 1080,
};
assert_eq!(g.pad_x(), 240);
assert_eq!(g.pad_y(), 135);
}
#[test]
fn test_scaled_size_convenience() {
let ap = AspectPreserver::new(FitMode::Stretch);
let (w, h) = ap.scaled_size(640, 480, 1920, 1080);
assert_eq!((w, h), (1920, 1080));
}
#[test]
fn test_fitmode_display() {
assert_eq!(FitMode::Contain.to_string(), "contain");
assert_eq!(FitMode::Cover.to_string(), "cover");
assert_eq!(FitMode::Stretch.to_string(), "stretch");
assert_eq!(FitMode::ContainNoUpscale.to_string(), "contain-no-upscale");
}
#[test]
fn test_frame_dimensions_always_equal_target() {
let ap = AspectPreserver::new(FitMode::Contain);
let g = ap.compute_output_dims(1280, 800, 1920, 1080);
assert_eq!(g.frame_width, 1920);
assert_eq!(g.frame_height, 1080);
}
#[test]
fn test_contain_square_source_into_landscape() {
let ap = AspectPreserver::new(FitMode::Contain);
let g = ap.compute_output_dims(1000, 1000, 1920, 1080);
assert_eq!(g.scaled_height, 1080);
assert_eq!(g.scaled_width, 1080);
assert_eq!(g.offset_x, 420);
}
#[test]
fn test_contain_no_upscale_large_source_does_scale() {
let ap = AspectPreserver::new(FitMode::ContainNoUpscale);
let g = ap.compute_output_dims(3840, 2160, 1920, 1080);
assert_eq!(g.scaled_width, 1920);
assert_eq!(g.scaled_height, 1080);
}
}