1#![deny(missing_docs)]
7#![deny(rustdoc::broken_intra_doc_links)]
8
9use std::path::PathBuf;
10use std::process;
11
12use clap::Parser;
13
14use agx::Preset;
15use agx_cli::{create_engine, BatchOpts, Cli, Commands, EditArgs, OutputOpts};
16
17mod batch;
18
19fn warn_unknown_preset_fields(preset_path: &std::path::Path) {
26 let toml_str = match std::fs::read_to_string(preset_path) {
27 Ok(s) => s,
28 Err(_) => return, };
30 let mut diags = agx::preset::validate::detect_unknown_fields(&toml_str);
31 diags.extend(agx::preset::validate::find_unknown_fields(&toml_str));
32 for diag in &diags {
33 eprintln!(
34 "warning: {}:{}: {}",
35 preset_path.display(),
36 diag.location.line,
37 diag.message,
38 );
39 }
40}
41
42fn main() {
43 let cli = Cli::parse();
44 let use_gpu = cli.gpu;
45
46 let result = match cli.command {
47 Commands::Validate {
48 paths,
49 quiet,
50 format,
51 } => {
52 process::exit(agx_cli::validate::run_validate(&paths, quiet, format));
53 }
54 Commands::Apply {
55 input,
56 preset,
57 presets,
58 output,
59 output_opts,
60 } => run_apply(
61 &input,
62 preset.as_deref(),
63 &presets,
64 &output,
65 &output_opts,
66 use_gpu,
67 ),
68 Commands::Edit {
69 input,
70 output,
71 edit,
72 output_opts,
73 } => run_edit(&input, &output, &edit, &output_opts, use_gpu),
74 Commands::BatchApply { preset, batch } => run_batch_apply(&preset, &batch, use_gpu),
75 Commands::BatchEdit { edit, batch } => run_batch_edit(&edit, &batch, use_gpu),
76 Commands::MultiApply {
77 input,
78 preset,
79 output,
80 noop,
81 jobs,
82 } => run_multi_apply(&input, &preset, &output, noop, jobs, use_gpu),
83 };
84
85 if let Err(e) = result {
86 eprintln!("Error: {e}");
87 process::exit(1);
88 }
89}
90
91#[cfg(feature = "profiling")]
92fn write_profile_entry(
93 path: &std::path::Path,
94 image_name: &str,
95 preset_name: &str,
96 dimensions: (u32, u32),
97 decode_ms: f64,
98 render_profile: &agx::RenderProfile,
99 encode_ms: f64,
100) -> agx::Result<()> {
101 use std::io::Write;
102
103 let mut stages = serde_json::Map::new();
104 stages.insert("decode".to_string(), serde_json::Value::from(decode_ms));
105 for (name, ms) in &render_profile.stages {
106 stages.insert(name.clone(), serde_json::Value::from(*ms));
107 }
108 stages.insert("encode".to_string(), serde_json::Value::from(encode_ms));
109
110 let total_ms = decode_ms + render_profile.total_ms + encode_ms;
111
112 let entry = serde_json::json!({
113 "image": image_name,
114 "preset": preset_name,
115 "dimensions": [dimensions.0, dimensions.1],
116 "stages": stages,
117 "total_ms": total_ms,
118 });
119
120 let mut entries: Vec<serde_json::Value> = match std::fs::read_to_string(path) {
121 Ok(content) => serde_json::from_str(&content).unwrap_or_default(),
122 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(),
123 Err(e) => return Err(agx::AgxError::Io(e)),
124 };
125 entries.push(entry);
126
127 let mut file = std::fs::File::create(path).map_err(agx::AgxError::Io)?;
128 file.write_all(serde_json::to_string_pretty(&entries).unwrap().as_bytes())
129 .map_err(agx::AgxError::Io)?;
130 Ok(())
131}
132
133fn run_apply(
134 input: &std::path::Path,
135 preset_path: Option<&std::path::Path>,
136 presets: &[PathBuf],
137 output: &std::path::Path,
138 output_opts: &OutputOpts,
139 use_gpu: bool,
140) -> agx::Result<()> {
141 #[cfg(feature = "profiling")]
142 let decode_start = std::time::Instant::now();
143
144 let metadata = agx::metadata::extract_metadata(input);
145 let linear = agx::decode::decode(input)?;
146
147 #[cfg(feature = "profiling")]
148 let decode_ms = decode_start.elapsed().as_secs_f64() * 1000.0;
149
150 let mut engine = create_engine(linear, use_gpu);
151
152 #[cfg(feature = "profiling")]
153 let preset_name = if !presets.is_empty() {
154 presets
155 .iter()
156 .map(|p| {
157 p.file_stem()
158 .unwrap_or_default()
159 .to_string_lossy()
160 .to_string()
161 })
162 .collect::<Vec<_>>()
163 .join("+")
164 } else if let Some(path) = preset_path {
165 path.file_stem()
166 .unwrap_or_default()
167 .to_string_lossy()
168 .to_string()
169 } else {
170 "none".to_string()
171 };
172
173 if !presets.is_empty() {
174 for path in presets {
175 warn_unknown_preset_fields(path);
176 let preset = Preset::load_from_file(path)?;
177 engine.layer_preset(&preset);
178 }
179 } else if let Some(path) = preset_path {
180 warn_unknown_preset_fields(path);
181 let preset = Preset::load_from_file(path)?;
182 engine.apply_preset(&preset);
183 }
184
185 let result = engine.render();
186 let rendered = result.image;
187 let opts = output_opts.encode_options()?;
188
189 #[cfg(feature = "profiling")]
190 let encode_start = std::time::Instant::now();
191
192 let final_path =
193 agx::encode::encode_to_file_with_options(&rendered, output, &opts, metadata.as_ref())?;
194
195 #[cfg(feature = "profiling")]
196 let encode_ms = encode_start.elapsed().as_secs_f64() * 1000.0;
197
198 println!("Saved to {}", final_path.display());
199
200 #[cfg(feature = "profiling")]
201 if let Some(ref profile_path) = output_opts.profile_output {
202 if let Some(profile) = result.profile {
203 let dims = (rendered.width(), rendered.height());
204 let image_name = input.file_name().unwrap_or_default().to_string_lossy();
205 write_profile_entry(
206 profile_path,
207 &image_name,
208 &preset_name,
209 dims,
210 decode_ms,
211 &profile,
212 encode_ms,
213 )?;
214 }
215 }
216
217 Ok(())
218}
219
220fn run_edit(
221 input: &std::path::Path,
222 output: &std::path::Path,
223 edit: &EditArgs,
224 output_opts: &OutputOpts,
225 use_gpu: bool,
226) -> agx::Result<()> {
227 #[cfg(feature = "profiling")]
228 let decode_start = std::time::Instant::now();
229
230 let metadata = agx::metadata::extract_metadata(input);
231 let linear = agx::decode::decode(input)?;
232
233 #[cfg(feature = "profiling")]
234 let decode_ms = decode_start.elapsed().as_secs_f64() * 1000.0;
235
236 let mut engine = create_engine(linear, use_gpu);
237 engine.set_params(edit.to_params()?);
238 if let Some(lut) = edit.load_lut()? {
239 engine.set_lut(Some(lut));
240 }
241 let result = engine.render();
242 let rendered = result.image;
243 let opts = output_opts.encode_options()?;
244
245 #[cfg(feature = "profiling")]
246 let encode_start = std::time::Instant::now();
247
248 let final_path =
249 agx::encode::encode_to_file_with_options(&rendered, output, &opts, metadata.as_ref())?;
250
251 #[cfg(feature = "profiling")]
252 let encode_ms = encode_start.elapsed().as_secs_f64() * 1000.0;
253
254 println!("Saved to {}", final_path.display());
255
256 #[cfg(feature = "profiling")]
257 if let Some(ref profile_path) = output_opts.profile_output {
258 if let Some(profile) = result.profile {
259 let dims = (rendered.width(), rendered.height());
260 let image_name = input.file_name().unwrap_or_default().to_string_lossy();
261 write_profile_entry(
262 profile_path,
263 &image_name,
264 "edit",
265 dims,
266 decode_ms,
267 &profile,
268 encode_ms,
269 )?;
270 }
271 }
272
273 Ok(())
274}
275
276fn run_batch_apply(
277 preset_path: &std::path::Path,
278 batch: &BatchOpts,
279 use_gpu: bool,
280) -> agx::Result<()> {
281 warn_unknown_preset_fields(preset_path);
282 let encode = batch.output.encode_options()?;
283 let run = batch::BatchRun {
284 input_dir: &batch.input_dir,
285 output_dir: &batch.output_dir,
286 recursive: batch.recursive,
287 encode: &encode,
288 suffix: batch.suffix.as_deref(),
289 jobs: batch.jobs,
290 skip_errors: batch.skip_errors,
291 use_gpu,
292 };
293 let summary = batch::run_batch_apply(&run, preset_path);
294 if !summary.failed.is_empty() {
295 process::exit(1);
296 }
297 Ok(())
298}
299
300fn run_batch_edit(edit: &EditArgs, batch: &BatchOpts, use_gpu: bool) -> agx::Result<()> {
301 let params = edit.to_params()?;
302 let lut_data = edit.load_lut()?;
303 let encode = batch.output.encode_options()?;
304 let run = batch::BatchRun {
305 input_dir: &batch.input_dir,
306 output_dir: &batch.output_dir,
307 recursive: batch.recursive,
308 encode: &encode,
309 suffix: batch.suffix.as_deref(),
310 jobs: batch.jobs,
311 skip_errors: batch.skip_errors,
312 use_gpu,
313 };
314 let summary = batch::run_batch_edit(&run, ¶ms, lut_data);
315 if !summary.failed.is_empty() {
316 process::exit(1);
317 }
318 Ok(())
319}
320
321fn render_and_encode(
324 image: image::Rgb32FImage,
325 preset: Option<&agx::Preset>,
326 output_path: &std::path::Path,
327 metadata: Option<&agx::metadata::ImageMetadata>,
328 use_gpu: bool,
329) -> agx::Result<()> {
330 let mut engine = create_engine(image, use_gpu);
331 if let Some(p) = preset {
332 engine.apply_preset(p);
333 }
334 let result = engine.render();
335 let final_path = agx::encode::encode_to_file_with_options(
336 &result.image,
337 output_path,
338 &agx::encode::EncodeOptions::default(),
339 metadata,
340 )?;
341 println!("Saved to {}", final_path.display());
342 Ok(())
343}
344
345fn run_multi_apply(
346 input: &std::path::Path,
347 presets: &[PathBuf],
348 output_dir: &std::path::Path,
349 noop: bool,
350 jobs: usize,
351 use_gpu: bool,
352) -> agx::Result<()> {
353 let image_stem = input
354 .file_stem()
355 .and_then(|s| s.to_str())
356 .unwrap_or("output");
357
358 std::fs::create_dir_all(output_dir).map_err(agx::AgxError::Io)?;
359
360 let metadata = agx::metadata::extract_metadata(input);
361 let decoded = agx::decode::decode(input)?;
362
363 for path in presets {
364 warn_unknown_preset_fields(path);
365 }
366
367 let loaded: Vec<(String, agx::Preset)> = presets
368 .iter()
369 .map(|path| {
370 let name = path
371 .file_stem()
372 .and_then(|s| s.to_str())
373 .unwrap_or("unknown")
374 .to_string();
375 let preset = agx::Preset::load_from_file(path)?;
376 Ok((name, preset))
377 })
378 .collect::<agx::Result<Vec<_>>>()?;
379
380 if noop {
381 let noop_path = output_dir.join(format!("{image_stem}_noop.png"));
382 render_and_encode(
383 decoded.clone(),
384 None,
385 &noop_path,
386 metadata.as_ref(),
387 use_gpu,
388 )?;
389 }
390
391 if jobs <= 1 {
392 for (name, preset) in &loaded {
393 let out_path = output_dir.join(format!("{image_stem}_{name}.png"));
394 render_and_encode(
395 decoded.clone(),
396 Some(preset),
397 &out_path,
398 metadata.as_ref(),
399 use_gpu,
400 )?;
401 }
402 } else {
403 let errors: std::sync::Mutex<Vec<agx::AgxError>> = std::sync::Mutex::new(Vec::new());
406
407 for chunk in loaded.chunks(jobs) {
408 std::thread::scope(|s| {
409 for (name, preset) in chunk {
410 let decoded = &decoded;
411 let metadata = &metadata;
412 let errors = &errors;
413 s.spawn(move || {
414 let out_path = output_dir.join(format!("{image_stem}_{name}.png"));
415 match render_and_encode(
416 decoded.clone(),
417 Some(preset),
418 &out_path,
419 metadata.as_ref(),
420 use_gpu,
421 ) {
422 Ok(()) => {}
423 Err(e) => errors.lock().unwrap().push(e),
424 }
425 });
426 }
427 });
428 }
429
430 let errs = errors.into_inner().unwrap();
431 if let Some(first) = errs.into_iter().next() {
432 return Err(first);
433 }
434 }
435
436 Ok(())
437}