mod font;
mod jitter;
mod svg;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "jitter")]
#[command(about = "Add natural handwriting-like variation to digital text")]
#[command(version)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Render {
text: String,
#[arg(short, long)]
font: PathBuf,
#[arg(short, long, default_value = "output.svg")]
output: PathBuf,
#[arg(short, long, default_value = "0.5", value_parser = parse_intensity)]
intensity: f64,
#[arg(short, long, default_value = "48")]
size: u32,
},
Bake {
input: PathBuf,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(short, long, default_value = "3", value_parser = parse_alternates)]
alternates: u32,
#[arg(short, long, default_value = "0.5", value_parser = parse_intensity)]
intensity: f64,
},
}
fn parse_alternates(s: &str) -> Result<u32, String> {
let v: u32 = s.parse().map_err(|e| format!("{e}"))?;
if v >= 1 {
Ok(v)
} else {
Err("alternates must be at least 1".to_string())
}
}
fn parse_intensity(s: &str) -> Result<f64, String> {
let v: f64 = s.parse().map_err(|e| format!("{e}"))?;
if (0.0..=1.0).contains(&v) {
Ok(v)
} else {
Err(format!("intensity must be between 0.0 and 1.0, got {v}"))
}
}
fn main() {
let cli = Cli::parse();
match cli.command {
Commands::Render {
text,
font: font_path,
output,
intensity,
size,
} => {
if text.is_empty() {
eprintln!("Error: text must not be empty");
std::process::exit(1);
}
let (glyphs, units_per_em) = match font::load_glyphs(&font_path, &text) {
Ok(v) => v,
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(1);
}
};
let commands: Vec<Vec<font::PathCommand>> =
glyphs.iter().map(|g| g.commands.clone()).collect();
let jittered = jitter::apply_jitter(&commands, intensity, units_per_em as f64);
let svg_output = svg::render_svg(&glyphs, &jittered, size, units_per_em);
if let Err(e) = std::fs::write(&output, &svg_output) {
eprintln!("Error writing output: {e}");
std::process::exit(1);
}
println!(
"Rendered \"{}\" -> {} ({} bytes)",
text,
output.display(),
svg_output.len()
);
}
Commands::Bake {
input,
output,
alternates,
intensity,
} => {
let output = output.unwrap_or_else(|| {
let stem = input.file_stem().and_then(|s| s.to_str()).unwrap_or("font");
let ext = input.extension().and_then(|s| s.to_str()).unwrap_or("ttf");
input.with_file_name(format!("{stem}-jittered.{ext}"))
});
println!(
"Baking {} with {} alternates (intensity: {}) -> {}",
input.display(),
alternates,
intensity,
output.display()
);
eprintln!("bake mode is not yet implemented");
std::process::exit(1);
}
}
}