10 Commits

Author SHA1 Message Date
Chris Tsang 2774fc06c9 Reduce default path precision 2023-11-12 23:05:45 +00:00
Chris Tsang fa7d021055 Readme 2023-11-12 20:39:25 +00:00
Chris Tsang cc43924601 cargo fmt 2023-11-12 20:38:01 +00:00
Chris Tsang 9dbbd100df Bump 2023-11-12 20:36:35 +00:00
linkmauve 37a2570f49 Add a function to do in-memory conversion (#59)
* Move path handling out of conversion functions

This lets us decouple file reading/writing from the actual conversion.

* Move Config::from_args() to main.rs

* Remove path support from Config

Instead the input_path and output_path have to be passed to
convert_image_to_svg() manually.

* Add a simplified convert() function

This lets the user convert an image from memory, without encoding it to
PNG, writing it to a file, then reopening this file and decoding it
using the image crate.

It also allows the user to not write a SVG directly to a file.
2023-11-13 04:28:05 +08:00
Chris Tsang b2cd1a9524 Expose as rlib too 2023-10-24 08:43:37 +01:00
Chris Tsang ac93bd4a51 Edit 2023-09-24 11:08:30 +01:00
Chris Tsang e62f071b34 Edit 2023-09-23 11:52:36 +01:00
Chris Tsang 884092a5b9 Edit 2023-09-23 11:49:54 +01:00
Chris Tsang 5f5a6c6648 README 2023-09-23 11:45:36 +01:00
11 changed files with 421 additions and 380 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2022 Tsang Hao Fung
Copyright (c) 2023 Tsang Hao Fung
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
+23 -7
View File
@@ -12,7 +12,7 @@
<span> | </span>
<a href="https://www.visioncortex.org/vtracer/">Demo</a>
<span> | </span>
<a href="https://github.com/visioncortex/vtracer/releases/latest">Download</a>
<a href="https://github.com/visioncortex/vtracer/releases/tag/0.6.0">Download</a>
</h3>
<sub>Built with 🦀 by <a href="https://www.visioncortex.org/">The Vision Cortex Research Group</a></sub>
@@ -22,7 +22,7 @@
visioncortex VTracer is an open source software to convert raster images (like jpg & png) into vector graphics (svg). It can vectorize graphics and photographs and trace the curves to output compact vector files.
Comparing to [Potrace](http://potrace.sourceforge.net/) which only accept binarized inputs (Black & White pixmap), VTracer has an image processing pipeline which can handle colored high resolution scans.
Comparing to [Potrace](http://potrace.sourceforge.net/) which only accept binarized inputs (Black & White pixmap), VTracer has an image processing pipeline which can handle colored high resolution scans. tl;dr: Potrace uses a `O(n^2)` fitting algorithm, whereas `vtracer` is entirely `O(n)`.
Comparing to Adobe Illustrator's [Image Trace](https://helpx.adobe.com/illustrator/using/image-trace.html), VTracer's output is much more compact (less shapes) as we adopt a stacking strategy and avoid producing shapes with holes.
@@ -73,7 +73,7 @@ OPTIONS:
### Install
You can download pre-built binaries from [Releases](https://github.com/visioncortex/vtracer/releases).
You can download pre-built binaries from [Releases](https://github.com/visioncortex/vtracer/releases/tag/0.6.0).
You can also install the program from source from [crates.io/vtracer](https://crates.io/crates/vtracer):
@@ -118,14 +118,30 @@ VTracer is used by the following projects (feel free to add yours!):
</tbody>
</table>
## Anecdotes
> The following content is an excerpt from my [unpublished](https://github.com/sponsors/tyt2y3) [memoir](https://github.com/visioncortex/memoir).
### How / when / where did VTracer come about?
At my teenage, two open source projects in the vector graphics space inspired me the most: Potrace and Anti-Grain Geometry (AGG).
Many years later, in 2020, I was developing a video processing engine. And it became evident that it requires way more investment to be commercially viable. So before abandoning the project, I wanted to publish *something* as open-source for posterity. At that time, I already developed a prototype vector graphics tracer. It can convert high-resolution scans of hand-drawn blueprints into vectors. But it can only process black and white images, and can only output polygons, not splines.
The plan was to fully develop the vectorizer: to handle color images and output splines. I recruited a very talented intern, [@shpun817](https://github.com/shpun817), to work on VTracer. I grafted the frontend of the video processing engine - the ["The Clustering Algorithm"](https://www.visioncortex.org/impression-docs#the-clustering-algorithm) as the pre-processor.
Three months later, we published the first version on Reddit. Out of my surprise, the response of such an underwhelming project was overwhelming.
## What's next?
There are several things in my mind:
1. Pencil tracing. Instead of tracing shapes as closed paths, may be we can attempt to skeletonize the shapes as open paths. The output would be clean, fixed width strokes.
1. Path simplification. Implement a post-process filter to the output paths to further reduce the number of splines.
2. Perfect cut-out mode. Right now in cut-out mode, the shapes do not share perfect boundaries, but have seams.
2. Perfect cut-out mode. Right now in cut-out mode, the shapes do not share boundaries perfectly, but have seams.
3. Image cleaning. Right now the tracer works best on losslessly compressed pngs. If an image suffered from jpeg noises, it could impact the tracing quality. We might be able to develop a pre-filtering pass that denoises the input.
3. Pencil tracing. Instead of tracing shapes as closed paths, may be we can attempt to skeletonize the shapes as open paths. The output would be clean, fixed width strokes.
If you are interested in working on them or willing to sponsor its development, feel free to get in touch.
4. Image cleaning. Right now the tracer works best on losslessly compressed pngs. If an image suffered from jpeg noises, it could impact the tracing quality. We might be able to develop a pre-filtering pass that denoises the input.
If you are interested in working on them or willing to sponsor its development, feel free to get in touch.
-27
View File
@@ -1,27 +0,0 @@
# Version 0.6.0 (2023-09-08)
- Python bindings
# Version 0.5.0 (2022-10-09)
- Handle transparent png images
# Version 0.4.0 (2021-07-23)
- SVG path string numeric precision
# Version 0.3.0 (2021-01-24)
- Added cutout mode
# Version 0.2.0 (2020-11-15)
- Use relative & closed paths
# Version 0.1.1 (2020-11-01)
- SVG namespace
# Version 0.1.0 (2020-10-31)
- Initial release
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "vtracer"
version = "0.6.1"
version = "0.6.2"
authors = ["Chris Tsang <chris.2y3@outlook.com>"]
edition = "2021"
description = "A cmd app to convert images into vector graphics."
@@ -22,4 +22,4 @@ python-binding = ["pyo3"]
[lib]
name = "vtracer"
crate-type = ["cdylib"]
crate-type = ["rlib", "cdylib"]
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2022 Tsang Hao Fung
Copyright (c) 2023 Tsang Hao Fung
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
+8 -237
View File
@@ -1,12 +1,10 @@
use std::str::FromStr;
use std::path::PathBuf;
use clap::{Arg, App};
use visioncortex::PathSimplifyMode;
pub enum Preset {
Bw,
Poster,
Photo
Photo,
}
pub enum ColorMode {
@@ -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,
@@ -67,7 +59,7 @@ impl Default for Config {
length_threshold: 4.0,
splice_threshold: 45,
max_iterations: 10,
path_precision: Some(8),
path_precision: Some(2),
}
}
}
@@ -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::<usize>().is_ok() { // is numeric
let value = value.trim().parse::<usize>().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::<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: {}.", 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: {}.", 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: {}.", 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: {}.", 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: {}.", value);
}
}
if let Some(value) = matches.value_of("path_precision") {
if value.trim().parse::<u32>().is_ok() { // is numeric
let value = value.trim().parse::<u32>().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,
@@ -338,11 +115,9 @@ impl Config {
length_threshold: 4.0,
max_iterations: 10,
splice_threshold: 45,
path_precision: Some(8),
path_precision: Some(2),
},
Preset::Poster => Self {
input_path,
output_path,
color_mode: ColorMode::Color,
hierarchical: Hierarchical::Stacked,
filter_speckle: 4,
@@ -353,11 +128,9 @@ impl Config {
length_threshold: 4.0,
max_iterations: 10,
splice_threshold: 45,
path_precision: Some(8),
path_precision: Some(2),
},
Preset::Photo => Self {
input_path,
output_path,
color_mode: ColorMode::Color,
hierarchical: Hierarchical::Stacked,
filter_speckle: 10,
@@ -368,15 +141,13 @@ impl Config {
length_threshold: 4.0,
max_iterations: 10,
splice_threshold: 45,
path_precision: Some(8),
}
path_precision: Some(2),
},
}
}
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
}
}
+92 -83
View File
@@ -1,32 +1,43 @@
use std::path::PathBuf;
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
/// 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<SvgFile, String> {
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 {
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;
}
}
}
@@ -35,27 +46,24 @@ fn color_exists_in_image(img: &ColorImage, color: Color) -> bool {
fn find_unused_color_in_image(img: &ColorImage) -> Result<Color, String> {
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 {
@@ -66,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 {
@@ -81,16 +95,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<SvgFile, String> {
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)?;
@@ -107,23 +114,26 @@ fn color_image_to_svg(config: ConverterConfig) -> Result<(), String> {
Color::default()
};
let runner = Runner::new(RunnerConfig {
diagonal: config.layer_difference == 0,
hierarchical: HIERARCHICAL_MAX,
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,
key_color,
keying_action: if matches!(config.hierarchical, Hierarchical::Cutout) {
KeyingAction::Keep
} else {
KeyingAction::Discard
let runner = Runner::new(
RunnerConfig {
diagonal: config.layer_difference == 0,
hierarchical: HIERARCHICAL_MAX,
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,
key_color,
keying_action: if matches!(config.hierarchical, Hierarchical::Cutout) {
KeyingAction::Keep
} else {
KeyingAction::Discard
},
},
}, img);
img,
);
let mut clusters = runner.run();
@@ -132,21 +142,24 @@ fn color_image_to_svg(config: ConverterConfig) -> Result<(), String> {
Hierarchical::Cutout => {
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,26 +174,18 @@ fn color_image_to_svg(config: ConverterConfig) -> Result<(), String> {
config.corner_threshold,
config.length_threshold,
config.max_iterations,
config.splice_threshold
config.splice_threshold,
);
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<SvgFile, String> {
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 +204,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<ColorImage, String> {
let img = image::open(input_path);
let img = match img {
Ok(file) => file.to_rgba8(),
@@ -210,12 +215,16 @@ 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};
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 +234,4 @@ fn write_svg(svg: SvgFile, output_path: PathBuf) -> Result<(), String> {
write!(&mut out_file, "{}", svg).expect("failed to write file.");
Ok(())
}
}
+4 -3
View File
@@ -10,12 +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 python::*;
pub use svg::*;
pub use visioncortex::ColorImage;
+270 -4
View File
@@ -2,15 +2,281 @@ mod config;
mod converter;
mod svg;
use clap::{App, Arg};
use config::{ColorMode, Config, Hierarchical, Preset};
use std::path::PathBuf;
use std::str::FromStr;
use visioncortex::PathSimplifyMode;
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::<usize>().is_ok() {
// is numeric
let value = value.trim().parse::<usize>().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::<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: {}.",
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: {}.", 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: {}.", 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: {}.", 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: {}.", value);
}
}
if let Some(value) = matches.value_of("path_precision") {
if value.trim().parse::<u32>().is_ok() {
// is numeric
let value = value.trim().parse::<u32>().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.");
},
}
Err(msg) => {
panic!("Conversion failed with error message: {}", msg);
}
}
}
}
+1 -3
View File
@@ -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(())
}
+19 -12
View File
@@ -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#"<?xml version="1.0" encoding="UTF-8"?>"#)?;
writeln!(f, r#"<!-- Generator: visioncortex VTracer {} -->"#, env!("CARGO_PKG_VERSION"))?;
writeln!(f,
writeln!(
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="{}">"#,
self.width, self.height
)?;
for path in &self.paths {
path.fmt_with_precision(f, self.path_precision)?;
};
}
writeln!(f, "</svg>")
}
@@ -56,11 +58,16 @@ impl fmt::Display for SvgPath {
impl SvgPath {
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!(
f, "<path d=\"{}\" fill=\"{}\" transform=\"translate({},{})\"/>",
string, self.color.to_hex_string(),
offset.x, offset.y
f,
"<path d=\"{}\" fill=\"{}\" transform=\"translate({},{})\"/>",
string,
self.color.to_hex_string(),
offset.x,
offset.y
)
}
}
}