| 307 | } |
| 308 | |
| 309 | fn parse_args() -> Result<Args, String> { |
| 310 | let mut reference_path: Option<String> = None; |
| 311 | let mut candidate_path: Option<String> = None; |
| 312 | let mut heatmap_path: Option<String> = None; |
| 313 | let mut composite_path: Option<String> = None; |
| 314 | let mut tolerance: f32 = 0.02; |
| 315 | let mut quiet = false; |
| 316 | |
| 317 | let mut iter = env::args().skip(1); |
| 318 | while let Some(arg) = iter.next() { |
| 319 | match arg.as_str() { |
| 320 | "--reference" | "-r" => reference_path = iter.next(), |
| 321 | "--candidate" | "-c" => candidate_path = iter.next(), |
| 322 | "--heatmap" => heatmap_path = iter.next(), |
| 323 | "--composite" => composite_path = iter.next(), |
| 324 | "--tolerance" => { |
| 325 | tolerance = iter |
| 326 | .next() |
| 327 | .ok_or("--tolerance needs a value")? |
| 328 | .parse() |
| 329 | .map_err(|e| format!("invalid --tolerance: {e}"))?; |
| 330 | } |
| 331 | "--quiet" | "-q" => quiet = true, |
| 332 | "-h" | "--help" => { |
| 333 | println!("bloom-diff — compare two PNG images"); |
| 334 | println!(); |
| 335 | println!(" --reference PATH ground-truth image (from bloom-reference)"); |
| 336 | println!(" --candidate PATH image to compare (e.g. realtime screenshot)"); |
| 337 | println!(" --heatmap PATH write per-pixel false-color diff"); |
| 338 | println!(" --composite PATH write 3-up side-by-side (ref|cand|heat)"); |
| 339 | println!(" --tolerance F per-pixel diff threshold for 'differs' %"); |
| 340 | println!(" (default 0.02 = 2/255 on any channel)"); |
| 341 | println!(" --quiet suppress stdout output"); |
| 342 | println!(); |
| 343 | println!("Exit code: 0 if max(RMSE_luminance, (1 - SSIM)) ≤ tolerance,"); |
| 344 | println!(" 1 otherwise. Intended for use in CI."); |
| 345 | std::process::exit(0); |
| 346 | } |
| 347 | other => return Err(format!("unknown argument: {other}")), |
| 348 | } |
| 349 | } |
| 350 | |
| 351 | Ok(Args { |
| 352 | reference_path: reference_path.ok_or("--reference is required")?, |
| 353 | candidate_path: candidate_path.ok_or("--candidate is required")?, |
| 354 | heatmap_path, |
| 355 | composite_path, |
| 356 | tolerance, |
| 357 | quiet, |
| 358 | }) |
| 359 | } |
| 360 | |
| 361 | fn main() -> ExitCode { |
| 362 | let args = match parse_args() { |