cargo fmt

This commit is contained in:
Chris Tsang
2023-11-12 20:38:01 +00:00
parent 9dbbd100df
commit cc43924601
5 changed files with 219 additions and 146 deletions

View File

@@ -4,7 +4,7 @@ use visioncortex::PathSimplifyMode;
pub enum Preset { pub enum Preset {
Bw, Bw,
Poster, Poster,
Photo Photo,
} }
pub enum ColorMode { pub enum ColorMode {
@@ -142,7 +142,7 @@ impl Config {
max_iterations: 10, max_iterations: 10,
splice_threshold: 45, splice_threshold: 45,
path_precision: Some(8), path_precision: Some(8),
} },
} }
} }

View File

@@ -1,11 +1,11 @@
use std::path::Path; use std::path::Path;
use std::{fs::File, io::Write}; use std::{fs::File, io::Write};
use fastrand::Rng; use super::config::{ColorMode, Config, ConverterConfig, Hierarchical};
use visioncortex::{Color, ColorImage, ColorName};
use visioncortex::color_clusters::{Runner, RunnerConfig, KeyingAction, HIERARCHICAL_MAX};
use super::config::{Config, ColorMode, Hierarchical, ConverterConfig};
use super::svg::SvgFile; 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; 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 /// 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<SvgFile, String> {
} }
/// Convert an image file into svg file /// 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 img = read_image(input_path)?;
let svg = convert(img, config)?; let svg = convert(img, config)?;
write_svg(svg, output_path) 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 { for x in 0..img.width {
let pixel_color = img.get_pixel(x, y); let pixel_color = img.get_pixel(x, y);
if pixel_color.r == color.r && pixel_color.g == color.g && pixel_color.b == color.b { 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<Color, String> { fn find_unused_color_in_image(img: &ColorImage) -> Result<Color, String> {
let special_colors = IntoIterator::into_iter([ let special_colors = IntoIterator::into_iter([
Color::new(255, 0, 0), Color::new(255, 0, 0),
Color::new(0, 255, 0), Color::new(0, 255, 0),
Color::new(0, 0, 255), Color::new(0, 0, 255),
Color::new(255, 255, 0), Color::new(255, 255, 0),
Color::new(0, 255, 255), Color::new(0, 255, 255),
Color::new(255, 0, 255), Color::new(255, 0, 255),
]); ]);
let rng = Rng::new(); let rng = Rng::new();
let random_colors = (0..NUM_UNUSED_COLOR_ITERATIONS).map(|_| { let random_colors =
Color::new( (0..NUM_UNUSED_COLOR_ITERATIONS).map(|_| Color::new(rng.u8(..), rng.u8(..), rng.u8(..)));
rng.u8(..),
rng.u8(..),
rng.u8(..),
)
});
for color in special_colors.chain(random_colors) { for color in special_colors.chain(random_colors) {
if !color_exists_in_image(img, color) { if !color_exists_in_image(img, color) {
return Ok(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 { fn should_key_image(img: &ColorImage) -> bool {
@@ -73,7 +74,13 @@ fn should_key_image(img: &ColorImage) -> bool {
// Check for transparency at several scanlines // Check for transparency at several scanlines
let threshold = ((img.width * 2) as f32 * KEYING_THRESHOLD) as usize; let threshold = ((img.width * 2) as f32 * KEYING_THRESHOLD) as usize;
let mut num_transparent_boundary_pixels = 0; 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 y in y_positions {
for x in 0..img.width { for x in 0..img.width {
if img.get_pixel(x, y).a == 0 { if img.get_pixel(x, y).a == 0 {
@@ -107,23 +114,26 @@ fn color_image_to_svg(mut img: ColorImage, config: ConverterConfig) -> Result<Sv
Color::default() Color::default()
}; };
let runner = Runner::new(RunnerConfig { let runner = Runner::new(
diagonal: config.layer_difference == 0, RunnerConfig {
hierarchical: HIERARCHICAL_MAX, diagonal: config.layer_difference == 0,
batch_size: 25600, hierarchical: HIERARCHICAL_MAX,
good_min_area: config.filter_speckle_area, batch_size: 25600,
good_max_area: (width * height), good_min_area: config.filter_speckle_area,
is_same_color_a: config.color_precision_loss, good_max_area: (width * height),
is_same_color_b: 1, is_same_color_a: config.color_precision_loss,
deepen_diff: config.layer_difference, is_same_color_b: 1,
hollow_neighbours: 1, deepen_diff: config.layer_difference,
key_color, hollow_neighbours: 1,
keying_action: if matches!(config.hierarchical, Hierarchical::Cutout) { key_color,
KeyingAction::Keep keying_action: if matches!(config.hierarchical, Hierarchical::Cutout) {
} else { KeyingAction::Keep
KeyingAction::Discard } else {
KeyingAction::Discard
},
}, },
}, img); img,
);
let mut clusters = runner.run(); let mut clusters = runner.run();
@@ -132,21 +142,24 @@ fn color_image_to_svg(mut img: ColorImage, config: ConverterConfig) -> Result<Sv
Hierarchical::Cutout => { Hierarchical::Cutout => {
let view = clusters.view(); let view = clusters.view();
let image = view.to_color_image(); let image = view.to_color_image();
let runner = Runner::new(RunnerConfig { let runner = Runner::new(
diagonal: false, RunnerConfig {
hierarchical: 64, diagonal: false,
batch_size: 25600, hierarchical: 64,
good_min_area: 0, batch_size: 25600,
good_max_area: (image.width * image.height) as usize, good_min_area: 0,
is_same_color_a: 0, good_max_area: (image.width * image.height) as usize,
is_same_color_b: 1, is_same_color_a: 0,
deepen_diff: 0, is_same_color_b: 1,
hollow_neighbours: 0, deepen_diff: 0,
key_color, hollow_neighbours: 0,
keying_action: KeyingAction::Discard, key_color,
}, image); keying_action: KeyingAction::Discard,
},
image,
);
clusters = runner.run(); clusters = runner.run();
}, }
} }
let view = clusters.view(); let view = clusters.view();
@@ -161,7 +174,7 @@ fn color_image_to_svg(mut img: ColorImage, config: ConverterConfig) -> Result<Sv
config.corner_threshold, config.corner_threshold,
config.length_threshold, config.length_threshold,
config.max_iterations, config.max_iterations,
config.splice_threshold config.splice_threshold,
); );
svg.add_path(paths, cluster.residue_color()); svg.add_path(paths, cluster.residue_color());
} }
@@ -202,7 +215,11 @@ fn read_image(input_path: &Path) -> Result<ColorImage, String> {
}; };
let (width, height) = (img.width() as usize, img.height() as usize); 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) Ok(img)
} }

View File

@@ -10,13 +10,13 @@
mod config; mod config;
mod converter; mod converter;
mod svg;
#[cfg(feature = "python-binding")] #[cfg(feature = "python-binding")]
mod python; mod python;
mod svg;
pub use config::*; pub use config::*;
pub use converter::*; pub use converter::*;
pub use svg::*;
#[cfg(feature = "python-binding")] #[cfg(feature = "python-binding")]
pub use python::*; pub use python::*;
pub use svg::*;
pub use visioncortex::ColorImage; pub use visioncortex::ColorImage;

View File

@@ -2,11 +2,11 @@ mod config;
mod converter; mod converter;
mod svg; mod svg;
use std::str::FromStr; use clap::{App, Arg};
use config::{ColorMode, Config, Hierarchical, Preset};
use std::path::PathBuf; use std::path::PathBuf;
use clap::{Arg, App}; use std::str::FromStr;
use visioncortex::PathSimplifyMode; use visioncortex::PathSimplifyMode;
use config::{Config, Preset, ColorMode, Hierarchical};
fn path_simplify_mode_from_str(s: &str) -> PathSimplifyMode { fn path_simplify_mode_from_str(s: &str) -> PathSimplifyMode {
match s { 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")) let app = App::new("visioncortex VTracer ".to_owned() + env!("CARGO_PKG_VERSION"))
.about("A cmd app to convert images into vector graphics."); .about("A cmd app to convert images into vector graphics.");
let app = app.arg(Arg::with_name("input") let app = app.arg(
.long("input") Arg::with_name("input")
.short("i") .long("input")
.takes_value(true) .short("i")
.help("Path to input raster image") .takes_value(true)
.required(true)); .help("Path to input raster image")
.required(true),
);
let app = app.arg(Arg::with_name("output") let app = app.arg(
.long("output") Arg::with_name("output")
.short("o") .long("output")
.takes_value(true) .short("o")
.help("Path to output vector graphics") .takes_value(true)
.required(true)); .help("Path to output vector graphics")
.required(true),
);
let app = app.arg(Arg::with_name("color_mode") let app = app.arg(
.long("colormode") Arg::with_name("color_mode")
.takes_value(true) .long("colormode")
.help("True color image `color` (default) or Binary image `bw`")); .takes_value(true)
.help("True color image `color` (default) or Binary image `bw`"),
);
let app = app.arg(Arg::with_name("hierarchical") let app = app.arg(
.long("hierarchical") Arg::with_name("hierarchical")
.takes_value(true) .long("hierarchical")
.help( .takes_value(true)
"Hierarchical clustering `stacked` (default) or non-stacked `cutout`. \ .help(
Only applies to color mode. " "Hierarchical clustering `stacked` (default) or non-stacked `cutout`. \
)); Only applies to color mode. ",
),
);
let app = app.arg(Arg::with_name("preset") let app = app.arg(
.long("preset") Arg::with_name("preset")
.takes_value(true) .long("preset")
.help("Use one of the preset configs `bw`, `poster`, `photo`")); .takes_value(true)
.help("Use one of the preset configs `bw`, `poster`, `photo`"),
);
let app = app.arg(Arg::with_name("filter_speckle") let app = app.arg(
.long("filter_speckle") Arg::with_name("filter_speckle")
.short("f") .long("filter_speckle")
.takes_value(true) .short("f")
.help("Discard patches smaller than X px in size")); .takes_value(true)
.help("Discard patches smaller than X px in size"),
);
let app = app.arg(Arg::with_name("color_precision") let app = app.arg(
.long("color_precision") Arg::with_name("color_precision")
.short("p") .long("color_precision")
.takes_value(true) .short("p")
.help("Number of significant bits to use in an RGB channel")); .takes_value(true)
.help("Number of significant bits to use in an RGB channel"),
);
let app = app.arg(Arg::with_name("gradient_step") let app = app.arg(
.long("gradient_step") Arg::with_name("gradient_step")
.short("g") .long("gradient_step")
.takes_value(true) .short("g")
.help("Color difference between gradient layers")); .takes_value(true)
.help("Color difference between gradient layers"),
);
let app = app.arg(Arg::with_name("corner_threshold") let app = app.arg(
.long("corner_threshold") Arg::with_name("corner_threshold")
.short("c") .long("corner_threshold")
.takes_value(true) .short("c")
.help("Minimum momentary angle (degree) to be considered a corner")); .takes_value(true)
.help("Minimum momentary angle (degree) to be considered a corner"),
);
let app = app.arg(Arg::with_name("segment_length") let app = app.arg(Arg::with_name("segment_length")
.long("segment_length") .long("segment_length")
@@ -83,29 +101,39 @@ pub fn config_from_args() -> (PathBuf, PathBuf, Config) {
.takes_value(true) .takes_value(true)
.help("Perform iterative subdivide smooth until all segments are shorter than this length")); .help("Perform iterative subdivide smooth until all segments are shorter than this length"));
let app = app.arg(Arg::with_name("splice_threshold") let app = app.arg(
.long("splice_threshold") Arg::with_name("splice_threshold")
.short("s") .long("splice_threshold")
.takes_value(true) .short("s")
.help("Minimum angle displacement (degree) to splice a spline")); .takes_value(true)
.help("Minimum angle displacement (degree) to splice a spline"),
);
let app = app.arg(Arg::with_name("mode") let app = app.arg(
.long("mode") Arg::with_name("mode")
.short("m") .long("mode")
.takes_value(true) .short("m")
.help("Curver fitting mode `pixel`, `polygon`, `spline`")); .takes_value(true)
.help("Curver fitting mode `pixel`, `polygon`, `spline`"),
);
let app = app.arg(Arg::with_name("path_precision") let app = app.arg(
.long("path_precision") Arg::with_name("path_precision")
.takes_value(true) .long("path_precision")
.help("Number of decimal places to use in path string")); .takes_value(true)
.help("Number of decimal places to use in path string"),
);
// Extract matches // Extract matches
let matches = app.get_matches(); let matches = app.get_matches();
let mut config = Config::default(); 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 input_path = matches
let output_path = matches.value_of("output").expect("Output path is required, please specify it by --output or -o."); .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 input_path = PathBuf::from(input_path);
let output_path = PathBuf::from(output_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") { 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") { 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 let Some(value) = matches.value_of("filter_speckle") {
if value.trim().parse::<usize>().is_ok() { // is numeric if value.trim().parse::<usize>().is_ok() {
// is numeric
let value = value.trim().parse::<usize>().unwrap(); let value = value.trim().parse::<usize>().unwrap();
if value > 16 { if value > 16 {
panic!("Out of Range Error: Filter speckle is invalid at {}. It must be within [0,16].", value); panic!("Out of Range Error: Filter speckle is invalid at {}. It must be within [0,16].", value);
} }
config.filter_speckle = value; config.filter_speckle = value;
} else { } 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 let Some(value) = matches.value_of("color_precision") {
if value.trim().parse::<i32>().is_ok() { // is numeric if value.trim().parse::<i32>().is_ok() {
// is numeric
let value = value.trim().parse::<i32>().unwrap(); let value = value.trim().parse::<i32>().unwrap();
if value < 1 || value > 8 { if value < 1 || value > 8 {
panic!("Out of Range Error: Color precision is invalid at {}. It must be within [1,8].", value); panic!("Out of Range Error: Color precision is invalid at {}. It must be within [1,8].", value);
} }
config.color_precision = value; config.color_precision = value;
} else { } 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 let Some(value) = matches.value_of("gradient_step") {
if value.trim().parse::<i32>().is_ok() { // is numeric if value.trim().parse::<i32>().is_ok() {
// is numeric
let value = value.trim().parse::<i32>().unwrap(); let value = value.trim().parse::<i32>().unwrap();
if value < 0 || value > 255 { if value < 0 || value > 255 {
panic!("Out of Range Error: Gradient step is invalid at {}. It must be within [0,255].", value); 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 let Some(value) = matches.value_of("corner_threshold") {
if value.trim().parse::<i32>().is_ok() { // is numeric if value.trim().parse::<i32>().is_ok() {
// is numeric
let value = value.trim().parse::<i32>().unwrap(); let value = value.trim().parse::<i32>().unwrap();
if value < 0 || value > 180 { if value < 0 || value > 180 {
panic!("Out of Range Error: Corner threshold is invalid at {}. It must be within [0,180].", value); 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 let Some(value) = matches.value_of("segment_length") {
if value.trim().parse::<f64>().is_ok() { // is numeric if value.trim().parse::<f64>().is_ok() {
// is numeric
let value = value.trim().parse::<f64>().unwrap(); let value = value.trim().parse::<f64>().unwrap();
if value < 3.5 || value > 10.0 { 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); 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 let Some(value) = matches.value_of("splice_threshold") {
if value.trim().parse::<i32>().is_ok() { // is numeric if value.trim().parse::<i32>().is_ok() {
// is numeric
let value = value.trim().parse::<i32>().unwrap(); let value = value.trim().parse::<i32>().unwrap();
if value < 0 || value > 180 { if value < 0 || value > 180 {
panic!("Out of Range Error: Segment length is invalid at {}. It must be within [0,180].", value); 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 let Some(value) = matches.value_of("path_precision") {
if value.trim().parse::<u32>().is_ok() { // is numeric if value.trim().parse::<u32>().is_ok() {
// is numeric
let value = value.trim().parse::<u32>().ok(); let value = value.trim().parse::<u32>().ok();
config.path_precision = value; config.path_precision = value;
} else { } 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 { match result {
Ok(()) => { Ok(()) => {
println!("Conversion successful."); println!("Conversion successful.");
}, }
Err(msg) => { Err(msg) => {
panic!("Conversion failed with error message: {}", msg); panic!("Conversion failed with error message: {}", msg);
} }

View File

@@ -24,25 +24,27 @@ impl SvgFile {
} }
pub fn add_path(&mut self, path: CompoundPath, color: Color) { pub fn add_path(&mut self, path: CompoundPath, color: Color) {
self.paths.push(SvgPath { self.paths.push(SvgPath { path, color })
path,
color,
})
} }
} }
impl fmt::Display for SvgFile { impl fmt::Display for SvgFile {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, r#"<?xml version="1.0" encoding="UTF-8"?>"#)?; writeln!(f, r#"<?xml version="1.0" encoding="UTF-8"?>"#)?;
writeln!(f, r#"<!-- Generator: visioncortex VTracer {} -->"#, env!("CARGO_PKG_VERSION"))?; writeln!(
writeln!(f, f,
r#"<!-- Generator: visioncortex VTracer {} -->"#,
env!("CARGO_PKG_VERSION")
)?;
writeln!(
f,
r#"<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="{}" height="{}">"#, r#"<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="{}" height="{}">"#,
self.width, self.height self.width, self.height
)?; )?;
for path in &self.paths { for path in &self.paths {
path.fmt_with_precision(f, self.path_precision)?; path.fmt_with_precision(f, self.path_precision)?;
}; }
writeln!(f, "</svg>") writeln!(f, "</svg>")
} }
@@ -56,11 +58,16 @@ impl fmt::Display for SvgPath {
impl SvgPath { impl SvgPath {
fn fmt_with_precision(&self, f: &mut fmt::Formatter, precision: Option<u32>) -> fmt::Result { fn fmt_with_precision(&self, f: &mut fmt::Formatter, precision: Option<u32>) -> 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!( writeln!(
f, "<path d=\"{}\" fill=\"{}\" transform=\"translate({},{})\"/>", f,
string, self.color.to_hex_string(), "<path d=\"{}\" fill=\"{}\" transform=\"translate({},{})\"/>",
offset.x, offset.y string,
self.color.to_hex_string(),
offset.x,
offset.y
) )
} }
} }