#![allow(dead_code)]
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CropStrategy {
Center,
RuleOfThirds,
Manual,
}
impl fmt::Display for CropStrategy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Center => write!(f, "center"),
Self::RuleOfThirds => write!(f, "rule_of_thirds"),
Self::Manual => write!(f, "manual"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CropRect {
pub x: u32,
pub y: u32,
pub w: u32,
pub h: u32,
}
impl CropRect {
pub fn new(x: u32, y: u32, w: u32, h: u32) -> Self {
Self { x, y, w, h }
}
pub fn area(&self) -> u64 {
self.w as u64 * self.h as u64
}
#[allow(clippy::cast_precision_loss)]
pub fn aspect_ratio(&self) -> f64 {
if self.h == 0 {
return 0.0;
}
self.w as f64 / self.h as f64
}
pub fn right(&self) -> u32 {
self.x + self.w
}
pub fn bottom(&self) -> u32 {
self.y + self.h
}
pub fn clamp(&self, frame_w: u32, frame_h: u32) -> Self {
let x = self.x.min(frame_w.saturating_sub(1));
let y = self.y.min(frame_h.saturating_sub(1));
let w = self.w.min(frame_w.saturating_sub(x));
let h = self.h.min(frame_h.saturating_sub(y));
Self { x, y, w, h }
}
pub fn fits(&self, frame_w: u32, frame_h: u32) -> bool {
self.right() <= frame_w && self.bottom() <= frame_h
}
}
#[derive(Debug, Clone)]
pub struct CropScaleParams {
pub src_width: u32,
pub src_height: u32,
pub dst_width: u32,
pub dst_height: u32,
pub strategy: CropStrategy,
pub manual_rect: Option<CropRect>,
}
impl CropScaleParams {
pub fn new(src_w: u32, src_h: u32, dst_w: u32, dst_h: u32) -> Self {
Self {
src_width: src_w,
src_height: src_h,
dst_width: dst_w,
dst_height: dst_h,
strategy: CropStrategy::Center,
manual_rect: None,
}
}
pub fn with_strategy(mut self, s: CropStrategy) -> Self {
self.strategy = s;
self
}
pub fn with_rect(mut self, rect: CropRect) -> Self {
self.strategy = CropStrategy::Manual;
self.manual_rect = Some(rect);
self
}
}
#[allow(clippy::cast_precision_loss)]
pub fn compute_crop_rect(params: &CropScaleParams) -> CropRect {
match params.strategy {
CropStrategy::Manual => {
if let Some(r) = ¶ms.manual_rect {
r.clamp(params.src_width, params.src_height)
} else {
CropRect::new(0, 0, params.src_width, params.src_height)
}
}
CropStrategy::Center => {
let dst_ar = params.dst_width as f64 / params.dst_height.max(1) as f64;
let src_ar = params.src_width as f64 / params.src_height.max(1) as f64;
let (cw, ch) = if src_ar > dst_ar {
let cw = (params.src_height as f64 * dst_ar) as u32;
(cw.min(params.src_width), params.src_height)
} else {
let ch = (params.src_width as f64 / dst_ar) as u32;
(params.src_width, ch.min(params.src_height))
};
let x = (params.src_width.saturating_sub(cw)) / 2;
let y = (params.src_height.saturating_sub(ch)) / 2;
CropRect::new(x, y, cw, ch)
}
CropStrategy::RuleOfThirds => {
let dst_ar = params.dst_width as f64 / params.dst_height.max(1) as f64;
let src_ar = params.src_width as f64 / params.src_height.max(1) as f64;
let (cw, ch) = if src_ar > dst_ar {
let cw = (params.src_height as f64 * dst_ar) as u32;
(cw.min(params.src_width), params.src_height)
} else {
let ch = (params.src_width as f64 / dst_ar) as u32;
(params.src_width, ch.min(params.src_height))
};
let x = (params.src_width.saturating_sub(cw)) / 3;
let y = (params.src_height.saturating_sub(ch)) / 3;
CropRect::new(x, y, cw, ch).clamp(params.src_width, params.src_height)
}
}
}
#[allow(clippy::cast_precision_loss)]
pub fn compute_scale_factors(crop: &CropRect, dst_w: u32, dst_h: u32) -> (f64, f64) {
let sx = dst_w as f64 / crop.w.max(1) as f64;
let sy = dst_h as f64 / crop.h.max(1) as f64;
(sx, sy)
}
#[allow(clippy::cast_precision_loss)]
pub fn crop_scale_nearest(
src: &[u8],
src_w: u32,
src_h: u32,
crop: &CropRect,
dst_w: u32,
dst_h: u32,
) -> Vec<u8> {
let mut out = vec![0u8; (dst_w * dst_h) as usize];
let (sx, sy) = compute_scale_factors(crop, dst_w, dst_h);
for dy in 0..dst_h {
for dx in 0..dst_w {
let cx = crop.x + (dx as f64 / sx) as u32;
let cy = crop.y + (dy as f64 / sy) as u32;
let cx = cx.min(src_w.saturating_sub(1));
let cy = cy.min(src_h.saturating_sub(1));
out[(dy * dst_w + dx) as usize] = src[(cy * src_w + cx) as usize];
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_crop_rect_area() {
let r = CropRect::new(0, 0, 100, 200);
assert_eq!(r.area(), 20_000);
}
#[test]
fn test_crop_rect_aspect_ratio() {
let r = CropRect::new(0, 0, 1920, 1080);
let ar = r.aspect_ratio();
assert!((ar - 16.0 / 9.0).abs() < 0.01);
}
#[test]
fn test_crop_rect_zero_height() {
let r = CropRect::new(0, 0, 100, 0);
assert_eq!(r.aspect_ratio(), 0.0);
}
#[test]
fn test_crop_rect_right_bottom() {
let r = CropRect::new(10, 20, 100, 50);
assert_eq!(r.right(), 110);
assert_eq!(r.bottom(), 70);
}
#[test]
fn test_crop_rect_clamp() {
let r = CropRect::new(1800, 900, 300, 300);
let c = r.clamp(1920, 1080);
assert!(c.right() <= 1920);
assert!(c.bottom() <= 1080);
}
#[test]
fn test_crop_rect_fits() {
let r = CropRect::new(0, 0, 1920, 1080);
assert!(r.fits(1920, 1080));
assert!(!r.fits(1280, 720));
}
#[test]
fn test_center_crop_16_9_to_9_16() {
let params = CropScaleParams::new(1920, 1080, 1080, 1920);
let rect = compute_crop_rect(¶ms);
assert!(rect.w <= rect.h);
assert!(rect.fits(1920, 1080));
}
#[test]
fn test_center_crop_same_aspect() {
let params = CropScaleParams::new(1920, 1080, 960, 540);
let rect = compute_crop_rect(¶ms);
assert_eq!(rect.x, 0);
assert_eq!(rect.y, 0);
assert_eq!(rect.w, 1920);
assert_eq!(rect.h, 1080);
}
#[test]
fn test_manual_crop() {
let params =
CropScaleParams::new(1920, 1080, 640, 480).with_rect(CropRect::new(100, 100, 800, 600));
let rect = compute_crop_rect(¶ms);
assert_eq!(rect.x, 100);
assert_eq!(rect.y, 100);
assert_eq!(rect.w, 800);
assert_eq!(rect.h, 600);
}
#[test]
fn test_rule_of_thirds_crop() {
let params =
CropScaleParams::new(1920, 1080, 1080, 1080).with_strategy(CropStrategy::RuleOfThirds);
let rect = compute_crop_rect(¶ms);
assert!(rect.fits(1920, 1080));
}
#[test]
fn test_scale_factors() {
let crop = CropRect::new(0, 0, 1920, 1080);
let (sx, sy) = compute_scale_factors(&crop, 960, 540);
assert!((sx - 0.5).abs() < 0.001);
assert!((sy - 0.5).abs() < 0.001);
}
#[test]
fn test_crop_scale_nearest_identity() {
let src: Vec<u8> = (0..16).collect();
let crop = CropRect::new(0, 0, 4, 4);
let out = crop_scale_nearest(&src, 4, 4, &crop, 4, 4);
assert_eq!(out, src);
}
#[test]
fn test_crop_scale_nearest_downscale() {
let src: Vec<u8> = (0..16).collect();
let crop = CropRect::new(0, 0, 4, 4);
let out = crop_scale_nearest(&src, 4, 4, &crop, 2, 2);
assert_eq!(out.len(), 4);
}
#[test]
fn test_crop_strategy_display() {
assert_eq!(CropStrategy::Center.to_string(), "center");
assert_eq!(CropStrategy::RuleOfThirds.to_string(), "rule_of_thirds");
assert_eq!(CropStrategy::Manual.to_string(), "manual");
}
#[test]
fn test_params_builder() {
let p =
CropScaleParams::new(3840, 2160, 1920, 1080).with_strategy(CropStrategy::RuleOfThirds);
assert_eq!(p.strategy, CropStrategy::RuleOfThirds);
assert_eq!(p.dst_width, 1920);
}
}