use image::DynamicImage;
use crate::error;
use std::fmt::Display;
use std::str::FromStr;
#[derive(Debug, PartialEq)]
pub enum Region {
Full,
Square,
Rect(u32, u32, u32, u32),
Pct(f32, f32, f32, f32),
}
impl FromStr for Region {
type Err = error::IiifError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s_lower = s.trim().to_lowercase();
match s_lower.as_str() {
"full" => Ok(Region::Full),
"square" => Ok(Region::Square),
s if s.starts_with("pct:") => Self::parse_pct_coordinates(&s[4..]),
s if s.contains(',') => Self::parse_rect_coordinates(s),
_ => Err(error::IiifError::BadRequest(format!(
"Invalid region format: {s}"
))),
}
}
}
impl Display for Region {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Region::Full => write!(f, "full"),
Region::Square => write!(f, "square"),
Region::Rect(x, y, w, h) => write!(f, "{x},{y},{w},{h}"),
Region::Pct(x, y, w, h) => write!(f, "pct:{x},{y},{w},{h}"),
}
}
}
impl Region {
fn parse_rect_coordinates(coords: &str) -> Result<Self, error::IiifError> {
let parts: Vec<&str> = coords.split(',').collect();
if parts.len() != 4 {
return Err(error::IiifError::BadRequest(
"Invalid rect region format".to_string(),
));
}
let values: Result<Vec<u32>, _> = parts.iter().map(|part| part.parse::<u32>()).collect();
match values {
Ok(vals) => Ok(Region::Rect(vals[0], vals[1], vals[2], vals[3])),
Err(_) => Err(error::IiifError::BadRequest(
"Invalid rect region format".to_string(),
)),
}
}
fn parse_pct_coordinates(coords: &str) -> Result<Self, error::IiifError> {
let parts: Vec<&str> = coords.split(',').collect();
if parts.len() != 4 {
return Err(error::IiifError::BadRequest(
"Invalid rect region format".to_string(),
));
}
let values: Result<Vec<f32>, _> = parts.iter().map(|part| part.parse::<f32>()).collect();
match values {
Ok(vals) => Ok(Region::Pct(vals[0], vals[1], vals[2], vals[3])),
Err(_) => Err(error::IiifError::BadRequest(
"Invalid rect region format".to_string(),
)),
}
}
pub fn process(&self, mut image: DynamicImage) -> Result<DynamicImage, error::IiifError> {
let width = image.width();
let height = image.height();
let (x, y, w, h) = self.get_region(width, height)?;
Ok(image.crop(x, y, w, h))
}
fn get_region(
&self,
width: u32,
height: u32,
) -> Result<(u32, u32, u32, u32), error::IiifError> {
match self {
Region::Full => Ok((0, 0, width, height)),
Region::Square => {
let min = width.min(height);
let x = (width - min) / 2;
let y = (height - min) / 2;
Ok((x, y, min, min))
}
Region::Rect(x, y, w, h) => {
if *w == 0 || *h == 0 {
return Err(error::IiifError::BadRequest(format!(
"Width or height is 0: {self}",
)));
}
if *x >= width || *y >= height {
return Err(error::IiifError::BadRequest(format!(
"X or Y is out of bounds: {self}",
)));
}
let rw = (*w).min(width - *x);
let rh = (*h).min(height - *y);
Ok((*x, *y, rw, rh))
}
Region::Pct(x, y, w, h) => {
if *w == 0.0 || *h == 0.0 {
return Err(error::IiifError::BadRequest(format!(
"Width or height is 0: {self}",
)));
}
if *x >= 100.0 || *y >= 100.0 {
return Err(error::IiifError::BadRequest(format!(
"X or Y is out of bounds: {self}",
)));
}
let rw = (*w).min(100.0 - *x);
let rh = (*h).min(100.0 - *y);
let px = (width as f32 * (*x / 100.0)).round() as u32;
let py = (height as f32 * (*y / 100.0)).round() as u32;
let pw = (width as f32 * (rw / 100.0)).round() as u32;
let ph = (height as f32 * (rh / 100.0)).round() as u32;
Ok((px, py, pw, ph))
}
}
}
}
#[cfg(test)]
mod tests {
use crate::storage::LocalStorage;
use crate::storage::Storage;
use super::*;
#[test]
fn test_region_from_str() {
assert_eq!(Region::from_str("full").unwrap(), Region::Full);
assert_eq!(Region::from_str("square").unwrap(), Region::Square);
assert_eq!(
Region::from_str("125,15,120,140").unwrap(),
Region::Rect(125, 15, 120, 140)
);
assert_eq!(
Region::from_str("pct:10,20,30,40").unwrap(),
Region::Pct(10.0, 20.0, 30.0, 40.0)
);
assert_eq!(
Region::from_str("pct:41.6,7.5,40,70").unwrap(),
Region::Pct(41.6, 7.5, 40.0, 70.0)
);
assert!(Region::from_str("invalid").is_err());
assert!(Region::from_str("125,15,120,140,150").is_err());
assert!(Region::from_str("125,15,120").is_err());
assert!(Region::from_str("125,15,120,140.11").is_err());
assert!(Region::from_str("pct:10,20,30,40,50").is_err());
assert!(Region::from_str("pct:10,20,30").is_err());
assert!(Region::from_str("pct:10,20,30,aa").is_err());
}
#[test]
fn test_region_display() {
assert_eq!(format!("{}", Region::Full), "full");
assert_eq!(format!("{}", Region::Square), "square");
assert_eq!(format!("{}", Region::Rect(10, 20, 30, 40)), "10,20,30,40");
assert_eq!(
format!("{}", Region::Pct(10.0, 20.0, 30.0, 40.0)),
"pct:10,20,30,40"
);
assert_eq!(
format!("{}", Region::Pct(41.6, 7.5, 40.0, 70.0)),
"pct:41.6,7.5,40,70"
);
let a: Region = "pct:41.6,7.5,40,70".parse().unwrap();
assert_eq!(a, Region::Pct(41.6, 7.5, 40.0, 70.0));
}
#[test]
fn test_region_get_region() {
let width = 300;
let height = 200;
let region1 = Region::Full;
let (x, y, w, h) = region1.get_region(width, height).unwrap();
assert_eq!((x, y, w, h), (0, 0, width, height));
let region2 = Region::Square;
let (x, y, w, h) = region2.get_region(width, height).unwrap();
assert_eq!((x, y, w, h), (50, 0, 200, 200));
let region3 = Region::Rect(125, 15, 120, 140);
let (x, y, w, h) = region3.get_region(width, height).unwrap();
assert_eq!((x, y, w, h), (125, 15, 120, 140));
let region4 = Region::Pct(41.6, 7.5, 40.0, 70.0);
let (x, y, w, h) = region4.get_region(width, height).unwrap();
assert_eq!((x, y, w, h), (125, 15, 120, 140));
let region5 = Region::Rect(125, 15, 200, 200);
let (x, y, w, h) = region5.get_region(width, height).unwrap();
assert_eq!((x, y, w, h), (125, 15, 175, 185));
let region6 = Region::Pct(41.6, 7.5, 66.6, 100.0);
let (x, y, w, h) = region6.get_region(width, height).unwrap();
assert_eq!((x, y, w, h), (125, 15, 175, 185));
}
#[test]
fn test_region_get_region_error() {
let width = 300;
let height = 200;
let region = Region::Rect(125, 15, 0, 140);
let result = region.get_region(width, height);
assert!(result.is_err());
let region = Region::Rect(125, 15, 120, 0);
let result = region.get_region(width, height);
assert!(result.is_err());
let region = Region::Rect(300, 15, 200, 200);
let result = region.get_region(width, height);
assert!(result.is_err());
let region = Region::Rect(125, 200, 200, 200);
let result = region.get_region(width, height);
assert!(result.is_err());
let region = Region::Pct(41.6, 7.5, 66.6, 0.0);
let result = region.get_region(width, height);
assert!(result.is_err());
let region = Region::Pct(41.6, 7.5, 0.0, 100.0);
let result = region.get_region(width, height);
assert!(result.is_err());
let region = Region::Pct(41.6, 107.5, 66.6, 100.0);
let result = region.get_region(width, height);
assert!(result.is_err());
}
#[test]
fn test_region_process() {
let storage = LocalStorage::new("./fixtures", "./fixtures/out");
let cases = vec![
("full", 300, 200),
("square", 200, 200),
("125,15,120,140", 120, 140),
("pct:41.6,7.5,40,70", 120, 140),
("125,15,200,200", 175, 185),
("pct:41.6,7.5,66.6,100", 175, 185),
];
for case in cases {
let region = case.0.parse::<Region>().unwrap();
let image = storage.get_origin_file("demo.jpg").unwrap();
let image = image::load_from_memory(&image).unwrap();
let cropped_image = region.process(image).unwrap();
assert_eq!(cropped_image.width(), case.1);
assert_eq!(cropped_image.height(), case.2);
}
}
}