kuwahara-filter 0.1.0

Fast Kuwahara filter implementation for artistic image effects
Documentation
pub use clap::Parser;
use std::path::PathBuf;

/// Fast Kuwahara filter CLI with integral images + Rayon
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
pub struct Args {
    /// Input image path (must be PNG or any format `image` supports)
    #[arg(short = 'i', long = "input")]
    pub input: PathBuf,

    /// Output image path. Defaults to `<input_stem>_kuwahara.png`.
    #[arg(short = 'o', long = "output")]
    pub output: Option<PathBuf>,

    /// Radius of the window (2 → 5×5, 3 → 7×7, etc.). 
    /// Can be a single value (e.g. "3") or range (e.g. "2,5"). Default = 2.
    #[arg(short = 'r', long = "radius", default_value = "2")]
    pub radius: String,
}

impl Args {
    /// Parse radius string to get a vector of radius values
    pub fn parse_radius(&self) -> Result<Vec<u32>, String> {
        let trimmed = self.radius.trim();
        
        if trimmed.contains(',') {
            // Range format: "min,max"
            let parts: Vec<&str> = trimmed.split(',').collect();
            if parts.len() != 2 {
                return Err(format!("Invalid radius format '{}'. Expected single value or 'min,max'", trimmed));
            }
            
            let min = parts[0].trim().parse::<u32>().map_err(|_| {
                format!("Invalid minimum radius '{}'. Must be a positive integer", parts[0])
            })?;
            
            let max = parts[1].trim().parse::<u32>().map_err(|_| {
                format!("Invalid maximum radius '{}'. Must be a positive integer", parts[1])
            })?;
            
            if min > max {
                return Err(format!("Minimum radius {} cannot be greater than maximum radius {}", min, max));
            }
            
            if min == 0 || max == 0 {
                return Err("Radius must be greater than 0".to_string());
            }
            
            Ok((min..=max).collect())
        } else {
            // Single value format: "value"
            let radius = trimmed.parse::<u32>().map_err(|_| {
                format!("Invalid radius '{}'. Must be a positive integer", trimmed)
            })?;
            
            if radius == 0 {
                return Err("Radius must be greater than 0".to_string());
            }
            
            Ok(vec![radius])
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_single_radius() {
        let args = Args {
            input: std::path::PathBuf::from("test.jpg"),
            output: None,
            radius: "3".to_string(),
        };
        
        let result = args.parse_radius().unwrap();
        assert_eq!(result, vec![3]);
    }

    #[test]
    fn test_parse_radius_range() {
        let args = Args {
            input: std::path::PathBuf::from("test.jpg"),
            output: None,
            radius: "2,5".to_string(),
        };
        
        let result = args.parse_radius().unwrap();
        assert_eq!(result, vec![2, 3, 4, 5]);
    }

    #[test]
    fn test_parse_radius_range_with_spaces() {
        let args = Args {
            input: std::path::PathBuf::from("test.jpg"),
            output: None,
            radius: " 1 , 3 ".to_string(),
        };
        
        let result = args.parse_radius().unwrap();
        assert_eq!(result, vec![1, 2, 3]);
    }

    #[test]
    fn test_parse_radius_invalid_format() {
        let args = Args {
            input: std::path::PathBuf::from("test.jpg"),
            output: None,
            radius: "1,2,3".to_string(),
        };
        
        assert!(args.parse_radius().is_err());
    }

    #[test]
    fn test_parse_radius_invalid_number() {
        let args = Args {
            input: std::path::PathBuf::from("test.jpg"),
            output: None,
            radius: "abc".to_string(),
        };
        
        assert!(args.parse_radius().is_err());
    }

    #[test]
    fn test_parse_radius_zero() {
        let args = Args {
            input: std::path::PathBuf::from("test.jpg"),
            output: None,
            radius: "0".to_string(),
        };
        
        assert!(args.parse_radius().is_err());
    }

    #[test]
    fn test_parse_radius_min_greater_than_max() {
        let args = Args {
            input: std::path::PathBuf::from("test.jpg"),
            output: None,
            radius: "5,2".to_string(),
        };
        
        assert!(args.parse_radius().is_err());
    }

    #[test]
    fn test_parse_radius_same_min_max() {
        let args = Args {
            input: std::path::PathBuf::from("test.jpg"),
            output: None,
            radius: "3,3".to_string(),
        };
        
        let result = args.parse_radius().unwrap();
        assert_eq!(result, vec![3]);
    }
}