use crate::error::{PanimgError, Result};
use crate::ops::{Operation, OperationDescription};
use crate::schema::{CommandSchema, ParamRange, ParamSchema, ParamType};
use image::DynamicImage;
pub struct HueRotateOp {
pub degrees: i32,
}
impl HueRotateOp {
pub fn new(degrees: i32) -> Result<Self> {
if !(-360..=360).contains(°rees) {
return Err(PanimgError::InvalidArgument {
message: format!("hue-rotate value {degrees} out of range"),
suggestion: "use a value between -360 and 360".into(),
});
}
Ok(Self { degrees })
}
}
impl Operation for HueRotateOp {
fn name(&self) -> &str {
"hue-rotate"
}
fn apply(&self, img: DynamicImage) -> Result<DynamicImage> {
Ok(img.huerotate(self.degrees))
}
fn describe(&self) -> OperationDescription {
OperationDescription {
operation: "hue-rotate".into(),
params: serde_json::json!({ "degrees": self.degrees }),
description: format!("Rotate hue by {}°", self.degrees),
}
}
fn schema() -> CommandSchema {
CommandSchema {
command: "hue-rotate".into(),
description: "Rotate image hue".into(),
params: vec![
ParamSchema {
name: "input".into(),
param_type: ParamType::Path,
required: true,
description: "Input image path".into(),
default: None,
choices: None,
range: None,
},
ParamSchema {
name: "output".into(),
param_type: ParamType::Path,
required: true,
description: "Output image path".into(),
default: None,
choices: None,
range: None,
},
ParamSchema {
name: "degrees".into(),
param_type: ParamType::Integer,
required: true,
description: "Hue rotation in degrees (-360 to 360)".into(),
default: None,
choices: None,
range: Some(ParamRange {
min: -360.0,
max: 360.0,
}),
},
],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_image() -> DynamicImage {
DynamicImage::ImageRgba8(image::RgbaImage::from_fn(4, 4, |_, _| {
image::Rgba([255, 0, 0, 255]) }))
}
#[test]
fn hue_rotate_preserves_dimensions() {
let op = HueRotateOp::new(90).unwrap();
let result = op.apply(test_image()).unwrap();
assert_eq!(result.width(), 4);
assert_eq!(result.height(), 4);
}
#[test]
fn hue_rotate_changes_color() {
let op = HueRotateOp::new(120).unwrap();
let result = op.apply(test_image()).unwrap();
let rgba = result.to_rgba8();
let p = rgba.get_pixel(0, 0);
assert!(p[1] > p[0]); }
#[test]
fn hue_rotate_360_is_identity() {
let op = HueRotateOp::new(360).unwrap();
let img = test_image();
let original = img.to_rgba8().get_pixel(0, 0).0;
let result = op.apply(img).unwrap();
let rgba = result.to_rgba8();
let p = rgba.get_pixel(0, 0).0;
assert!((p[0] as i32 - original[0] as i32).unsigned_abs() <= 1);
}
#[test]
fn hue_rotate_out_of_range() {
assert!(HueRotateOp::new(361).is_err());
assert!(HueRotateOp::new(-361).is_err());
}
}