use crate::codec::ImageData;
use crate::error::{Error, Result};
#[derive(Debug, Clone, PartialEq)]
pub enum CropMode {
Region {
x: u32,
y: u32,
width: u32,
height: u32,
},
AspectRatio { width: u32, height: u32 },
}
pub fn calculate_crop_region(
img_w: u32,
img_h: u32,
mode: &CropMode,
) -> Result<(u32, u32, u32, u32)> {
match *mode {
CropMode::Region {
x,
y,
width,
height,
} => {
if width == 0 || height == 0 {
return Err(Error::Crop("crop dimensions must be non-zero".to_string()));
}
let x_in_bounds = width <= img_w && x <= img_w - width;
let y_in_bounds = height <= img_h && y <= img_h - height;
if !x_in_bounds || !y_in_bounds {
return Err(Error::Crop(format!(
"crop region ({x},{y},{width},{height}) exceeds image bounds ({img_w}x{img_h})"
)));
}
Ok((x, y, width, height))
}
CropMode::AspectRatio {
width: rw,
height: rh,
} => {
if rw == 0 || rh == 0 {
return Err(Error::Crop("aspect ratio must be non-zero".to_string()));
}
let target_ratio = rw as f64 / rh as f64;
let img_ratio = img_w as f64 / img_h as f64;
let (crop_w, crop_h) = if img_ratio > target_ratio {
let h = img_h;
let w = (h as f64 * target_ratio).round() as u32;
(w, h)
} else {
let w = img_w;
let h = (w as f64 / target_ratio).round() as u32;
(w, h)
};
let x = (img_w - crop_w) / 2;
let y = (img_h - crop_h) / 2;
Ok((x, y, crop_w, crop_h))
}
}
}
pub fn crop(image: &ImageData, mode: &CropMode) -> Result<ImageData> {
let expected_size = image.width as usize * image.height as usize * 4;
if image.data.len() != expected_size {
return Err(Error::Crop(format!(
"invalid image data: expected {} bytes ({}x{}x4), got {}",
expected_size,
image.width,
image.height,
image.data.len()
)));
}
let (x, y, crop_w, crop_h) = calculate_crop_region(image.width, image.height, mode)?;
let bytes_per_pixel = 4usize;
let src_stride = image.width as usize * bytes_per_pixel;
let dst_stride = crop_w as usize * bytes_per_pixel;
let mut data = vec![0u8; crop_h as usize * dst_stride];
for row in 0..crop_h as usize {
let src_offset = (y as usize + row) * src_stride + x as usize * bytes_per_pixel;
let dst_offset = row * dst_stride;
data[dst_offset..dst_offset + dst_stride]
.copy_from_slice(&image.data[src_offset..src_offset + dst_stride]);
}
Ok(ImageData::new(crop_w, crop_h, data))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn region_valid_crop() {
let (x, y, w, h) = calculate_crop_region(
200,
100,
&CropMode::Region {
x: 10,
y: 20,
width: 50,
height: 30,
},
)
.unwrap();
assert_eq!((x, y, w, h), (10, 20, 50, 30));
}
#[test]
fn region_full_image() {
let (x, y, w, h) = calculate_crop_region(
200,
100,
&CropMode::Region {
x: 0,
y: 0,
width: 200,
height: 100,
},
)
.unwrap();
assert_eq!((x, y, w, h), (0, 0, 200, 100));
}
#[test]
fn region_exceeds_bounds() {
let result = calculate_crop_region(
200,
100,
&CropMode::Region {
x: 150,
y: 0,
width: 100,
height: 50,
},
);
assert!(result.is_err());
}
#[test]
fn region_offset_overflow_is_rejected() {
let result = calculate_crop_region(
200,
100,
&CropMode::Region {
x: u32::MAX,
y: 0,
width: 100,
height: 50,
},
);
assert!(
result.is_err(),
"x + width overflow must not pass bounds check"
);
let result = calculate_crop_region(
200,
100,
&CropMode::Region {
x: 0,
y: u32::MAX,
width: 100,
height: 50,
},
);
assert!(
result.is_err(),
"y + height overflow must not pass bounds check"
);
}
#[test]
fn crop_invalid_data_length_returns_error() {
let img = ImageData {
width: 100,
height: 50,
data: vec![0u8; 16],
};
let result = crop(
&img,
&CropMode::Region {
x: 0,
y: 0,
width: 10,
height: 10,
},
);
assert!(result.is_err());
}
#[test]
fn region_zero_width() {
let result = calculate_crop_region(
200,
100,
&CropMode::Region {
x: 0,
y: 0,
width: 0,
height: 50,
},
);
assert!(result.is_err());
}
#[test]
fn region_zero_height() {
let result = calculate_crop_region(
200,
100,
&CropMode::Region {
x: 0,
y: 0,
width: 50,
height: 0,
},
);
assert!(result.is_err());
}
#[test]
fn aspect_square_on_landscape() {
let (x, y, w, h) = calculate_crop_region(
200,
100,
&CropMode::AspectRatio {
width: 1,
height: 1,
},
)
.unwrap();
assert_eq!((x, y, w, h), (50, 0, 100, 100));
}
#[test]
fn aspect_square_on_portrait() {
let (x, y, w, h) = calculate_crop_region(
100,
200,
&CropMode::AspectRatio {
width: 1,
height: 1,
},
)
.unwrap();
assert_eq!((x, y, w, h), (0, 50, 100, 100));
}
#[test]
fn aspect_16_9_on_square() {
let (x, y, w, h) = calculate_crop_region(
100,
100,
&CropMode::AspectRatio {
width: 16,
height: 9,
},
)
.unwrap();
assert_eq!(w, 100);
assert_eq!(h, 56);
assert_eq!(x, 0);
assert_eq!(y, 22);
}
#[test]
fn aspect_same_as_image() {
let (x, y, w, h) = calculate_crop_region(
200,
100,
&CropMode::AspectRatio {
width: 2,
height: 1,
},
)
.unwrap();
assert_eq!((x, y, w, h), (0, 0, 200, 100));
}
#[test]
fn aspect_zero_ratio() {
let result = calculate_crop_region(
200,
100,
&CropMode::AspectRatio {
width: 0,
height: 1,
},
);
assert!(result.is_err());
}
fn create_test_image(width: u32, height: u32) -> ImageData {
let data = vec![128u8; (width * height * 4) as usize];
ImageData::new(width, height, data)
}
#[test]
fn crop_region_returns_correct_dimensions() {
let img = create_test_image(200, 100);
let result = crop(
&img,
&CropMode::Region {
x: 10,
y: 20,
width: 50,
height: 30,
},
)
.unwrap();
assert_eq!(result.width, 50);
assert_eq!(result.height, 30);
assert_eq!(result.data.len(), (50 * 30 * 4) as usize);
}
#[test]
fn crop_aspect_returns_correct_dimensions() {
let img = create_test_image(200, 100);
let result = crop(
&img,
&CropMode::AspectRatio {
width: 1,
height: 1,
},
)
.unwrap();
assert_eq!(result.width, 100);
assert_eq!(result.height, 100);
}
#[test]
fn crop_preserves_pixel_data() {
let mut data = vec![0u8; 4 * 2 * 4];
for y in 0..2u32 {
for x in 0..4u32 {
let i = ((y * 4 + x) * 4) as usize;
data[i] = x as u8;
data[i + 1] = y as u8;
data[i + 2] = 0;
data[i + 3] = 255;
}
}
let img = ImageData::new(4, 2, data);
let result = crop(
&img,
&CropMode::Region {
x: 1,
y: 0,
width: 2,
height: 1,
},
)
.unwrap();
assert_eq!(result.width, 2);
assert_eq!(result.height, 1);
assert_eq!(result.data[0], 1);
assert_eq!(result.data[4], 2);
}
#[test]
fn crop_full_image_returns_clone() {
let img = create_test_image(100, 50);
let result = crop(
&img,
&CropMode::Region {
x: 0,
y: 0,
width: 100,
height: 50,
},
)
.unwrap();
assert_eq!(result.width, 100);
assert_eq!(result.height, 50);
assert_eq!(result.data, img.data);
}
}