use crate::codec::ImageData;
use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FillColor {
Solid([u8; 4]),
Transparent,
}
impl FillColor {
pub fn as_rgba(&self) -> [u8; 4] {
match *self {
FillColor::Solid(c) => c,
FillColor::Transparent => [0, 0, 0, 0],
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ExtendMode {
AspectRatio { width: u32, height: u32 },
Size { width: u32, height: u32 },
}
pub fn calculate_extend_region(
img_w: u32,
img_h: u32,
mode: &ExtendMode,
) -> Result<(u32, u32, u32, u32)> {
match *mode {
ExtendMode::AspectRatio {
width: rw,
height: rh,
} => {
if rw == 0 || rh == 0 {
return Err(Error::Extend("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 (canvas_w, canvas_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 off_x = (canvas_w - img_w) / 2;
let off_y = (canvas_h - img_h) / 2;
Ok((canvas_w, canvas_h, off_x, off_y))
}
ExtendMode::Size { width, height } => {
if width == 0 || height == 0 {
return Err(Error::Extend(
"extend dimensions must be non-zero".to_string(),
));
}
if width < img_w || height < img_h {
return Err(Error::Extend(format!(
"target size ({width}x{height}) is smaller than image ({img_w}x{img_h})"
)));
}
let off_x = (width - img_w) / 2;
let off_y = (height - img_h) / 2;
Ok((width, height, off_x, off_y))
}
}
}
pub fn extend(image: &ImageData, mode: &ExtendMode, fill: &FillColor) -> Result<ImageData> {
let (canvas_w, canvas_h, off_x, off_y) =
calculate_extend_region(image.width, image.height, mode)?;
let expected_size = image.width as usize * image.height as usize * 4;
if image.data.len() != expected_size {
return Err(Error::Extend(format!(
"invalid image data: expected {} bytes ({}x{}x4), got {}",
expected_size,
image.width,
image.height,
image.data.len()
)));
}
if canvas_w == image.width && canvas_h == image.height {
return Ok(image.clone());
}
let bytes_per_pixel = 4usize;
let canvas_stride = canvas_w as usize * bytes_per_pixel;
let src_stride = image.width as usize * bytes_per_pixel;
let mut data = vec![0u8; canvas_h as usize * canvas_stride];
if !matches!(fill, FillColor::Transparent) {
let fill_rgba = fill.as_rgba();
for pixel in data.chunks_exact_mut(bytes_per_pixel) {
pixel.copy_from_slice(&fill_rgba);
}
}
for row in 0..image.height as usize {
let src_offset = row * src_stride;
let dst_offset = (off_y as usize + row) * canvas_stride + off_x as usize * bytes_per_pixel;
data[dst_offset..dst_offset + src_stride]
.copy_from_slice(&image.data[src_offset..src_offset + src_stride]);
}
Ok(ImageData::new(canvas_w, canvas_h, data))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn aspect_square_on_landscape() {
let (w, h, ox, oy) = calculate_extend_region(
200,
100,
&ExtendMode::AspectRatio {
width: 1,
height: 1,
},
)
.unwrap();
assert_eq!((w, h, ox, oy), (200, 200, 0, 50));
}
#[test]
fn aspect_square_on_portrait() {
let (w, h, ox, oy) = calculate_extend_region(
100,
200,
&ExtendMode::AspectRatio {
width: 1,
height: 1,
},
)
.unwrap();
assert_eq!((w, h, ox, oy), (200, 200, 50, 0));
}
#[test]
fn aspect_16_9_on_square() {
let (w, h, ox, oy) = calculate_extend_region(
100,
100,
&ExtendMode::AspectRatio {
width: 16,
height: 9,
},
)
.unwrap();
assert_eq!((w, h, ox, oy), (178, 100, 39, 0));
}
#[test]
fn aspect_9_16_on_square() {
let (w, h, ox, oy) = calculate_extend_region(
100,
100,
&ExtendMode::AspectRatio {
width: 9,
height: 16,
},
)
.unwrap();
assert_eq!((w, h, ox, oy), (100, 178, 0, 39));
}
#[test]
fn aspect_same_as_image() {
let (w, h, ox, oy) = calculate_extend_region(
200,
100,
&ExtendMode::AspectRatio {
width: 2,
height: 1,
},
)
.unwrap();
assert_eq!((w, h, ox, oy), (200, 100, 0, 0));
}
#[test]
fn aspect_zero_ratio_errors() {
let result = calculate_extend_region(
200,
100,
&ExtendMode::AspectRatio {
width: 0,
height: 1,
},
);
assert!(result.is_err());
}
#[test]
fn size_larger_canvas() {
let (w, h, ox, oy) = calculate_extend_region(
800,
600,
&ExtendMode::Size {
width: 1000,
height: 1000,
},
)
.unwrap();
assert_eq!((w, h, ox, oy), (1000, 1000, 100, 200));
}
#[test]
fn size_same_as_image() {
let (w, h, ox, oy) = calculate_extend_region(
800,
600,
&ExtendMode::Size {
width: 800,
height: 600,
},
)
.unwrap();
assert_eq!((w, h, ox, oy), (800, 600, 0, 0));
}
#[test]
fn size_only_width_larger() {
let (w, h, ox, oy) = calculate_extend_region(
800,
600,
&ExtendMode::Size {
width: 1000,
height: 600,
},
)
.unwrap();
assert_eq!((w, h, ox, oy), (1000, 600, 100, 0));
}
#[test]
fn size_smaller_than_image_errors() {
let result = calculate_extend_region(
800,
600,
&ExtendMode::Size {
width: 500,
height: 500,
},
);
assert!(result.is_err());
}
#[test]
fn size_width_smaller_errors() {
let result = calculate_extend_region(
800,
600,
&ExtendMode::Size {
width: 700,
height: 600,
},
);
assert!(result.is_err());
}
#[test]
fn size_zero_errors() {
let result = calculate_extend_region(
800,
600,
&ExtendMode::Size {
width: 0,
height: 0,
},
);
assert!(result.is_err());
}
use crate::codec::ImageData;
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 extend_returns_correct_dimensions() {
let img = create_test_image(200, 100);
let result = extend(
&img,
&ExtendMode::AspectRatio {
width: 1,
height: 1,
},
&FillColor::Solid([255, 255, 255, 255]),
)
.unwrap();
assert_eq!(result.width, 200);
assert_eq!(result.height, 200);
assert_eq!(result.data.len(), (200 * 200 * 4) as usize);
}
#[test]
fn extend_fills_with_solid_color() {
let img = create_test_image(2, 2);
let result = extend(
&img,
&ExtendMode::Size {
width: 4,
height: 4,
},
&FillColor::Solid([255, 0, 0, 255]),
)
.unwrap();
assert_eq!(result.data[0], 255); assert_eq!(result.data[1], 0); assert_eq!(result.data[2], 0); assert_eq!(result.data[3], 255); }
#[test]
fn extend_fills_with_transparent() {
let img = create_test_image(2, 2);
let result = extend(
&img,
&ExtendMode::Size {
width: 4,
height: 4,
},
&FillColor::Transparent,
)
.unwrap();
assert_eq!(&result.data[0..4], &[0, 0, 0, 0]);
}
#[test]
#[allow(clippy::identity_op)] fn extend_preserves_pixel_data() {
let data = vec![10, 20, 30, 255, 40, 50, 60, 255];
let img = ImageData::new(2, 1, data);
let result = extend(
&img,
&ExtendMode::Size {
width: 4,
height: 3,
},
&FillColor::Solid([0, 0, 0, 0]),
)
.unwrap();
let stride = 4 * 4; let offset = 1 * stride + 1 * 4; assert_eq!(&result.data[offset..offset + 4], &[10, 20, 30, 255]);
let offset2 = 1 * stride + 2 * 4;
assert_eq!(&result.data[offset2..offset2 + 4], &[40, 50, 60, 255]);
}
#[test]
fn extend_noop_when_already_matching() {
let img = create_test_image(200, 100);
let result = extend(
&img,
&ExtendMode::AspectRatio {
width: 2,
height: 1,
},
&FillColor::Solid([255, 255, 255, 255]),
)
.unwrap();
assert_eq!(result.width, 200);
assert_eq!(result.height, 100);
assert_eq!(result.data, img.data);
}
}