fn usage() -> String { [ "splined: iterative image reconstruction with random cubic bézier strokes (metal-accelerated)", "", "usage: splined [args]", "", "args:", " -n, --number max splines to draw (default: (w*h)^0.7)", " -b, --batch batch size per gpu step (default: 32)", " -s, --seed rng seed (default: 0)", " --max-gpu max gpu usage in (0, 1] (default: 1.0)", " -l, --log <0..3> logging level (default: 1)", " -o, --output output file or dir (default: output.png)", " -c, --current current canvas image to resume from (single-file only)", " --nth save every nth accepted stroke (uses -o as dir)", " --bg initial canvas color (default: avg)", " -a, --alpha stroke alpha in [0, 1] (default: 1)", " --min-accept-ratio stagnant if accepted < batch*ratio (default: 0.02)", " --max-stagnant-batches stop after this many stagnant batches (default: 10)", "", "input:", " - file: writes one image to -o/--output (default: output.png)", " - dir: -o/--output must be a dir; mirrors input tree under it", " - --nth: saves frames to output dir every nth accepted stroke; also writes final.png", "", "examples:", " splined in.png -o out.png", " splined in.png -n 5000 -b 64 -s 42 -o out.png", " splined in.png --nth 50 -o frames/", " splined images/ -o results/ -n 5000 -b 64 -s 42 --nth 50", "", ].join("\n") } #[derive(Clone, Debug)] pub struct Config { pub input : String , pub number : Option , pub batch : u32 , pub min_accept_ratio : f32 , pub seed : u64 , pub max_gpu : f32 , pub log : u8 , pub output : String , pub output_provided : bool , pub current : Option , pub nth : Option , pub bg : Bg , pub alpha : f32 , pub max_stagnant_batches : u32 } #[derive(Clone, Copy, Debug)] pub enum Bg { Avg , RgbU8 { r: u8, g: u8, b: u8 } } impl Config { pub fn parse(argv: &[String]) -> Result { if argv.len() < 2 { return Err(usage()); } let mut input : Option = None; let mut number : Option = None; let mut batch : u32 = 32; let mut min_accept_ratio : Option = None; let mut seed : u64 = 0; let mut max_gpu : f32 = 1.0; let mut log : u8 = 1; let mut output : String = "output.png".to_string(); let mut output_provided : bool = false; let mut current : Option = None; let mut nth : Option = None; let mut bg : Bg = Bg::Avg; let mut alpha : f32 = 1.0; let mut max_stagnant_batches : u32 = 10; let mut i = 1usize; while i < argv.len() { let a = argv[i].as_str(); if !a.starts_with('-') && input.is_none() { input = Some(argv[i].clone()); i += 1; continue; } match a { "-n" | "--number" => { i += 1; number = Some(parse_val(argv, i, a)?); i += 1; } "-b" | "--batch" => { i += 1; batch = parse_val(argv, i, a)?; i += 1; } "--min-accept-ratio" => { i += 1; min_accept_ratio = Some(parse_val(argv, i, a)?); i += 1; } "-s" | "--seed" => { i += 1; seed = parse_val(argv, i, a)?; i += 1; } "--max-gpu" => { i += 1; max_gpu = parse_val(argv, i, a)?; i += 1; } "-l" | "--log" => { i += 1; log = parse_val(argv, i, a)?; i += 1; } "-o" | "--output" => { i += 1; output = parse_val(argv, i, a)?; output_provided = true; i += 1; } "-c" | "--current" => { i += 1; current = Some(parse_val(argv, i, a)?); i += 1; } "--nth" => { i += 1; nth = Some(parse_val(argv, i, a)?); i += 1; } "--bg" => { i += 1; bg = parse_bg(argv, i, a)?; i += 1; } "-a" | "--alpha" => { i += 1; alpha = parse_val(argv, i, a)?; i += 1; } "--max-stagnant-batches" => { i += 1; max_stagnant_batches = parse_val(argv, i, a)?; i += 1; } "-h" | "--help" => return Err(usage()), _ => return Err(format!("unknown arg: {a}\n\n{}", usage())), } } let Some(input) = input else { return Err(usage()); }; if batch == 0 { return Err("batch must be > 0".to_string()); } let min_accept_ratio = min_accept_ratio.unwrap_or(0.02); if !(0.0..=1.0).contains(&min_accept_ratio) { return Err("min-accept-ratio must be in [0, 1]".to_string()); } if log > 3 { return Err("log must be in [0, 3]".to_string()); } if !(0.0 < max_gpu && max_gpu <= 1.0) { return Err("max-gpu must be in (0, 1]".to_string()); } if let Some(nth) = nth { if nth == 0 { return Err("nth must be > 0".to_string()); } } if !(0.0..=1.0).contains(&alpha) { return Err("alpha must be in [0, 1]".to_string()); } if max_stagnant_batches == 0 { return Err("max-stagnant-batches must be > 0".to_string()); } Ok(Self { input, number, batch, min_accept_ratio, seed, max_gpu, log, output, output_provided, current, nth, bg, alpha, max_stagnant_batches, }) } } fn parse_val(argv: &[String], idx: usize, flag: &str) -> Result { let s = argv.get(idx).ok_or_else(|| format!("missing value for {flag}"))?; s.parse::().map_err(|_| format!("invalid value for {flag}: {s}")) } fn parse_bg(argv: &[String], idx: usize, flag: &str) -> Result { let s = argv.get(idx).ok_or_else(|| format!("missing value for {flag}"))?; if s == "avg" { return Ok(Bg::Avg); } let parts: Vec<_> = s.split(',').collect(); if parts.len() != 3 { return Err(format!("invalid bg, expected avg or r,g,b: {s}")); } let parse = |i: usize| parts[i].parse::().map_err(|_| format!("invalid bg component: {s}")); Ok(Bg::RgbU8 { r: parse(0)?, g: parse(1)?, b: parse(2)? }) }