61 Commits

Author SHA1 Message Date
Chris Tsang 66f6bd98f4 Feature guard 2023-09-08 13:20:28 +01:00
Chris Tsang 59448884cb Edit RELEASES.md 2023-09-08 13:10:19 +01:00
Chris Tsang edf1311425 Edit Readme 2023-09-08 13:05:15 +01:00
Chris Tsang 02c844dc7a Move code around 2023-09-08 13:03:43 +01:00
Evan Jones c3090ac50b Python bindings sep 2023 (#52)
* Added maturin-based Python binding, to be deployed to https://pypi.org/project/vtracer/

* Removed poetry mentions from pyproject.toml, added README_PY.md for use on PYPI

* ->   v0.6.1
-> moved Python bindings to bottom of converter.rs

* - README_PY.md needed to be inside the cmdapp directory to display on PyPi.irg
->  v0.6.3
2023-09-08 19:43:13 +08:00
Chris Tsang d5dfa9fd73 Readme 2023-07-24 22:55:26 +08:00
Chris Tsang 685009bd21 Allow filter_speckle to be 0 #47 2023-06-30 05:45:35 +08:00
Chris Tsang 971bffa948 Mentions Aliyun
Foot note: I was in touch with one of their engineers
2022-11-10 17:53:12 +08:00
Chris Tsang 7be6882d6b Readme 2022-10-14 00:32:02 +08:00
Chris Tsang 386a0bcaab Filestar 2022-10-14 00:29:13 +08:00
Chris Tsang 82284ab470 0.5.0 2022-10-09 18:01:35 +08:00
Chris Tsang ead104f0ce Tweak should_key_image 2022-10-09 17:21:00 +08:00
zachwolfe a7219fd370 Alpha channel handling in CLI (#23)
* Support transparent color images

* Remove unnecessary conditional

* Add temporary git url to visioncortex dependency

* Use fastrand instead of rand

* Reduce the number of random iterations when keying

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

* Add three additional special keying colours

* Add transparency check to some inner pixels
2022-10-09 15:41:43 +08:00
Oskar Skuteli 2448dbb3ba fix docs typo (#25) 2022-09-25 15:06:19 +08:00
Chris Tsang c9d9073b17 Merge pull request #16 from wolfgangmeyers/fix-converter-warning
Fix deprecation warning from converter.rs
2022-02-11 21:45:33 +08:00
Wolfgang Meyers 47091b0bf5 Fix deprecation warning from converter.ts 2022-02-10 18:13:51 -08:00
Chris Tsang 8439cc995d Refactor path_precision 2021-07-27 21:01:20 +08:00
Chris Tsang 0b292ad35f Readme 2021-07-24 16:48:47 +08:00
Chris Tsang ebb17ac22b vtracer-webapp 2021-07-24 16:37:41 +08:00
Chris Tsang 227850dd83 Release 2021-07-24 15:58:51 +08:00
Chris Tsang e0dac19c31 Article link 2021-07-24 15:42:35 +08:00
Chris Tsang d18d4f8b81 Readme 2021-07-24 00:00:59 +08:00
Chris Tsang 60a4ce579f Release 2021-07-23 23:35:54 +08:00
Chris Tsang c1c09d964c Readme 2021-07-23 23:35:54 +08:00
Chris Tsang c2a5626afa 0.4.0 2021-07-23 23:35:54 +08:00
Bobby Ng d0593e716a SVG path string numeric precision 2021-07-23 23:35:45 +08:00
Chris Tsang 9ce7df176a Update .gitattributes 2021-07-23 18:29:12 +08:00
Chris Tsang 5928c0a7f5 Update screenshots 2021-03-01 21:13:24 +08:00
Chris Tsang b2c95a50cd Update README.md 2021-03-01 21:13:24 +08:00
Chris Tsang bcd3ffb40e Update COPYRIGHT 2021-02-07 14:22:26 +08:00
Chris Tsang 20da3efd3c Create .gitattributes 2021-01-25 00:57:44 +08:00
Chris Tsang 0c27d1f06a Update README.md 2021-01-25 00:53:01 +08:00
Chris Tsang 0c7c7fc808 Demo fixup 2021-01-24 22:12:01 +08:00
Chris Tsang 97ac767844 0.3.0 2021-01-24 21:31:55 +08:00
Chris Tsang 0b37051d40 Cutout mode 2021-01-24 21:31:08 +08:00
Chris Tsang 2695d7b59b Release (with cutout mode) 2021-01-24 21:17:18 +08:00
Chris Tsang 433a68d6d6 Update to visioncortex 0.4.0 2020-12-19 01:34:01 +08:00
Chris Tsang bb3b66780b UI tweaks 2020-12-18 00:32:35 +08:00
Chris Tsang 049ba7f503 UI tweaks 2020-12-18 00:19:57 +08:00
Chris Tsang 9d67b7c554 Release 2020-12-18 00:14:55 +08:00
Chris Tsang fa5d2906c0 Change sample artwork 2020-12-09 15:21:12 +08:00
Chris Tsang fae37a709e Update README.md 2020-12-09 14:51:15 +08:00
Chris Tsang 5c9d5cd757 Example for pixel art 2020-12-09 14:36:07 +08:00
Chris Tsang 1b49880f85 Create rust.yml 2020-11-15 21:10:13 +08:00
Chris Tsang 0b0d69839a Release 2020-11-15 17:33:32 +08:00
Chris Tsang 7ea4600176 Readme 2020-11-15 16:54:46 +08:00
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
48 changed files with 1115 additions and 264 deletions
+1
View File
@@ -0,0 +1 @@
docs/** linguist-generated
+117
View File
@@ -0,0 +1,117 @@
# This file is autogenerated by maturin v1.2.3
# To update, run
#
# maturin generate-ci github
#
name: CI
on:
push:
tags:
- '*'
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
linux:
runs-on: ubuntu-latest
strategy:
matrix:
target: [x86_64, x86, aarch64, armv7, s390x, ppc64le]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist --find-interpreter
sccache: 'true'
manylinux: auto
- name: Upload wheels
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
windows:
runs-on: windows-latest
strategy:
matrix:
target: [x64, x86]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.10'
architecture: ${{ matrix.target }}
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist --find-interpreter
sccache: 'true'
- name: Upload wheels
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
macos:
runs-on: macos-latest
strategy:
matrix:
target: [x86_64, aarch64]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist --find-interpreter
sccache: 'true'
- name: Upload wheels
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
sdist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build sdist
uses: PyO3/maturin-action@v1
with:
command: sdist
args: --out dist
- name: Upload sdist
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
release:
name: Release
runs-on: ubuntu-latest
if: "startsWith(github.ref, 'refs/tags/')"
needs: [linux, windows, macos, sdist]
steps:
- uses: actions/download-artifact@v3
with:
name: wheels
- name: Publish to PyPI
uses: PyO3/maturin-action@v1
env:
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
with:
command: upload
args: --non-interactive --skip-existing *
+22
View File
@@ -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
+30
View File
@@ -0,0 +1,30 @@
# 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.6.0 - 2023-09-08
* Python Binding
## 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
+3 -2
View File
@@ -1,6 +1,7 @@
[workspace]
members = [
"cmdapp",
"cmdapp",
"webapp",
]
]
resolver = "2"
+25
View File
@@ -0,0 +1,25 @@
Copyright (c) 2022 Tsang Hao Fung
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
+108
View File
@@ -0,0 +1,108 @@
<div align="center">
<img src="docs/images/visioncortex-banner.png">
<h1>VTracer</h1>
<p>
<strong>Raster to Vector Graphics Converter built on top of visioncortex</strong>
</p>
<h3>
<a href="//www.visioncortex.org/vtracer-docs">Article</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.
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 [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).
## 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 + wasm platform.
![screenshot](docs/images/screenshot-01.png)
![screenshot](docs/images/screenshot-02.png)
## Cmd App
```sh
visioncortex VTracer 0.4.0
A cmd app to convert images into vector graphics.
USAGE:
vtracer [OPTIONS] --input <input> --output <output>
FLAGS:
-h, --help Prints help information
-V, --version Prints version information
OPTIONS:
--colormode <color_mode> True color image `color` (default) or Binary image `bw`
-p, --color_precision <color_precision> Number of significant bits to use in an RGB channel
-c, --corner_threshold <corner_threshold> Minimum momentary angle (degree) to be considered a corner
-f, --filter_speckle <filter_speckle> Discard patches smaller than X px in size
-g, --gradient_step <gradient_step> Color difference between gradient layers
--hierarchical <hierarchical>
Hierarchical clustering `stacked` (default) or non-stacked `cutout`. Only applies to color mode.
-i, --input <input> Path to input raster image
-m, --mode <mode> Curver fitting mode `pixel`, `polygon`, `spline`
-o, --output <output> Path to output vector graphics
--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
```
./vtracer --input input.jpg --output output.svg
```
## Library
The library can be found on [crates.io/vtracer](//crates.io/crates/vtracer) and [crates.io/vtracer-webapp](//crates.io/crates/vtracer-webapp).
## Install
Download pre-built binaries from [Releases](https://github.com/visioncortex/vtracer/releases).
or
Install from source (Rust toolchain needed):
```
cargo install vtracer
```
## In the wild
VTracer is used by the following products (feel free to add yours to the list):
<table>
<tbody>
<tr>
<td><a href="https://logo.aliyun.com/logo#/name"><img src="docs/images/aliyun-logo.png" width="250"/></a>
<br>Smart logo design
</td>
<td></td>
</tr>
</tbody>
</table>
+27
View File
@@ -0,0 +1,27 @@
# Version 0.6.0 (2023-09-08)
- Python bindings
# Version 0.5.0 (2022-10-09)
- Handle transparent png images
# Version 0.4.0 (2021-07-23)
- SVG path string numeric precision
# Version 0.3.0 (2021-01-24)
- Added cutout mode
# Version 0.2.0 (2020-11-15)
- Use relative & closed paths
# Version 0.1.1 (2020-11-01)
- SVG namespace
# Version 0.1.0 (2020-10-31)
- Initial release
-52
View File
@@ -1,52 +0,0 @@
![logo](docs/images/visioncortex-banner.png)
# visioncortex VTracer
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 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.
A technical description of the algorithm is on [visioncortex.org/vtracer-docs](//www.visioncortex.org/vtracer-docs).
## 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.
![screenshot](docs/images/screenshot-01.png)
![screenshot](docs/images/screenshot-02.png)
## Command Line
```
visioncortex VTracer
A cmd app to convert images into vector graphics.
USAGE:
vtracer [OPTIONS] --input <input> --output <output>
FLAGS:
-h, --help Prints help information
-V, --version Prints version information
OPTIONS:
--colormode <color_mode> True color image `color` (default) or Binary image `bw`
-p, --color_precision <color_precision> Number of significant bits to use in an RGB channel
-c, --corner_threshold <corner_threshold> Minimum momentary angle (degree) to be considered a corner
-f, --filter_speckle <filter_speckle> Discard patches smaller than X px in size
-g, --gradient_step <gradient_step> Color difference between gradient layers
-i, --input <input> Path to input raster image
-m, --mode <mode> Curver fitting mode `pixel`, `polygon`, `spline`
-o, --output <output> Path to output vector graphics
--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
```
./vtracer --input input.jpg --output output.svg
```
+9 -4
View File
@@ -1,8 +1,8 @@
[package]
name = "vtracer"
version = "0.1.0"
authors = ["Chris Tsang <tyt2y7@gmail.com>"]
edition = "2018"
version = "0.6.3"
authors = ["Chris Tsang <chris.2y3@outlook.com>"]
edition = "2021"
description = "A cmd app to convert images into vector graphics."
license = "MIT OR Apache-2.0"
homepage = "http://www.visioncortex.org/vtracer"
@@ -13,4 +13,9 @@ keywords = ["svg", "computer-graphics"]
[dependencies]
clap = "2.33.3"
image = "0.23.10"
visioncortex = "0.2.0"
visioncortex = { version = "0.8.0" }
fastrand = "1.8"
pyo3 = { version = "0.19.0", optional = true }
[features]
python-binding = ["pyo3"]
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2020 Tsang Hao Fung
Copyright (c) 2022 Tsang Hao Fung
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
+28
View File
@@ -0,0 +1,28 @@
[project]
name = "vtracer"
version = "0.6.3"
description = "Python bindings for the Rust Vtracer raster-to-vector library"
authors = [ { name = "Chris Tsang", email = "chris.2y3@outlook.com" } ]
readme = "vtracer/README.md"
requires-python = ">=3.7"
license = "MIT"
classifiers = [
"Programming Language :: Rust",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
[dependencies]
python = "^3.7"
[dev-dependencies]
maturin = "^1.2"
[build-system]
requires = ["maturin>=1.2,<2.0"]
build-backend = "maturin"
[tool.maturin]
features = ["pyo3/extension-module"]
compatibility = "linux"
sdist-include = ["../LICENSE", "../README.md"]
+69 -14
View File
@@ -1,7 +1,7 @@
use std::str::FromStr;
use std::path::PathBuf;
use clap::{Arg, App};
use visioncortex::path::PathSimplifyMode;
use visioncortex::PathSimplifyMode;
pub enum Preset {
Bw,
@@ -9,18 +9,22 @@ pub enum Preset {
Photo
}
#[derive(Debug)]
pub enum ColorMode {
Color,
Binary,
}
pub enum Hierarchical {
Stacked,
Cutout,
}
/// Converter config
#[derive(Debug)]
pub struct Config {
pub input_path: PathBuf,
pub output_path: PathBuf,
pub color_mode: ColorMode,
pub hierarchical: Hierarchical,
pub filter_speckle: usize,
pub color_precision: i32,
pub layer_difference: i32,
@@ -29,13 +33,14 @@ pub struct Config {
pub length_threshold: f64,
pub max_iterations: usize,
pub splice_threshold: i32,
pub path_precision: Option<u32>,
}
#[derive(Debug)]
pub(crate) struct ConverterConfig {
pub input_path: PathBuf,
pub output_path: PathBuf,
pub color_mode: ColorMode,
pub hierarchical: Hierarchical,
pub filter_speckle_area: usize,
pub color_precision_loss: i32,
pub layer_difference: i32,
@@ -44,6 +49,7 @@ pub(crate) struct ConverterConfig {
pub length_threshold: f64,
pub max_iterations: usize,
pub splice_threshold: f64,
pub path_precision: Option<u32>,
}
impl Default for Config {
@@ -52,6 +58,7 @@ impl Default for Config {
input_path: PathBuf::default(),
output_path: PathBuf::default(),
color_mode: ColorMode::Color,
hierarchical: Hierarchical::Stacked,
mode: PathSimplifyMode::Spline,
filter_speckle: 4,
color_precision: 6,
@@ -60,6 +67,7 @@ impl Default for Config {
length_threshold: 4.0,
splice_threshold: 45,
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 {
type Err = String;
@@ -100,7 +120,8 @@ fn path_simplify_mode_from_str(s: &str) -> PathSimplifyMode {
impl Config {
pub fn from_args() -> Self {
let app = App::new("visioncortex VTracer").about("A cmd app to convert images into vector graphics.");
let app = App::new("visioncortex VTracer ".to_owned() + env!("CARGO_PKG_VERSION"))
.about("A cmd app to convert images into vector graphics.");
let app = app.arg(Arg::with_name("input")
.long("input")
@@ -121,6 +142,14 @@ impl Config {
.takes_value(true)
.help("True color image `color` (default) or Binary image `bw`"));
let app = app.arg(Arg::with_name("hierarchical")
.long("hierarchical")
.takes_value(true)
.help(
"Hierarchical clustering `stacked` (default) or non-stacked `cutout`. \
Only applies to color mode. "
));
let app = app.arg(Arg::with_name("preset")
.long("preset")
.takes_value(true)
@@ -168,6 +197,11 @@ impl Config {
.takes_value(true)
.help("Curver fitting mode `pixel`, `polygon`, `spline`"));
let app = app.arg(Arg::with_name("path_precision")
.long("path_precision")
.takes_value(true)
.help("Number of decimal places to use in path string"));
// Extract matches
let matches = app.get_matches();
@@ -186,6 +220,10 @@ impl Config {
config.color_mode = ColorMode::from_str(if value.trim() == "bw" || value.trim() == "BW" {"binary"} else {"color"}).unwrap()
}
if let Some(value) = matches.value_of("hierarchical") {
config.hierarchical = Hierarchical::from_str(value).unwrap()
}
if let Some(value) = matches.value_of("mode") {
let value = value.trim();
config.mode = path_simplify_mode_from_str(if value == "pixel" {
@@ -195,19 +233,19 @@ impl Config {
} else if value == "spline" {
"spline"
} else {
panic!("Parser Error: Curve fitting mode is invalid with value {}", value);
panic!("Parser Error: Curve fitting mode is invalid: {}", value);
});
}
if let Some(value) = matches.value_of("filter_speckle") {
if value.trim().parse::<usize>().is_ok() { // is numeric
let value = value.trim().parse::<usize>().unwrap();
if value < 1 || value > 16 {
panic!("Out of Range Error: Filter speckle is invalid at {}. It must be within [1,16].", value);
if value > 16 {
panic!("Out of Range Error: Filter speckle is invalid at {}. It must be within [0,16].", value);
}
config.filter_speckle = value;
} else {
panic!("Parser Error: Filter speckle is not a positive integer with value {}.", value);
panic!("Parser Error: Filter speckle is not a positive integer: {}.", value);
}
}
@@ -219,7 +257,7 @@ impl Config {
}
config.color_precision = value;
} else {
panic!("Parser Error: Color precision is not an integer with value {}.", value);
panic!("Parser Error: Color precision is not an integer: {}.", value);
}
}
@@ -231,7 +269,7 @@ impl Config {
}
config.layer_difference = value;
} else {
panic!("Parser Error: Gradient step is not an integer with value {}.", value);
panic!("Parser Error: Gradient step is not an integer: {}.", value);
}
}
@@ -243,7 +281,7 @@ impl Config {
}
config.corner_threshold = value
} else {
panic!("Parser Error: Corner threshold is not numeric with value {}.", value);
panic!("Parser Error: Corner threshold is not numeric: {}.", value);
}
}
@@ -255,7 +293,7 @@ impl Config {
}
config.length_threshold = value;
} else {
panic!("Parser Error: Segment length is not numeric with value {}.", value);
panic!("Parser Error: Segment length is not numeric: {}.", value);
}
}
@@ -267,7 +305,16 @@ impl Config {
}
config.splice_threshold = value;
} 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);
}
}
@@ -282,6 +329,7 @@ impl Config {
input_path,
output_path,
color_mode: ColorMode::Binary,
hierarchical: Hierarchical::Stacked,
filter_speckle: 4,
color_precision: 6,
layer_difference: 16,
@@ -290,11 +338,13 @@ impl Config {
length_threshold: 4.0,
max_iterations: 10,
splice_threshold: 45,
path_precision: Some(8),
},
Preset::Poster => Self {
input_path,
output_path,
color_mode: ColorMode::Color,
hierarchical: Hierarchical::Stacked,
filter_speckle: 4,
color_precision: 8,
layer_difference: 16,
@@ -303,11 +353,13 @@ impl Config {
length_threshold: 4.0,
max_iterations: 10,
splice_threshold: 45,
path_precision: Some(8),
},
Preset::Photo => Self {
input_path,
output_path,
color_mode: ColorMode::Color,
hierarchical: Hierarchical::Stacked,
filter_speckle: 10,
color_precision: 8,
layer_difference: 48,
@@ -316,6 +368,7 @@ impl Config {
length_threshold: 4.0,
max_iterations: 10,
splice_threshold: 45,
path_precision: Some(8),
}
}
}
@@ -325,6 +378,7 @@ impl Config {
input_path: self.input_path,
output_path: self.output_path,
color_mode: self.color_mode,
hierarchical: self.hierarchical,
filter_speckle_area: self.filter_speckle * self.filter_speckle,
color_precision_loss: 8 - self.color_precision,
layer_difference: self.layer_difference,
@@ -333,6 +387,7 @@ impl Config {
length_threshold: self.length_threshold,
max_iterations: self.max_iterations,
splice_threshold: deg2rad(self.splice_threshold),
path_precision: self.path_precision,
}
}
}
+128 -25
View File
@@ -1,10 +1,17 @@
use std::path::PathBuf;
use std::{fs::File, io::Write};
use visioncortex::{Color, ColorImage, ColorName, color_clusters::RunnerConfig, color_clusters::Runner};
use super::config::{Config, ColorMode, ConverterConfig};
use fastrand::Rng;
use visioncortex::{Color, ColorImage, ColorName};
use visioncortex::color_clusters::{Runner, RunnerConfig, KeyingAction, HIERARCHICAL_MAX};
use super::config::{Config, ColorMode, Hierarchical, ConverterConfig};
use super::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
pub fn convert_image_to_svg(config: Config) -> Result<(), String> {
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> {
let (img, width, height);
let (mut img, width, height);
match read_image(config.input_path) {
Ok(values) => {
img = values.0;
@@ -25,7 +92,24 @@ fn color_image_to_svg(config: ConverterConfig) -> Result<(), String> {
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 {
diagonal: config.layer_difference == 0,
hierarchical: HIERARCHICAL_MAX,
batch_size: 25600,
good_min_area: config.filter_speckle_area,
good_max_area: (width * height),
@@ -33,16 +117,44 @@ fn color_image_to_svg(config: ConverterConfig) -> Result<(), String> {
is_same_color_b: 1,
deepen_diff: config.layer_difference,
hollow_neighbours: 1,
key_color,
keying_action: if matches!(config.hierarchical, Hierarchical::Cutout) {
KeyingAction::Keep
} else {
KeyingAction::Discard
},
}, img);
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 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() {
let cluster = view.get_cluster(cluster_index);
let svg_path = cluster.to_svg_path(
let paths = cluster.to_compound_path(
&view,
false,
config.mode,
@@ -51,22 +163,14 @@ fn color_image_to_svg(config: ConverterConfig) -> Result<(), String> {
config.max_iterations,
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);
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(())
write_svg(svg, config.output_path)
}
fn binary_image_to_svg(config: ConverterConfig) -> Result<(), String> {
let (img, width, height);
match read_image(config.input_path) {
Ok(values) => {
@@ -79,20 +183,19 @@ fn binary_image_to_svg(config: ConverterConfig) -> Result<(), String> {
let img = img.to_binary_image(|x| x.r < 128);
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() {
let cluster = clusters.get_cluster(i);
if cluster.size() >= config.filter_speckle_area {
let svg_path = cluster.to_svg_path(
let paths = cluster.to_compound_path(
config.mode,
config.corner_threshold,
config.length_threshold,
config.max_iterations,
config.splice_threshold,
);
let color = Color::color(&ColorName::Black);
svg.add_path(svg_path, color);
svg.add_path(paths, Color::color(&ColorName::Black));
}
}
@@ -102,7 +205,7 @@ fn binary_image_to_svg(config: ConverterConfig) -> Result<(), String> {
fn read_image(input_path: PathBuf) -> Result<(ColorImage, usize, usize), String> {
let img = image::open(input_path);
let img = match img {
Ok(file) => file.to_rgba(),
Ok(file) => file.to_rgba8(),
Err(_) => return Err(String::from("No image file found at specified input path")),
};
@@ -118,8 +221,8 @@ fn write_svg(svg: SvgFile, output_path: PathBuf) -> Result<(), String> {
Ok(file) => 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(())
}
+5 -1
View File
@@ -11,7 +11,11 @@
mod config;
mod converter;
mod svg;
#[cfg(feature = "python-binding")]
mod python;
pub use config::*;
pub use converter::*;
pub use svg::*;
pub use svg::*;
#[cfg(feature = "python-binding")]
pub use python::*;
+81
View File
@@ -0,0 +1,81 @@
use pyo3::prelude::*;
use visioncortex::{PathSimplifyMode};
use super::converter::*;
/// Python binding
#[pyfunction]
fn convert_image_to_svg_py( image_path: &str,
out_path: &str,
colormode: Option<&str>, // "color" or "binary"
hierarchical: Option<&str>, // "stacked" or "cutout"
mode: Option<&str>, // "polygon", "spline", "none"
filter_speckle: Option<usize>, // default: 4
color_precision: Option<i32>, // default: 6
layer_difference: Option<i32>, // default: 16
corner_threshold: Option<i32>, // default: 60
length_threshold: Option<f64>, // in [3.5, 10] default: 4.0
max_iterations: Option<usize>, // default: 10
splice_threshold: Option<i32>, // default: 45
path_precision: Option<u32> // default: 8
) -> PyResult<()> {
let input_path = PathBuf::from(image_path);
let output_path = PathBuf::from(out_path);
// TODO: enforce color mode with an enum so that we only
// accept the strings 'color' or 'binary'
let color_mode = match colormode.unwrap_or("color") {
"color" => ColorMode::Color,
"binary" => ColorMode::Binary,
_ => ColorMode::Color,
};
let hierarchical = match hierarchical.unwrap_or("stacked") {
"stacked" => Hierarchical::Stacked,
"cutout" => Hierarchical::Cutout,
_ => Hierarchical::Stacked,
};
let mode = match mode.unwrap_or("spline") {
"spline" => PathSimplifyMode::Spline,
"polygon" => PathSimplifyMode::Polygon,
"none" => PathSimplifyMode::None,
_ => PathSimplifyMode::Spline,
};
let filter_speckle = filter_speckle.unwrap_or(4);
let color_precision = color_precision.unwrap_or(6);
let layer_difference = layer_difference.unwrap_or(16);
let corner_threshold = corner_threshold.unwrap_or(60);
let length_threshold = length_threshold.unwrap_or(4.0);
let splice_threshold = splice_threshold.unwrap_or(45);
let max_iterations = max_iterations.unwrap_or(10);
let config = Config {
input_path,
output_path,
color_mode,
hierarchical,
filter_speckle,
color_precision,
layer_difference,
mode,
corner_threshold,
length_threshold,
max_iterations,
splice_threshold,
path_precision,
..Default::default()
};
convert_image_to_svg(config).unwrap();
Ok(())
}
/// A Python module implemented in Rust.
#[pymodule]
fn vtracer(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(convert_image_to_svg_py, m)?)?;
Ok(())
}
+45 -23
View File
@@ -1,43 +1,65 @@
use visioncortex::Color;
pub struct SvgPath {
path: String,
color: Color,
}
use std::fmt;
use visioncortex::{Color, CompoundPath, PointF64};
pub struct SvgFile {
patches: Vec<SvgPath>,
width: usize,
height: usize,
pub paths: Vec<SvgPath>,
pub width: usize,
pub height: usize,
pub path_precision: Option<u32>,
}
pub struct SvgPath {
pub path: CompoundPath,
pub color: Color,
}
impl SvgFile {
pub fn new(width: usize, height: usize) -> Self {
pub fn new(width: usize, height: usize, path_precision: Option<u32>) -> Self {
SvgFile {
patches: vec![],
paths: vec![],
width,
height,
path_precision,
}
}
pub fn add_path(&mut self, path: String, color: Color) {
self.patches.push(SvgPath {
pub fn add_path(&mut self, path: CompoundPath, color: Color) {
self.paths.push(SvgPath {
path,
color
color,
})
}
}
pub fn to_svg_file(&self) -> String {
let mut result: Vec<String> = vec![format!(r#"<?xml version="1.0" encoding="UTF-8"?>
<svg width="{}" height="{}">
"#, self.width, self.height)];
impl fmt::Display for SvgFile {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, r#"<?xml version="1.0" encoding="UTF-8"?>"#)?;
writeln!(f,
r#"<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="{}" height="{}">"#,
self.width, self.height
)?;
for patch in &self.patches {
let color = patch.color;
result.push(format!("<path d=\"{}\" fill=\"#{:02x}{:02x}{:02x}\"/>\n", patch.path, color.r, color.g, color.b));
for path in &self.paths {
path.fmt_with_precision(f, self.path_precision)?;
};
result.push(String::from("</svg>"));
result.concat()
writeln!(f, "</svg>")
}
}
impl fmt::Display for SvgPath {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
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!(
f, "<path d=\"{}\" fill=\"{}\" transform=\"translate({},{})\"/>",
string, self.color.to_hex_string(),
offset.x, offset.y
)
}
}
+75
View File
@@ -0,0 +1,75 @@
<div align="center">
<img src="https://github.com/visioncortex/vtracer/raw/master/docs/images/visioncortex-banner.png">
<h1>VTracer: Python Binding</h1>
<p>
<strong>Raster to Vector Graphics Converter built on top of visioncortex</strong>
</p>
<h3>
<a href="//www.visioncortex.org/vtracer-docs">Article</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.
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 [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).
## Install (Python)
```shell
pip install vtracer
```
### Usage (Python)
```python
import vtracer
input_path = "/path/to/some_file.jpg"
output_path = "/path/to/some_file.vtracer.jpg"
# Minimal example: use all default values, generate a multicolor SVG
vtracer.convert_image_to_svg_py(inp, out)
# Single-color example. Good for line art, and much faster than full color:
vtracer.convert_image_to_svg_py(inp, out, colormode='binary')
# All the bells & whistles
vtracer.convert_image_to_svg_py(inp,
out,
colormode = 'color', # ["color"] or "binary"
hierarchical = 'stacked', # ["stacked"] or "cutout"
mode = 'spline', # ["spline"] "polygon", or "none"
filter_speckle = 4, # default: 4
color_precision = 6, # default: 6
layer_difference = 16, # default: 16
corner_threshold = 60, # default: 60
length_threshold = 4.0, # in [3.5, 10] default: 4.0
max_iterations = 10, # default: 10
splice_threshold = 45, # default: 45
path_precision = 3 # default: 8
)
```
## Rust Library
The (Rust) library can be found on [crates.io/vtracer](//crates.io/crates/vtracer) and [crates.io/vtracer-webapp](//crates.io/crates/vtracer-webapp).
+1
View File
@@ -0,0 +1 @@
from .vtracer import convert_image_to_svg_py
+17
View File
@@ -0,0 +1,17 @@
from typing import Optional
def convert_image_to_svg_py(image_path: str,
out_path: str,
colormode: Optional[str] = None, # ["color"] or "binary"
hierarchical: Optional[str] = None, # ["stacked"] or "cutout"
mode: Optional[str] = None, # ["spline"], "polygon", "none"
filter_speckle: Optional[int] = None, # default: 4
color_precision: Optional[int] = None, # default: 6
layer_difference: Optional[int] = None, # default: 16
corner_threshold: Optional[int] = None, # default: 60
length_threshold: Optional[float] = None, # in [3.5, 10] default: 4.0
max_iterations: Optional[int] = None, # default: 10
splice_threshold: Optional[int] = None, # default: 45
path_precision: Optional[int] = None, # default: 8
) -> None:
...
+14 -3
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
The content in the /docs directory is for GitHub pages, and is not covered under open source licenses.
Binary file not shown.
-23
View File
@@ -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&amp;utm_medium=referral&amp;utm_content=creditCopyText">averie woodard</a> on <a href="https://unsplash.com/s/photos/portrait?utm_source=unsplash&amp;utm_medium=referral&amp;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&amp;utm_medium=referral&amp;utm_content=creditCopyText">Angel Luciano</a> on <a href="https://unsplash.com/s/photos/dog?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></span>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Generated Vendored
+1 -1
View File
@@ -258,7 +258,7 @@
/******/ promises.push(installedWasmModuleData);
/******/ else {
/******/ 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":"589cb57662b50620abca"}[wasmModuleId] + ".module.wasm");
/******/ var promise;
/******/ if(importObject instanceof Promise && typeof WebAssembly.compileStreaming === 'function') {
/******/ promise = Promise.all([WebAssembly.compileStreaming(req), importObject]).then(function(items) {
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

+44
View File
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1783.4 441.4" style="enable-background:new 0 0 1783.4 441.4;" xml:space="preserve">
<style type="text/css">
.st0{fill:#A1FCFE;}
.st1{fill:#010357;}
</style>
<rect class="st0" width="1783.4" height="441.4"/>
<g>
<path class="st1" d="M61.4,393.2V73.8H250V113H109v100.4h124.2v39.7H109v140.1L61.4,393.2L61.4,393.2z"/>
<path class="st1" d="M280,393.2V159.7h45.3v233.5L280,393.2L280,393.2z M330.9,92.4c0-15.5-12.5-28-28-28s-28,12.5-28,28
s12.5,28,28,28S330.9,107.9,330.9,92.4z"/>
<path class="st1" d="M383.5,338.6V57.9h45.3v278.8c0,21,3.7,26.1,18.7,26.1c6.1,0,11.7-0.5,16.8-1.4v30.4
c-9.8,2.8-20.1,4.2-29.9,4.2C400.3,396,383.5,376.9,383.5,338.6z"/>
<path class="st1" d="M480.6,275.5c0-78.5,41.6-121.4,110.2-121.4c31.3,0,55.6,9.8,72.9,29.9c17.3,19.6,26.2,45.3,26.2,77.5
c0,7-0.5,14.9-1.9,24.3H525.4c0.9,50.9,22.4,76.6,64.4,76.6c31.8,0,53.2-14.5,53.2-42H687c0,48.6-39.7,77.5-97.6,77.5
C521.7,397.9,480.6,357.3,480.6,275.5z M645,251.7c-0.9-41.6-19.6-62.1-55.6-62.1c-39.7,0-59.8,24.3-63.5,62.1H645z"/>
<path class="st1" d="M720.7,323.2h44.8c1.9,28.5,18.7,38.8,51.8,38.8c28.5,0,46.7-15,46.7-35c0-20.6-16.8-28.5-48.1-36l-13.1-2.8
l-14.5-3.7l-12.6-4.2c-6.1-1.9-10.7-3.3-13.1-4.7c-5.1-3.3-17.3-8.9-21.5-14.5c-7.5-8.9-15.4-21.5-14-38.8c0-21,8.4-37.8,25.2-50
c17.3-12.1,37.8-18.2,62.1-18.2c55.6,0,91.5,22.4,91.5,72.4h-43c-0.5-26.6-16.8-36.4-49-36.4c-24.3,0-40.6,11.2-40.6,31.3
c0,18.7,15.9,25.7,49,33.6l5.6,1.4c10.3,2.3,18.2,4.7,23.3,6.1c5.1,1.4,12.1,4.2,21,8.4c23.8,10.3,34.6,25.7,36.9,56
c0,43.4-37.8,71-92.5,71C754.8,397.9,720.7,371.7,720.7,323.2z"/>
<path class="st1" d="M964,336.7V192.4h-39.2v-32.7h39.7V94.3h44.8v65.4h64v32.7h-64v143.4c0,17.3,8.9,25.7,26.2,25.7
c14.5,0,28-1.9,39.7-5.1v34.6c-13.6,4.2-28,6.1-43.4,6.1C986.9,397,964,375.5,964,336.7z"/>
<path class="st1" d="M1095.3,325.5c0-44.8,35-69.6,93.4-69.6h58.4v-20.6c0-28.5-16.8-45.8-50-45.8c-35.5,0-51.8,16.8-51.8,41.6
h-43.4c0-44.8,33.2-77.1,95.3-77.1c61.2,0,95.7,30.4,95.7,83.1v92.5c0,26.1,3.7,32.2,17.3,33.6h6.5v29.4
c-4.7,1.4-11.7,2.3-21.5,2.3c-26.6,0-41.6-15.9-43.4-41.6c-8.9,23.8-36.4,44.4-74.3,44.4C1127.5,397.9,1095.3,368.5,1095.3,325.5z
M1247.1,297.5v-9.3h-58.4c-32.2,0-48.6,12.6-48.6,37.4c0,22.9,14,36.9,41.6,36.9C1222.3,362.4,1247.1,334.4,1247.1,297.5z"/>
<path class="st1" d="M1351.7,395.1V161.6h43.9l0.9,36.4h0.9c11.7-24.8,34.1-40.6,62.1-40.6c8.4,0,15.9,0.9,22.9,3.3v38.8
c-7.5-1.4-15.9-1.9-25.7-1.9c-33.2,0-59.3,28-59.3,72.9v124.7H1351.7z"/>
<g>
<g>
<rect x="1549" y="146.5" transform="matrix(0.866 -0.5 0.5 0.866 137.8024 841.2704)" class="st1" width="179.4" height="34"/>
</g>
<g>
<rect x="1621.7" y="73.8" transform="matrix(0.5 -0.866 0.866 0.5 677.7373 1500.9166)" class="st1" width="34" height="179.4"/>
</g>
<g>
<rect x="1621.7" y="73.8" class="st1" width="34" height="179.4"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 154 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

+47 -19
View File
@@ -11,6 +11,13 @@
<!-- UIkit JS -->
<script src="./uikit/js/uikit.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>
html, body {
width: 100%;
@@ -72,8 +79,7 @@
}
#canvas-container {
position: relative;
width: 100%;
height: 100%;
height: max-content;
}
#svg, #frame {
position: absolute;
@@ -84,7 +90,7 @@
stroke: #ff0;
}
#droptext {
height: 100%;
height: 480px;
color: #CC972E;
}
input[type=range]::-webkit-slider-thumb {
@@ -100,11 +106,11 @@
background: #00275D;
border-color: #CC972E;
}
#gallery, #options {
#drop, #gallery, #options {
border-top: 1px solid #686868;
}
#gallery {
border-bottom: 1px solid #686868;
padding-bottom: 0;
}
.galleryitem {
cursor: pointer;
@@ -133,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-light" class="uk-logo" href="//www.visioncortex.org"><img style="height: 50px; margin: -30px 0 -25px 0;" src="./assets/visioncortex-logo-light.svg"></a>
&nbsp;
VTracer
/ VTracer
</div>
<div class="uk-navbar-right uk-padding-small">
<a class="uk-button uk-button-default" href="//www.visioncortex.org/vtracer-docs">Article</a>
&nbsp;&nbsp;
<a class="uk-button uk-button-default" href="//github.com/visioncortex/vtracer">GitHub</a>
&nbsp;&nbsp;
<a id="export" class="uk-button uk-button-primary">Download as SVG</a>
@@ -145,6 +153,15 @@
<progress id="progressbar" class="uk-progress uk-align-right" value="0" max="100" style="width: 98%;"></progress>
</div>
<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 class="uk-inline" uk-slider>
<ul id="galleryslider" class="uk-slider-items uk-child-width-1-6 uk-grid uk-grid-small">
@@ -153,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-right uk-position-small uk-hidden-hover" href="#" uk-slidenav-next uk-slider-item="next"></a>
</div>
<a class="uk-align-right uk-text-meta" style="margin:-5px;" uk-toggle="target: #credits-modal">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 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>
<a class="uk-align-right uk-text-meta" style="margin:-5px;" uk-toggle="target: #credits-modal">Photo Credits</a>
</div>
</div>
<div id="options" class="uk-width-1-4 uk-height-1-1 uk-grid-small uk-flex uk-flex-center" uk-grid>
@@ -186,6 +194,13 @@
</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 uk-tooltip="pos: left; title: Discard patches small than X px in size">
Filter Speckle <span class="uk-text-meta">(Cleaner)</span>
@@ -195,7 +210,7 @@
4
</div>
<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="0" max="128" step="1" value="4">
</div>
<div class="clustering-color-options uk-width-1-1 uk-flex uk-flex-right">
@@ -219,7 +234,7 @@
16
</div>
<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 class="uk-width-1-1 uk-flex uk-flex-right">
@@ -262,7 +277,7 @@
<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">
Splice Threshold <span class="uk-text-meta">(More accurate)</span>
Splice Threshold <span class="uk-text-meta">(Less accurate)</span>
</div>
</div>
<div id="splicevalue" class="spline-options uk-width-1-6">
@@ -272,6 +287,19 @@
<input id="splice" class="uk-range" type="range" min="0" max="180" step="1" value="45">
</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>
+5 -4
View File
@@ -1,9 +1,10 @@
[package]
name = "vtracer-webapp"
version = "0.1.0"
authors = ["Chris Tsang <tyt2y7@gmail.com>"]
edition = "2018"
version = "0.4.0"
authors = ["Chris Tsang <chris.2y3@outlook.com>"]
edition = "2021"
description = "A web app to convert images into vector graphics."
license = "MIT OR Apache-2.0"
homepage = "http://www.visioncortex.org/vtracer"
repository = "https://github.com/visioncortex/vtracer/"
categories = ["graphics"]
@@ -21,7 +22,7 @@ console_log = { version = "0.2", features = ["color"] }
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
visioncortex = "0.2.0"
visioncortex = "0.6.0"
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
View File
+4
View File
@@ -1,3 +1,7 @@
<div align="center">
<img src="https://github.com/visioncortex/vtracer/raw/master/docs/images/visioncortex-banner.png">
</div>
# visioncortex VTracer
A web app to convert raster images into vector graphics.
+20
View File
@@ -90,6 +90,13 @@
</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 title="Discard patches small than X px in size">
Filter Speckle <span>(Cleaner)</span>
@@ -178,6 +185,19 @@
<input id="splice" type="range" min="0" max="180" step="1" value="45">
</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>
<script src="./bootstrap.js"></script>
+125 -73
View File
@@ -7,7 +7,7 @@ const svg = document.getElementById('svg');
const img = new Image();
const progress = document.getElementById('progressbar');
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
canvas.style.display = 'none';
@@ -15,22 +15,22 @@ svg.style.display = 'none';
// Paste from clipboard
document.addEventListener('paste', function (e) {
if (e.clipboardData) {
var items = e.clipboardData.items;
if (!items) return;
if (e.clipboardData) {
var items = e.clipboardData.items;
if (!items) return;
//access data directly
for (var i = 0; i < items.length; i++) {
if (items[i].type.indexOf("image") !== -1) {
//image
var blob = items[i].getAsFile();
var URLObj = window.URL || window.webkitURL;
var source = URLObj.createObjectURL(blob);
setSourceAndRestart(source);
}
}
e.preventDefault();
}
//access data directly
for (var i = 0; i < items.length; i++) {
if (items[i].type.indexOf("image") !== -1) {
//image
var blob = items[i].getAsFile();
var URLObj = window.URL || window.webkitURL;
var source = URLObj.createObjectURL(blob);
setSourceAndRestart(source);
}
}
e.preventDefault();
}
});
// Download as SVG
@@ -49,8 +49,10 @@ var presetConfigs = [
{
src: 'assets/samples/K1_drawing.jpg',
clustering_mode: 'binary',
clustering_hierarchical: 'stacked',
filter_speckle: 4,
color_precision: 6,
path_precision: 8,
layer_difference: 16,
mode: 'spline',
corner_threshold: 60,
@@ -62,8 +64,10 @@ var presetConfigs = [
{
src: 'assets/samples/Cityscape Sunset_DFM3-01.jpg',
clustering_mode: 'color',
clustering_hierarchical: 'stacked',
filter_speckle: 4,
color_precision: 8,
path_precision: 8,
layer_difference: 25,
mode: 'spline',
corner_threshold: 60,
@@ -75,8 +79,10 @@ var presetConfigs = [
{
src: 'assets/samples/Gum Tree Vector.jpg',
clustering_mode: 'color',
clustering_hierarchical: 'stacked',
filter_speckle: 4,
color_precision: 8,
path_precision: 8,
layer_difference: 28,
mode: 'spline',
corner_threshold: 60,
@@ -88,8 +94,10 @@ var presetConfigs = [
{
src: 'assets/samples/vectorstock_31191940.png',
clustering_mode: 'color',
clustering_hierarchical: 'stacked',
filter_speckle: 8,
color_precision: 7,
path_precision: 8,
layer_difference: 64,
mode: 'spline',
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',
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&amp;utm_medium=referral&amp;utm_content=creditCopyText">averie woodard</a> on <a href="https://unsplash.com/s/photos/portrait?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></span>',
},
{
src: 'assets/samples/angel-luciano-LATYeZyw88c-unsplash-s.jpg',
clustering_mode: 'color',
clustering_hierarchical: 'stacked',
filter_speckle: 10,
color_precision: 8,
path_precision: 8,
layer_difference: 48,
mode: 'spline',
corner_threshold: 180,
@@ -124,66 +121,87 @@ var presetConfigs = [
source: 'https://unsplash.com/photos/LATYeZyw88c',
credit: '<span>Photo by <a href="https://unsplash.com/@roaming_angel?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Angel Luciano</a> on <a href="https://unsplash.com/s/photos/dog?utm_source=unsplash&amp;utm_medium=referral&amp;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
for (let i = 0; i < presetConfigs.length; ++i) {
document.getElementById('galleryslider').innerHTML +=
`<li>
<div class="galleryitem uk-panel uk-flex uk-flex-center">
<a href="#">
<img src="${presetConfigs[i].src}" title="${presetConfigs[i].source}">
</a>
</div>
</li>`;
document.getElementById('credits-modal-content').innerHTML +=
`<p>${presetConfigs[i].credit}</p>`;
if (document.getElementById('galleryslider')) {
for (let i = 0; i < presetConfigs.length; i++) {
document.getElementById('galleryslider').innerHTML +=
`<li>
<div class="galleryitem uk-panel uk-flex uk-flex-center">
<a href="#">
<img src="${presetConfigs[i].src}" title="${presetConfigs[i].source}">
</a>
</div>
</li>`;
document.getElementById('credits-modal-content').innerHTML +=
`<p>${presetConfigs[i].credit}</p>`;
}
}
// Function to load a given config WITHOUT restarting
function loadConfig(config) {
mode = config.mode;
clustering_mode = config.clustering_mode;
mode = config.mode;
clustering_mode = config.clustering_mode;
clustering_hierarchical = config.clustering_hierarchical;
globalcorner = config.corner_threshold;
document.getElementById('cornervalue').innerHTML = globalcorner;
document.getElementById('corner').value = globalcorner;
globallength = config.length_threshold;
document.getElementById('lengthvalue').innerHTML = globallength;
document.getElementById('length').value = globallength;
globalsplice = config.splice_threshold;
document.getElementById('splicevalue').innerHTML = globalsplice;
document.getElementById('splice').value = globalsplice;
globalcorner = config.corner_threshold;
document.getElementById('cornervalue').innerHTML = globalcorner;
document.getElementById('corner').value = globalcorner;
globallength = config.length_threshold;
document.getElementById('lengthvalue').innerHTML = globallength;
document.getElementById('length').value = globallength;
globalsplice = config.splice_threshold;
document.getElementById('splicevalue').innerHTML = globalsplice;
document.getElementById('splice').value = globalsplice;
globalfilterspeckle = config.filter_speckle;
document.getElementById('filterspecklevalue').innerHTML = globalfilterspeckle;
document.getElementById('filterspeckle').value = globalfilterspeckle;
globalfilterspeckle = config.filter_speckle;
document.getElementById('filterspecklevalue').innerHTML = globalfilterspeckle;
document.getElementById('filterspeckle').value = globalfilterspeckle;
globalcolorprecision = config.color_precision;
document.getElementById('colorprecisionvalue').innerHTML = globalcolorprecision;
document.getElementById('colorprecision').value = globalcolorprecision;
globalcolorprecision = config.color_precision;
document.getElementById('colorprecisionvalue').innerHTML = globalcolorprecision;
document.getElementById('colorprecision').value = globalcolorprecision;
globallayerdifference = config.layer_difference;
document.getElementById('layerdifferencevalue').innerHTML = globallayerdifference;
document.getElementById('layerdifference').value = globallayerdifference;
globallayerdifference = config.layer_difference;
document.getElementById('layerdifferencevalue').innerHTML = globallayerdifference;
document.getElementById('layerdifference').value = globallayerdifference;
globalpathprecision = config.path_precision;
document.getElementById('pathprecisionvalue').innerHTML = globalpathprecision;
document.getElementById('pathprecision').value = globalpathprecision;
}
// Choose template from gallery
let chooseGalleryButtons = document.querySelectorAll('.galleryitem a');
chooseGalleryButtons.forEach(item => {
item.addEventListener('click', function (e) {
// Load preset template config
let i = Array.prototype.indexOf.call(chooseGalleryButtons, item);
if (presetConfigs.length > i) {
loadConfig(presetConfigs[i]);
}
item.addEventListener('click', function (e) {
// Load preset template config
let i = Array.prototype.indexOf.call(chooseGalleryButtons, item);
if (presetConfigs.length > i) {
loadConfig(presetConfigs[i]);
}
// Set source as specified
setSourceAndRestart(this.firstElementChild.src);
});
// Set source as specified
setSourceAndRestart(this.firstElementChild.src);
});
});
// Upload button
@@ -235,7 +253,8 @@ var globalcorner = parseInt(document.getElementById('corner').value),
globalsplice = parseInt(document.getElementById('splice').value),
globalfilterspeckle = parseInt(document.getElementById('filterspeckle').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
/*
@@ -272,6 +291,16 @@ document.getElementById('clustering-color').addEventListener('click', function (
restart();
}, 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) {
globalfilterspeckle = parseInt(this.value);
document.getElementById('filterspecklevalue').innerHTML = this.value;
@@ -308,6 +337,12 @@ document.getElementById('splice').addEventListener('change', function (e) {
restart();
});
document.getElementById('pathprecision').addEventListener('change', function (e) {
globalpathprecision = parseInt(this.value);
document.getElementById('pathprecisionvalue').innerHTML = this.value;
restart();
});
// Save inputs before unloading
/*
window.addEventListener('beforeunload', function () {
@@ -322,9 +357,19 @@ window.addEventListener('beforeunload', function () {
function setSourceAndRestart(source) {
img.src = source instanceof File ? URL.createObjectURL(source) : source;
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.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();
}
// Show display
@@ -342,6 +387,10 @@ function restart() {
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('polygon').classList.remove('selected');
document.getElementById('spline').classList.remove('selected');
@@ -363,6 +412,7 @@ function restart() {
'svg_id': svg.id,
'mode': mode,
'clustering_mode': clustering_mode,
'hierarchical': clustering_hierarchical,
'corner_threshold': deg2rad(globalcorner),
'length_threshold': globallength,
'max_iterations': 10,
@@ -370,6 +420,7 @@ function restart() {
'filter_speckle': globalfilterspeckle*globalfilterspeckle,
'color_precision': 8-globalcolorprecision,
'layer_difference': globallayerdifference,
'path_precision': globalpathprecision,
});
if (runner) {
runner.stop();
@@ -430,5 +481,6 @@ class ConverterRunner {
stop () {
this.stopped = true;
this.converter.free();
}
}
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "vtracer-app",
"version": "0.1.0",
"description": "VTracer Webapp",
"author": "Chris Tsang <tyt2y7@gmail.com>",
"author": "Chris Tsang <chris.2y3@outlook.com>",
"license": "proprietary",
"private": true,
"main": "index.js",
+1 -1
View File
@@ -46,7 +46,7 @@ impl Canvas {
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 {
pixels: self.get_image_data(x, y, width, height),
width: width as usize,
+7 -6
View File
@@ -1,5 +1,5 @@
use wasm_bindgen::prelude::*;
use visioncortex::{clusters::Clusters, Color, ColorName, PointI32, PathSimplifyMode};
use visioncortex::{clusters::Clusters, Color, ColorName, PathSimplifyMode};
use crate::{canvas::*};
use crate::svg::*;
@@ -17,6 +17,7 @@ pub struct BinaryImageConverterParams {
pub max_iterations: usize,
pub splice_threshold: f64,
pub filter_speckle: usize,
pub path_precision: u32,
}
#[wasm_bindgen]
@@ -54,7 +55,7 @@ impl BinaryImageConverter {
pub fn init(&mut self) {
let width = self.canvas.width() 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);
self.clusters = binary_image.to_clusters(false);
self.canvas.log(&format!(
@@ -69,7 +70,7 @@ impl BinaryImageConverter {
self.canvas.log(&format!("tick {}", self.counter));
let cluster = self.clusters.get_cluster(self.counter);
if cluster.size() >= self.params.filter_speckle {
let svg_path = cluster.to_svg_path(
let paths = cluster.to_compound_path(
self.mode,
self.params.corner_threshold,
self.params.length_threshold,
@@ -77,10 +78,10 @@ impl BinaryImageConverter {
self.params.splice_threshold
);
let color = Color::color(&ColorName::White);
self.svg.prepend_path_with_fill(
&svg_path,
&PointI32::default(),
self.svg.prepend_path(
&paths,
&color,
Some(self.params.path_precision),
);
}
self.counter += 1;
+44 -7
View File
@@ -1,6 +1,6 @@
use wasm_bindgen::prelude::*;
use visioncortex::{PathSimplifyMode, PointI32};
use visioncortex::color_clusters::{IncrementalBuilder, Clusters, Runner, RunnerConfig};
use visioncortex::PathSimplifyMode;
use visioncortex::color_clusters::{IncrementalBuilder, Clusters, Runner, RunnerConfig, HIERARCHICAL_MAX};
use crate::canvas::*;
use crate::svg::*;
@@ -13,6 +13,7 @@ pub struct ColorImageConverterParams {
pub canvas_id: String,
pub svg_id: String,
pub mode: String,
pub hierarchical: String,
pub corner_threshold: f64,
pub length_threshold: f64,
pub max_iterations: usize,
@@ -20,6 +21,7 @@ pub struct ColorImageConverterParams {
pub filter_speckle: usize,
pub color_precision: i32,
pub layer_difference: i32,
pub path_precision: u32,
}
#[wasm_bindgen]
@@ -35,6 +37,7 @@ pub struct ColorImageConverter {
pub enum Stage {
New,
Clustering(IncrementalBuilder),
Reclustering(IncrementalBuilder),
Vectorize(Clusters),
}
@@ -64,8 +67,10 @@ impl ColorImageConverter {
pub fn init(&mut self) {
let width = self.canvas.width() 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 {
diagonal: self.params.layer_difference == 0,
hierarchical: HIERARCHICAL_MAX,
batch_size: 25600,
good_min_area: self.params.filter_speckle,
good_max_area: (width * height) as usize,
@@ -84,6 +89,35 @@ impl ColorImageConverter {
},
Stage::Clustering(builder) => {
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() {
self.stage = Stage::Vectorize(builder.result())
}
@@ -94,17 +128,17 @@ impl ColorImageConverter {
if self.counter < view.clusters_output.len() {
self.canvas.log("Vectorize tick");
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,
self.params.corner_threshold,
self.params.length_threshold,
self.params.max_iterations,
self.params.splice_threshold
);
self.svg.prepend_path_with_fill(
&svg_path,
&PointI32::new(0, 0),
self.svg.prepend_path(
&paths,
&cluster.residue_color(),
Some(self.params.path_precision),
);
self.counter += 1;
false
@@ -124,6 +158,9 @@ impl ColorImageConverter {
Stage::Clustering(builder) => {
builder.progress() / 2
},
Stage::Reclustering(_builder) => {
50
},
Stage::Vectorize(clusters) => {
50 + 50 * self.counter as u32 / clusters.view().clusters_output.len() as u32
}
+3 -1
View File
@@ -7,7 +7,9 @@
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// 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::*;
mod conversion;
+4 -3
View File
@@ -1,5 +1,5 @@
use web_sys::Element;
use visioncortex::{Color, PointI32};
use visioncortex::{Color, CompoundPath, PointF64};
use super::common::document;
pub struct Svg {
@@ -13,11 +13,12 @@ impl Svg {
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, precision: Option<u32>) {
let path = document()
.create_element_ns(Some("http://www.w3.org/2000/svg"), "path")
.unwrap();
path.set_attribute("d", path_string).unwrap();
let (string, offset) = paths.to_svg_string(true, PointF64::default(), precision);
path.set_attribute("d", &string).unwrap();
path.set_attribute(
"transform",
format!("translate({},{})", offset.x, offset.y).as_str(),