Skip to content

Commit 4226d7b

Browse files
narrowstacksclaude
andcommitted
refactor: consolidate CLI args and add white balance support
- Add clap_complete for shell completion generation - Move preset subcommand logic into commands/mod.rs, remove preset.rs - Refactor batch command to use shared ProcessingParams - Add WhiteBalance enum with temperature/tint/auto options - Add --white-balance flag to convert and batch commands - Simplify output path handling with determine_output_path helper - Add verbose flag for detailed processing output Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent e550b59 commit 4226d7b

8 files changed

Lines changed: 311 additions & 273 deletions

File tree

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/invers-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ gpu = ["invers-core/gpu"]
1818

1919
[dependencies]
2020
clap = { version = "4.5.48", features = ["derive"] }
21+
clap_complete = "4.5"
2122
invers-core = { path = "../invers-core" }
2223
rayon = "1.11.0"
2324
serde = { version = "1.0", features = ["derive"] }

crates/invers-cli/src/commands/batch.rs

Lines changed: 89 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
use rayon::prelude::*;
22
use std::path::PathBuf;
33
use std::sync::atomic::{AtomicUsize, Ordering};
4+
use std::sync::Mutex;
45
use std::time::Instant;
56

67
use invers_cli::{
78
determine_output_path, expand_inputs, make_base_from_rgb, parse_base_rgb, process_single_image,
8-
ProcessingParams,
9+
ProcessingParams, WhiteBalance,
910
};
1011

