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)? }) }