#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum FitMode {
#[default]
Fit,
Within,
Cover,
Stretch,
}
pub fn fit_dims(in_w: u32, in_h: u32, max_w: u32, max_h: u32, mode: FitMode) -> (u32, u32) {
if in_w == 0 || in_h == 0 || max_w == 0 || max_h == 0 {
return (0, 0);
}
match mode {
FitMode::Stretch => (max_w, max_h),
FitMode::Cover => (max_w, max_h),
FitMode::Fit => fit_inside(in_w, in_h, max_w, max_h),
FitMode::Within => {
if in_w <= max_w && in_h <= max_h {
(in_w, in_h)
} else {
fit_inside(in_w, in_h, max_w, max_h)
}
}
}
}
pub fn fit_cover_source_crop(in_w: u32, in_h: u32, max_w: u32, max_h: u32) -> (u32, u32, u32, u32) {
if in_w == 0 || in_h == 0 || max_w == 0 || max_h == 0 {
return (0, 0, 0, 0);
}
let cross_s = in_w as u64 * max_h as u64;
let cross_t = in_h as u64 * max_w as u64;
if cross_s == cross_t {
return (0, 0, in_w, in_h);
}
if cross_s > cross_t {
let new_w = proportional(max_w, max_h, in_h, false, in_w, in_h);
if new_w >= in_w {
return (0, 0, in_w, in_h);
}
let x = (in_w - new_w) / 2;
(x, 0, new_w, in_h)
} else {
let new_h = proportional(max_w, max_h, in_w, true, in_w, in_h);
if new_h >= in_h {
return (0, 0, in_w, in_h);
}
let y = (in_h - new_h) / 2;
(0, y, in_w, new_h)
}
}
fn fit_inside(sw: u32, sh: u32, tw: u32, th: u32) -> (u32, u32) {
let ratio_w = tw as f64 / sw as f64;
let ratio_h = th as f64 / sh as f64;
if ratio_w <= ratio_h {
let h = proportional(sw, sh, tw, true, tw, th);
(tw, h)
} else {
let w = proportional(sw, sh, th, false, tw, th);
(w, th)
}
}
fn proportional(
ratio_w: u32,
ratio_h: u32,
basis: u32,
basis_is_width: bool,
target_w: u32,
target_h: u32,
) -> u32 {
let ratio = ratio_w as f64 / ratio_h as f64;
let snap_amount = if basis_is_width {
rounding_loss_height(ratio_w, ratio_h, target_h)
} else {
rounding_loss_width(ratio_w, ratio_h, target_w)
};
let snap_a = if basis_is_width { ratio_h } else { ratio_w };
let snap_b = if basis_is_width { target_h } else { target_w };
let float = if basis_is_width {
basis as f64 / ratio
} else {
ratio * basis as f64
};
let delta_a = (float - snap_a as f64).abs();
let delta_b = (float - snap_b as f64).abs();
let v = if delta_a <= snap_amount && delta_a <= delta_b {
snap_a
} else if delta_b <= snap_amount {
snap_b
} else {
float.round() as u32
};
if v == 0 { 1 } else { v }
}
fn rounding_loss_width(ratio_w: u32, ratio_h: u32, target_width: u32) -> f64 {
let ratio = ratio_w as f64 / ratio_h as f64;
let target_x_to_self_x = target_width as f64 / ratio_w as f64;
let recreate_y = ratio_h as f64 * target_x_to_self_x;
let rounded_y = recreate_y.round();
let recreate_x_from_rounded_y = rounded_y * ratio;
(target_width as f64 - recreate_x_from_rounded_y).abs()
}
fn rounding_loss_height(ratio_w: u32, ratio_h: u32, target_height: u32) -> f64 {
let ratio = ratio_w as f64 / ratio_h as f64;
let target_y_to_self_y = target_height as f64 / ratio_h as f64;
let recreate_x = ratio_w as f64 * target_y_to_self_y;
let rounded_x = recreate_x.round();
let recreate_y_from_rounded_x = rounded_x / ratio;
(target_height as f64 - recreate_y_from_rounded_x).abs()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zero_inputs() {
assert_eq!(fit_dims(0, 100, 800, 600, FitMode::Fit), (0, 0));
assert_eq!(fit_dims(100, 0, 800, 600, FitMode::Fit), (0, 0));
assert_eq!(fit_dims(100, 100, 0, 600, FitMode::Fit), (0, 0));
assert_eq!(fit_dims(100, 100, 800, 0, FitMode::Fit), (0, 0));
}
#[test]
fn stretch_ignores_aspect() {
assert_eq!(fit_dims(100, 100, 800, 600, FitMode::Stretch), (800, 600));
assert_eq!(fit_dims(1, 10000, 500, 500, FitMode::Stretch), (500, 500));
}
#[test]
fn fit_wider_source() {
assert_eq!(fit_dims(1600, 900, 800, 600, FitMode::Fit), (800, 450));
}
#[test]
fn fit_taller_source() {
assert_eq!(fit_dims(900, 1600, 800, 600, FitMode::Fit), (338, 600));
}
#[test]
fn fit_upscales() {
assert_eq!(fit_dims(400, 300, 800, 600, FitMode::Fit), (800, 600));
}
#[test]
fn within_refuses_upscale() {
assert_eq!(fit_dims(400, 300, 800, 600, FitMode::Within), (400, 300));
assert_eq!(fit_dims(1600, 900, 800, 600, FitMode::Within), (800, 450));
}
#[test]
fn cover_returns_target_dims() {
assert_eq!(fit_dims(1600, 900, 800, 600, FitMode::Cover), (800, 600));
assert_eq!(fit_dims(900, 1600, 800, 600, FitMode::Cover), (800, 600));
}
#[test]
fn never_collapses_to_zero() {
let (w, h) = fit_dims(10_000, 1, 10, 10, FitMode::Fit);
assert!(w >= 1 && h >= 1, "got {w}x{h}");
}
#[test]
fn cover_source_crop_wider_source() {
let (_x, y, w, h) = fit_cover_source_crop(1600, 900, 800, 600);
assert_eq!((w, h), (1200, 900));
assert_eq!(y, 0);
}
#[test]
fn cover_source_crop_taller_source() {
let (x, _y, w, h) = fit_cover_source_crop(900, 1600, 800, 600);
assert_eq!((w, h), (900, 675));
assert_eq!(x, 0);
}
#[test]
fn cover_source_crop_exact_aspect() {
assert_eq!(
fit_cover_source_crop(1000, 500, 800, 400),
(0, 0, 1000, 500)
);
}
#[test]
fn cover_source_crop_zero_inputs() {
assert_eq!(fit_cover_source_crop(0, 100, 800, 600), (0, 0, 0, 0));
assert_eq!(fit_cover_source_crop(100, 100, 0, 600), (0, 0, 0, 0));
}
#[test]
fn defaults() {
assert_eq!(FitMode::default(), FitMode::Fit);
}
#[test]
fn fit_snap_to_target() {
assert_eq!(fit_dims(1000, 500, 400, 200, FitMode::Fit), (400, 200));
assert_eq!(fit_dims(1001, 500, 400, 200, FitMode::Fit), (400, 200));
}
}