15 Commits

Author SHA1 Message Date
Chris Tsang 20187e0993 0.2.0 2020-11-15 16:27:37 +08:00
Chris Tsang 04d575dec6 UI tweaks 2020-11-15 16:03:26 +08:00
Chris Tsang c8f93acf04 Release 2020-11-09 12:17:55 +08:00
Chris Tsang a33a659b22 Fix memory leak 2020-11-09 12:17:48 +08:00
Chris Tsang 31c3f109a8 Release 2020-11-09 01:00:21 +08:00
Chris Tsang 5f18837b61 Relative & compound path 2020-11-09 00:38:44 +08:00
Chris Tsang 99cb79895b Move license files 2020-11-07 19:20:17 +08:00
Chris Tsang 08483eb7a8 Clarity tracking code 2020-11-07 19:15:30 +08:00
Chris Tsang b5f8753410 Update Readme.md 2020-11-01 17:37:11 +08:00
Chris Tsang 6b86baad75 Add download link 2020-11-01 14:59:40 +08:00
Chris Tsang ba0ab63a92 vtracer 0.1.1 2020-11-01 14:54:33 +08:00
Chris Tsang 3df69f6274 svg namespace 2020-11-01 14:52:44 +08:00
Chris Tsang 5efd479885 Docs 2020-10-31 19:18:45 +08:00
Chris Tsang 105bdfc7a3 Space 2020-10-31 19:16:41 +08:00
Chris Tsang d8b6d1cf33 Docs 2020-10-31 19:12:15 +08:00
20 changed files with 126 additions and 66 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[workspace] [workspace]
members = [ members = [
"cmdapp", "cmdapp",
"webapp", "webapp",
] ]
+29 -2
View File
@@ -1,6 +1,24 @@
![logo](docs/images/visioncortex-banner.png) <div align="center">
# visioncortex VTracer <img src="docs/images/visioncortex-banner.png">
<h1>visioncortex VTracer</h1>
<p>
<strong>Raster to Vector Graphics Converter built on top of visioncortex</strong>
</p>
<h3>
<a href="//www.visioncortex.org/vtracer-docs">Document</a>
<span> | </span>
<a href="//www.visioncortex.org/vtracer/">Demo</a>
<span> | </span>
<a href="//github.com/visioncortex/vtracer/releases/latest">Download</a>
</h3>
<sub>Built with 🦀 by <a href="//www.visioncortex.org/">The Vision Cortex Research Group</a></sub>
</div>
## Introduction
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.
@@ -50,3 +68,12 @@ OPTIONS:
``` ```
./vtracer --input input.jpg --output output.svg ./vtracer --input input.jpg --output output.svg
``` ```
## Library
The library can be found on [crates.io/vtracer](//crates.io/crates/vtracer).
### Install
```
vtracer = "0.1.0"
```
+16
View File
@@ -0,0 +1,16 @@
Version 0.2.0 (2020-10-31)
==========================
- Use relative & closed paths
Version 0.1.1 (2020-10-31)
==========================
- SVG namespace
Version 0.1.0 (2020-10-31)
==========================
- Initial release
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "vtracer" name = "vtracer"
version = "0.1.0" version = "0.2.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,4 @@ keywords = ["svg", "computer-graphics"]
[dependencies] [dependencies]
clap = "2.33.3" clap = "2.33.3"
image = "0.23.10" image = "0.23.10"
visioncortex = "0.2.0" visioncortex = "0.3.0"
+2 -1
View File
@@ -100,7 +100,8 @@ fn path_simplify_mode_from_str(s: &str) -> PathSimplifyMode {
impl Config { impl Config {
pub fn from_args() -> Self { pub fn from_args() -> Self {
let app = App::new("visioncortex VTracer").about("A cmd app to convert images into vector graphics."); 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") let app = app.arg(Arg::with_name("input")
.long("input") .long("input")
+6 -15
View File
@@ -42,7 +42,7 @@ fn color_image_to_svg(config: ConverterConfig) -> Result<(), String> {
let mut svg = SvgFile::new(width, height); let mut svg = SvgFile::new(width, height);
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 svg_path = cluster.to_svg_path( let paths = cluster.to_compound_path(
&view, &view,
false, false,
config.mode, config.mode,
@@ -51,18 +51,10 @@ fn color_image_to_svg(config: ConverterConfig) -> Result<(), String> {
config.max_iterations, config.max_iterations,
config.splice_threshold config.splice_threshold
); );
svg.add_path(svg_path, cluster.residue_color()); svg.add_path(paths, cluster.residue_color());
} }
let out_file = File::create(config.output_path); write_svg(svg, 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(())
} }
fn binary_image_to_svg(config: ConverterConfig) -> Result<(), String> { fn binary_image_to_svg(config: ConverterConfig) -> Result<(), String> {
@@ -84,15 +76,14 @@ fn binary_image_to_svg(config: ConverterConfig) -> Result<(), String> {
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 {
let svg_path = cluster.to_svg_path( let paths = cluster.to_compound_path(
config.mode, config.mode,
config.corner_threshold, config.corner_threshold,
config.length_threshold, config.length_threshold,
config.max_iterations, config.max_iterations,
config.splice_threshold, config.splice_threshold,
); );
let color = Color::color(&ColorName::Black); svg.add_path(paths, Color::color(&ColorName::Black));
svg.add_path(svg_path, color);
} }
} }
@@ -119,7 +110,7 @@ fn write_svg(svg: SvgFile, output_path: PathBuf) -> Result<(), String> {
Err(_) => return Err(String::from("Cannot create output file.")), Err(_) => return Err(String::from("Cannot create output file.")),
}; };
out_file.write_all(&svg.to_svg_file().as_bytes()).unwrap(); write!(&mut out_file, "{}", svg).expect("failed to write file.");
Ok(()) Ok(())
} }
+36 -22
View File
@@ -1,43 +1,57 @@
use visioncortex::Color; use std::fmt;
use visioncortex::{Color, CompoundPath, PointF64};
pub struct SvgPath {
path: String,
color: Color,
}
pub struct SvgFile { pub struct SvgFile {
patches: Vec<SvgPath>, pub paths: Vec<SvgPath>,
width: usize, pub width: usize,
height: usize, pub height: usize,
}
pub struct SvgPath {
pub path: CompoundPath,
pub color: Color,
} }
impl SvgFile { impl SvgFile {
pub fn new(width: usize, height: usize) -> Self { pub fn new(width: usize, height: usize) -> Self {
SvgFile { SvgFile {
patches: vec![], paths: vec![],
width, width,
height, height,
} }
} }
pub fn add_path(&mut self, path: String, color: Color) { pub fn add_path(&mut self, path: CompoundPath, color: Color) {
self.patches.push(SvgPath { self.paths.push(SvgPath {
path, path,
color color,
}) })
} }
}
pub fn to_svg_file(&self) -> String { impl fmt::Display for SvgFile {
let mut result: Vec<String> = vec![format!(r#"<?xml version="1.0" encoding="UTF-8"?> fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
<svg width="{}" height="{}"> writeln!(f, r#"<?xml version="1.0" encoding="UTF-8"?>"#)?;
"#, self.width, self.height)]; writeln!(f,
r#"<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="{}" height="{}">"#,
self.width, self.height
)?;
for patch in &self.patches { for path in &self.paths {
let color = patch.color; path.fmt(f)?;
result.push(format!("<path d=\"{}\" fill=\"#{:02x}{:02x}{:02x}\"/>\n", patch.path, color.r, color.g, color.b));
}; };
result.push(String::from("</svg>")); writeln!(f, "</svg>")
result.concat() }
}
impl fmt::Display for SvgPath {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let (string, offset) = self.path.to_svg_string(true, PointF64::default());
writeln!(
f, "<path d=\"{}\" fill=\"{}\" transform=\"translate({},{})\"/>",
string, self.color.to_hex_string(),
offset.x, offset.y
)
} }
} }
+1 -1
View File
File diff suppressed because one or more lines are too long
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
Copyright (c) 2020 Tsang Hao Fung
All Rights Reserved
Binary file not shown.
+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":"a62b3494f02004811a57"}[wasmModuleId] + ".module.wasm"); /******/ var req = fetch(__webpack_require__.p + "" + {"../pkg/vtracer_webapp_bg.wasm":"7ef94c5089d40af744f2"}[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) {
+11 -4
View File
@@ -11,6 +11,13 @@
<!-- UIkit JS --> <!-- UIkit JS -->
<script src="./uikit/js/uikit.min.js"></script> <script src="./uikit/js/uikit.min.js"></script>
<script src="./uikit/js/uikit-icons.min.js"></script> <script src="./uikit/js/uikit-icons.min.js"></script>
<script type="text/javascript">
(function(c,l,a,r,i,t,y){
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
})(window, document, "clarity", "script", "403biw7tra");
</script>
<style> <style>
html, body { html, body {
width: 100%; width: 100%;
@@ -158,7 +165,7 @@
<div id="drop" class="uk-padding uk-flex uk-flex-center"> <div id="drop" class="uk-padding uk-flex uk-flex-center">
<div id="canvas-container" class="uk-width-1-1" style="height: 480px;"> <div id="canvas-container" class="uk-width-1-1" style="height: 480px;">
<div id="droptext" class="uk-flex uk-flex-middle uk-flex-center"> <div id="droptext" class="uk-flex uk-flex-middle uk-flex-center">
<p>Drag an image here or <a href="#" id="imageSelect">Select file</a></p> <p>Drag an image here, Cmd-V to paste or <a href="#" id="imageSelect">Select file</a></p>
</div> </div>
<canvas id="frame"></canvas> <canvas id="frame"></canvas>
<svg id="svg" version="1.1" xmlns="http://www.w3.org/2000/svg"></svg> <svg id="svg" version="1.1" xmlns="http://www.w3.org/2000/svg"></svg>
@@ -195,7 +202,7 @@
4 4
</div> </div>
<div class="uk-width-5-6"> <div class="uk-width-5-6">
<input id="filterspeckle" class="uk-range" type="range" min="1" max="16" step="1" value="4"> <input id="filterspeckle" class="uk-range" type="range" min="1" max="128" step="1" value="4">
</div> </div>
<div class="clustering-color-options uk-width-1-1 uk-flex uk-flex-right"> <div class="clustering-color-options uk-width-1-1 uk-flex uk-flex-right">
@@ -219,7 +226,7 @@
16 16
</div> </div>
<div class="clustering-color-options uk-width-5-6"> <div class="clustering-color-options uk-width-5-6">
<input id="layerdifference" class="uk-range" type="range" min="0" max="255" step="1" value="16"> <input id="layerdifference" class="uk-range" type="range" min="0" max="128" step="1" value="16">
</div> </div>
<div class="uk-width-1-1 uk-flex uk-flex-right"> <div class="uk-width-1-1 uk-flex uk-flex-right">
@@ -262,7 +269,7 @@
<div class="spline-options uk-width-1-1 uk-flex uk-flex-right"> <div class="spline-options uk-width-1-1 uk-flex uk-flex-right">
<div uk-tooltip="pos: left; title: Minimum Angle Displacement (in degrees) to be considered a cutting point between curves"> <div uk-tooltip="pos: left; title: Minimum Angle Displacement (in degrees) to be considered a cutting point between curves">
Splice Threshold <span class="uk-text-meta">(More accurate)</span> Splice Threshold <span class="uk-text-meta">(Less accurate)</span>
</div> </div>
</div> </div>
<div id="splicevalue" class="spline-options uk-width-1-6"> <div id="splicevalue" class="spline-options uk-width-1-6">
+2 -1
View File
@@ -4,6 +4,7 @@ version = "0.1.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."
license = "MIT OR Apache-2.0"
homepage = "http://www.visioncortex.org/vtracer" homepage = "http://www.visioncortex.org/vtracer"
repository = "https://github.com/visioncortex/vtracer/" repository = "https://github.com/visioncortex/vtracer/"
categories = ["graphics"] categories = ["graphics"]
@@ -21,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.2.0" visioncortex = "0.3.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
View File
+1
View File
@@ -430,5 +430,6 @@ class ConverterRunner {
stop () { stop () {
this.stopped = true; this.stopped = true;
this.converter.free();
} }
} }
+4 -5
View File
@@ -1,5 +1,5 @@
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use visioncortex::{clusters::Clusters, Color, ColorName, PointI32, PathSimplifyMode}; use visioncortex::{clusters::Clusters, Color, ColorName, PathSimplifyMode};
use crate::{canvas::*}; use crate::{canvas::*};
use crate::svg::*; use crate::svg::*;
@@ -69,7 +69,7 @@ impl BinaryImageConverter {
self.canvas.log(&format!("tick {}", self.counter)); self.canvas.log(&format!("tick {}", self.counter));
let cluster = self.clusters.get_cluster(self.counter); let cluster = self.clusters.get_cluster(self.counter);
if cluster.size() >= self.params.filter_speckle { if cluster.size() >= self.params.filter_speckle {
let svg_path = cluster.to_svg_path( let paths = cluster.to_compound_path(
self.mode, self.mode,
self.params.corner_threshold, self.params.corner_threshold,
self.params.length_threshold, self.params.length_threshold,
@@ -77,9 +77,8 @@ impl BinaryImageConverter {
self.params.splice_threshold self.params.splice_threshold
); );
let color = Color::color(&ColorName::White); let color = Color::color(&ColorName::White);
self.svg.prepend_path_with_fill( self.svg.prepend_path(
&svg_path, &paths,
&PointI32::default(),
&color, &color,
); );
} }
+4 -5
View File
@@ -1,5 +1,5 @@
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use visioncortex::{PathSimplifyMode, PointI32}; use visioncortex::PathSimplifyMode;
use visioncortex::color_clusters::{IncrementalBuilder, Clusters, Runner, RunnerConfig}; use visioncortex::color_clusters::{IncrementalBuilder, Clusters, Runner, RunnerConfig};
use crate::canvas::*; use crate::canvas::*;
@@ -94,16 +94,15 @@ impl ColorImageConverter {
if self.counter < view.clusters_output.len() { if self.counter < view.clusters_output.len() {
self.canvas.log("Vectorize tick"); self.canvas.log("Vectorize tick");
let cluster = view.get_cluster(view.clusters_output[self.counter]); let cluster = view.get_cluster(view.clusters_output[self.counter]);
let svg_path = cluster.to_svg_path( let paths = cluster.to_compound_path(
&view, false, self.mode, &view, false, self.mode,
self.params.corner_threshold, self.params.corner_threshold,
self.params.length_threshold, self.params.length_threshold,
self.params.max_iterations, self.params.max_iterations,
self.params.splice_threshold self.params.splice_threshold
); );
self.svg.prepend_path_with_fill( self.svg.prepend_path(
&svg_path, &paths,
&PointI32::new(0, 0),
&cluster.residue_color(), &cluster.residue_color(),
); );
self.counter += 1; self.counter += 1;
+4 -3
View File
@@ -1,5 +1,5 @@
use web_sys::Element; use web_sys::Element;
use visioncortex::{Color, PointI32}; use visioncortex::{Color, CompoundPath, PointF64};
use super::common::document; use super::common::document;
pub struct Svg { pub struct Svg {
@@ -13,11 +13,12 @@ impl Svg {
Self { element } Self { element }
} }
pub fn prepend_path_with_fill(&mut self, path_string: &str, offset: &PointI32, color: &Color) { pub fn prepend_path(&mut self, paths: &CompoundPath, color: &Color) {
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();
path.set_attribute("d", path_string).unwrap(); let (string, offset) = paths.to_svg_string(true, PointF64::default());
path.set_attribute("d", &string).unwrap();
path.set_attribute( path.set_attribute(
"transform", "transform",
format!("translate({},{})", offset.x, offset.y).as_str(), format!("translate({},{})", offset.x, offset.y).as_str(),