Add rotate, flip, watermark, and rename CLI flags with helper functions
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user