Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 82284ab470 | |||
| ead104f0ce | |||
| a7219fd370 | |||
| 2448dbb3ba | |||
| c9d9073b17 | |||
| 47091b0bf5 | |||
| 8439cc995d | |||
| 0b292ad35f | |||
| ebb17ac22b | |||
| 227850dd83 | |||
| e0dac19c31 | |||
| d18d4f8b81 | |||
| 60a4ce579f | |||
| c1c09d964c | |||
| c2a5626afa | |||
| d0593e716a | |||
| 9ce7df176a | |||
| 5928c0a7f5 | |||
| b2c95a50cd | |||
| bcd3ffb40e | |||
| 20da3efd3c | |||
| 0c27d1f06a | |||
| 0c7c7fc808 | |||
| 97ac767844 | |||
| 0b37051d40 | |||
| 2695d7b59b | |||
| 433a68d6d6 | |||
| bb3b66780b | |||
| 049ba7f503 | |||
| 9d67b7c554 | |||
| fa5d2906c0 | |||
| fae37a709e | |||
| 5c9d5cd757 | |||
| 1b49880f85 | |||
| 0b0d69839a | |||
| 7ea4600176 |
@@ -0,0 +1 @@
|
|||||||
|
docs/** linguist-generated
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
name: Rust
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --verbose
|
||||||
|
- name: Run tests
|
||||||
|
run: cargo test --verbose
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
<img src="docs/images/visioncortex-banner.png">
|
<img src="docs/images/visioncortex-banner.png">
|
||||||
<h1>visioncortex VTracer</h1>
|
<h1>VTracer</h1>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<strong>Raster to Vector Graphics Converter built on top of visioncortex</strong>
|
<strong>Raster to Vector Graphics Converter built on top of visioncortex</strong>
|
||||||
</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,46 +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. 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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 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
|
||||||
@@ -71,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 = "0.1.0"
|
cargo install vtracer
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,10 +1,22 @@
|
|||||||
Version 0.2.0 (2020-10-31)
|
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
|
- Use relative & closed paths
|
||||||
|
|
||||||
|
|
||||||
Version 0.1.1 (2020-10-31)
|
Version 0.1.1 (2020-11-01)
|
||||||
==========================
|
==========================
|
||||||
|
|
||||||
- SVG namespace
|
- SVG namespace
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "vtracer"
|
name = "vtracer"
|
||||||
version = "0.2.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.3.0"
|
visioncortex = { version = "0.8.0" }
|
||||||
|
fastrand = "1.8"
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use clap::{Arg, App};
|
use clap::{Arg, App};
|
||||||
use visioncortex::path::PathSimplifyMode;
|
use visioncortex::PathSimplifyMode;
|
||||||
|
|
||||||
pub enum Preset {
|
pub enum Preset {
|
||||||
Bw,
|
Bw,
|
||||||
@@ -9,18 +9,22 @@ pub enum Preset {
|
|||||||
Photo
|
Photo
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum ColorMode {
|
pub enum ColorMode {
|
||||||
Color,
|
Color,
|
||||||
Binary,
|
Binary,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum Hierarchical {
|
||||||
|
Stacked,
|
||||||
|
Cutout,
|
||||||
|
}
|
||||||
|
|
||||||
/// Converter config
|
/// Converter config
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub input_path: PathBuf,
|
pub input_path: PathBuf,
|
||||||
pub output_path: PathBuf,
|
pub output_path: PathBuf,
|
||||||
pub color_mode: ColorMode,
|
pub color_mode: ColorMode,
|
||||||
|
pub hierarchical: Hierarchical,
|
||||||
pub filter_speckle: usize,
|
pub filter_speckle: usize,
|
||||||
pub color_precision: i32,
|
pub color_precision: i32,
|
||||||
pub layer_difference: i32,
|
pub layer_difference: i32,
|
||||||
@@ -29,13 +33,14 @@ 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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub(crate) struct ConverterConfig {
|
pub(crate) struct ConverterConfig {
|
||||||
pub input_path: PathBuf,
|
pub input_path: PathBuf,
|
||||||
pub output_path: PathBuf,
|
pub output_path: PathBuf,
|
||||||
pub color_mode: ColorMode,
|
pub color_mode: ColorMode,
|
||||||
|
pub hierarchical: Hierarchical,
|
||||||
pub filter_speckle_area: usize,
|
pub filter_speckle_area: usize,
|
||||||
pub color_precision_loss: i32,
|
pub color_precision_loss: i32,
|
||||||
pub layer_difference: i32,
|
pub layer_difference: i32,
|
||||||
@@ -44,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 {
|
||||||
@@ -52,6 +58,7 @@ impl Default for Config {
|
|||||||
input_path: PathBuf::default(),
|
input_path: PathBuf::default(),
|
||||||
output_path: PathBuf::default(),
|
output_path: PathBuf::default(),
|
||||||
color_mode: ColorMode::Color,
|
color_mode: ColorMode::Color,
|
||||||
|
hierarchical: Hierarchical::Stacked,
|
||||||
mode: PathSimplifyMode::Spline,
|
mode: PathSimplifyMode::Spline,
|
||||||
filter_speckle: 4,
|
filter_speckle: 4,
|
||||||
color_precision: 6,
|
color_precision: 6,
|
||||||
@@ -60,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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,6 +84,18 @@ impl FromStr for ColorMode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FromStr for Hierarchical {
|
||||||
|
type Err = String;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"stacked" => Ok(Self::Stacked),
|
||||||
|
"cutout" => Ok(Self::Cutout),
|
||||||
|
_ => Err(format!("unknown Hierarchical {}", s)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl FromStr for Preset {
|
impl FromStr for Preset {
|
||||||
type Err = String;
|
type Err = String;
|
||||||
|
|
||||||
@@ -122,6 +142,14 @@ impl Config {
|
|||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.help("True color image `color` (default) or Binary image `bw`"));
|
.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")
|
let app = app.arg(Arg::with_name("preset")
|
||||||
.long("preset")
|
.long("preset")
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
@@ -169,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();
|
||||||
|
|
||||||
@@ -187,6 +220,10 @@ impl Config {
|
|||||||
config.color_mode = ColorMode::from_str(if value.trim() == "bw" || value.trim() == "BW" {"binary"} else {"color"}).unwrap()
|
config.color_mode = ColorMode::from_str(if value.trim() == "bw" || value.trim() == "BW" {"binary"} else {"color"}).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(value) = matches.value_of("hierarchical") {
|
||||||
|
config.hierarchical = Hierarchical::from_str(value).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(value) = matches.value_of("mode") {
|
if let Some(value) = matches.value_of("mode") {
|
||||||
let value = value.trim();
|
let value = value.trim();
|
||||||
config.mode = path_simplify_mode_from_str(if value == "pixel" {
|
config.mode = path_simplify_mode_from_str(if value == "pixel" {
|
||||||
@@ -196,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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,6 +329,7 @@ impl Config {
|
|||||||
input_path,
|
input_path,
|
||||||
output_path,
|
output_path,
|
||||||
color_mode: ColorMode::Binary,
|
color_mode: ColorMode::Binary,
|
||||||
|
hierarchical: Hierarchical::Stacked,
|
||||||
filter_speckle: 4,
|
filter_speckle: 4,
|
||||||
color_precision: 6,
|
color_precision: 6,
|
||||||
layer_difference: 16,
|
layer_difference: 16,
|
||||||
@@ -291,11 +338,13 @@ 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,
|
||||||
output_path,
|
output_path,
|
||||||
color_mode: ColorMode::Color,
|
color_mode: ColorMode::Color,
|
||||||
|
hierarchical: Hierarchical::Stacked,
|
||||||
filter_speckle: 4,
|
filter_speckle: 4,
|
||||||
color_precision: 8,
|
color_precision: 8,
|
||||||
layer_difference: 16,
|
layer_difference: 16,
|
||||||
@@ -304,11 +353,13 @@ 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,
|
||||||
output_path,
|
output_path,
|
||||||
color_mode: ColorMode::Color,
|
color_mode: ColorMode::Color,
|
||||||
|
hierarchical: Hierarchical::Stacked,
|
||||||
filter_speckle: 10,
|
filter_speckle: 10,
|
||||||
color_precision: 8,
|
color_precision: 8,
|
||||||
layer_difference: 48,
|
layer_difference: 48,
|
||||||
@@ -317,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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -326,6 +378,7 @@ impl Config {
|
|||||||
input_path: self.input_path,
|
input_path: self.input_path,
|
||||||
output_path: self.output_path,
|
output_path: self.output_path,
|
||||||
color_mode: self.color_mode,
|
color_mode: self.color_mode,
|
||||||
|
hierarchical: self.hierarchical,
|
||||||
filter_speckle_area: self.filter_speckle * self.filter_speckle,
|
filter_speckle_area: self.filter_speckle * self.filter_speckle,
|
||||||
color_precision_loss: 8 - self.color_precision,
|
color_precision_loss: 8 - self.color_precision,
|
||||||
layer_difference: self.layer_difference,
|
layer_difference: self.layer_difference,
|
||||||
@@ -334,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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::{fs::File, io::Write};
|
use std::{fs::File, io::Write};
|
||||||
|
|
||||||
use visioncortex::{Color, ColorImage, ColorName, color_clusters::RunnerConfig, color_clusters::Runner};
|
use fastrand::Rng;
|
||||||
use super::config::{Config, ColorMode, ConverterConfig};
|
use visioncortex::{Color, ColorImage, ColorName};
|
||||||
|
use visioncortex::color_clusters::{Runner, RunnerConfig, KeyingAction, HIERARCHICAL_MAX};
|
||||||
|
use super::config::{Config, ColorMode, Hierarchical, ConverterConfig};
|
||||||
use super::svg::SvgFile;
|
use super::svg::SvgFile;
|
||||||
|
|
||||||
|
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();
|
||||||
@@ -14,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;
|
||||||
@@ -25,7 +92,24 @@ 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,
|
||||||
|
hierarchical: HIERARCHICAL_MAX,
|
||||||
batch_size: 25600,
|
batch_size: 25600,
|
||||||
good_min_area: config.filter_speckle_area,
|
good_min_area: config.filter_speckle_area,
|
||||||
good_max_area: (width * height),
|
good_max_area: (width * height),
|
||||||
@@ -33,13 +117,41 @@ 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 clusters = runner.run();
|
let mut clusters = runner.run();
|
||||||
|
|
||||||
|
match config.hierarchical {
|
||||||
|
Hierarchical::Stacked => {}
|
||||||
|
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);
|
||||||
|
clusters = runner.run();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
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(
|
||||||
@@ -72,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 {
|
||||||
@@ -93,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")),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
K1_drawing
|
|
||||||
B/W 4
|
|
||||||
https://commons.wikimedia.org/wiki/File:K1_drawing.jpg
|
|
||||||
|
|
||||||
Cityscape Sunset_DFM3-01
|
|
||||||
Color 4,8,32
|
|
||||||
<a href="https://www.vecteezy.com/free-vector/building">Building Vectors by Vecteezy</a>
|
|
||||||
|
|
||||||
Gum Tree Vector
|
|
||||||
Color 4,8,32
|
|
||||||
<a href="https://www.vecteezy.com/free-vector/nature">Nature Vectors by Vecteezy</a>
|
|
||||||
|
|
||||||
vectorstock_31191940
|
|
||||||
Color 8,7,64
|
|
||||||
<a href="https://www.vectorstock.com/royalty-free-vector/dessert-poster-design-with-chocolate-cake-mousses-vector-31191940">Vector image by VectorStock / vectorstock</a>
|
|
||||||
|
|
||||||
averie-woodard-4nulm-JUYFo-unsplash
|
|
||||||
Color 8,7,24
|
|
||||||
<span>Photo by <a href="https://unsplash.com/@averieclaire?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">averie woodard</a> on <a href="https://unsplash.com/s/photos/portrait?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></span>
|
|
||||||
|
|
||||||
angel-luciano-LATYeZyw88c-unsplash
|
|
||||||
Color 10,8,48 180
|
|
||||||
<span>Photo by <a href="https://unsplash.com/@roaming_angel?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Angel Luciano</a> on <a href="https://unsplash.com/s/photos/dog?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></span>
|
|
||||||
|
Before Width: | Height: | Size: 304 KiB |
|
Before Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 9.7 KiB |
@@ -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":"7ef94c5089d40af744f2"}[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) {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 243 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 5.8 KiB |
@@ -79,8 +79,7 @@
|
|||||||
}
|
}
|
||||||
#canvas-container {
|
#canvas-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
height: max-content;
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
#svg, #frame {
|
#svg, #frame {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -91,7 +90,7 @@
|
|||||||
stroke: #ff0;
|
stroke: #ff0;
|
||||||
}
|
}
|
||||||
#droptext {
|
#droptext {
|
||||||
height: 100%;
|
height: 480px;
|
||||||
color: #CC972E;
|
color: #CC972E;
|
||||||
}
|
}
|
||||||
input[type=range]::-webkit-slider-thumb {
|
input[type=range]::-webkit-slider-thumb {
|
||||||
@@ -107,11 +106,11 @@
|
|||||||
background: #00275D;
|
background: #00275D;
|
||||||
border-color: #CC972E;
|
border-color: #CC972E;
|
||||||
}
|
}
|
||||||
#gallery, #options {
|
#drop, #gallery, #options {
|
||||||
border-top: 1px solid #686868;
|
border-top: 1px solid #686868;
|
||||||
}
|
}
|
||||||
#gallery {
|
#gallery {
|
||||||
border-bottom: 1px solid #686868;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
.galleryitem {
|
.galleryitem {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -140,9 +139,11 @@
|
|||||||
<a id="vc-logo" class="uk-logo" href="//www.visioncortex.org"><img style="height: 50px; margin: -30px 0 -25px 0;" src="./assets/visioncortex-logo.svg"></a>
|
<a id="vc-logo" class="uk-logo" href="//www.visioncortex.org"><img style="height: 50px; margin: -30px 0 -25px 0;" src="./assets/visioncortex-logo.svg"></a>
|
||||||
<a id="vc-logo-light" class="uk-logo" href="//www.visioncortex.org"><img style="height: 50px; margin: -30px 0 -25px 0;" src="./assets/visioncortex-logo-light.svg"></a>
|
<a id="vc-logo-light" class="uk-logo" href="//www.visioncortex.org"><img style="height: 50px; margin: -30px 0 -25px 0;" src="./assets/visioncortex-logo-light.svg"></a>
|
||||||
|
|
||||||
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>
|
||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
<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>
|
||||||
@@ -152,6 +153,15 @@
|
|||||||
<progress id="progressbar" class="uk-progress uk-align-right" value="0" max="100" style="width: 98%;"></progress>
|
<progress id="progressbar" class="uk-progress uk-align-right" value="0" max="100" style="width: 98%;"></progress>
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-width-3-4">
|
<div class="uk-width-3-4">
|
||||||
|
<div id="drop" class="uk-padding uk-flex uk-flex-center">
|
||||||
|
<div id="canvas-container" class="uk-width-1-1">
|
||||||
|
<div id="droptext" class="uk-flex uk-flex-middle uk-flex-center">
|
||||||
|
<p>Drag an image here, Cmd-V to paste or <a href="#" id="imageSelect">Select file</a></p>
|
||||||
|
</div>
|
||||||
|
<canvas id="frame"></canvas>
|
||||||
|
<svg id="svg" version="1.1" xmlns="http://www.w3.org/2000/svg"></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="gallery" class="uk-padding">
|
<div id="gallery" class="uk-padding">
|
||||||
<div class="uk-inline" uk-slider>
|
<div class="uk-inline" uk-slider>
|
||||||
<ul id="galleryslider" class="uk-slider-items uk-child-width-1-6 uk-grid uk-grid-small">
|
<ul id="galleryslider" class="uk-slider-items uk-child-width-1-6 uk-grid uk-grid-small">
|
||||||
@@ -160,16 +170,7 @@
|
|||||||
<a class="uk-position-center-left uk-position-small uk-hidden-hover" href="#" uk-slidenav-previous uk-slider-item="previous"></a>
|
<a class="uk-position-center-left uk-position-small uk-hidden-hover" href="#" uk-slidenav-previous uk-slider-item="previous"></a>
|
||||||
<a class="uk-position-center-right uk-position-small uk-hidden-hover" href="#" uk-slidenav-next uk-slider-item="next"></a>
|
<a class="uk-position-center-right uk-position-small uk-hidden-hover" href="#" uk-slidenav-next uk-slider-item="next"></a>
|
||||||
</div>
|
</div>
|
||||||
<a class="uk-align-right uk-text-meta" style="margin:-5px;" uk-toggle="target: #credits-modal">Credits</a>
|
<a class="uk-align-right uk-text-meta" style="margin:-5px;" uk-toggle="target: #credits-modal">Photo Credits</a>
|
||||||
</div>
|
|
||||||
<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="droptext" class="uk-flex uk-flex-middle uk-flex-center">
|
|
||||||
<p>Drag an image here, Cmd-V to paste or <a href="#" id="imageSelect">Select file</a></p>
|
|
||||||
</div>
|
|
||||||
<canvas id="frame"></canvas>
|
|
||||||
<svg id="svg" version="1.1" xmlns="http://www.w3.org/2000/svg"></svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="options" class="uk-width-1-4 uk-height-1-1 uk-grid-small uk-flex uk-flex-center" uk-grid>
|
<div id="options" class="uk-width-1-4 uk-height-1-1 uk-grid-small uk-flex uk-flex-center" uk-grid>
|
||||||
@@ -193,6 +194,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="clustering-color-options uk-width-1-1 uk-flex uk-flex-right">
|
||||||
|
<div class="uk-button-group">
|
||||||
|
<button id="clustering-cutout" class="uk-button uk-button-default uk-button-small" uk-tooltip="Shapes disjoint with others">Cutout</button>
|
||||||
|
<button id="clustering-stacked" class="selected uk-button uk-button-default uk-button-small" uk-tooltip="Stack shapes on top of another">Stacked</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="uk-width-1-1 uk-flex uk-flex-right">
|
<div class="uk-width-1-1 uk-flex uk-flex-right">
|
||||||
<div uk-tooltip="pos: left; title: Discard patches small than X px in size">
|
<div uk-tooltip="pos: left; title: Discard patches small than X px in size">
|
||||||
Filter Speckle <span class="uk-text-meta">(Cleaner)</span>
|
Filter Speckle <span class="uk-text-meta">(Cleaner)</span>
|
||||||
@@ -202,7 +210,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="128" step="1" value="4">
|
<input id="filterspeckle" class="uk-range" type="range" min="0" 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">
|
||||||
@@ -279,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>
|
||||||
|
|
||||||
|
|||||||
@@ -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.3.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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -90,6 +90,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<button id="clustering-cutout" title="Shapes disjoint with others">Cutout</button>
|
||||||
|
<button id="clustering-stacked" title="Stack shapes on top of another">Stacked</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div title="Discard patches small than X px in size">
|
<div title="Discard patches small than X px in size">
|
||||||
Filter Speckle <span>(Cleaner)</span>
|
Filter Speckle <span>(Cleaner)</span>
|
||||||
@@ -178,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>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const svg = document.getElementById('svg');
|
|||||||
const img = new Image();
|
const img = new Image();
|
||||||
const progress = document.getElementById('progressbar');
|
const progress = document.getElementById('progressbar');
|
||||||
const progressregion = document.getElementById('progressregion');
|
const progressregion = document.getElementById('progressregion');
|
||||||
let mode = 'spline', clustering_mode = 'color';
|
let mode = 'spline', clustering_mode = 'color', clustering_hierarchical = 'stacked';
|
||||||
|
|
||||||
// Hide canas and svg on load
|
// Hide canas and svg on load
|
||||||
canvas.style.display = 'none';
|
canvas.style.display = 'none';
|
||||||
@@ -15,22 +15,22 @@ svg.style.display = 'none';
|
|||||||
|
|
||||||
// Paste from clipboard
|
// Paste from clipboard
|
||||||
document.addEventListener('paste', function (e) {
|
document.addEventListener('paste', function (e) {
|
||||||
if (e.clipboardData) {
|
if (e.clipboardData) {
|
||||||
var items = e.clipboardData.items;
|
var items = e.clipboardData.items;
|
||||||
if (!items) return;
|
if (!items) return;
|
||||||
|
|
||||||
//access data directly
|
//access data directly
|
||||||
for (var i = 0; i < items.length; i++) {
|
for (var i = 0; i < items.length; i++) {
|
||||||
if (items[i].type.indexOf("image") !== -1) {
|
if (items[i].type.indexOf("image") !== -1) {
|
||||||
//image
|
//image
|
||||||
var blob = items[i].getAsFile();
|
var blob = items[i].getAsFile();
|
||||||
var URLObj = window.URL || window.webkitURL;
|
var URLObj = window.URL || window.webkitURL;
|
||||||
var source = URLObj.createObjectURL(blob);
|
var source = URLObj.createObjectURL(blob);
|
||||||
setSourceAndRestart(source);
|
setSourceAndRestart(source);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Download as SVG
|
// Download as SVG
|
||||||
@@ -49,8 +49,10 @@ var presetConfigs = [
|
|||||||
{
|
{
|
||||||
src: 'assets/samples/K1_drawing.jpg',
|
src: 'assets/samples/K1_drawing.jpg',
|
||||||
clustering_mode: 'binary',
|
clustering_mode: 'binary',
|
||||||
|
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,
|
||||||
@@ -62,8 +64,10 @@ var presetConfigs = [
|
|||||||
{
|
{
|
||||||
src: 'assets/samples/Cityscape Sunset_DFM3-01.jpg',
|
src: 'assets/samples/Cityscape Sunset_DFM3-01.jpg',
|
||||||
clustering_mode: 'color',
|
clustering_mode: 'color',
|
||||||
|
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,
|
||||||
@@ -75,8 +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: '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,
|
||||||
@@ -88,8 +94,10 @@ var presetConfigs = [
|
|||||||
{
|
{
|
||||||
src: 'assets/samples/vectorstock_31191940.png',
|
src: 'assets/samples/vectorstock_31191940.png',
|
||||||
clustering_mode: 'color',
|
clustering_mode: 'color',
|
||||||
|
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,
|
||||||
@@ -98,24 +106,13 @@ var presetConfigs = [
|
|||||||
source: 'https://www.vectorstock.com/royalty-free-vector/dessert-poster-design-with-chocolate-cake-mousses-vector-31191940',
|
source: 'https://www.vectorstock.com/royalty-free-vector/dessert-poster-design-with-chocolate-cake-mousses-vector-31191940',
|
||||||
credit: '<a href="https://www.vectorstock.com/royalty-free-vector/dessert-poster-design-with-chocolate-cake-mousses-vector-31191940">Vector image by VectorStock / vectorstock</a>',
|
credit: '<a href="https://www.vectorstock.com/royalty-free-vector/dessert-poster-design-with-chocolate-cake-mousses-vector-31191940">Vector image by VectorStock / vectorstock</a>',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
src: 'assets/samples/averie-woodard-4nulm-JUYFo-unsplash-s.jpg',
|
|
||||||
clustering_mode: 'color',
|
|
||||||
filter_speckle: 8,
|
|
||||||
color_precision: 7,
|
|
||||||
layer_difference: 24,
|
|
||||||
mode: 'spline',
|
|
||||||
corner_threshold: 60,
|
|
||||||
length_threshold: 4,
|
|
||||||
splice_threshold: 45,
|
|
||||||
source: 'https://unsplash.com/photos/4nulm-JUYFo',
|
|
||||||
credit: '<span>Photo by <a href="https://unsplash.com/@averieclaire?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">averie woodard</a> on <a href="https://unsplash.com/s/photos/portrait?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></span>',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
src: 'assets/samples/angel-luciano-LATYeZyw88c-unsplash-s.jpg',
|
src: 'assets/samples/angel-luciano-LATYeZyw88c-unsplash-s.jpg',
|
||||||
clustering_mode: 'color',
|
clustering_mode: 'color',
|
||||||
|
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,
|
||||||
@@ -124,66 +121,87 @@ var presetConfigs = [
|
|||||||
source: 'https://unsplash.com/photos/LATYeZyw88c',
|
source: 'https://unsplash.com/photos/LATYeZyw88c',
|
||||||
credit: '<span>Photo by <a href="https://unsplash.com/@roaming_angel?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Angel Luciano</a> on <a href="https://unsplash.com/s/photos/dog?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></span>',
|
credit: '<span>Photo by <a href="https://unsplash.com/@roaming_angel?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Angel Luciano</a> on <a href="https://unsplash.com/s/photos/dog?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></span>',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
src: 'assets/samples/tank-unit-preview.png',
|
||||||
|
clustering_mode: 'color',
|
||||||
|
clustering_hierarchical: 'stacked',
|
||||||
|
filter_speckle: 0,
|
||||||
|
color_precision: 8,
|
||||||
|
path_precision: 8,
|
||||||
|
layer_difference: 0,
|
||||||
|
mode: 'none',
|
||||||
|
corner_threshold: 180,
|
||||||
|
length_threshold: 4,
|
||||||
|
splice_threshold: 45,
|
||||||
|
source: 'https://opengameart.org/content/sideview-sci-fi-patreon-collection',
|
||||||
|
credit: '<span>Artwork by <a href="https://opengameart.org/content/sideview-sci-fi-patreon-collection">Luis Zuno</a> on <a href="https://opengameart.org/">opengameart.org</a></span>',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Insert gallery items dynamically
|
// Insert gallery items dynamically
|
||||||
for (let i = 0; i < presetConfigs.length; ++i) {
|
if (document.getElementById('galleryslider')) {
|
||||||
document.getElementById('galleryslider').innerHTML +=
|
for (let i = 0; i < presetConfigs.length; i++) {
|
||||||
`<li>
|
document.getElementById('galleryslider').innerHTML +=
|
||||||
<div class="galleryitem uk-panel uk-flex uk-flex-center">
|
`<li>
|
||||||
<a href="#">
|
<div class="galleryitem uk-panel uk-flex uk-flex-center">
|
||||||
<img src="${presetConfigs[i].src}" title="${presetConfigs[i].source}">
|
<a href="#">
|
||||||
</a>
|
<img src="${presetConfigs[i].src}" title="${presetConfigs[i].source}">
|
||||||
</div>
|
</a>
|
||||||
</li>`;
|
</div>
|
||||||
document.getElementById('credits-modal-content').innerHTML +=
|
</li>`;
|
||||||
`<p>${presetConfigs[i].credit}</p>`;
|
document.getElementById('credits-modal-content').innerHTML +=
|
||||||
|
`<p>${presetConfigs[i].credit}</p>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to load a given config WITHOUT restarting
|
// Function to load a given config WITHOUT restarting
|
||||||
function loadConfig(config) {
|
function loadConfig(config) {
|
||||||
mode = config.mode;
|
mode = config.mode;
|
||||||
clustering_mode = config.clustering_mode;
|
clustering_mode = config.clustering_mode;
|
||||||
|
clustering_hierarchical = config.clustering_hierarchical;
|
||||||
|
|
||||||
globalcorner = config.corner_threshold;
|
globalcorner = config.corner_threshold;
|
||||||
document.getElementById('cornervalue').innerHTML = globalcorner;
|
document.getElementById('cornervalue').innerHTML = globalcorner;
|
||||||
document.getElementById('corner').value = globalcorner;
|
document.getElementById('corner').value = globalcorner;
|
||||||
|
|
||||||
globallength = config.length_threshold;
|
globallength = config.length_threshold;
|
||||||
document.getElementById('lengthvalue').innerHTML = globallength;
|
document.getElementById('lengthvalue').innerHTML = globallength;
|
||||||
document.getElementById('length').value = globallength;
|
document.getElementById('length').value = globallength;
|
||||||
|
|
||||||
globalsplice = config.splice_threshold;
|
globalsplice = config.splice_threshold;
|
||||||
document.getElementById('splicevalue').innerHTML = globalsplice;
|
document.getElementById('splicevalue').innerHTML = globalsplice;
|
||||||
document.getElementById('splice').value = globalsplice;
|
document.getElementById('splice').value = globalsplice;
|
||||||
|
|
||||||
globalfilterspeckle = config.filter_speckle;
|
globalfilterspeckle = config.filter_speckle;
|
||||||
document.getElementById('filterspecklevalue').innerHTML = globalfilterspeckle;
|
document.getElementById('filterspecklevalue').innerHTML = globalfilterspeckle;
|
||||||
document.getElementById('filterspeckle').value = globalfilterspeckle;
|
document.getElementById('filterspeckle').value = globalfilterspeckle;
|
||||||
|
|
||||||
globalcolorprecision = config.color_precision;
|
globalcolorprecision = config.color_precision;
|
||||||
document.getElementById('colorprecisionvalue').innerHTML = globalcolorprecision;
|
document.getElementById('colorprecisionvalue').innerHTML = globalcolorprecision;
|
||||||
document.getElementById('colorprecision').value = globalcolorprecision;
|
document.getElementById('colorprecision').value = globalcolorprecision;
|
||||||
|
|
||||||
globallayerdifference = config.layer_difference;
|
globallayerdifference = config.layer_difference;
|
||||||
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
|
||||||
let chooseGalleryButtons = document.querySelectorAll('.galleryitem a');
|
let chooseGalleryButtons = document.querySelectorAll('.galleryitem a');
|
||||||
chooseGalleryButtons.forEach(item => {
|
chooseGalleryButtons.forEach(item => {
|
||||||
item.addEventListener('click', function (e) {
|
item.addEventListener('click', function (e) {
|
||||||
// Load preset template config
|
// Load preset template config
|
||||||
let i = Array.prototype.indexOf.call(chooseGalleryButtons, item);
|
let i = Array.prototype.indexOf.call(chooseGalleryButtons, item);
|
||||||
if (presetConfigs.length > i) {
|
if (presetConfigs.length > i) {
|
||||||
loadConfig(presetConfigs[i]);
|
loadConfig(presetConfigs[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set source as specified
|
// Set source as specified
|
||||||
setSourceAndRestart(this.firstElementChild.src);
|
setSourceAndRestart(this.firstElementChild.src);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Upload button
|
// Upload button
|
||||||
@@ -235,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
|
||||||
/*
|
/*
|
||||||
@@ -272,6 +291,16 @@ document.getElementById('clustering-color').addEventListener('click', function (
|
|||||||
restart();
|
restart();
|
||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
|
document.getElementById('clustering-cutout').addEventListener('click', function (e) {
|
||||||
|
clustering_hierarchical = 'cutout';
|
||||||
|
restart();
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
document.getElementById('clustering-stacked').addEventListener('click', function (e) {
|
||||||
|
clustering_hierarchical = 'stacked';
|
||||||
|
restart();
|
||||||
|
}, false);
|
||||||
|
|
||||||
document.getElementById('filterspeckle').addEventListener('change', function (e) {
|
document.getElementById('filterspeckle').addEventListener('change', function (e) {
|
||||||
globalfilterspeckle = parseInt(this.value);
|
globalfilterspeckle = parseInt(this.value);
|
||||||
document.getElementById('filterspecklevalue').innerHTML = this.value;
|
document.getElementById('filterspecklevalue').innerHTML = this.value;
|
||||||
@@ -308,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 () {
|
||||||
@@ -322,9 +357,19 @@ window.addEventListener('beforeunload', function () {
|
|||||||
function setSourceAndRestart(source) {
|
function setSourceAndRestart(source) {
|
||||||
img.src = source instanceof File ? URL.createObjectURL(source) : source;
|
img.src = source instanceof File ? URL.createObjectURL(source) : source;
|
||||||
img.onload = function () {
|
img.onload = function () {
|
||||||
svg.setAttribute('viewBox', `0 0 ${img.naturalWidth} ${img.naturalHeight}`);
|
const width = img.naturalWidth, height = img.naturalHeight;
|
||||||
|
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
||||||
canvas.width = img.naturalWidth;
|
canvas.width = img.naturalWidth;
|
||||||
canvas.height = img.naturalHeight;
|
canvas.height = img.naturalHeight;
|
||||||
|
if (height > width) {
|
||||||
|
document.getElementById('canvas-container').style.width = '50%';
|
||||||
|
document.getElementById('canvas-container').style.marginBottom = (height / width * 50) + '%';
|
||||||
|
} else {
|
||||||
|
document.getElementById('canvas-container').style.width = '';
|
||||||
|
document.getElementById('canvas-container').style.marginBottom = (height / width * 100) + '%';
|
||||||
|
}
|
||||||
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
restart();
|
restart();
|
||||||
}
|
}
|
||||||
// Show display
|
// Show display
|
||||||
@@ -342,6 +387,10 @@ function restart() {
|
|||||||
el.style.display = clustering_mode == 'color' ? '' : 'none';
|
el.style.display = clustering_mode == 'color' ? '' : 'none';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('clustering-cutout').classList.remove('selected');
|
||||||
|
document.getElementById('clustering-stacked').classList.remove('selected');
|
||||||
|
document.getElementById('clustering-' + clustering_hierarchical).classList.add('selected');
|
||||||
|
|
||||||
document.getElementById('none').classList.remove('selected');
|
document.getElementById('none').classList.remove('selected');
|
||||||
document.getElementById('polygon').classList.remove('selected');
|
document.getElementById('polygon').classList.remove('selected');
|
||||||
document.getElementById('spline').classList.remove('selected');
|
document.getElementById('spline').classList.remove('selected');
|
||||||
@@ -363,6 +412,7 @@ function restart() {
|
|||||||
'svg_id': svg.id,
|
'svg_id': svg.id,
|
||||||
'mode': mode,
|
'mode': mode,
|
||||||
'clustering_mode': clustering_mode,
|
'clustering_mode': clustering_mode,
|
||||||
|
'hierarchical': clustering_hierarchical,
|
||||||
'corner_threshold': deg2rad(globalcorner),
|
'corner_threshold': deg2rad(globalcorner),
|
||||||
'length_threshold': globallength,
|
'length_threshold': globallength,
|
||||||
'max_iterations': 10,
|
'max_iterations': 10,
|
||||||
@@ -370,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();
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ impl Canvas {
|
|||||||
image.data().to_vec()
|
image.data().to_vec()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_image_data_as_image(&self, x: u32, y: u32, width: u32, height: u32) -> ColorImage {
|
pub fn get_image_data_as_color_image(&self, x: u32, y: u32, width: u32, height: u32) -> ColorImage {
|
||||||
ColorImage {
|
ColorImage {
|
||||||
pixels: self.get_image_data(x, y, width, height),
|
pixels: self.get_image_data(x, y, width, height),
|
||||||
width: width as usize,
|
width: width as usize,
|
||||||
|
|||||||
@@ -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]
|
||||||
@@ -54,7 +55,7 @@ impl BinaryImageConverter {
|
|||||||
pub fn init(&mut self) {
|
pub fn init(&mut self) {
|
||||||
let width = self.canvas.width() as u32;
|
let width = self.canvas.width() as u32;
|
||||||
let height = self.canvas.height() as u32;
|
let height = self.canvas.height() as u32;
|
||||||
let image = self.canvas.get_image_data_as_image(0, 0, width, height);
|
let image = self.canvas.get_image_data_as_color_image(0, 0, width, height);
|
||||||
let binary_image = image.to_binary_image(|x| x.r < 128);
|
let binary_image = image.to_binary_image(|x| x.r < 128);
|
||||||
self.clusters = binary_image.to_clusters(false);
|
self.clusters = binary_image.to_clusters(false);
|
||||||
self.canvas.log(&format!(
|
self.canvas.log(&format!(
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
use visioncortex::PathSimplifyMode;
|
use visioncortex::PathSimplifyMode;
|
||||||
use visioncortex::color_clusters::{IncrementalBuilder, Clusters, Runner, RunnerConfig};
|
use visioncortex::color_clusters::{IncrementalBuilder, Clusters, Runner, RunnerConfig, HIERARCHICAL_MAX};
|
||||||
|
|
||||||
use crate::canvas::*;
|
use crate::canvas::*;
|
||||||
use crate::svg::*;
|
use crate::svg::*;
|
||||||
@@ -13,6 +13,7 @@ pub struct ColorImageConverterParams {
|
|||||||
pub canvas_id: String,
|
pub canvas_id: String,
|
||||||
pub svg_id: String,
|
pub svg_id: String,
|
||||||
pub mode: String,
|
pub mode: String,
|
||||||
|
pub hierarchical: String,
|
||||||
pub corner_threshold: f64,
|
pub corner_threshold: f64,
|
||||||
pub length_threshold: f64,
|
pub length_threshold: f64,
|
||||||
pub max_iterations: usize,
|
pub max_iterations: usize,
|
||||||
@@ -20,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]
|
||||||
@@ -35,6 +37,7 @@ pub struct ColorImageConverter {
|
|||||||
pub enum Stage {
|
pub enum Stage {
|
||||||
New,
|
New,
|
||||||
Clustering(IncrementalBuilder),
|
Clustering(IncrementalBuilder),
|
||||||
|
Reclustering(IncrementalBuilder),
|
||||||
Vectorize(Clusters),
|
Vectorize(Clusters),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,8 +67,10 @@ impl ColorImageConverter {
|
|||||||
pub fn init(&mut self) {
|
pub fn init(&mut self) {
|
||||||
let width = self.canvas.width() as u32;
|
let width = self.canvas.width() as u32;
|
||||||
let height = self.canvas.height() as u32;
|
let height = self.canvas.height() as u32;
|
||||||
let image = self.canvas.get_image_data_as_image(0, 0, width, height);
|
let image = self.canvas.get_image_data_as_color_image(0, 0, width, height);
|
||||||
let runner = Runner::new(RunnerConfig {
|
let runner = Runner::new(RunnerConfig {
|
||||||
|
diagonal: self.params.layer_difference == 0,
|
||||||
|
hierarchical: HIERARCHICAL_MAX,
|
||||||
batch_size: 25600,
|
batch_size: 25600,
|
||||||
good_min_area: self.params.filter_speckle,
|
good_min_area: self.params.filter_speckle,
|
||||||
good_max_area: (width * height) as usize,
|
good_max_area: (width * height) as usize,
|
||||||
@@ -84,6 +89,35 @@ impl ColorImageConverter {
|
|||||||
},
|
},
|
||||||
Stage::Clustering(builder) => {
|
Stage::Clustering(builder) => {
|
||||||
self.canvas.log("Clustering tick");
|
self.canvas.log("Clustering tick");
|
||||||
|
if builder.tick() {
|
||||||
|
match self.params.hierarchical.as_str() {
|
||||||
|
"stacked" => {
|
||||||
|
self.stage = Stage::Vectorize(builder.result());
|
||||||
|
},
|
||||||
|
"cutout" => {
|
||||||
|
let clusters = builder.result();
|
||||||
|
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,
|
||||||
|
}, image);
|
||||||
|
self.stage = Stage::Reclustering(runner.start());
|
||||||
|
},
|
||||||
|
_ => panic!("unknown hierarchical `{}`", self.params.hierarchical)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
},
|
||||||
|
Stage::Reclustering(builder) => {
|
||||||
|
self.canvas.log("Reclustering tick");
|
||||||
if builder.tick() {
|
if builder.tick() {
|
||||||
self.stage = Stage::Vectorize(builder.result())
|
self.stage = Stage::Vectorize(builder.result())
|
||||||
}
|
}
|
||||||
@@ -104,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
|
||||||
@@ -123,6 +158,9 @@ impl ColorImageConverter {
|
|||||||
Stage::Clustering(builder) => {
|
Stage::Clustering(builder) => {
|
||||||
builder.progress() / 2
|
builder.progress() / 2
|
||||||
},
|
},
|
||||||
|
Stage::Reclustering(_builder) => {
|
||||||
|
50
|
||||||
|
},
|
||||||
Stage::Vectorize(clusters) => {
|
Stage::Vectorize(clusters) => {
|
||||||
50 + 50 * self.counter as u32 / clusters.view().clusters_output.len() as u32
|
50 + 50 * self.counter as u32 / clusters.view().clusters_output.len() as u32
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||