Add rotate, flip, watermark, and rename CLI flags with helper functions

This commit is contained in:
2026-03-06 12:33:21 +02:00
parent e969c4165e
commit b21b9edb36

View File

@@ -18,6 +18,7 @@ struct Cli {
} }
#[derive(Subcommand)] #[derive(Subcommand)]
#[allow(clippy::large_enum_variant)]
enum Commands { enum Commands {
/// Process images with a preset or custom operations /// Process images with a preset or custom operations
Process { Process {
@@ -37,7 +38,7 @@ enum Commands {
#[arg(long)] #[arg(long)]
resize: Option<String>, resize: Option<String>,
/// Output format (jpeg, png, webp, avif) /// Output format (jpeg, png, webp, avif, gif, tiff)
#[arg(long)] #[arg(long)]
format: Option<String>, format: Option<String>,
@@ -49,6 +50,38 @@ enum Commands {
#[arg(long)] #[arg(long)]
strip_metadata: bool, strip_metadata: bool,
/// Rotate images (90, 180, 270, auto)
#[arg(long)]
rotate: Option<String>,
/// Flip images (horizontal, vertical)
#[arg(long)]
flip: Option<String>,
/// Add text watermark (e.g., "(c) 2026 My Name")
#[arg(long)]
watermark: Option<String>,
/// Watermark position (top-left, top-center, top-right, center, bottom-left, bottom-center, bottom-right)
#[arg(long, default_value = "bottom-right")]
watermark_position: String,
/// Watermark opacity (0.0-1.0)
#[arg(long, default_value = "0.5")]
watermark_opacity: f32,
/// Rename with prefix
#[arg(long)]
rename_prefix: Option<String>,
/// Rename with suffix
#[arg(long)]
rename_suffix: Option<String>,
/// Rename template (e.g., "{name}_{counter:3}.{ext}")
#[arg(long)]
rename_template: Option<String>,
/// Include subdirectories /// Include subdirectories
#[arg(short, long)] #[arg(short, long)]
recursive: bool, recursive: bool,
@@ -93,9 +126,17 @@ fn main() {
format, format,
quality, quality,
strip_metadata, strip_metadata,
rotate,
flip,
watermark,
watermark_position,
watermark_opacity,
rename_prefix,
rename_suffix,
rename_template,
recursive, recursive,
} => { } => {
cmd_process( cmd_process(CmdProcessArgs {
input, input,
preset, preset,
output, output,
@@ -103,8 +144,16 @@ fn main() {
format, format,
quality, quality,
strip_metadata, strip_metadata,
rotate,
flip,
watermark,
watermark_position,
watermark_opacity,
rename_prefix,
rename_suffix,
rename_template,
recursive, recursive,
); });
} }
Commands::Preset { action } => match action { Commands::Preset { action } => match action {
PresetAction::List => cmd_preset_list(), PresetAction::List => cmd_preset_list(),
@@ -116,23 +165,32 @@ fn main() {
} }
} }
#[allow(clippy::too_many_arguments)] struct CmdProcessArgs {
fn cmd_process(
input: Vec<String>, input: Vec<String>,
preset_name: Option<String>, preset: Option<String>,
output: Option<String>, output: Option<String>,
resize: Option<String>, resize: Option<String>,
format: Option<String>, format: Option<String>,
quality: Option<String>, quality: Option<String>,
strip_metadata: bool, strip_metadata: bool,
rotate: Option<String>,
flip: Option<String>,
watermark: Option<String>,
watermark_position: String,
watermark_opacity: f32,
rename_prefix: Option<String>,
rename_suffix: Option<String>,
rename_template: Option<String>,
recursive: bool, recursive: bool,
) { }
fn cmd_process(args: CmdProcessArgs) {
// Collect input files // Collect input files
let mut source_files = Vec::new(); let mut source_files = Vec::new();
for path_str in &input { for path_str in &args.input {
let path = PathBuf::from(path_str); let path = PathBuf::from(path_str);
if path.is_dir() { if path.is_dir() {
let files = discovery::discover_images(&path, recursive); let files = discovery::discover_images(&path, args.recursive);
source_files.extend(files); source_files.extend(files);
} else if path.is_file() { } else if path.is_file() {
source_files.push(path); source_files.push(path);
@@ -148,19 +206,17 @@ fn cmd_process(
println!("Found {} image(s)", source_files.len()); println!("Found {} image(s)", source_files.len());
// Determine input directory (use parent of first file)
let input_dir = source_files[0] let input_dir = source_files[0]
.parent() .parent()
.unwrap_or_else(|| std::path::Path::new(".")) .unwrap_or_else(|| std::path::Path::new("."))
.to_path_buf(); .to_path_buf();
// Determine output directory let output_dir = args.output
let output_dir = output .as_ref()
.map(PathBuf::from) .map(PathBuf::from)
.unwrap_or_else(|| input_dir.join("processed")); .unwrap_or_else(|| input_dir.join("processed"));
// Build job from preset or CLI args let mut job = if let Some(ref name) = args.preset {
let mut job = if let Some(ref name) = preset_name {
let preset = find_preset(name); let preset = find_preset(name);
println!("Using preset: {}", preset.name); println!("Using preset: {}", preset.name);
preset.to_job(&input_dir, &output_dir) preset.to_job(&input_dir, &output_dir)
@@ -169,24 +225,48 @@ fn cmd_process(
}; };
// Override with CLI flags // Override with CLI flags
if let Some(ref resize_str) = resize { if let Some(ref resize_str) = args.resize {
job.resize = Some(parse_resize(resize_str)); job.resize = Some(parse_resize(resize_str));
} }
if let Some(ref fmt_str) = format if let Some(ref fmt_str) = args.format
&& let Some(fmt) = parse_format(fmt_str) && let Some(fmt) = parse_format(fmt_str)
{ {
job.convert = Some(ConvertConfig::SingleFormat(fmt)); job.convert = Some(ConvertConfig::SingleFormat(fmt));
} }
if let Some(ref q_str) = quality if let Some(ref q_str) = args.quality
&& let Some(preset) = parse_quality(q_str) && let Some(preset) = parse_quality(q_str)
{ {
job.compress = Some(CompressConfig::Preset(preset)); job.compress = Some(CompressConfig::Preset(preset));
} }
if strip_metadata { if args.strip_metadata {
job.metadata = Some(MetadataConfig::StripAll); job.metadata = Some(MetadataConfig::StripAll);
} }
if let Some(ref rot) = args.rotate {
job.rotation = Some(parse_rotation(rot));
}
if let Some(ref fl) = args.flip {
job.flip = Some(parse_flip(fl));
}
if let Some(ref text) = args.watermark {
let position = parse_watermark_position(&args.watermark_position);
job.watermark = Some(WatermarkConfig::Text {
text: text.clone(),
position,
font_size: 24.0,
opacity: args.watermark_opacity,
color: [255, 255, 255, 255],
});
}
if args.rename_prefix.is_some() || args.rename_suffix.is_some() || args.rename_template.is_some() {
job.rename = Some(RenameConfig {
prefix: args.rename_prefix.unwrap_or_default(),
suffix: args.rename_suffix.unwrap_or_default(),
counter_start: 1,
counter_padding: 3,
template: args.rename_template,
});
}
// Add sources
for file in &source_files { for file in &source_files {
job.add_source(file); job.add_source(file);
} }
@@ -251,7 +331,7 @@ fn cmd_process(
timestamp: chrono_timestamp(), timestamp: chrono_timestamp(),
input_dir: input_dir.to_string_lossy().into(), input_dir: input_dir.to_string_lossy().into(),
output_dir: output_dir.to_string_lossy().into(), output_dir: output_dir.to_string_lossy().into(),
preset_name, preset_name: args.preset,
total: result.total, total: result.total,
succeeded: result.succeeded, succeeded: result.succeeded,
failed: result.failed, failed: result.failed,
@@ -429,6 +509,50 @@ fn parse_quality(s: &str) -> Option<QualityPreset> {
} }
} }
fn parse_rotation(s: &str) -> Rotation {
match s.to_lowercase().as_str() {
"90" | "cw90" => Rotation::Cw90,
"180" => Rotation::Cw180,
"270" | "cw270" => Rotation::Cw270,
"auto" => Rotation::AutoOrient,
"none" | "0" => Rotation::None,
_ => {
eprintln!("Unknown rotation: '{}'. Supported: 90, 180, 270, auto, none", s);
std::process::exit(1);
}
}
}
fn parse_flip(s: &str) -> Flip {
match s.to_lowercase().as_str() {
"horizontal" | "h" => Flip::Horizontal,
"vertical" | "v" => Flip::Vertical,
"none" => Flip::None,
_ => {
eprintln!("Unknown flip: '{}'. Supported: horizontal, vertical, none", s);
std::process::exit(1);
}
}
}
fn parse_watermark_position(s: &str) -> WatermarkPosition {
match s.to_lowercase().replace(' ', "-").as_str() {
"top-left" | "tl" => WatermarkPosition::TopLeft,
"top-center" | "tc" | "top" => WatermarkPosition::TopCenter,
"top-right" | "tr" => WatermarkPosition::TopRight,
"middle-left" | "ml" | "left" => WatermarkPosition::MiddleLeft,
"center" | "c" | "middle" => WatermarkPosition::Center,
"middle-right" | "mr" | "right" => WatermarkPosition::MiddleRight,
"bottom-left" | "bl" => WatermarkPosition::BottomLeft,
"bottom-center" | "bc" | "bottom" => WatermarkPosition::BottomCenter,
"bottom-right" | "br" => WatermarkPosition::BottomRight,
_ => {
eprintln!("Unknown position: '{}'. Supported: top-left, top-center, top-right, center, bottom-left, bottom-center, bottom-right", s);
std::process::exit(1);
}
}
}
fn format_bytes(bytes: u64) -> String { fn format_bytes(bytes: u64) -> String {
if bytes < 1024 { if bytes < 1024 {
format!("{} B", bytes) format!("{} B", bytes)