diff --git a/cmdapp/src/config.rs b/cmdapp/src/config.rs index c031b68..9a7c908 100644 --- a/cmdapp/src/config.rs +++ b/cmdapp/src/config.rs @@ -1,6 +1,4 @@ use std::str::FromStr; -use std::path::PathBuf; -use clap::{Arg, App}; use visioncortex::PathSimplifyMode; pub enum Preset { @@ -21,8 +19,6 @@ pub enum Hierarchical { /// Converter config pub struct Config { - pub input_path: PathBuf, - pub output_path: PathBuf, pub color_mode: ColorMode, pub hierarchical: Hierarchical, pub filter_speckle: usize, @@ -37,8 +33,6 @@ pub struct Config { } pub(crate) struct ConverterConfig { - pub input_path: PathBuf, - pub output_path: PathBuf, pub color_mode: ColorMode, pub hierarchical: Hierarchical, pub filter_speckle_area: usize, @@ -55,8 +49,6 @@ pub(crate) struct ConverterConfig { impl Default for Config { fn default() -> Self { Self { - input_path: PathBuf::default(), - output_path: PathBuf::default(), color_mode: ColorMode::Color, hierarchical: Hierarchical::Stacked, mode: PathSimplifyMode::Spline, @@ -109,225 +101,10 @@ impl FromStr for Preset { } } -fn path_simplify_mode_from_str(s: &str) -> PathSimplifyMode { - match s { - "polygon" => PathSimplifyMode::Polygon, - "spline" => PathSimplifyMode::Spline, - "none" => PathSimplifyMode::None, - _ => panic!("unknown PathSimplifyMode {}", s), - } -} - impl Config { - pub fn from_args() -> Self { - let app = App::new("visioncortex VTracer ".to_owned() + env!("CARGO_PKG_VERSION")) - .about("A cmd app to convert images into vector graphics."); - - let app = app.arg(Arg::with_name("input") - .long("input") - .short("i") - .takes_value(true) - .help("Path to input raster image") - .required(true)); - - let app = app.arg(Arg::with_name("output") - .long("output") - .short("o") - .takes_value(true) - .help("Path to output vector graphics") - .required(true)); - - let app = app.arg(Arg::with_name("color_mode") - .long("colormode") - .takes_value(true) - .help("True color image `color` (default) or Binary image `bw`")); - - let app = app.arg(Arg::with_name("hierarchical") - .long("hierarchical") - .takes_value(true) - .help( - "Hierarchical clustering `stacked` (default) or non-stacked `cutout`. \ - Only applies to color mode. " - )); - - let app = app.arg(Arg::with_name("preset") - .long("preset") - .takes_value(true) - .help("Use one of the preset configs `bw`, `poster`, `photo`")); - - let app = app.arg(Arg::with_name("filter_speckle") - .long("filter_speckle") - .short("f") - .takes_value(true) - .help("Discard patches smaller than X px in size")); - - let app = app.arg(Arg::with_name("color_precision") - .long("color_precision") - .short("p") - .takes_value(true) - .help("Number of significant bits to use in an RGB channel")); - - let app = app.arg(Arg::with_name("gradient_step") - .long("gradient_step") - .short("g") - .takes_value(true) - .help("Color difference between gradient layers")); - - let app = app.arg(Arg::with_name("corner_threshold") - .long("corner_threshold") - .short("c") - .takes_value(true) - .help("Minimum momentary angle (degree) to be considered a corner")); - - let app = app.arg(Arg::with_name("segment_length") - .long("segment_length") - .short("l") - .takes_value(true) - .help("Perform iterative subdivide smooth until all segments are shorter than this length")); - - let app = app.arg(Arg::with_name("splice_threshold") - .long("splice_threshold") - .short("s") - .takes_value(true) - .help("Minimum angle displacement (degree) to splice a spline")); - - let app = app.arg(Arg::with_name("mode") - .long("mode") - .short("m") - .takes_value(true) - .help("Curver fitting mode `pixel`, `polygon`, `spline`")); - - let app = app.arg(Arg::with_name("path_precision") - .long("path_precision") - .takes_value(true) - .help("Number of decimal places to use in path string")); - - // Extract matches - let matches = app.get_matches(); - - let mut config = Config::default(); - let input_path = matches.value_of("input").expect("Input path is required, please specify it by --input or -i."); - let output_path = matches.value_of("output").expect("Output path is required, please specify it by --output or -o."); - - if let Some(value) = matches.value_of("preset") { - config = Self::from_preset(Preset::from_str(value).unwrap(), input_path, output_path); - } - - config.input_path = PathBuf::from(input_path); - config.output_path = PathBuf::from(output_path); - - if let Some(value) = matches.value_of("color_mode") { - config.color_mode = ColorMode::from_str(if value.trim() == "bw" || value.trim() == "BW" {"binary"} else {"color"}).unwrap() - } - - if let Some(value) = matches.value_of("hierarchical") { - config.hierarchical = Hierarchical::from_str(value).unwrap() - } - - if let Some(value) = matches.value_of("mode") { - let value = value.trim(); - config.mode = path_simplify_mode_from_str(if value == "pixel" { - "none" - } else if value == "polygon" { - "polygon" - } else if value == "spline" { - "spline" - } else { - panic!("Parser Error: Curve fitting mode is invalid: {}", value); - }); - } - - if let Some(value) = matches.value_of("filter_speckle") { - if value.trim().parse::().is_ok() { // is numeric - let value = value.trim().parse::().unwrap(); - if value > 16 { - panic!("Out of Range Error: Filter speckle is invalid at {}. It must be within [0,16].", value); - } - config.filter_speckle = value; - } else { - panic!("Parser Error: Filter speckle is not a positive integer: {}.", value); - } - } - - if let Some(value) = matches.value_of("color_precision") { - if value.trim().parse::().is_ok() { // is numeric - let value = value.trim().parse::().unwrap(); - if value < 1 || value > 8 { - panic!("Out of Range Error: Color precision is invalid at {}. It must be within [1,8].", value); - } - config.color_precision = value; - } else { - panic!("Parser Error: Color precision is not an integer: {}.", value); - } - } - - if let Some(value) = matches.value_of("gradient_step") { - if value.trim().parse::().is_ok() { // is numeric - let value = value.trim().parse::().unwrap(); - if value < 0 || value > 255 { - panic!("Out of Range Error: Gradient step is invalid at {}. It must be within [0,255].", value); - } - config.layer_difference = value; - } else { - panic!("Parser Error: Gradient step is not an integer: {}.", value); - } - } - - if let Some(value) = matches.value_of("corner_threshold") { - if value.trim().parse::().is_ok() { // is numeric - let value = value.trim().parse::().unwrap(); - if value < 0 || value > 180 { - panic!("Out of Range Error: Corner threshold is invalid at {}. It must be within [0,180].", value); - } - config.corner_threshold = value - } else { - panic!("Parser Error: Corner threshold is not numeric: {}.", value); - } - } - - if let Some(value) = matches.value_of("segment_length") { - if value.trim().parse::().is_ok() { // is numeric - let value = value.trim().parse::().unwrap(); - if value < 3.5 || value > 10.0 { - panic!("Out of Range Error: Segment length is invalid at {}. It must be within [3.5,10].", value); - } - config.length_threshold = value; - } else { - panic!("Parser Error: Segment length is not numeric: {}.", value); - } - } - - if let Some(value) = matches.value_of("splice_threshold") { - if value.trim().parse::().is_ok() { // is numeric - let value = value.trim().parse::().unwrap(); - if value < 0 || value > 180 { - panic!("Out of Range Error: Segment length is invalid at {}. It must be within [0,180].", value); - } - config.splice_threshold = value; - } else { - panic!("Parser Error: Segment length is not numeric: {}.", value); - } - } - - if let Some(value) = matches.value_of("path_precision") { - if value.trim().parse::().is_ok() { // is numeric - let value = value.trim().parse::().ok(); - config.path_precision = value; - } else { - panic!("Parser Error: Path precision is not an unsigned integer: {}.", value); - } - } - - config - } - - pub fn from_preset(preset: Preset, input_path: &str, output_path: &str) -> Self { - let input_path = PathBuf::from(input_path); - let output_path = PathBuf::from(output_path); + pub fn from_preset(preset: Preset) -> Self { match preset { Preset::Bw => Self { - input_path, - output_path, color_mode: ColorMode::Binary, hierarchical: Hierarchical::Stacked, filter_speckle: 4, @@ -341,8 +118,6 @@ impl Config { path_precision: Some(8), }, Preset::Poster => Self { - input_path, - output_path, color_mode: ColorMode::Color, hierarchical: Hierarchical::Stacked, filter_speckle: 4, @@ -356,8 +131,6 @@ impl Config { path_precision: Some(8), }, Preset::Photo => Self { - input_path, - output_path, color_mode: ColorMode::Color, hierarchical: Hierarchical::Stacked, filter_speckle: 10, @@ -375,8 +148,6 @@ impl Config { pub(crate) fn into_converter_config(self) -> ConverterConfig { ConverterConfig { - input_path: self.input_path, - output_path: self.output_path, color_mode: self.color_mode, hierarchical: self.hierarchical, filter_speckle_area: self.filter_speckle * self.filter_speckle, @@ -394,4 +165,4 @@ impl Config { fn deg2rad(deg: i32) -> f64 { deg as f64 / 180.0 * std::f64::consts::PI -} \ No newline at end of file +} diff --git a/cmdapp/src/converter.rs b/cmdapp/src/converter.rs index 55d4cb9..76bb651 100644 --- a/cmdapp/src/converter.rs +++ b/cmdapp/src/converter.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::path::Path; use std::{fs::File, io::Write}; use fastrand::Rng; @@ -12,15 +12,22 @@ const NUM_UNUSED_COLOR_ITERATIONS: usize = 6; /// the entire image will be keyed. const KEYING_THRESHOLD: f32 = 0.2; -/// Convert an image file into svg file -pub fn convert_image_to_svg(config: Config) -> Result<(), String> { +/// Convert an in-memory image into an in-memory SVG +pub fn convert(img: ColorImage, config: Config) -> Result { let config = config.into_converter_config(); match config.color_mode { - ColorMode::Color => color_image_to_svg(config), - ColorMode::Binary => binary_image_to_svg(config), + ColorMode::Color => color_image_to_svg(img, config), + ColorMode::Binary => binary_image_to_svg(img, config), } } +/// Convert an image file into svg file +pub fn convert_image_to_svg(input_path: &Path, output_path: &Path, config: Config) -> Result<(), String> { + let img = read_image(input_path)?; + let svg = convert(img, config)?; + write_svg(svg, output_path) +} + fn color_exists_in_image(img: &ColorImage, color: Color) -> bool { for y in 0..img.height { for x in 0..img.width { @@ -81,16 +88,9 @@ fn should_key_image(img: &ColorImage) -> bool { false } -fn color_image_to_svg(config: ConverterConfig) -> Result<(), String> { - let (mut img, width, height); - match read_image(config.input_path) { - Ok(values) => { - img = values.0; - width = values.1; - height = values.2; - }, - Err(msg) => return Err(msg), - } +fn color_image_to_svg(mut img: ColorImage, config: ConverterConfig) -> Result { + let width = img.width; + let height = img.height; let key_color = if should_key_image(&img) { let key_color = find_unused_color_in_image(&img)?; @@ -166,21 +166,13 @@ fn color_image_to_svg(config: ConverterConfig) -> Result<(), String> { svg.add_path(paths, cluster.residue_color()); } - write_svg(svg, config.output_path) + Ok(svg) } -fn binary_image_to_svg(config: ConverterConfig) -> Result<(), String> { - - let (img, width, height); - match read_image(config.input_path) { - Ok(values) => { - img = values.0; - width = values.1; - height = values.2; - }, - Err(msg) => return Err(msg), - } +fn binary_image_to_svg(img: ColorImage, config: ConverterConfig) -> Result { let img = img.to_binary_image(|x| x.r < 128); + let width = img.width; + let height = img.height; let clusters = img.to_clusters(false); @@ -199,10 +191,10 @@ fn binary_image_to_svg(config: ConverterConfig) -> Result<(), String> { } } - write_svg(svg, config.output_path) + Ok(svg) } -fn read_image(input_path: PathBuf) -> Result<(ColorImage, usize, usize), String> { +fn read_image(input_path: &Path) -> Result { let img = image::open(input_path); let img = match img { Ok(file) => file.to_rgba8(), @@ -212,10 +204,10 @@ fn read_image(input_path: PathBuf) -> Result<(ColorImage, usize, usize), String> let (width, height) = (img.width() as usize, img.height() as usize); let img = ColorImage {pixels: img.as_raw().to_vec(), width, height}; - Ok((img, width, height)) + Ok(img) } -fn write_svg(svg: SvgFile, output_path: PathBuf) -> Result<(), String> { +fn write_svg(svg: SvgFile, output_path: &Path) -> Result<(), String> { let out_file = File::create(output_path); let mut out_file = match out_file { Ok(file) => file, @@ -225,4 +217,4 @@ fn write_svg(svg: SvgFile, output_path: PathBuf) -> Result<(), String> { write!(&mut out_file, "{}", svg).expect("failed to write file."); Ok(()) -} \ No newline at end of file +} diff --git a/cmdapp/src/lib.rs b/cmdapp/src/lib.rs index 752f842..cac0954 100644 --- a/cmdapp/src/lib.rs +++ b/cmdapp/src/lib.rs @@ -18,4 +18,5 @@ pub use config::*; pub use converter::*; pub use svg::*; #[cfg(feature = "python-binding")] -pub use python::*; \ No newline at end of file +pub use python::*; +pub use visioncortex::ColorImage; diff --git a/cmdapp/src/main.rs b/cmdapp/src/main.rs index 07a3c80..69d0eac 100644 --- a/cmdapp/src/main.rs +++ b/cmdapp/src/main.rs @@ -2,9 +2,226 @@ mod config; mod converter; mod svg; +use std::str::FromStr; +use std::path::PathBuf; +use clap::{Arg, App}; +use visioncortex::PathSimplifyMode; +use config::{Config, Preset, ColorMode, Hierarchical}; + +fn path_simplify_mode_from_str(s: &str) -> PathSimplifyMode { + match s { + "polygon" => PathSimplifyMode::Polygon, + "spline" => PathSimplifyMode::Spline, + "none" => PathSimplifyMode::None, + _ => panic!("unknown PathSimplifyMode {}", s), + } +} + +pub fn config_from_args() -> (PathBuf, PathBuf, Config) { + let app = App::new("visioncortex VTracer ".to_owned() + env!("CARGO_PKG_VERSION")) + .about("A cmd app to convert images into vector graphics."); + + let app = app.arg(Arg::with_name("input") + .long("input") + .short("i") + .takes_value(true) + .help("Path to input raster image") + .required(true)); + + let app = app.arg(Arg::with_name("output") + .long("output") + .short("o") + .takes_value(true) + .help("Path to output vector graphics") + .required(true)); + + let app = app.arg(Arg::with_name("color_mode") + .long("colormode") + .takes_value(true) + .help("True color image `color` (default) or Binary image `bw`")); + + let app = app.arg(Arg::with_name("hierarchical") + .long("hierarchical") + .takes_value(true) + .help( + "Hierarchical clustering `stacked` (default) or non-stacked `cutout`. \ + Only applies to color mode. " + )); + + let app = app.arg(Arg::with_name("preset") + .long("preset") + .takes_value(true) + .help("Use one of the preset configs `bw`, `poster`, `photo`")); + + let app = app.arg(Arg::with_name("filter_speckle") + .long("filter_speckle") + .short("f") + .takes_value(true) + .help("Discard patches smaller than X px in size")); + + let app = app.arg(Arg::with_name("color_precision") + .long("color_precision") + .short("p") + .takes_value(true) + .help("Number of significant bits to use in an RGB channel")); + + let app = app.arg(Arg::with_name("gradient_step") + .long("gradient_step") + .short("g") + .takes_value(true) + .help("Color difference between gradient layers")); + + let app = app.arg(Arg::with_name("corner_threshold") + .long("corner_threshold") + .short("c") + .takes_value(true) + .help("Minimum momentary angle (degree) to be considered a corner")); + + let app = app.arg(Arg::with_name("segment_length") + .long("segment_length") + .short("l") + .takes_value(true) + .help("Perform iterative subdivide smooth until all segments are shorter than this length")); + + let app = app.arg(Arg::with_name("splice_threshold") + .long("splice_threshold") + .short("s") + .takes_value(true) + .help("Minimum angle displacement (degree) to splice a spline")); + + let app = app.arg(Arg::with_name("mode") + .long("mode") + .short("m") + .takes_value(true) + .help("Curver fitting mode `pixel`, `polygon`, `spline`")); + + let app = app.arg(Arg::with_name("path_precision") + .long("path_precision") + .takes_value(true) + .help("Number of decimal places to use in path string")); + + // Extract matches + let matches = app.get_matches(); + + let mut config = Config::default(); + let input_path = matches.value_of("input").expect("Input path is required, please specify it by --input or -i."); + let output_path = matches.value_of("output").expect("Output path is required, please specify it by --output or -o."); + + let input_path = PathBuf::from(input_path); + let output_path = PathBuf::from(output_path); + + if let Some(value) = matches.value_of("preset") { + config = Config::from_preset(Preset::from_str(value).unwrap()); + } + + if let Some(value) = matches.value_of("color_mode") { + config.color_mode = ColorMode::from_str(if value.trim() == "bw" || value.trim() == "BW" {"binary"} else {"color"}).unwrap() + } + + if let Some(value) = matches.value_of("hierarchical") { + config.hierarchical = Hierarchical::from_str(value).unwrap() + } + + if let Some(value) = matches.value_of("mode") { + let value = value.trim(); + config.mode = path_simplify_mode_from_str(if value == "pixel" { + "none" + } else if value == "polygon" { + "polygon" + } else if value == "spline" { + "spline" + } else { + panic!("Parser Error: Curve fitting mode is invalid: {}", value); + }); + } + + if let Some(value) = matches.value_of("filter_speckle") { + if value.trim().parse::().is_ok() { // is numeric + let value = value.trim().parse::().unwrap(); + if value > 16 { + panic!("Out of Range Error: Filter speckle is invalid at {}. It must be within [0,16].", value); + } + config.filter_speckle = value; + } else { + panic!("Parser Error: Filter speckle is not a positive integer: {}.", value); + } + } + + if let Some(value) = matches.value_of("color_precision") { + if value.trim().parse::().is_ok() { // is numeric + let value = value.trim().parse::().unwrap(); + if value < 1 || value > 8 { + panic!("Out of Range Error: Color precision is invalid at {}. It must be within [1,8].", value); + } + config.color_precision = value; + } else { + panic!("Parser Error: Color precision is not an integer: {}.", value); + } + } + + if let Some(value) = matches.value_of("gradient_step") { + if value.trim().parse::().is_ok() { // is numeric + let value = value.trim().parse::().unwrap(); + if value < 0 || value > 255 { + panic!("Out of Range Error: Gradient step is invalid at {}. It must be within [0,255].", value); + } + config.layer_difference = value; + } else { + panic!("Parser Error: Gradient step is not an integer: {}.", value); + } + } + + if let Some(value) = matches.value_of("corner_threshold") { + if value.trim().parse::().is_ok() { // is numeric + let value = value.trim().parse::().unwrap(); + if value < 0 || value > 180 { + panic!("Out of Range Error: Corner threshold is invalid at {}. It must be within [0,180].", value); + } + config.corner_threshold = value + } else { + panic!("Parser Error: Corner threshold is not numeric: {}.", value); + } + } + + if let Some(value) = matches.value_of("segment_length") { + if value.trim().parse::().is_ok() { // is numeric + let value = value.trim().parse::().unwrap(); + if value < 3.5 || value > 10.0 { + panic!("Out of Range Error: Segment length is invalid at {}. It must be within [3.5,10].", value); + } + config.length_threshold = value; + } else { + panic!("Parser Error: Segment length is not numeric: {}.", value); + } + } + + if let Some(value) = matches.value_of("splice_threshold") { + if value.trim().parse::().is_ok() { // is numeric + let value = value.trim().parse::().unwrap(); + if value < 0 || value > 180 { + panic!("Out of Range Error: Segment length is invalid at {}. It must be within [0,180].", value); + } + config.splice_threshold = value; + } else { + panic!("Parser Error: Segment length is not numeric: {}.", value); + } + } + + if let Some(value) = matches.value_of("path_precision") { + if value.trim().parse::().is_ok() { // is numeric + let value = value.trim().parse::().ok(); + config.path_precision = value; + } else { + panic!("Parser Error: Path precision is not an unsigned integer: {}.", value); + } + } + + (input_path, output_path, config) +} + fn main() { - let config = config::Config::from_args(); - let result = converter::convert_image_to_svg(config); + let (input_path, output_path, config) = config_from_args(); + let result = converter::convert_image_to_svg(&input_path, &output_path, config); match result { Ok(()) => { println!("Conversion successful."); @@ -13,4 +230,4 @@ fn main() { panic!("Conversion failed with error message: {}", msg); } } -} \ No newline at end of file +} diff --git a/cmdapp/src/python.rs b/cmdapp/src/python.rs index 36caa54..d3cf7a0 100644 --- a/cmdapp/src/python.rs +++ b/cmdapp/src/python.rs @@ -53,8 +53,6 @@ fn convert_image_to_svg_py( let max_iterations = max_iterations.unwrap_or(10); let config = Config { - input_path, - output_path, color_mode, hierarchical, filter_speckle, @@ -69,7 +67,7 @@ fn convert_image_to_svg_py( ..Default::default() }; - convert_image_to_svg(config).unwrap(); + convert_image_to_svg(&input_path, &output_path, config).unwrap(); Ok(()) }