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 fmt = batch.output.parse_format()?;
283 let summary = batch::run_batch_apply(
284 &batch.input_dir,
285 preset_path,
286 &batch.output_dir,
287 batch.recursive,
288 batch.output.quality,
289 fmt,
290 batch.suffix.as_deref(),
291 batch.jobs,
292 batch.skip_errors,
293 use_gpu,
294 );
295 if !summary.failed.is_empty() {
296 process::exit(1);
297 }
298 Ok(())
299}
300
301fn run_batch_edit(edit: &EditArgs, batch: &BatchOpts, use_gpu: bool) -> agx::Result<()> {
302 let params = edit.to_params()?;
303 let lut_data = edit.load_lut()?;
304 let fmt = batch.output.parse_format()?;
305 let summary = batch::run_batch_edit(
306 &batch.input_dir,
307 &batch.output_dir,
308 batch.recursive,
309 ¶ms,
310 lut_data,
311 batch.output.quality,
312 fmt,
313 batch.suffix.as_deref(),
314 batch.jobs,
315 batch.skip_errors,
316 use_gpu,
317 );
318 if !summary.failed.is_empty() {
319 process::exit(1);
320 }
321 Ok(())
322}
323
324fn render_and_encode(
327 image: image::Rgb32FImage,
328 preset: Option<&agx::Preset>,
329 output_path: &std::path::Path,
330 metadata: Option<&agx::metadata::ImageMetadata>,
331 use_gpu: bool,
332) -> agx::Result<()> {
333 let mut engine = create_engine(image, use_gpu);
334 if let Some(p) = preset {
335 engine.apply_preset(p);
336 }
337 let result = engine.render();
338 let final_path = agx::encode::encode_to_file_with_options(
339 &result.image,
340 output_path,
341 &agx::encode::EncodeOptions::default(),
342 metadata,
343 )?;
344 println!("Saved to {}", final_path.display());
345 Ok(())
346}
347
348fn run_multi_apply(
349 input: &std::path::Path,
350 presets: &[PathBuf],
351 output_dir: &std::path::Path,
352 noop: bool,
353 jobs: usize,
354 use_gpu: bool,
355) -> agx::Result<()> {
356 let image_stem = input
357 .file_stem()
358 .and_then(|s| s.to_str())
359 .unwrap_or("output");
360
361 std::fs::create_dir_all(output_dir).map_err(agx::AgxError::Io)?;
362
363 let metadata = agx::metadata::extract_metadata(input);
364 let decoded = agx::decode::decode(input)?;
365
366 for path in presets {
367 warn_unknown_preset_fields(path);
368 }
369
370 let loaded: Vec<(String, agx::Preset)> = presets
371 .iter()
372 .map(|path| {
373 let name = path
374 .file_stem()
375 .and_then(|s| s.to_str())
376 .unwrap_or("unknown")
377 .to_string();
378 let preset = agx::Preset::load_from_file(path)?;
379 Ok((name, preset))
380 })
381 .collect::<agx::Result<Vec<_>>>()?;
382
383 if noop {
384 let noop_path = output_dir.join(format!("{image_stem}_noop.png"));
385 render_and_encode(
386 decoded.clone(),
387 None,
388 &noop_path,
389 metadata.as_ref(),
390 use_gpu,
391 )?;
392 }
393
394 if jobs <= 1 {
395 for (name, preset) in &loaded {
396 let out_path = output_dir.join(format!("{image_stem}_{name}.png"));
397 render_and_encode(
398 decoded.clone(),
399 Some(preset),
400 &out_path,
401 metadata.as_ref(),
402 use_gpu,
403 )?;
404 }
405 } else {
406 let errors: std::sync::Mutex<Vec<agx::AgxError>> = std::sync::Mutex::new(Vec::new());
409
410 for chunk in loaded.chunks(jobs) {
411 std::thread::scope(|s| {
412 for (name, preset) in chunk {
413 let decoded = &decoded;
414 let metadata = &metadata;
415 let errors = &errors;
416 s.spawn(move || {
417 let out_path = output_dir.join(format!("{image_stem}_{name}.png"));
418 match render_and_encode(
419 decoded.clone(),
420 Some(preset),
421 &out_path,
422 metadata.as_ref(),
423 use_gpu,
424 ) {
425 Ok(()) => {}
426 Err(e) => errors.lock().unwrap().push(e),
427 }
428 });
429 }
430 });
431 }
432
433 let errs = errors.into_inner().unwrap();
434 if let Some(first) = errs.into_iter().next() {
435 return Err(first);
436 }
437 }
438
439 Ok(())
440}