1112
use invers_core::models::BaseEstimation;
@@ -43,15 +44,15 @@ pub fn cmd_batch(
4344
base_from: Option<PathBuf>,
4445
base: Option<String>,
4546
per_image: bool,
46-
preset: Option<PathBuf>,
47-
scan_profile_path: Option<PathBuf>,
4847
export: String,
48+
white_balance: WhiteBalance,
4949
exposure: f32,
5050
out: Option<PathBuf>,
5151
threads: Option<usize>,
5252
silent: bool,
5353
verbose: bool,
5454
cpu_only: bool,
55+
dry_run: bool,
5556
// Debug options
5657
no_tonecurve: bool,
5758
no_colormatrix: bool,
@@ -72,6 +73,8 @@ pub fn cmd_batch(
7273
cb_color: Option<String>,
7374
cb_film: Option<String>,
7475
cb_wb: Option<String>,
76+
// B&W conversion flag
77+
bw: bool,
7578
) -> Result<(), String> {
7679
let batch_start = Instant::now();
7780

@@ -98,6 +101,37 @@ pub fn cmd_batch(
98101
println!("Found {} image files to process", inputs.len());
99102
}
100103

104+
// Determine base estimation strategy description for messaging
105+
let strategy_message = if let Some(ref base_str) = base {
106+
let base_rgb = parse_base_rgb(base_str)?;
107+
format!(
108+
"Using manual base values: [{:.4}, {:.4}, {:.4}]",
109+
base_rgb[0], base_rgb[1], base_rgb[2]
110+
)
111+
} else if per_image {
112+
"Using per-image base estimation (each image analyzed separately)".to_string()
113+
} else if base_from.is_some() {
114+
"Using base estimation from file".to_string()
115+
} else {
116+
"Using same-roll mode: base will be estimated from first image".to_string()
117+
};
118+
119+
if !silent {
120+
println!("{}", strategy_message);
121+
}
122+
123+
// Handle dry-run mode
124+
if dry_run {
125+
println!("\n=== DRY RUN MODE ===");
126+
println!("Files that would be processed ({}):", inputs.len());
127+
for input in &inputs {
128+
println!(" {}", input.display());
129+
}
130+
println!("\nBase estimation strategy: {}", strategy_message);
131+
println!("No files were processed.");
132+
return Ok(());
133+
}
134+
101135
// Configure thread pool if specified
102136
if let Some(num_threads) = threads {
103137
rayon::ThreadPoolBuilder::new()
@@ -198,24 +232,11 @@ pub fn cmd_batch(
198232
BaseStrategy::PerImage => None, // Each image will estimate its own
199233
};
200234

201-
// Load shared scan profile if provided
202-
let scan_profile = if let Some(profile_path) = &scan_profile_path {
203-
if !silent {
204-
println!("Loading scan profile from {}...", profile_path.display());
205-
}
206-
Some(invers_core::presets::load_scan_profile(profile_path)?)
235+
// Override inversion mode if --bw flag is set
236+
let effective_inversion = if bw {
237+
Some("bw".to_string())
207238
} else {
208-
None
209-
};
210-
211-
// Load shared film preset if provided
212-
let film_preset = if let Some(preset_path) = &preset {
213-
if !silent {
214-
println!("Loading film preset from {}...", preset_path.display());
215-
}
216-
Some(invers_core::presets::load_film_preset(preset_path)?)
217-
} else {
218-
None
239+
inversion.clone()
219240
};
220241

221242
// Build shared processing params (reused for all images)
@@ -226,6 +247,7 @@ pub fn cmd_batch(
226247
silent: true, // Suppress per-image output in batch mode
227248
verbose,
228249
debug,
250+
white_balance,
229251
pipeline: pipeline.clone(),
230252
db_red,
231253
db_blue,
@@ -237,7 +259,7 @@ pub fn cmd_batch(
237259
cb_wb: cb_wb.clone(),
238260
no_tonecurve,
239261
no_colormatrix,
240-
inversion: inversion.clone(),
262+
inversion: effective_inversion,
241263
auto_wb,
242264
auto_wb_strength,
243265
auto_wb_mode: auto_wb_mode.clone(),
@@ -251,6 +273,9 @@ pub fn cmd_batch(
251273
// Progress tracking
252274
let processed_count = AtomicUsize::new(0);
253275
let total_files = inputs.len();
276+
let processing_start = Instant::now();
277+
// Track cumulative time for averaging (used for ETA calculation)
278+
let cumulative_time = Mutex::new(0.0f64);
254279

255280
// Process files in parallel
256281
let results: Vec<Result<(PathBuf, f64), String>> = inputs
@@ -281,8 +306,8 @@ pub fn cmd_batch(
281306
process_single_image(
282307
decoded,
283308
base_estimation,
284-
film_preset.clone(),
285-
scan_profile.clone(),
309+
None, // No film preset
310+
None, // No scan profile
286311
&output_path,
287312
&params,
288313
)?;
@@ -291,14 +316,32 @@ pub fn cmd_batch(
291316

292317
// Update progress
293318
let count = processed_count.fetch_add(1, Ordering::SeqCst) + 1;
319+
320+
// Update cumulative time for ETA calculation
321+
{
322+
let mut cum = cumulative_time.lock().unwrap();
323+
*cum += file_elapsed;
324+
}
325+
294326
if !silent {
327+
// Calculate estimated time remaining based on wall-clock time
328+
let wall_elapsed = processing_start.elapsed().as_secs_f64();
329+
let remaining_files = total_files - count;
330+
let avg_time_per_file = wall_elapsed / count as f64;
331+
let est_remaining_secs = avg_time_per_file * remaining_files as f64;
332+
333+
// Format estimated remaining time
334+
let est_remaining_str = format_duration(est_remaining_secs);
335+
336+
// Get just the filename for cleaner output
337+
let filename = input
338+
.file_name()
339+
.map(|n| n.to_string_lossy().to_string())
340+
.unwrap_or_else(|| input.display().to_string());
341+
295342
println!(
296-
"[{}/{}] {} -> {} ({:.2}s)",
297-
count,
298-
total_files,
299-
input.display(),
300-
output_path.display(),
301-
file_elapsed
343+
"[{}/{}] processing {} - {:.1}s (est. {} remaining)",
344+
count, total_files, filename, file_elapsed, est_remaining_str
302345
);
303346
} else {
304347
println!("{}", output_path.display());
@@ -352,3 +395,20 @@ pub fn cmd_batch(
352395
Err(format!("{} files failed to process", errors.len()))
353396
}
354397
}
398+
399+
/// Format a duration in seconds to a human-readable string.
400+
///
401+
/// Examples: "2.3s", "1m 30s", "4m 30s"
402+
fn format_duration(seconds: f64) -> String {
403+
if seconds < 60.0 {
404+
format!("{:.1}s", seconds)
405+
} else {
406+
let minutes = (seconds / 60.0).floor() as u64;
407+
let remaining_secs = (seconds % 60.0).round() as u64;
408+
if remaining_secs == 0 {
409+
format!("{}m", minutes)
410+
} else {
411+
format!("{}m {}s", minutes, remaining_secs)
412+
}
413+
}
414+
}

crates/invers-cli/src/commands/convert.rs

Lines changed: 17 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use std::path::PathBuf;
22
use std::time::Instant;
33

4-
use invers_cli::{determine_output_path, parse_base_rgb, process_single_image, ProcessingParams};
4+
use invers_cli::{
5+
determine_output_path, parse_base_rgb, process_single_image, ProcessingParams, WhiteBalance,
6+
};
57

68
/// Execute the convert command for a single image.
79
///
@@ -13,7 +15,6 @@ use invers_cli::{determine_output_path, parse_base_rgb, process_single_image, Pr
1315
/// - Automatic or manual base estimation
1416
/// - B&W auto-detection (switches to BlackAndWhite inversion mode)
1517
/// - Pipeline mode selection (Legacy, Research, or CB)
16-
/// - Film preset and scan profile application
1718
/// - Export to TIFF16 or Linear DNG
1819
///
1920
/// # Returns
@@ -23,9 +24,8 @@ use invers_cli::{determine_output_path, parse_base_rgb, process_single_image, Pr
2324
pub fn cmd_convert(
2425
input: PathBuf,
2526
out: Option<PathBuf>,
26-
preset: Option<PathBuf>,
27-
scan_profile_path: Option<PathBuf>,
2827
export: String,
28+
white_balance: WhiteBalance,
2929
no_tonecurve: bool,
3030
no_colormatrix: bool,
3131
exposure: f32,
@@ -50,6 +50,8 @@ pub fn cmd_convert(
5050
cb_color: Option<String>,
5151
cb_film: Option<String>,
5252
cb_wb: Option<String>,
53+
// B&W conversion flag
54+
bw: bool,
5355
) -> Result<(), String> {
5456
let start_time = Instant::now();
5557

@@ -114,31 +116,6 @@ pub fn cmd_convert(
114116
);
115117
}
116118

117-
// Load scan profile if provided
118-
let scan_profile = if let Some(profile_path) = scan_profile_path {
119-
if !silent {
120-
println!("Loading scan profile from {}...", profile_path.display());
121-
}
122-
let profile = invers_core::presets::load_scan_profile(&profile_path)?;
123-
if !silent {
124-
println!(" Profile: {} ({})", profile.name, profile.source_type);
125-
if let Some(ref hsl) = profile.hsl_adjustments {
126-
if hsl.has_adjustments() {
127-
println!(" HSL adjustments: enabled");
128-
}
129-
}
130-
if let Some(gamma) = profile.default_gamma {
131-
println!(
132-
" Default gamma: [{:.2}, {:.2}, {:.2}]",
133-
gamma[0], gamma[1], gamma[2]
134-
);
135-
}
136-
}
137-
Some(profile)
138-
} else {
139-
None
140-
};
141-
142119
// Parse manual base values or auto-estimate using default method
143120
let base_estimation = if let Some(base_str) = base {
144121
// Parse manual base values (R,G,B format)
@@ -179,22 +156,19 @@ pub fn cmd_convert(
179156
estimation
180157
};
181158

182-
// Load film preset if provided
183-
let film_preset = if let Some(preset_path) = preset {
184-
if !silent {
185-
println!("Loading film preset from {}...", preset_path.display());
186-
}
187-
Some(invers_core::presets::load_film_preset(&preset_path)?)
188-
} else {
189-
None
190-
};
191-
192159
// Prepare output path
193160
let output_path = determine_output_path(&input, &out, &export)?;
194161
if !silent {
195162
println!("Output: {}", output_path.display());
196163
}
197164

165+
// Override inversion mode if --bw flag is set
166+
let effective_inversion = if bw {
167+
Some("bw".to_string())
168+
} else {
169+
inversion.clone()
170+
};
171+
198172
// Build processing params
199173
let params = ProcessingParams {
200174
export: export.clone(),
@@ -203,6 +177,7 @@ pub fn cmd_convert(
203177
silent,
204178
verbose,
205179
debug,
180+
white_balance,
206181
pipeline: pipeline.clone(),
207182
db_red,
208183
db_blue,
@@ -214,7 +189,7 @@ pub fn cmd_convert(
214189
cb_wb: cb_wb.clone(),
215190
no_tonecurve,
216191
no_colormatrix,
217-
inversion: inversion.clone(),
192+
inversion: effective_inversion,
218193
auto_wb,
219194
auto_wb_strength,
220195
auto_wb_mode: auto_wb_mode.clone(),
@@ -225,8 +200,8 @@ pub fn cmd_convert(
225200
process_single_image(
226201
decoded,
227202
base_estimation,
228-
film_preset,
229-
scan_profile,
203+
None, // No film preset
204+
None, // No scan profile
230205
&output_path,
231206
&params,
232207
)?;

crates/invers-cli/src/commands/mod.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ mod analyze;
44
mod batch;
55
mod convert;
66
mod init;
7-
mod preset;
87

98
#[cfg(debug_assertions)]
109
mod debug;
@@ -14,7 +13,6 @@ pub use analyze::cmd_analyze;
1413
pub use batch::cmd_batch;
1514
pub use convert::cmd_convert;
1615
pub use init::cmd_init;
17-
pub use preset::{cmd_preset_create, cmd_preset_list, cmd_preset_show};
1816

1917
#[cfg(debug_assertions)]
2018
pub use debug::{cmd_diagnose, cmd_test_params};

0 commit comments

Comments
 (0)