23 Commits
Author SHA1 Message Date
Chris Tsang 82284ab470 0.5.0 2022-10-09 18:01:35 +08:00
Chris Tsang ead104f0ce Tweak should_key_image 2022-10-09 17:21:00 +08:00
zachwolfe a7219fd370 Alpha channel handling in CLI (#23)
* Support transparent color images

* Remove unnecessary conditional

* Add temporary git url to visioncortex dependency

* Use fastrand instead of rand

* Reduce the number of random iterations when keying

* Add heuristic to avoid expensive calculations for non-transparent input

* Add three additional special keying colours

* Add transparency check to some inner pixels
2022-10-09 15:41:43 +08:00
Oskar Skuteli 2448dbb3ba fix docs typo (#25) 2022-09-25 15:06:19 +08:00
Chris Tsang c9d9073b17 Merge pull request #16 from wolfgangmeyers/fix-converter-warning
Fix deprecation warning from converter.rs
2022-02-11 21:45:33 +08:00
Wolfgang Meyers 47091b0bf5 Fix deprecation warning from converter.ts 2022-02-10 18:13:51 -08:00
Chris Tsang 8439cc995d Refactor path_precision 2021-07-27 21:01:20 +08:00
Chris Tsang 0b292ad35f Readme 2021-07-24 16:48:47 +08:00
Chris Tsang ebb17ac22b vtracer-webapp 2021-07-24 16:37:41 +08:00
Chris Tsang 227850dd83 Release 2021-07-24 15:58:51 +08:00
Chris Tsang e0dac19c31 Article link 2021-07-24 15:42:35 +08:00
Chris Tsang d18d4f8b81 Readme 2021-07-24 00:00:59 +08:00
Chris Tsang 60a4ce579f Release 2021-07-23 23:35:54 +08:00
Chris Tsang c1c09d964c Readme 2021-07-23 23:35:54 +08:00
Chris Tsang c2a5626afa 0.4.0 2021-07-23 23:35:54 +08:00
Bobby Ng d0593e716a SVG path string numeric precision 2021-07-23 23:35:45 +08:00
Chris Tsang 9ce7df176a Update .gitattributes 2021-07-23 18:29:12 +08:00
Chris Tsang 5928c0a7f5 Update screenshots 2021-03-01 21:13:24 +08:00
Chris Tsang b2c95a50cd Update README.md 2021-03-01 21:13:24 +08:00
Chris Tsang bcd3ffb40e Update COPYRIGHT 2021-02-07 14:22:26 +08:00
Chris Tsang 20da3efd3c Create .gitattributes 2021-01-25 00:57:44 +08:00
Chris Tsang 0c27d1f06a Update README.md 2021-01-25 00:53:01 +08:00
Chris Tsang 0c7c7fc808 Demo fixup 2021-01-24 22:12:01 +08:00
27 changed files with 311 additions and 63 deletions
+1
View File
@@ -0,0 +1 @@
docs/** linguist-generated
+26
View File
@@ -0,0 +1,26 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## 0.5.0 - 2022-10-09
* Handle transparent png images (cli) (#23)
## 0.4.0 - 2021-07-23
* SVG path string numeric precision
## 0.3.0 - 2021-01-24
* Added cutout mode
## 0.2.0 - 2020-11-15
* Use relative & closed paths
## 0.1.0 - 2020-10-31
* Initial release
+25
View File
@@ -0,0 +1,25 @@
Copyright (c) 2022 Tsang Hao Fung
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
+39 -34
View File
@@ -8,7 +8,7 @@
</p> </p>
<h3> <h3>
<a href="//www.visioncortex.org/vtracer-docs">Document</a> <a href="//www.visioncortex.org/vtracer-docs">Article</a>
<span> | </span> <span> | </span>
<a href="//www.visioncortex.org/vtracer/">Demo</a> <a href="//www.visioncortex.org/vtracer/">Demo</a>
<span> | </span> <span> | </span>
@@ -22,48 +22,53 @@
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. 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]() 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.
Comparing to Adobe Illustrator's Live Trace, VTracer's output is much more compact (less shapes) as we adopt a stacking strategy and avoid producing shapes with holes. 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.
VTracer is originally designed for processing high resolution scans of historic blueprints up to gigapixels. By setting both `filter_speckle` and `layer_difference` to `0`, VTracer can also handle low resolution pixel art, effectively achieving `image-rendering: pixelated` for retro game artworks. VTracer is originally designed for processing high resolution scans of historic blueprints up to gigapixels. At the same time, VTracer can also handle low resolution pixel art, simulating `image-rendering: pixelated` for retro game artworks.
A technical description of the algorithm is on [visioncortex.org/vtracer-docs](//www.visioncortex.org/vtracer-docs). A technical description of the algorithm is on [visioncortex.org/vtracer-docs](//www.visioncortex.org/vtracer-docs).
## Web App ## Web App
VTracer and its [core library](//github.com/visioncortex/visioncortex) is implemented in [Rust](//www.rust-lang.org/). It provides us a solid foundation to develop robust and efficient algorithms and easily bring it to interactive applications. The webapp is a perfect showcase of the capability of the Rust + HTML5 platform. VTracer and its [core library](//github.com/visioncortex/visioncortex) is implemented in [Rust](//www.rust-lang.org/). It provides us a solid foundation to develop robust and efficient algorithms and easily bring it to interactive applications. The webapp is a perfect showcase of the capability of the Rust + wasm platform.
![screenshot](docs/images/screenshot-01.png) ![screenshot](docs/images/screenshot-01.png)
![screenshot](docs/images/screenshot-02.png) ![screenshot](docs/images/screenshot-02.png)
## Command Line ## Cmd App
```
visioncortex VTracer ```sh
A cmd app to convert images into vector graphics. visioncortex VTracer 0.4.0
A cmd app to convert images into vector graphics.
USAGE:
vtracer [OPTIONS] --input <input> --output <output> USAGE:
vtracer [OPTIONS] --input <input> --output <output>
FLAGS:
-h, --help Prints help information FLAGS:
-V, --version Prints version information -h, --help Prints help information
-V, --version Prints version information
OPTIONS:
--colormode <color_mode> True color image `color` (default) or Binary image `bw` OPTIONS:
-p, --color_precision <color_precision> Number of significant bits to use in an RGB channel --colormode <color_mode> True color image `color` (default) or Binary image `bw`
-c, --corner_threshold <corner_threshold> Minimum momentary angle (degree) to be considered a corner -p, --color_precision <color_precision> Number of significant bits to use in an RGB channel
-f, --filter_speckle <filter_speckle> Discard patches smaller than X px in size -c, --corner_threshold <corner_threshold> Minimum momentary angle (degree) to be considered a corner
-g, --gradient_step <gradient_step> Color difference between gradient layers -f, --filter_speckle <filter_speckle> Discard patches smaller than X px in size
-i, --input <input> Path to input raster image -g, --gradient_step <gradient_step> Color difference between gradient layers
-m, --mode <mode> Curver fitting mode `pixel`, `polygon`, `spline` --hierarchical <hierarchical>
-o, --output <output> Path to output vector graphics Hierarchical clustering `stacked` (default) or non-stacked `cutout`. Only applies to color mode.
--preset <preset> Use one of the preset configs `bw`, `poster`, `photo`
-l, --segment_length <segment_length> -i, --input <input> Path to input raster image
Perform iterative subdivide smooth until all segments are shorter than this length -m, --mode <mode> Curver fitting mode `pixel`, `polygon`, `spline`
-o, --output <output> Path to output vector graphics
-s, --splice_threshold <splice_threshold> Minimum angle displacement (degree) to splice a spline --path_precision <path_precision> Number of decimal places to use in path string
--preset <preset> Use one of the preset configs `bw`, `poster`, `photo`
-l, --segment_length <segment_length>
Perform iterative subdivide smooth until all segments are shorter than this length
-s, --splice_threshold <splice_threshold> Minimum angle displacement (degree) to splice a spline
``` ```
### Usage ### Usage
@@ -73,9 +78,9 @@ OPTIONS:
## Library ## Library
The library can be found on [crates.io/vtracer](//crates.io/crates/vtracer). The library can be found on [crates.io/vtracer](//crates.io/crates/vtracer) and [crates.io/vtracer-webapp](//crates.io/crates/vtracer-webapp).
### Install ## Install
``` ```
vtracer = "*" cargo install vtracer
``` ```
+6
View File
@@ -1,3 +1,9 @@
Version 0.4.0 (2021-07-23)
==========================
- SVG path string numeric precision
Version 0.3.0 (2021-01-24) Version 0.3.0 (2021-01-24)
========================== ==========================
+3 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "vtracer" name = "vtracer"
version = "0.3.0" version = "0.5.0"
authors = ["Chris Tsang <tyt2y7@gmail.com>"] authors = ["Chris Tsang <tyt2y7@gmail.com>"]
edition = "2018" edition = "2018"
description = "A cmd app to convert images into vector graphics." description = "A cmd app to convert images into vector graphics."
@@ -13,4 +13,5 @@ keywords = ["svg", "computer-graphics"]
[dependencies] [dependencies]
clap = "2.33.3" clap = "2.33.3"
image = "0.23.10" image = "0.23.10"
visioncortex = "0.4.0" visioncortex = { version = "0.8.0" }
fastrand = "1.8"
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2020 Tsang Hao Fung Copyright (c) 2022 Tsang Hao Fung
Permission is hereby granted, free of charge, to any Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated person obtaining a copy of this software and associated
+28 -7
View File
@@ -33,6 +33,7 @@ pub struct Config {
pub length_threshold: f64, pub length_threshold: f64,
pub max_iterations: usize, pub max_iterations: usize,
pub splice_threshold: i32, pub splice_threshold: i32,
pub path_precision: Option<u32>,
} }
pub(crate) struct ConverterConfig { pub(crate) struct ConverterConfig {
@@ -48,6 +49,7 @@ pub(crate) struct ConverterConfig {
pub length_threshold: f64, pub length_threshold: f64,
pub max_iterations: usize, pub max_iterations: usize,
pub splice_threshold: f64, pub splice_threshold: f64,
pub path_precision: Option<u32>,
} }
impl Default for Config { impl Default for Config {
@@ -65,6 +67,7 @@ impl Default for Config {
length_threshold: 4.0, length_threshold: 4.0,
splice_threshold: 45, splice_threshold: 45,
max_iterations: 10, max_iterations: 10,
path_precision: Some(8),
} }
} }
} }
@@ -194,6 +197,11 @@ impl Config {
.takes_value(true) .takes_value(true)
.help("Curver fitting mode `pixel`, `polygon`, `spline`")); .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 // Extract matches
let matches = app.get_matches(); let matches = app.get_matches();
@@ -225,7 +233,7 @@ impl Config {
} else if value == "spline" { } else if value == "spline" {
"spline" "spline"
} else { } else {
panic!("Parser Error: Curve fitting mode is invalid with value {}", value); panic!("Parser Error: Curve fitting mode is invalid: {}", value);
}); });
} }
@@ -237,7 +245,7 @@ impl Config {
} }
config.filter_speckle = value; config.filter_speckle = value;
} else { } else {
panic!("Parser Error: Filter speckle is not a positive integer with value {}.", value); panic!("Parser Error: Filter speckle is not a positive integer: {}.", value);
} }
} }
@@ -249,7 +257,7 @@ impl Config {
} }
config.color_precision = value; config.color_precision = value;
} else { } else {
panic!("Parser Error: Color precision is not an integer with value {}.", value); panic!("Parser Error: Color precision is not an integer: {}.", value);
} }
} }
@@ -261,7 +269,7 @@ impl Config {
} }
config.layer_difference = value; config.layer_difference = value;
} else { } else {
panic!("Parser Error: Gradient step is not an integer with value {}.", value); panic!("Parser Error: Gradient step is not an integer: {}.", value);
} }
} }
@@ -273,7 +281,7 @@ impl Config {
} }
config.corner_threshold = value config.corner_threshold = value
} else { } else {
panic!("Parser Error: Corner threshold is not numeric with value {}.", value); panic!("Parser Error: Corner threshold is not numeric: {}.", value);
} }
} }
@@ -285,7 +293,7 @@ impl Config {
} }
config.length_threshold = value; config.length_threshold = value;
} else { } else {
panic!("Parser Error: Segment length is not numeric with value {}.", value); panic!("Parser Error: Segment length is not numeric: {}.", value);
} }
} }
@@ -297,7 +305,16 @@ impl Config {
} }
config.splice_threshold = value; config.splice_threshold = value;
} else { } else {
panic!("Parser Error: Segment length is not numeric with value {}.", value); 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);
} }
} }
@@ -321,6 +338,7 @@ impl Config {
length_threshold: 4.0, length_threshold: 4.0,
max_iterations: 10, max_iterations: 10,
splice_threshold: 45, splice_threshold: 45,
path_precision: Some(8),
}, },
Preset::Poster => Self { Preset::Poster => Self {
input_path, input_path,
@@ -335,6 +353,7 @@ impl Config {
length_threshold: 4.0, length_threshold: 4.0,
max_iterations: 10, max_iterations: 10,
splice_threshold: 45, splice_threshold: 45,
path_precision: Some(8),
}, },
Preset::Photo => Self { Preset::Photo => Self {
input_path, input_path,
@@ -349,6 +368,7 @@ impl Config {
length_threshold: 4.0, length_threshold: 4.0,
max_iterations: 10, max_iterations: 10,
splice_threshold: 45, splice_threshold: 45,
path_precision: Some(8),
} }
} }
} }
@@ -367,6 +387,7 @@ impl Config {
length_threshold: self.length_threshold, length_threshold: self.length_threshold,
max_iterations: self.max_iterations, max_iterations: self.max_iterations,
splice_threshold: deg2rad(self.splice_threshold), splice_threshold: deg2rad(self.splice_threshold),
path_precision: self.path_precision,
} }
} }
} }
+94 -5
View File
@@ -1,11 +1,17 @@
use std::path::PathBuf; use std::path::PathBuf;
use std::{fs::File, io::Write}; use std::{fs::File, io::Write};
use fastrand::Rng;
use visioncortex::{Color, ColorImage, ColorName}; use visioncortex::{Color, ColorImage, ColorName};
use visioncortex::color_clusters::{Runner, RunnerConfig, HIERARCHICAL_MAX}; use visioncortex::color_clusters::{Runner, RunnerConfig, KeyingAction, HIERARCHICAL_MAX};
use super::config::{Config, ColorMode, Hierarchical, ConverterConfig}; use super::config::{Config, ColorMode, Hierarchical, ConverterConfig};
use super::svg::SvgFile; use super::svg::SvgFile;
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 /// Convert an image file into svg file
pub fn convert_image_to_svg(config: Config) -> Result<(), String> { pub fn convert_image_to_svg(config: Config) -> Result<(), String> {
let config = config.into_converter_config(); let config = config.into_converter_config();
@@ -15,8 +21,68 @@ pub fn convert_image_to_svg(config: Config) -> Result<(), String> {
} }
} }
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
}
}
}
false
}
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, 255, 0),
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(..),
)
});
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"))
}
fn should_key_image(img: &ColorImage) -> bool {
if img.width == 0 || img.height == 0 {
return false;
}
// 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];
for y in y_positions {
for x in 0..img.width {
if img.get_pixel(x, y).a == 0 {
num_transparent_boundary_pixels += 1;
}
if num_transparent_boundary_pixels >= threshold {
return true;
}
}
}
false
}
fn color_image_to_svg(config: ConverterConfig) -> Result<(), String> { fn color_image_to_svg(config: ConverterConfig) -> Result<(), String> {
let (img, width, height); let (mut img, width, height);
match read_image(config.input_path) { match read_image(config.input_path) {
Ok(values) => { Ok(values) => {
img = values.0; img = values.0;
@@ -26,6 +92,21 @@ fn color_image_to_svg(config: ConverterConfig) -> Result<(), String> {
Err(msg) => return Err(msg), Err(msg) => return Err(msg),
} }
let key_color = if should_key_image(&img) {
let key_color = find_unused_color_in_image(&img)?;
for y in 0..height {
for x in 0..width {
if img.get_pixel(x, y).a == 0 {
img.set_pixel(x, y, &key_color);
}
}
}
key_color
} else {
// The default color is all zeroes, which is treated by visioncortex as a special value meaning no keying will be applied.
Color::default()
};
let runner = Runner::new(RunnerConfig { let runner = Runner::new(RunnerConfig {
diagonal: config.layer_difference == 0, diagonal: config.layer_difference == 0,
hierarchical: HIERARCHICAL_MAX, hierarchical: HIERARCHICAL_MAX,
@@ -36,6 +117,12 @@ fn color_image_to_svg(config: ConverterConfig) -> Result<(), String> {
is_same_color_b: 1, is_same_color_b: 1,
deepen_diff: config.layer_difference, deepen_diff: config.layer_difference,
hollow_neighbours: 1, 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(); let mut clusters = runner.run();
@@ -55,6 +142,8 @@ fn color_image_to_svg(config: ConverterConfig) -> Result<(), String> {
is_same_color_b: 1, is_same_color_b: 1,
deepen_diff: 0, deepen_diff: 0,
hollow_neighbours: 0, hollow_neighbours: 0,
key_color,
keying_action: KeyingAction::Discard,
}, image); }, image);
clusters = runner.run(); clusters = runner.run();
}, },
@@ -62,7 +151,7 @@ fn color_image_to_svg(config: ConverterConfig) -> Result<(), String> {
let view = clusters.view(); let view = clusters.view();
let mut svg = SvgFile::new(width, height); let mut svg = SvgFile::new(width, height, config.path_precision);
for &cluster_index in view.clusters_output.iter().rev() { for &cluster_index in view.clusters_output.iter().rev() {
let cluster = view.get_cluster(cluster_index); let cluster = view.get_cluster(cluster_index);
let paths = cluster.to_compound_path( let paths = cluster.to_compound_path(
@@ -95,7 +184,7 @@ fn binary_image_to_svg(config: ConverterConfig) -> Result<(), String> {
let clusters = img.to_clusters(false); let clusters = img.to_clusters(false);
let mut svg = SvgFile::new(width, height); let mut svg = SvgFile::new(width, height, config.path_precision);
for i in 0..clusters.len() { for i in 0..clusters.len() {
let cluster = clusters.get_cluster(i); let cluster = clusters.get_cluster(i);
if cluster.size() >= config.filter_speckle_area { if cluster.size() >= config.filter_speckle_area {
@@ -116,7 +205,7 @@ fn binary_image_to_svg(config: ConverterConfig) -> Result<(), String> {
fn read_image(input_path: PathBuf) -> Result<(ColorImage, usize, usize), String> { fn read_image(input_path: PathBuf) -> Result<(ColorImage, usize, usize), String> {
let img = image::open(input_path); let img = image::open(input_path);
let img = match img { let img = match img {
Ok(file) => file.to_rgba(), Ok(file) => file.to_rgba8(),
Err(_) => return Err(String::from("No image file found at specified input path")), Err(_) => return Err(String::from("No image file found at specified input path")),
}; };
+11 -3
View File
@@ -5,6 +5,7 @@ pub struct SvgFile {
pub paths: Vec<SvgPath>, pub paths: Vec<SvgPath>,
pub width: usize, pub width: usize,
pub height: usize, pub height: usize,
pub path_precision: Option<u32>,
} }
pub struct SvgPath { pub struct SvgPath {
@@ -13,11 +14,12 @@ pub struct SvgPath {
} }
impl SvgFile { impl SvgFile {
pub fn new(width: usize, height: usize) -> Self { pub fn new(width: usize, height: usize, path_precision: Option<u32>) -> Self {
SvgFile { SvgFile {
paths: vec![], paths: vec![],
width, width,
height, height,
path_precision,
} }
} }
@@ -38,7 +40,7 @@ impl fmt::Display for SvgFile {
)?; )?;
for path in &self.paths { for path in &self.paths {
path.fmt(f)?; path.fmt_with_precision(f, self.path_precision)?;
}; };
writeln!(f, "</svg>") writeln!(f, "</svg>")
@@ -47,7 +49,13 @@ impl fmt::Display for SvgFile {
impl fmt::Display for SvgPath { impl fmt::Display for SvgPath {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let (string, offset) = self.path.to_svg_string(true, PointF64::default()); self.fmt_with_precision(f, None)
}
}
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);
writeln!( writeln!(
f, "<path d=\"{}\" fill=\"{}\" transform=\"translate({},{})\"/>", f, "<path d=\"{}\" fill=\"{}\" transform=\"translate({},{})\"/>",
string, self.color.to_hex_string(), string, self.color.to_hex_string(),
+13 -2
View File
File diff suppressed because one or more lines are too long
Binary file not shown.
+1 -1
View File
@@ -1,3 +1,3 @@
Copyright (c) 2020 Tsang Hao Fung Copyright (c) 2020 Tsang Hao Fung
All Rights Reserved The content in the /docs directory is for GitHub pages, and is not covered under open source licenses.
Binary file not shown.
Generated Vendored
+1 -1
View File
@@ -258,7 +258,7 @@
/******/ promises.push(installedWasmModuleData); /******/ promises.push(installedWasmModuleData);
/******/ else { /******/ else {
/******/ var importObject = wasmImportObjects[wasmModuleId](); /******/ var importObject = wasmImportObjects[wasmModuleId]();
/******/ var req = fetch(__webpack_require__.p + "" + {"../pkg/vtracer_webapp_bg.wasm":"bad37dda49ff45dee696"}[wasmModuleId] + ".module.wasm"); /******/ var req = fetch(__webpack_require__.p + "" + {"../pkg/vtracer_webapp_bg.wasm":"589cb57662b50620abca"}[wasmModuleId] + ".module.wasm");
/******/ var promise; /******/ var promise;
/******/ if(importObject instanceof Promise && typeof WebAssembly.compileStreaming === 'function') { /******/ if(importObject instanceof Promise && typeof WebAssembly.compileStreaming === 'function') {
/******/ promise = Promise.all([WebAssembly.compileStreaming(req), importObject]).then(function(items) { /******/ promise = Promise.all([WebAssembly.compileStreaming(req), importObject]).then(function(items) {
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 154 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

+15
View File
@@ -142,6 +142,8 @@
/ VTracer / VTracer
</div> </div>
<div class="uk-navbar-right uk-padding-small"> <div class="uk-navbar-right uk-padding-small">
<a class="uk-button uk-button-default" href="//www.visioncortex.org/vtracer-docs">Article</a>
&nbsp;&nbsp;
<a class="uk-button uk-button-default" href="//github.com/visioncortex/vtracer">GitHub</a> <a class="uk-button uk-button-default" href="//github.com/visioncortex/vtracer">GitHub</a>
&nbsp;&nbsp; &nbsp;&nbsp;
<a id="export" class="uk-button uk-button-primary">Download as SVG</a> <a id="export" class="uk-button uk-button-primary">Download as SVG</a>
@@ -285,6 +287,19 @@
<input id="splice" class="uk-range" type="range" min="0" max="180" step="1" value="45"> <input id="splice" class="uk-range" type="range" min="0" max="180" step="1" value="45">
</div> </div>
<div style="display: none">
<div class="spline-options uk-width-1-1 uk-flex uk-flex-right">
<div uk-tooltip="pos: left; title: Number of decimal places used in svg path string">
Path Precision <span class="uk-text-meta">(More digits)</span>
</div>
</div>
<div id="pathprecisionvalue" class="spline-options uk-width-1-6">
8
</div>
<div class="spline-options uk-width-5-6">
<input id="pathprecision" class="uk-range" type="range" min="0" max="16" step="1" value="8">
</div>
</div>
</div> </div>
</div> </div>
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "vtracer-webapp" name = "vtracer-webapp"
version = "0.1.0" version = "0.4.0"
authors = ["Chris Tsang <tyt2y7@gmail.com>"] authors = ["Chris Tsang <tyt2y7@gmail.com>"]
edition = "2018" edition = "2018"
description = "A web app to convert images into vector graphics." description = "A web app to convert images into vector graphics."
@@ -22,7 +22,7 @@ console_log = { version = "0.2", features = ["color"] }
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
visioncortex = "0.4.0" visioncortex = "0.6.0"
# The `console_error_panic_hook` crate provides better debugging of panics by # The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires # logging them with `console.error`. This is great for development, but requires
+4
View File
@@ -1,3 +1,7 @@
<div align="center">
<img src="https://github.com/visioncortex/vtracer/raw/master/docs/images/visioncortex-banner.png">
</div>
# visioncortex VTracer # visioncortex VTracer
A web app to convert raster images into vector graphics. A web app to convert raster images into vector graphics.
+13
View File
@@ -185,6 +185,19 @@
<input id="splice" type="range" min="0" max="180" step="1" value="45"> <input id="splice" type="range" min="0" max="180" step="1" value="45">
</div> </div>
<div style="display: none">
<div class="spline-options">
<div title="Number of decimal places used in svg path string">
Path Precision <span>(More digits)</span>
</div>
</div>
<div id="pathprecisionvalue" class="spline-options">
8
</div>
<div class="spline-options">
<input id="pathprecision" class="uk-range" type="range" min="0" max="16" step="1" value="8">
</div>
</div>
</div> </div>
</div> </div>
<script src="./bootstrap.js"></script> <script src="./bootstrap.js"></script>
+19 -2
View File
@@ -52,6 +52,7 @@ var presetConfigs = [
clustering_hierarchical: 'stacked', clustering_hierarchical: 'stacked',
filter_speckle: 4, filter_speckle: 4,
color_precision: 6, color_precision: 6,
path_precision: 8,
layer_difference: 16, layer_difference: 16,
mode: 'spline', mode: 'spline',
corner_threshold: 60, corner_threshold: 60,
@@ -66,6 +67,7 @@ var presetConfigs = [
clustering_hierarchical: 'stacked', clustering_hierarchical: 'stacked',
filter_speckle: 4, filter_speckle: 4,
color_precision: 8, color_precision: 8,
path_precision: 8,
layer_difference: 25, layer_difference: 25,
mode: 'spline', mode: 'spline',
corner_threshold: 60, corner_threshold: 60,
@@ -77,9 +79,10 @@ var presetConfigs = [
{ {
src: 'assets/samples/Gum Tree Vector.jpg', src: 'assets/samples/Gum Tree Vector.jpg',
clustering_mode: 'color', clustering_mode: 'color',
clustering_hierarchical: 'cutout', clustering_hierarchical: 'stacked',
filter_speckle: 4, filter_speckle: 4,
color_precision: 8, color_precision: 8,
path_precision: 8,
layer_difference: 28, layer_difference: 28,
mode: 'spline', mode: 'spline',
corner_threshold: 60, corner_threshold: 60,
@@ -94,6 +97,7 @@ var presetConfigs = [
clustering_hierarchical: 'stacked', clustering_hierarchical: 'stacked',
filter_speckle: 8, filter_speckle: 8,
color_precision: 7, color_precision: 7,
path_precision: 8,
layer_difference: 64, layer_difference: 64,
mode: 'spline', mode: 'spline',
corner_threshold: 60, corner_threshold: 60,
@@ -108,6 +112,7 @@ var presetConfigs = [
clustering_hierarchical: 'stacked', clustering_hierarchical: 'stacked',
filter_speckle: 10, filter_speckle: 10,
color_precision: 8, color_precision: 8,
path_precision: 8,
layer_difference: 48, layer_difference: 48,
mode: 'spline', mode: 'spline',
corner_threshold: 180, corner_threshold: 180,
@@ -122,6 +127,7 @@ var presetConfigs = [
clustering_hierarchical: 'stacked', clustering_hierarchical: 'stacked',
filter_speckle: 0, filter_speckle: 0,
color_precision: 8, color_precision: 8,
path_precision: 8,
layer_difference: 0, layer_difference: 0,
mode: 'none', mode: 'none',
corner_threshold: 180, corner_threshold: 180,
@@ -178,6 +184,9 @@ function loadConfig(config) {
document.getElementById('layerdifferencevalue').innerHTML = globallayerdifference; document.getElementById('layerdifferencevalue').innerHTML = globallayerdifference;
document.getElementById('layerdifference').value = globallayerdifference; document.getElementById('layerdifference').value = globallayerdifference;
globalpathprecision = config.path_precision;
document.getElementById('pathprecisionvalue').innerHTML = globalpathprecision;
document.getElementById('pathprecision').value = globalpathprecision;
} }
// Choose template from gallery // Choose template from gallery
@@ -244,7 +253,8 @@ var globalcorner = parseInt(document.getElementById('corner').value),
globalsplice = parseInt(document.getElementById('splice').value), globalsplice = parseInt(document.getElementById('splice').value),
globalfilterspeckle = parseInt(document.getElementById('filterspeckle').value), globalfilterspeckle = parseInt(document.getElementById('filterspeckle').value),
globalcolorprecision = parseInt(document.getElementById('colorprecision').value), globalcolorprecision = parseInt(document.getElementById('colorprecision').value),
globallayerdifference = parseInt(document.getElementById('layerdifference').value); globallayerdifference = parseInt(document.getElementById('layerdifference').value),
globalpathprecision = parseInt(document.getElementById('pathprecision').value);
// Load past inputs from localStorage // Load past inputs from localStorage
/* /*
@@ -327,6 +337,12 @@ document.getElementById('splice').addEventListener('change', function (e) {
restart(); restart();
}); });
document.getElementById('pathprecision').addEventListener('change', function (e) {
globalpathprecision = parseInt(this.value);
document.getElementById('pathprecisionvalue').innerHTML = this.value;
restart();
});
// Save inputs before unloading // Save inputs before unloading
/* /*
window.addEventListener('beforeunload', function () { window.addEventListener('beforeunload', function () {
@@ -404,6 +420,7 @@ function restart() {
'filter_speckle': globalfilterspeckle*globalfilterspeckle, 'filter_speckle': globalfilterspeckle*globalfilterspeckle,
'color_precision': 8-globalcolorprecision, 'color_precision': 8-globalcolorprecision,
'layer_difference': globallayerdifference, 'layer_difference': globallayerdifference,
'path_precision': globalpathprecision,
}); });
if (runner) { if (runner) {
runner.stop(); runner.stop();
+2
View File
@@ -17,6 +17,7 @@ pub struct BinaryImageConverterParams {
pub max_iterations: usize, pub max_iterations: usize,
pub splice_threshold: f64, pub splice_threshold: f64,
pub filter_speckle: usize, pub filter_speckle: usize,
pub path_precision: u32,
} }
#[wasm_bindgen] #[wasm_bindgen]
@@ -80,6 +81,7 @@ impl BinaryImageConverter {
self.svg.prepend_path( self.svg.prepend_path(
&paths, &paths,
&color, &color,
Some(self.params.path_precision),
); );
} }
self.counter += 1; self.counter += 1;
+2
View File
@@ -21,6 +21,7 @@ pub struct ColorImageConverterParams {
pub filter_speckle: usize, pub filter_speckle: usize,
pub color_precision: i32, pub color_precision: i32,
pub layer_difference: i32, pub layer_difference: i32,
pub path_precision: u32,
} }
#[wasm_bindgen] #[wasm_bindgen]
@@ -137,6 +138,7 @@ impl ColorImageConverter {
self.svg.prepend_path( self.svg.prepend_path(
&paths, &paths,
&cluster.residue_color(), &cluster.residue_color(),
Some(self.params.path_precision),
); );
self.counter += 1; self.counter += 1;
false false
+3 -1
View File
@@ -7,7 +7,9 @@
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed // option. This file may not be copied, modified, or distributed
// except according to those terms. // except according to those terms.
#![doc(
html_logo_url = "https://github.com/visioncortex/vtracer/raw/master/docs/images/visioncortex icon.png"
)]
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
mod conversion; mod conversion;
+2 -2
View File
@@ -13,11 +13,11 @@ impl Svg {
Self { element } Self { element }
} }
pub fn prepend_path(&mut self, paths: &CompoundPath, color: &Color) { pub fn prepend_path(&mut self, paths: &CompoundPath, color: &Color, precision: Option<u32>) {
let path = document() let path = document()
.create_element_ns(Some("http://www.w3.org/2000/svg"), "path") .create_element_ns(Some("http://www.w3.org/2000/svg"), "path")
.unwrap(); .unwrap();
let (string, offset) = paths.to_svg_string(true, PointF64::default()); let (string, offset) = paths.to_svg_string(true, PointF64::default(), precision);
path.set_attribute("d", &string).unwrap(); path.set_attribute("d", &string).unwrap();
path.set_attribute( path.set_attribute(
"transform", "transform",