use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; use crate::operations::*; use crate::types::{ImageFormat, ImageSource}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProcessingJob { pub input_dir: PathBuf, pub output_dir: PathBuf, #[serde(skip)] pub sources: Vec, pub resize: Option, pub resize_algorithm: ResizeAlgorithm, pub rotation: Option, pub flip: Option, pub adjustments: Option, pub convert: Option, pub compress: Option, pub metadata: Option, pub watermark: Option, pub rename: Option, pub overwrite_behavior: OverwriteAction, pub preserve_directory_structure: bool, pub progressive_jpeg: bool, pub avif_speed: u8, pub output_dpi: u32, } impl ProcessingJob { pub fn new(input_dir: impl AsRef, output_dir: impl AsRef) -> Self { Self { input_dir: input_dir.as_ref().to_path_buf(), output_dir: output_dir.as_ref().to_path_buf(), sources: Vec::new(), resize: None, resize_algorithm: ResizeAlgorithm::default(), rotation: None, flip: None, adjustments: None, convert: None, compress: None, metadata: None, watermark: None, rename: None, overwrite_behavior: OverwriteAction::default(), preserve_directory_structure: false, progressive_jpeg: false, avif_speed: 6, output_dpi: 0, } } pub fn add_source(&mut self, path: impl AsRef) { self.sources.push(ImageSource::from_path(path)); } pub fn operation_count(&self) -> usize { let mut count = 0; if self.resize.is_some() { count += 1; } if self.rotation.is_some() { count += 1; } if self.flip.is_some() { count += 1; } if self.adjustments.as_ref().is_some_and(|a| !a.is_noop()) { count += 1; } if self.convert.is_some() { count += 1; } if self.compress.is_some() { count += 1; } if self.metadata.is_some() { count += 1; } if self.watermark.is_some() { count += 1; } if self.rename.is_some() { count += 1; } count } /// Returns true if the job requires decoding/encoding pixel data. /// When false, we can use a fast copy-and-rename path. pub fn needs_pixel_processing(&self) -> bool { self.resize.is_some() || matches!(self.rotation, Some(r) if !matches!(r, Rotation::None)) || matches!(self.flip, Some(f) if !matches!(f, Flip::None)) || self.adjustments.as_ref().is_some_and(|a| !a.is_noop()) || self.convert.is_some() || self.compress.is_some() || self.watermark.is_some() } pub fn output_path_for( &self, source: &ImageSource, output_format: Option, ) -> PathBuf { let stem = source .path .file_stem() .and_then(|s| s.to_str()) .unwrap_or("output"); let ext = output_format .map(|f| f.extension()) .or_else(|| { source .path .extension() .and_then(|e| e.to_str()) }) .unwrap_or("bin"); let filename = format!("{}.{}", stem, ext); if self.preserve_directory_structure { // Maintain relative path from input_dir if let Ok(rel) = source.path.strip_prefix(&self.input_dir) { if let Some(parent) = rel.parent() { if parent.components().count() > 0 { return self.output_dir.join(parent).join(filename); } } } } self.output_dir.join(filename) } }