mirror of
https://github.com/visioncortex/vtracer.git
synced 2026-10-15 23:20:01 -07:00
Rewrite into a vectorization framework (pillars 1–4)
Replace the 0.6.x single-pipeline crate with a stage-based framework, per docs/design/. Implements Motivation pillars 1–4 (frontend, curve fitting, color fitting, optimizer); mosaic (5) and bindings are deferred. Workspace: - crates/vtracer — the framework library (wasm-safe, no I/O) - crates/vtracer-cli — thin CLI wrapper (clap 4 + image I/O) - cmdapp/ and webapp/ excluded from the workspace (git-preserved) Stages behind object-safe traits, composed by a Pipeline driver: - Frontend: ColorClusterFrontend (+ transparency keying), BinaryFrontend - CurveFitter: Pixel / Polygon / Spline (region tracing via visioncortex) - ColorFitter: Identity, FixedPalette (OKLab-nearest), AutoQuantize (area-weighted median cut), MergeAdjacent - OptimizerPass: QuantizePass, SimplifyPass - SvgWriter: relative/absolute shortest encoding, H/V/S shorthands, compact number formatting, <g fill> grouping visioncortex is a path dependency on the local 0.9.0 checkout. Verified: 14 unit/integration tests pass; framework builds for wasm32-unknown-unknown; CLI output renders faithfully via rsvg.
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
//! Thin command-line front-end over the `vtracer` framework.
|
||||
//!
|
||||
//! Handles the two things the framework deliberately leaves out: image file
|
||||
//! I/O and argument parsing. Everything else is delegated to
|
||||
//! [`vtracer::Config`] / [`vtracer::Pipeline`].
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::process::ExitCode;
|
||||
|
||||
use clap::Parser;
|
||||
use visioncortex::{Color, ColorImage};
|
||||
use vtracer::{ColorMode, Config, FitMode, Hierarchical, Preset};
|
||||
|
||||
/// Convert an image into vector graphics.
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "vtracer", version, about, rename_all = "kebab-case")]
|
||||
struct Args {
|
||||
/// Path to the input raster image.
|
||||
#[arg(short, long)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to the output SVG.
|
||||
#[arg(short, long)]
|
||||
output: PathBuf,
|
||||
|
||||
/// Start from a preset: bw, poster, photo.
|
||||
#[arg(long)]
|
||||
preset: Option<Preset>,
|
||||
|
||||
/// Color image (`color`) or binary image (`bw`).
|
||||
#[arg(long = "colormode")]
|
||||
colormode: Option<ColorMode>,
|
||||
|
||||
/// Hierarchical clustering: `stacked` (default) or `cutout` (mosaic).
|
||||
#[arg(long)]
|
||||
hierarchical: Option<Hierarchical>,
|
||||
|
||||
/// Curve-fitting mode: pixel, polygon, spline.
|
||||
#[arg(short, long)]
|
||||
mode: Option<FitMode>,
|
||||
|
||||
/// Discard patches smaller than X px in size (0..=16).
|
||||
#[arg(short = 'f', long, value_parser = clap::value_parser!(i64).range(0..=16))]
|
||||
filter_speckle: Option<i64>,
|
||||
|
||||
/// Significant bits per RGB channel (1..=8).
|
||||
#[arg(short = 'p', long, value_parser = clap::value_parser!(i64).range(1..=8))]
|
||||
color_precision: Option<i64>,
|
||||
|
||||
/// Color difference between gradient layers (0..=255).
|
||||
#[arg(short = 'g', long, value_parser = clap::value_parser!(i64).range(0..=255))]
|
||||
gradient_step: Option<i64>,
|
||||
|
||||
/// Minimum momentary angle (degrees) to be a corner (0..=180).
|
||||
#[arg(short = 'c', long, value_parser = clap::value_parser!(i64).range(0..=180))]
|
||||
corner_threshold: Option<i64>,
|
||||
|
||||
/// Subdivide until all segments are shorter than this length (3.5..=10).
|
||||
#[arg(short = 'l', long, value_parser = parse_segment_length)]
|
||||
segment_length: Option<f64>,
|
||||
|
||||
/// Minimum angle displacement (degrees) to splice a spline (0..=180).
|
||||
#[arg(short = 's', long, value_parser = clap::value_parser!(i64).range(0..=180))]
|
||||
splice_threshold: Option<i64>,
|
||||
|
||||
/// Decimal places to use in path coordinates.
|
||||
#[arg(long)]
|
||||
path_precision: Option<u32>,
|
||||
|
||||
/// Fixed palette: comma-separated hex colors, e.g. '#112233,#445566'.
|
||||
#[arg(long)]
|
||||
palette: Option<String>,
|
||||
|
||||
/// Fixed palette from a file (one hex color per line or comma-separated).
|
||||
#[arg(long)]
|
||||
palette_file: Option<PathBuf>,
|
||||
|
||||
/// Auto-quantize to at most N colors.
|
||||
#[arg(long)]
|
||||
max_colors: Option<usize>,
|
||||
|
||||
/// Optimization level: 0 = off, 1 = quantize+simplify, 2 = + shorthands/grouping.
|
||||
#[arg(long, value_parser = clap::value_parser!(u8).range(0..=2))]
|
||||
optimize: Option<u8>,
|
||||
}
|
||||
|
||||
fn parse_segment_length(s: &str) -> Result<f64, String> {
|
||||
let v: f64 = s
|
||||
.parse()
|
||||
.map_err(|_| format!("`{s}` is not a number"))?;
|
||||
if !(3.5..=10.0).contains(&v) {
|
||||
return Err(format!("segment length {v} is out of range [3.5, 10]"));
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
/// Parse a comma/whitespace/newline separated list of `#rrggbb` colors.
|
||||
fn parse_palette(text: &str) -> Result<Vec<Color>, String> {
|
||||
let mut colors = Vec::new();
|
||||
for token in text.split(|c: char| c == ',' || c.is_whitespace()) {
|
||||
let token = token.trim();
|
||||
if token.is_empty() {
|
||||
continue;
|
||||
}
|
||||
colors.push(parse_hex_color(token)?);
|
||||
}
|
||||
Ok(colors)
|
||||
}
|
||||
|
||||
fn parse_hex_color(token: &str) -> Result<Color, String> {
|
||||
let hex = token.strip_prefix('#').unwrap_or(token);
|
||||
if hex.len() != 6 {
|
||||
return Err(format!("`{token}` is not a #rrggbb color"));
|
||||
}
|
||||
let parse = |range: std::ops::Range<usize>| {
|
||||
u8::from_str_radix(&hex[range], 16).map_err(|_| format!("`{token}` is not a #rrggbb color"))
|
||||
};
|
||||
Ok(Color::new(parse(0..2)?, parse(2..4)?, parse(4..6)?))
|
||||
}
|
||||
|
||||
fn build_config(args: &Args) -> Result<Config, String> {
|
||||
let mut config = match args.preset {
|
||||
Some(preset) => Config::from_preset(preset),
|
||||
None => Config::default(),
|
||||
};
|
||||
|
||||
if let Some(v) = args.colormode {
|
||||
config.color_mode = v;
|
||||
}
|
||||
if let Some(v) = args.hierarchical {
|
||||
config.hierarchical = v;
|
||||
}
|
||||
if let Some(v) = args.mode {
|
||||
config.mode = v;
|
||||
}
|
||||
if let Some(v) = args.filter_speckle {
|
||||
config.filter_speckle = v as usize;
|
||||
}
|
||||
if let Some(v) = args.color_precision {
|
||||
config.color_precision = v as i32;
|
||||
}
|
||||
if let Some(v) = args.gradient_step {
|
||||
config.layer_difference = v as i32;
|
||||
}
|
||||
if let Some(v) = args.corner_threshold {
|
||||
config.corner_threshold = v as i32;
|
||||
}
|
||||
if let Some(v) = args.segment_length {
|
||||
config.length_threshold = v;
|
||||
}
|
||||
if let Some(v) = args.splice_threshold {
|
||||
config.splice_threshold = v as i32;
|
||||
}
|
||||
if args.path_precision.is_some() {
|
||||
config.path_precision = args.path_precision;
|
||||
}
|
||||
if let Some(v) = args.optimize {
|
||||
config.optimize = v;
|
||||
}
|
||||
if let Some(v) = args.max_colors {
|
||||
config.max_colors = Some(v);
|
||||
}
|
||||
|
||||
// Palette: inline flag wins over file; both parse to a color list.
|
||||
if let Some(text) = &args.palette {
|
||||
config.palette = parse_palette(text)?;
|
||||
} else if let Some(path) = &args.palette_file {
|
||||
let text = std::fs::read_to_string(path)
|
||||
.map_err(|e| format!("cannot read palette file: {e}"))?;
|
||||
config.palette = parse_palette(&text)?;
|
||||
}
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn read_image(path: &std::path::Path) -> Result<ColorImage, String> {
|
||||
let img = image::open(path)
|
||||
.map_err(|_| "no image file found at specified input path".to_string())?
|
||||
.to_rgba8();
|
||||
let (width, height) = (img.width() as usize, img.height() as usize);
|
||||
Ok(ColorImage {
|
||||
pixels: img.into_raw(),
|
||||
width,
|
||||
height,
|
||||
})
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
let args = Args::parse();
|
||||
let config = build_config(&args)?;
|
||||
let pipeline = config.build().map_err(|e| e.to_string())?;
|
||||
let img = read_image(&args.input)?;
|
||||
let svg = pipeline.to_svg(&img).map_err(|e| e.to_string())?;
|
||||
std::fs::write(&args.output, svg).map_err(|e| format!("cannot write output file: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
match run() {
|
||||
Ok(()) => {
|
||||
println!("Conversion successful.");
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(msg) => {
|
||||
eprintln!("Conversion failed: {msg}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user