diff --git a/cmdapp/src/config.rs b/cmdapp/src/config.rs index 9a7c908..4988bda 100644 --- a/cmdapp/src/config.rs +++ b/cmdapp/src/config.rs @@ -4,7 +4,7 @@ use visioncortex::PathSimplifyMode; pub enum Preset { Bw, Poster, - Photo + Photo, } pub enum ColorMode { @@ -142,7 +142,7 @@ impl Config { max_iterations: 10, splice_threshold: 45, path_precision: Some(8), - } + }, } } diff --git a/cmdapp/src/converter.rs b/cmdapp/src/converter.rs index 76bb651..d0c4c2b 100644 --- a/cmdapp/src/converter.rs +++ b/cmdapp/src/converter.rs @@ -1,11 +1,11 @@ use std::path::Path; use std::{fs::File, io::Write}; -use fastrand::Rng; -use visioncortex::{Color, ColorImage, ColorName}; -use visioncortex::color_clusters::{Runner, RunnerConfig, KeyingAction, HIERARCHICAL_MAX}; -use super::config::{Config, ColorMode, Hierarchical, ConverterConfig}; +use super::config::{ColorMode, Config, ConverterConfig, Hierarchical}; use super::svg::SvgFile; +use fastrand::Rng; +use visioncortex::color_clusters::{KeyingAction, Runner, RunnerConfig, HIERARCHICAL_MAX}; +use visioncortex::{Color, ColorImage, ColorName}; const NUM_UNUSED_COLOR_ITERATIONS: usize = 6; /// The fraction of pixels in the top/bottom rows of the image that need to be transparent before @@ -22,7 +22,11 @@ pub fn convert(img: ColorImage, config: Config) -> Result { } /// Convert an image file into svg file -pub fn convert_image_to_svg(input_path: &Path, output_path: &Path, config: Config) -> Result<(), String> { +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) @@ -33,7 +37,7 @@ fn color_exists_in_image(img: &ColorImage, color: Color) -> bool { for x in 0..img.width { let pixel_color = img.get_pixel(x, y); if pixel_color.r == color.r && pixel_color.g == color.g && pixel_color.b == color.b { - return true + return true; } } } @@ -42,27 +46,24 @@ fn color_exists_in_image(img: &ColorImage, color: Color) -> bool { fn find_unused_color_in_image(img: &ColorImage) -> Result { let special_colors = IntoIterator::into_iter([ - Color::new(255, 0, 0), - Color::new(0, 255, 0), - Color::new(0, 0, 255), + Color::new(255, 0, 0), + Color::new(0, 255, 0), + Color::new(0, 0, 255), Color::new(255, 255, 0), - Color::new(0, 255, 255), - Color::new(255, 0, 255), + Color::new(0, 255, 255), + Color::new(255, 0, 255), ]); let rng = Rng::new(); - let random_colors = (0..NUM_UNUSED_COLOR_ITERATIONS).map(|_| { - Color::new( - rng.u8(..), - rng.u8(..), - rng.u8(..), - ) - }); + let random_colors = + (0..NUM_UNUSED_COLOR_ITERATIONS).map(|_| Color::new(rng.u8(..), rng.u8(..), rng.u8(..))); for color in special_colors.chain(random_colors) { if !color_exists_in_image(img, color) { return Ok(color); } } - Err(String::from("unable to find unused color in image to use as key")) + Err(String::from( + "unable to find unused color in image to use as key", + )) } fn should_key_image(img: &ColorImage) -> bool { @@ -73,7 +74,13 @@ fn should_key_image(img: &ColorImage) -> bool { // Check for transparency at several scanlines let threshold = ((img.width * 2) as f32 * KEYING_THRESHOLD) as usize; let mut num_transparent_boundary_pixels = 0; - let y_positions = [0, img.height / 4, img.height / 2, 3 * img.height / 4, img.height - 1]; + let y_positions = [ + 0, + img.height / 4, + img.height / 2, + 3 * img.height / 4, + img.height - 1, + ]; for y in y_positions { for x in 0..img.width { if img.get_pixel(x, y).a == 0 { @@ -107,23 +114,26 @@ fn color_image_to_svg(mut img: ColorImage, config: ConverterConfig) -> Result Result { let view = clusters.view(); let image = view.to_color_image(); - let runner = Runner::new(RunnerConfig { - diagonal: false, - hierarchical: 64, - batch_size: 25600, - good_min_area: 0, - good_max_area: (image.width * image.height) as usize, - is_same_color_a: 0, - is_same_color_b: 1, - deepen_diff: 0, - hollow_neighbours: 0, - key_color, - keying_action: KeyingAction::Discard, - }, image); + let runner = Runner::new( + RunnerConfig { + diagonal: false, + hierarchical: 64, + batch_size: 25600, + good_min_area: 0, + good_max_area: (image.width * image.height) as usize, + is_same_color_a: 0, + is_same_color_b: 1, + deepen_diff: 0, + hollow_neighbours: 0, + key_color, + keying_action: KeyingAction::Discard, + }, + image, + ); clusters = runner.run(); - }, + } } let view = clusters.view(); @@ -161,7 +174,7 @@ fn color_image_to_svg(mut img: ColorImage, config: ConverterConfig) -> Result Result { }; let (width, height) = (img.width() as usize, img.height() as usize); - let img = ColorImage {pixels: img.as_raw().to_vec(), width, height}; + let img = ColorImage { + pixels: img.as_raw().to_vec(), + width, + height, + }; Ok(img) } diff --git a/cmdapp/src/lib.rs b/cmdapp/src/lib.rs index cac0954..bd1e0c3 100644 --- a/cmdapp/src/lib.rs +++ b/cmdapp/src/lib.rs @@ -10,13 +10,13 @@ mod config; mod converter; -mod svg; #[cfg(feature = "python-binding")] mod python; +mod svg; pub use config::*; pub use converter::*; -pub use svg::*; #[cfg(feature = "python-binding")] pub use python::*; +pub use svg::*; pub use visioncortex::ColorImage; diff --git a/cmdapp/src/main.rs b/cmdapp/src/main.rs index 69d0eac..b33bdf2 100644 --- a/cmdapp/src/main.rs +++ b/cmdapp/src/main.rs @@ -2,11 +2,11 @@ mod config; mod converter; mod svg; -use std::str::FromStr; +use clap::{App, Arg}; +use config::{ColorMode, Config, Hierarchical, Preset}; use std::path::PathBuf; -use clap::{Arg, App}; +use std::str::FromStr; use visioncortex::PathSimplifyMode; -use config::{Config, Preset, ColorMode, Hierarchical}; fn path_simplify_mode_from_str(s: &str) -> PathSimplifyMode { match s { @@ -21,61 +21,79 @@ 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("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("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("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("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("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("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("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("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("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") @@ -83,29 +101,39 @@ pub fn config_from_args() -> (PathBuf, PathBuf, Config) { .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("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("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")); + 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 = 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); @@ -115,7 +143,12 @@ pub fn config_from_args() -> (PathBuf, PathBuf, Config) { } 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() + 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") { @@ -136,31 +169,40 @@ pub fn config_from_args() -> (PathBuf, PathBuf, Config) { } if let Some(value) = matches.value_of("filter_speckle") { - if value.trim().parse::().is_ok() { // is numeric + 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); + 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 + 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); + 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 + 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); @@ -172,7 +214,8 @@ pub fn config_from_args() -> (PathBuf, PathBuf, Config) { } if let Some(value) = matches.value_of("corner_threshold") { - if value.trim().parse::().is_ok() { // is numeric + 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); @@ -184,7 +227,8 @@ pub fn config_from_args() -> (PathBuf, PathBuf, Config) { } if let Some(value) = matches.value_of("segment_length") { - if value.trim().parse::().is_ok() { // is numeric + 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); @@ -196,7 +240,8 @@ pub fn config_from_args() -> (PathBuf, PathBuf, Config) { } if let Some(value) = matches.value_of("splice_threshold") { - if value.trim().parse::().is_ok() { // is numeric + 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); @@ -208,11 +253,15 @@ pub fn config_from_args() -> (PathBuf, PathBuf, Config) { } if let Some(value) = matches.value_of("path_precision") { - if value.trim().parse::().is_ok() { // is numeric + 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); + panic!( + "Parser Error: Path precision is not an unsigned integer: {}.", + value + ); } } @@ -225,7 +274,7 @@ fn main() { match result { Ok(()) => { println!("Conversion successful."); - }, + } Err(msg) => { panic!("Conversion failed with error message: {}", msg); } diff --git a/cmdapp/src/svg.rs b/cmdapp/src/svg.rs index 2cc1aab..86881e2 100644 --- a/cmdapp/src/svg.rs +++ b/cmdapp/src/svg.rs @@ -24,25 +24,27 @@ impl SvgFile { } pub fn add_path(&mut self, path: CompoundPath, color: Color) { - self.paths.push(SvgPath { - path, - color, - }) + self.paths.push(SvgPath { path, color }) } } impl fmt::Display for SvgFile { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, r#""#)?; - writeln!(f, r#""#, env!("CARGO_PKG_VERSION"))?; - writeln!(f, + writeln!( + f, + r#""#, + env!("CARGO_PKG_VERSION") + )?; + writeln!( + f, r#""#, self.width, self.height )?; for path in &self.paths { path.fmt_with_precision(f, self.path_precision)?; - }; + } writeln!(f, "") } @@ -56,11 +58,16 @@ impl fmt::Display for SvgPath { impl SvgPath { fn fmt_with_precision(&self, f: &mut fmt::Formatter, precision: Option) -> fmt::Result { - let (string, offset) = self.path.to_svg_string(true, PointF64::default(), precision); + let (string, offset) = self + .path + .to_svg_string(true, PointF64::default(), precision); writeln!( - f, "", - string, self.color.to_hex_string(), - offset.x, offset.y + f, + "", + string, + self.color.to_hex_string(), + offset.x, + offset.y ) } -} \ No newline at end of file +}