pub mod adjustments; pub mod metadata; pub mod rename; pub mod resize; pub mod watermark; use serde::{Deserialize, Serialize}; use crate::types::{Dimensions, ImageFormat, QualityPreset}; // --- Resize --- #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ResizeConfig { ByWidth(u32), ByHeight(u32), FitInBox { max: Dimensions, allow_upscale: bool, }, Exact(Dimensions), } impl ResizeConfig { pub fn target_for(&self, original: Dimensions) -> Dimensions { if original.width == 0 || original.height == 0 { return original; } let result = match self { Self::ByWidth(w) => { if *w == 0 { return original; } let scale = *w as f64 / original.width as f64; Dimensions { width: *w, height: (original.height as f64 * scale).round().max(1.0) as u32, } } Self::ByHeight(h) => { if *h == 0 { return original; } let scale = *h as f64 / original.height as f64; Dimensions { width: (original.width as f64 * scale).round().max(1.0) as u32, height: *h, } } Self::FitInBox { max, allow_upscale } => { original.fit_within(*max, *allow_upscale) } Self::Exact(dims) => { if dims.width == 0 || dims.height == 0 { return original; } *dims } }; Dimensions { width: result.width.max(1), height: result.height.max(1), } } } // --- Resize Algorithm --- #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub enum ResizeAlgorithm { Lanczos3, CatmullRom, Bilinear, Nearest, } impl Default for ResizeAlgorithm { fn default() -> Self { Self::Lanczos3 } } // --- Rotation / Flip --- #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub enum Rotation { None, Cw90, Cw180, Cw270, AutoOrient, } #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub enum Flip { None, Horizontal, Vertical, } // --- Convert --- #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ConvertConfig { KeepOriginal, SingleFormat(ImageFormat), FormatMapping(Vec<(ImageFormat, ImageFormat)>), } impl ConvertConfig { pub fn output_format(&self, input: ImageFormat) -> ImageFormat { match self { Self::KeepOriginal => input, Self::SingleFormat(fmt) => *fmt, Self::FormatMapping(map) => { map.iter() .find(|(from, _)| *from == input) .map(|(_, to)| *to) .unwrap_or(input) } } } } // --- Compress --- #[derive(Debug, Clone, Serialize, Deserialize)] pub enum CompressConfig { Preset(QualityPreset), Custom { jpeg_quality: Option, png_level: Option, webp_quality: Option, avif_quality: Option, }, } // --- Metadata --- #[derive(Debug, Clone, Serialize, Deserialize)] pub enum MetadataConfig { StripAll, KeepAll, Privacy, Custom { strip_gps: bool, strip_camera: bool, strip_software: bool, strip_timestamps: bool, strip_copyright: bool, }, } impl MetadataConfig { pub fn should_strip_gps(&self) -> bool { match self { Self::StripAll => true, Self::KeepAll => false, Self::Privacy => true, Self::Custom { strip_gps, .. } => *strip_gps, } } pub fn should_strip_camera(&self) -> bool { match self { Self::StripAll => true, Self::KeepAll => false, Self::Privacy => true, Self::Custom { strip_camera, .. } => *strip_camera, } } pub fn should_strip_copyright(&self) -> bool { match self { Self::StripAll => true, Self::KeepAll => false, Self::Privacy => false, Self::Custom { strip_copyright, .. } => *strip_copyright, } } pub fn should_strip_software(&self) -> bool { match self { Self::StripAll => true, Self::KeepAll => false, Self::Privacy => true, Self::Custom { strip_software, .. } => *strip_software, } } pub fn should_strip_timestamps(&self) -> bool { match self { Self::StripAll => true, Self::KeepAll => false, Self::Privacy => false, Self::Custom { strip_timestamps, .. } => *strip_timestamps, } } } // --- Watermark --- #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum WatermarkPosition { TopLeft, TopCenter, TopRight, MiddleLeft, Center, MiddleRight, BottomLeft, BottomCenter, BottomRight, } #[derive(Debug, Clone, Serialize, Deserialize)] pub enum WatermarkConfig { Text { text: String, position: WatermarkPosition, font_size: f32, opacity: f32, color: [u8; 4], font_family: Option, rotation: Option, tiled: bool, margin: u32, }, Image { path: std::path::PathBuf, position: WatermarkPosition, opacity: f32, scale: f32, rotation: Option, tiled: bool, margin: u32, }, } #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub enum WatermarkRotation { Degrees45, DegreesNeg45, Degrees90, Custom(f32), } // --- Adjustments --- #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AdjustmentsConfig { pub brightness: i32, pub contrast: i32, pub saturation: i32, pub sharpen: bool, pub grayscale: bool, pub sepia: bool, pub crop_aspect_ratio: Option<(f64, f64)>, pub trim_whitespace: bool, pub canvas_padding: u32, } impl AdjustmentsConfig { pub fn is_noop(&self) -> bool { self.brightness == 0 && self.contrast == 0 && self.saturation == 0 && !self.sharpen && !self.grayscale && !self.sepia && self.crop_aspect_ratio.is_none() && !self.trim_whitespace && self.canvas_padding == 0 } } // --- Overwrite Action (concrete action, no "Ask" variant) --- #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub enum OverwriteAction { AutoRename, Overwrite, Skip, } impl Default for OverwriteAction { fn default() -> Self { Self::AutoRename } } // --- Rename --- #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RenameConfig { pub prefix: String, pub suffix: String, pub counter_start: u32, pub counter_padding: u32, #[serde(default)] pub counter_enabled: bool, /// 0=before prefix, 1=before name, 2=after name, 3=after suffix, 4=replace name #[serde(default = "default_counter_position")] pub counter_position: u32, pub template: Option, /// 0=none, 1=lowercase, 2=uppercase, 3=title case pub case_mode: u32, /// 0=none, 1=underscore, 2=hyphen, 3=dot, 4=camelcase, 5=remove #[serde(default)] pub replace_spaces: u32, /// 0=keep all, 1=filesystem-safe, 2=web-safe, 3=hyphens+underscores, 4=hyphens only, 5=alphanumeric only #[serde(default)] pub special_chars: u32, pub regex_find: String, pub regex_replace: String, } fn default_counter_position() -> u32 { 3 } impl RenameConfig { /// Pre-compile the regex for batch use. Call once before a loop of apply_simple_compiled. pub fn compile_regex(&self) -> Option { rename::compile_rename_regex(&self.regex_find) } pub fn apply_simple(&self, original_name: &str, extension: &str, index: u32) -> String { // 1. Apply regex find-and-replace on the original name let working_name = rename::apply_regex_replace(original_name, &self.regex_find, &self.regex_replace); // 2. Apply space replacement let working_name = rename::apply_space_replacement(&working_name, self.replace_spaces); // 3. Apply special character filtering let working_name = rename::apply_special_chars(&working_name, self.special_chars); // 4. Build counter string let counter_str = if self.counter_enabled { let counter = self.counter_start.saturating_add(index.saturating_sub(1)); let padding = (self.counter_padding as usize).min(10); format!("{:0>width$}", counter, width = padding) } else { String::new() }; let has_counter = self.counter_enabled && !counter_str.is_empty(); // 5. Assemble parts based on counter position // Positions: 0=before prefix, 1=before name, 2=after name, 3=after suffix, 4=replace name let mut result = String::new(); if has_counter && self.counter_position == 0 { result.push_str(&counter_str); result.push('_'); } result.push_str(&self.prefix); if has_counter && self.counter_position == 1 { result.push_str(&counter_str); result.push('_'); } if has_counter && self.counter_position == 4 { result.push_str(&counter_str); } else { result.push_str(&working_name); } if has_counter && self.counter_position == 2 { result.push('_'); result.push_str(&counter_str); } result.push_str(&self.suffix); if has_counter && self.counter_position == 3 { result.push('_'); result.push_str(&counter_str); } // 6. Apply case conversion let result = rename::apply_case_conversion(&result, self.case_mode); format!("{}.{}", result, extension) } /// Like apply_simple but uses a pre-compiled regex (avoids recompiling per file). pub fn apply_simple_compiled(&self, original_name: &str, extension: &str, index: u32, compiled_re: Option<®ex::Regex>) -> String { // 1. Apply regex find-and-replace on the original name let working_name = match compiled_re { Some(re) => rename::apply_regex_replace_compiled(original_name, re, &self.regex_replace), None => original_name.to_string(), }; // 2. Apply space replacement let working_name = rename::apply_space_replacement(&working_name, self.replace_spaces); // 3. Apply special character filtering let working_name = rename::apply_special_chars(&working_name, self.special_chars); // 4. Build counter string let counter_str = if self.counter_enabled { let counter = self.counter_start.saturating_add(index.saturating_sub(1)); let padding = (self.counter_padding as usize).min(10); format!("{:0>width$}", counter, width = padding) } else { String::new() }; let has_counter = self.counter_enabled && !counter_str.is_empty(); // 5. Assemble parts based on counter position let mut result = String::new(); if has_counter && self.counter_position == 0 { result.push_str(&counter_str); result.push('_'); } result.push_str(&self.prefix); if has_counter && self.counter_position == 1 { result.push_str(&counter_str); result.push('_'); } if has_counter && self.counter_position == 4 { result.push_str(&counter_str); } else { result.push_str(&working_name); } if has_counter && self.counter_position == 2 { result.push('_'); result.push_str(&counter_str); } result.push_str(&self.suffix); if has_counter && self.counter_position == 3 { result.push('_'); result.push_str(&counter_str); } // 6. Apply case conversion let result = rename::apply_case_conversion(&result, self.case_mode); format!("{}.{}", result, extension) } }