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)]
#[allow(clippy::large_enum_variant)]
enum Commands {
/// Process images with a preset or custom operations
Process {
@@ -37,7 +38,7 @@ enum Commands {
#[arg(long)]
resize: Option<String>,
/// Output format (jpeg, png, webp, avif)
/// Output format (jpeg, png, webp, avif, gif, tiff)
#[arg(long)]
format: Option<String>,
@@ -49,6 +50,38 @@ enum Commands {
#[arg(long)]
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
#[arg(short, long)]
recursive: bool,
@@ -93,9 +126,17 @@ fn main() {
format,
quality,
strip_metadata,
rotate,
flip,
watermark,
watermark_position,
watermark_opacity,
rename_prefix,
rename_suffix,
rename_template,
recursive,
} => {
cmd_process(
cmd_process(CmdProcessArgs {
input,
preset,
output,
@@ -103,8 +144,16 @@ fn main() {
format,
quality,
strip_metadata,
rotate,
flip,
watermark,
watermark_position,
watermark_opacity,
rename_prefix,
rename_suffix,
rename_template,
recursive,
);
});
}
Commands::Preset { action } => match action {
PresetAction::List => cmd_preset_list(),
@@ -116,23 +165,32 @@ fn main() {
}
}
#[allow(clippy::too_many_arguments)]
fn cmd_process(
struct CmdProcessArgs {
input: Vec<String>,
preset_name: Option<String>,
preset: Option<String>,
output: Option<String>,
resize: Option<String>,
format: Option<String>,
quality: Option<String>,
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,
) {
}
fn cmd_process(args: CmdProcessArgs) {
// Collect input files
let mut source_files = Vec::new();
for path_str in &input {
for path_str in &args.input {
let path = PathBuf::from(path_str);
if path.is_dir() {
let files = discovery::discover_images(&path, recursive);
let files = discovery::discover_images(&path, args.recursive);
source_files.extend(files);
} else if path.is_file() {
source_files.push(path);
@@ -148,19 +206,17 @@ fn cmd_process(
println!("Found {} image(s)", source_files.len());
// Determine input directory (use parent of first file)
let input_dir = source_files[0]
.parent()
.unwrap_or_else(|| std::path::Path::new("."))
.to_path_buf();
// Determine output directory
let output_dir = output
let output_dir = args.output
.as_ref()
.map(PathBuf::from)
.unwrap_or_else(|| input_dir.join("processed"));
// Build job from preset or CLI args
let mut job = if let Some(ref name) = preset_name {
let mut job = if let Some(ref name) = args.preset {
let preset = find_preset(name);
println!("Using preset: {}", preset.name);
preset.to_job(&input_dir, &output_dir)
@@ -169,24 +225,48 @@ fn cmd_process(
};
// 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));
}
if let Some(ref fmt_str) = format
if let Some(ref fmt_str) = args.format
&& let Some(fmt) = parse_format(fmt_str)
{
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)
{
job.compress = Some(CompressConfig::Preset(preset));
}
if strip_metadata {
if args.strip_metadata {
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 {
job.add_source(file);
}
@@ -251,7 +331,7 @@ fn cmd_process(
timestamp: chrono_timestamp(),
input_dir: input_dir.to_string_lossy().into(),
output_dir: output_dir.to_string_lossy().into(),
preset_name,
preset_name: args.preset,
total: result.total,
succeeded: result.succeeded,
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 {
if bytes < 1024 {
format!("{} B", bytes)