mirror of
https://github.com/visioncortex/vtracer.git
synced 2025-12-06 17:15:41 -08:00
CMD app
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
|
|
||||||
members = [
|
members = [
|
||||||
|
"cmdapp",
|
||||||
"webapp",
|
"webapp",
|
||||||
]
|
]
|
||||||
3
cmdapp/.gitignore
vendored
Normal file
3
cmdapp/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
*.svg
|
||||||
|
*.png
|
||||||
|
*.jpg
|
||||||
15
cmdapp/Cargo.toml
Normal file
15
cmdapp/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "vtracer"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Chris Tsang <tyt2y7@gmail.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
description = "A cmd app to convert images into vector graphics."
|
||||||
|
homepage = "http://www.visioncortex.org/vtracer"
|
||||||
|
repository = "https://github.com/visioncortex/vtracer/"
|
||||||
|
categories = ["graphics"]
|
||||||
|
keywords = ["svg", "computer-graphics"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = "2.33.3"
|
||||||
|
image = "0.23.10"
|
||||||
|
visioncortex = { path = "../visioncortex" }
|
||||||
297
cmdapp/src/config.rs
Normal file
297
cmdapp/src/config.rs
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
use clap::{Arg, App};
|
||||||
|
use visioncortex::path::PathSimplifyMode;
|
||||||
|
use super::util::{path_simplify_mode, preset, deg2rad};
|
||||||
|
|
||||||
|
pub enum Preset {
|
||||||
|
Bw,
|
||||||
|
Poster,
|
||||||
|
Photo
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Config {
|
||||||
|
pub input_path: PathBuf,
|
||||||
|
pub output_path: PathBuf,
|
||||||
|
pub color_mode: String,
|
||||||
|
pub filter_speckle: usize,
|
||||||
|
pub color_precision: i32,
|
||||||
|
pub layer_difference: i32,
|
||||||
|
pub mode: PathSimplifyMode,
|
||||||
|
pub corner_threshold: i32,
|
||||||
|
pub length_threshold: f64,
|
||||||
|
pub max_iterations: usize,
|
||||||
|
pub splice_threshold: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ConverterConfig {
|
||||||
|
pub input_path: PathBuf,
|
||||||
|
pub output_path: PathBuf,
|
||||||
|
pub color_mode: String,
|
||||||
|
pub filter_speckle_area: usize,
|
||||||
|
pub color_precision_loss: i32,
|
||||||
|
pub layer_difference: i32,
|
||||||
|
pub mode: PathSimplifyMode,
|
||||||
|
pub corner_threshold: f64,
|
||||||
|
pub length_threshold: f64,
|
||||||
|
pub max_iterations: usize,
|
||||||
|
pub splice_threshold: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
input_path: PathBuf::default(),
|
||||||
|
output_path: PathBuf::default(),
|
||||||
|
color_mode: String::from("color"),
|
||||||
|
mode: path_simplify_mode("spline"),
|
||||||
|
filter_speckle: 4,
|
||||||
|
color_precision: 6,
|
||||||
|
layer_difference: 16,
|
||||||
|
corner_threshold: 60,
|
||||||
|
length_threshold: 4.0,
|
||||||
|
splice_threshold: 45,
|
||||||
|
max_iterations: 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn from_args() -> Self {
|
||||||
|
let app = App::new("visioncortex VTracer").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("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`"));
|
||||||
|
|
||||||
|
// 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(value), 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 = String::from(if value.trim() == "bw" || value.trim() == "BW" {"binary"} else {"color"})
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(value) = matches.value_of("mode") {
|
||||||
|
let value = value.trim();
|
||||||
|
config.mode = path_simplify_mode(if value == "pixel" {
|
||||||
|
"none"
|
||||||
|
} else if value == "polygon" {
|
||||||
|
"polygon"
|
||||||
|
} else if value == "spline" {
|
||||||
|
"spline"
|
||||||
|
} else {
|
||||||
|
panic!("Parser Error: Curve fitting mode is invalid with value {}", value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(value) = matches.value_of("filter_speckle") {
|
||||||
|
if value.trim().parse::<usize>().is_ok() { // is numeric
|
||||||
|
let value = value.trim().parse::<usize>().unwrap();
|
||||||
|
if value < 1 || value > 16 {
|
||||||
|
panic!("Out of Range Error: Filter speckle is invalid at {}. It must be within [1,16].", value);
|
||||||
|
}
|
||||||
|
config.filter_speckle = value;
|
||||||
|
} else {
|
||||||
|
panic!("Parser Error: Filter speckle is not a positive integer with value {}.", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(value) = matches.value_of("color_precision") {
|
||||||
|
if value.trim().parse::<i32>().is_ok() { // is numeric
|
||||||
|
let value = value.trim().parse::<i32>().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 with value {}.", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(value) = matches.value_of("gradient_step") {
|
||||||
|
if value.trim().parse::<i32>().is_ok() { // is numeric
|
||||||
|
let value = value.trim().parse::<i32>().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 with value {}.", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(value) = matches.value_of("corner_threshold") {
|
||||||
|
if value.trim().parse::<i32>().is_ok() { // is numeric
|
||||||
|
let value = value.trim().parse::<i32>().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 with value {}.", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(value) = matches.value_of("segment_length") {
|
||||||
|
if value.trim().parse::<f64>().is_ok() { // is numeric
|
||||||
|
let value = value.trim().parse::<f64>().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 with value {}.", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(value) = matches.value_of("splice_threshold") {
|
||||||
|
if value.trim().parse::<i32>().is_ok() { // is numeric
|
||||||
|
let value = value.trim().parse::<i32>().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 with value {}.", 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);
|
||||||
|
match preset {
|
||||||
|
Preset::Bw => Self {
|
||||||
|
input_path,
|
||||||
|
output_path,
|
||||||
|
color_mode: String::from("binary"),
|
||||||
|
filter_speckle: 4,
|
||||||
|
color_precision: 6,
|
||||||
|
layer_difference: 16,
|
||||||
|
mode: path_simplify_mode("spline"),
|
||||||
|
corner_threshold: 60,
|
||||||
|
length_threshold: 4.0,
|
||||||
|
max_iterations: 10,
|
||||||
|
splice_threshold: 45,
|
||||||
|
},
|
||||||
|
Preset::Poster => Self {
|
||||||
|
input_path,
|
||||||
|
output_path,
|
||||||
|
color_mode: String::from("color"),
|
||||||
|
filter_speckle: 4,
|
||||||
|
color_precision: 8,
|
||||||
|
layer_difference: 16,
|
||||||
|
mode: path_simplify_mode("spline"),
|
||||||
|
corner_threshold: 60,
|
||||||
|
length_threshold: 4.0,
|
||||||
|
max_iterations: 10,
|
||||||
|
splice_threshold: 45,
|
||||||
|
},
|
||||||
|
Preset::Photo => Self {
|
||||||
|
input_path,
|
||||||
|
output_path,
|
||||||
|
color_mode: String::from("color"),
|
||||||
|
filter_speckle: 10,
|
||||||
|
color_precision: 8,
|
||||||
|
layer_difference: 48,
|
||||||
|
mode: path_simplify_mode("spline"),
|
||||||
|
corner_threshold: 180,
|
||||||
|
length_threshold: 4.0,
|
||||||
|
max_iterations: 10,
|
||||||
|
splice_threshold: 45,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_converter_config(self) -> ConverterConfig {
|
||||||
|
ConverterConfig {
|
||||||
|
input_path: self.input_path,
|
||||||
|
output_path: self.output_path,
|
||||||
|
color_mode: self.color_mode,
|
||||||
|
filter_speckle_area: self.filter_speckle * self.filter_speckle,
|
||||||
|
color_precision_loss: 8 - self.color_precision,
|
||||||
|
layer_difference: self.layer_difference,
|
||||||
|
mode: self.mode,
|
||||||
|
corner_threshold: deg2rad(self.corner_threshold),
|
||||||
|
length_threshold: self.length_threshold,
|
||||||
|
max_iterations: self.max_iterations,
|
||||||
|
splice_threshold: deg2rad(self.splice_threshold),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
129
cmdapp/src/converter.rs
Normal file
129
cmdapp/src/converter.rs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
use std::{fs::File, io::Write};
|
||||||
|
|
||||||
|
use visioncortex::Color;
|
||||||
|
use visioncortex::{ColorName, color_clusters::RunnerConfig};
|
||||||
|
use visioncortex::{ColorImage, color_clusters::Runner};
|
||||||
|
use super::config::{Config, ConverterConfig};
|
||||||
|
use super::svg::SvgFile;
|
||||||
|
|
||||||
|
pub fn convert_image_to_svg(config: Config) -> Result<(), String> {
|
||||||
|
let config = config.into_converter_config();
|
||||||
|
if config.color_mode == "color" {
|
||||||
|
color_image_to_svg(config)
|
||||||
|
} else if config.color_mode == "binary" {
|
||||||
|
binary_image_to_svg(config)
|
||||||
|
} else {
|
||||||
|
Err(format!("Unknown color mode: {}", config.color_mode))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn color_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),
|
||||||
|
}
|
||||||
|
|
||||||
|
let runner = Runner::new(RunnerConfig {
|
||||||
|
batch_size: 25600,
|
||||||
|
good_min_area: config.filter_speckle_area,
|
||||||
|
good_max_area: (width * height),
|
||||||
|
is_same_color_a: config.color_precision_loss,
|
||||||
|
is_same_color_b: 1,
|
||||||
|
deepen_diff: config.layer_difference,
|
||||||
|
hollow_neighbours: 1,
|
||||||
|
}, img);
|
||||||
|
|
||||||
|
let clusters = runner.run();
|
||||||
|
|
||||||
|
let view = clusters.view();
|
||||||
|
|
||||||
|
let mut svg = SvgFile::new(width, height);
|
||||||
|
for &cluster_index in view.clusters_output.iter().rev() {
|
||||||
|
let cluster = view.get_cluster(cluster_index);
|
||||||
|
let svg_path = cluster.to_svg_path(
|
||||||
|
&view,
|
||||||
|
false,
|
||||||
|
config.mode,
|
||||||
|
config.corner_threshold,
|
||||||
|
config.length_threshold,
|
||||||
|
config.max_iterations,
|
||||||
|
config.splice_threshold
|
||||||
|
);
|
||||||
|
svg.add_path(svg_path, cluster.residue_color());
|
||||||
|
}
|
||||||
|
|
||||||
|
let out_file = File::create(config.output_path);
|
||||||
|
let mut out_file = match out_file {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(_) => return Err(String::from("Cannot create output file.")),
|
||||||
|
};
|
||||||
|
|
||||||
|
out_file.write_all(&svg.to_svg_file().as_bytes()).unwrap();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub 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),
|
||||||
|
}
|
||||||
|
let img = img.to_binary_image(|x| x.r < 128);
|
||||||
|
|
||||||
|
let clusters = img.to_clusters(false);
|
||||||
|
|
||||||
|
let mut svg = SvgFile::new(width, height);
|
||||||
|
for i in 0..clusters.len() {
|
||||||
|
let cluster = clusters.get_cluster(i);
|
||||||
|
if cluster.size() >= config.filter_speckle_area {
|
||||||
|
let svg_path = cluster.to_svg_path(
|
||||||
|
config.mode,
|
||||||
|
config.corner_threshold,
|
||||||
|
config.length_threshold,
|
||||||
|
config.max_iterations,
|
||||||
|
config.splice_threshold,
|
||||||
|
);
|
||||||
|
let color = Color::color(&ColorName::Black);
|
||||||
|
svg.add_path(svg_path, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write_svg(svg, config.output_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_image(input_path: PathBuf) -> Result<(ColorImage, usize, usize), String> {
|
||||||
|
let img = image::open(input_path);
|
||||||
|
let img = match img {
|
||||||
|
Ok(file) => file.to_rgba(),
|
||||||
|
Err(_) => return Err(String::from("No image file found at specified input path")),
|
||||||
|
};
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_svg(svg: SvgFile, output_path: PathBuf) -> Result<(), String> {
|
||||||
|
let out_file = File::create(output_path);
|
||||||
|
let mut out_file = match out_file {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(_) => return Err(String::from("Cannot create output file.")),
|
||||||
|
};
|
||||||
|
|
||||||
|
out_file.write_all(&svg.to_svg_file().as_bytes()).unwrap();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
9
cmdapp/src/lib.rs
Normal file
9
cmdapp/src/lib.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
mod config;
|
||||||
|
mod converter;
|
||||||
|
mod svg;
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
pub use config::*;
|
||||||
|
pub use converter::*;
|
||||||
|
pub use svg::*;
|
||||||
|
pub use util::*;
|
||||||
14
cmdapp/src/main.rs
Normal file
14
cmdapp/src/main.rs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
use vtracer::{Config, convert_image_to_svg};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let config = Config::from_args();
|
||||||
|
let result = convert_image_to_svg(config);
|
||||||
|
match result {
|
||||||
|
Ok(()) => {
|
||||||
|
println!("Conversion successful.");
|
||||||
|
},
|
||||||
|
Err(msg) => {
|
||||||
|
panic!("Conversion failed with error message: {}", msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
cmdapp/src/svg.rs
Normal file
43
cmdapp/src/svg.rs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
use visioncortex::Color;
|
||||||
|
|
||||||
|
pub struct SvgPath {
|
||||||
|
path: String,
|
||||||
|
color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SvgFile {
|
||||||
|
patches: Vec<SvgPath>,
|
||||||
|
width: usize,
|
||||||
|
height: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SvgFile {
|
||||||
|
pub fn new(width: usize, height: usize) -> Self {
|
||||||
|
SvgFile {
|
||||||
|
patches: vec![],
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_path(&mut self, path: String, color: Color) {
|
||||||
|
self.patches.push(SvgPath {
|
||||||
|
path,
|
||||||
|
color
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_svg_file(&self) -> String {
|
||||||
|
let mut result: Vec<String> = vec![format!(r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="{}" height="{}">
|
||||||
|
"#, self.width, self.height)];
|
||||||
|
|
||||||
|
for patch in &self.patches {
|
||||||
|
let color = patch.color;
|
||||||
|
result.push(format!("<path d=\"{}\" fill=\"#{:02x}{:02x}{:02x}\"/>\n", patch.path, color.r, color.g, color.b));
|
||||||
|
};
|
||||||
|
|
||||||
|
result.push(String::from("</svg>"));
|
||||||
|
result.concat()
|
||||||
|
}
|
||||||
|
}
|
||||||
24
cmdapp/src/util.rs
Normal file
24
cmdapp/src/util.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
use visioncortex::PathSimplifyMode;
|
||||||
|
use super::Preset;
|
||||||
|
|
||||||
|
pub fn path_simplify_mode(s: &str) -> PathSimplifyMode {
|
||||||
|
match s {
|
||||||
|
"polygon" => PathSimplifyMode::Polygon,
|
||||||
|
"spline" => PathSimplifyMode::Spline,
|
||||||
|
"none" => PathSimplifyMode::None,
|
||||||
|
_ => panic!("unknown PathSimplifyMode {}", s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn preset(s: &str) -> Preset {
|
||||||
|
match s {
|
||||||
|
"bw" => Preset::Bw,
|
||||||
|
"poster" => Preset::Poster,
|
||||||
|
"photo" => Preset::Photo,
|
||||||
|
_ => panic!("unknown Preset {}", s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deg2rad(deg: i32) -> f64 {
|
||||||
|
deg as f64 / 180.0 * std::f64::consts::PI
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user