24 Commits

Author SHA1 Message Date
Chris Tsang
efa4351b2c Update README.md 2024-09-27 10:43:47 +01:00
Chris Tsang
a46292b5ed Update README.md 2024-09-27 10:43:02 +01:00
Chris Tsang
8889cbc7ea Tweaks
Some checks failed
Rust / build (push) Has been cancelled
2024-09-26 12:59:45 +01:00
Wil Carmon
6b379a02ef Update svg.rs (#92) 2024-09-26 12:58:43 +01:00
Wil Carmon
2635d5b874 Update config.rs (#91)
added #[derive(Clone, Debug)]
2024-09-26 12:58:30 +01:00
Chris Tsang
f6cf3e8705 Refactor 2024-05-30 10:04:23 +01:00
Chris Tsang
36b16de17a Key transparent image in webapp 2024-05-29 15:10:11 +01:00
Chris Tsang
7887c1ebf8 Update readme 2024-05-02 23:57:13 +01:00
Chris Tsang
6fdfec8610 python 0.6.11 2024-05-02 22:58:11 +01:00
York
1aff9a300a Add support for conversion from python bytes (#79)
* Allow conversion from python bytes

* Update function name

* Add convert_pixels_to_svg python function

* Update README.md

* Update README.md
2024-05-02 22:24:56 +01:00
Chris Tsang
ac0a89e08a README 2024-04-20 16:24:41 +01:00
Chris Tsang
b09f71a2b5 LICENSE 2024-04-20 16:21:36 +01:00
Chris Tsang
e4897dfe99 README 2024-04-20 16:16:57 +01:00
Chris Tsang
4544ca740d README 2024-04-20 16:14:21 +01:00
Chris Tsang
3d92586e33 Update CI script 2024-04-20 15:52:58 +01:00
Chris Tsang
725adf5364 Update README.md 2024-03-30 14:46:52 +00:00
Chris Tsang
05f82c7bb5 Test 2024-03-29 19:26:28 +00:00
Chris Tsang
b7ac336b6d Add release script 2024-03-29 19:06:29 +00:00
Chris Tsang
c03a8ffced 0.6.4 2024-03-29 19:01:16 +00:00
Chris Tsang
3223ba56ec Upgrade dependency 2024-03-29 18:59:55 +00:00
Chris Tsang
ddb47e1ad4 Revert "Experiment with idealizing small circles"
This reverts commit c3012c6aef.
2024-03-29 18:58:06 +00:00
Chris Tsang
177797108d Changelog 2024-03-29 18:57:57 +00:00
Chris Tsang
370083f818 0.6.3 2024-03-29 18:57:57 +00:00
Chris Tsang
c3012c6aef Experiment with idealizing small circles 2024-03-29 18:57:57 +00:00
20 changed files with 376 additions and 40 deletions

View File

@@ -3,7 +3,7 @@
# #
# maturin generate-ci github # maturin generate-ci github
# #
name: python name: Python
on: on:
push: push:

30
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: Release
on:
release:
types: [published]
jobs:
release:
strategy:
matrix:
include:
- target: aarch64-unknown-linux-musl
os: ubuntu-latest
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
- target: aarch64-apple-darwin
os: macos-latest
- target: x86_64-apple-darwin
os: macos-latest
- target: x86_64-pc-windows-msvc
os: windows-latest
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: taiki-e/upload-rust-binary-action@v1
with:
bin: vtracer
target: ${{ matrix.target }}
# (required) GitHub token for uploading assets to GitHub Releases.
token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,10 +1,23 @@
name: Rust name: Rust
on: on:
push:
branches: [ master ]
pull_request: pull_request:
branches: [ master ] paths-ignore:
- '**.md'
- '.github/ISSUE_TEMPLATE/**'
push:
paths-ignore:
- '**.md'
- '.github/ISSUE_TEMPLATE/**'
branches:
- master
- 0.*.x
- pr/**/ci
- ci-*
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref || github.run_id }}
cancel-in-progress: true
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always

View File

@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/) The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/). and this project adheres to [Semantic Versioning](http://semver.org/).
## 0.6.4 - 2024-03-29
* Update `visioncortex` version to `0.8.8`
## 0.6.3 - 2023-11-21
* New converter API https://github.com/visioncortex/vtracer/pull/59
## 0.6.1 - 2023-09-23 ## 0.6.1 - 2023-09-23
* Fixed "The two lines are parallel!" * Fixed "The two lines are parallel!"

View File

@@ -1,4 +1,4 @@
Copyright (c) 2023 Tsang Hao Fung Copyright (c) 2024 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

View File

@@ -10,9 +10,9 @@
<h3> <h3>
<a href="https://www.visioncortex.org/vtracer-docs">Article</a> <a href="https://www.visioncortex.org/vtracer-docs">Article</a>
<span> | </span> <span> | </span>
<a href="https://www.visioncortex.org/vtracer/">Demo</a> <a href="https://www.visioncortex.org/vtracer/">Web App</a>
<span> | </span> <span> | </span>
<a href="https://github.com/visioncortex/vtracer/releases/tag/0.6.0">Download</a> <a href="https://github.com/visioncortex/vtracer/releases">Download</a>
</h3> </h3>
<sub>Built with 🦀 by <a href="https://www.visioncortex.org/">The Vision Cortex Research Group</a></sub> <sub>Built with 🦀 by <a href="https://www.visioncortex.org/">The Vision Cortex Research Group</a></sub>
@@ -28,7 +28,7 @@ Comparing to Adobe Illustrator's [Image Trace](https://helpx.adobe.com/illustrat
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. 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](https://www.visioncortex.org/vtracer-docs). Technical descriptions of the [tracing algorithm](https://www.visioncortex.org/vtracer-docs) and [clustering algorithm](https://www.visioncortex.org/impression-docs).
## Web App ## Web App
@@ -71,9 +71,9 @@ OPTIONS:
-s, --splice_threshold <splice_threshold> Minimum angle displacement (degree) to splice a spline -s, --splice_threshold <splice_threshold> Minimum angle displacement (degree) to splice a spline
``` ```
### Install ## Downloads
You can download pre-built binaries from [Releases](https://github.com/visioncortex/vtracer/releases/tag/0.6.0). You can download pre-built binaries from [Releases](https://github.com/visioncortex/vtracer/releases).
You can also install the program from source from [crates.io/vtracer](https://crates.io/crates/vtracer): You can also install the program from source from [crates.io/vtracer](https://crates.io/crates/vtracer):
@@ -81,13 +81,15 @@ You can also install the program from source from [crates.io/vtracer](https://cr
cargo install vtracer cargo install vtracer
``` ```
> You are strongly advised to not download from any other third-party sources
### Usage ### Usage
```sh ```sh
./vtracer --input input.jpg --output output.svg ./vtracer --input input.jpg --output output.svg
``` ```
## Rust Library ### Rust Library
You can install [`vtracer`](https://crates.io/crates/vtracer) as a Rust library. You can install [`vtracer`](https://crates.io/crates/vtracer) as a Rust library.
@@ -95,7 +97,7 @@ You can install [`vtracer`](https://crates.io/crates/vtracer) as a Rust library.
cargo add vtracer cargo add vtracer
``` ```
## Python Library ### Python Library
Since `0.6`, [`vtracer`](https://pypi.org/project/vtracer/) is also packaged as Python native extensions, thanks to the awesome [pyo3](https://github.com/PyO3/pyo3) project. Since `0.6`, [`vtracer`](https://pypi.org/project/vtracer/) is also packaged as Python native extensions, thanks to the awesome [pyo3](https://github.com/PyO3/pyo3) project.
@@ -105,7 +107,7 @@ pip install vtracer
## In the wild ## In the wild
VTracer is used by the following projects (feel free to add yours!): VTracer is used by the following products (open a PR to add yours):
<table> <table>
<tbody> <tbody>
@@ -118,11 +120,19 @@ VTracer is used by the following projects (feel free to add yours!):
</tbody> </tbody>
</table> </table>
## Anecdotes ## Citations
> The following content is an excerpt from my [unpublished](https://github.com/sponsors/tyt2y3) [memoir](https://github.com/visioncortex/memoir). VTracer has since been cited by a few academic papers in computer graphics / vision research. Please kindly let us know if you have cited our work:
### How / when / where did VTracer come about? + SKILL 2023 [Framework to Vectorize Digital Artworks for Physical Fabrication based on Geometric Stylization Techniques](https://www.researchgate.net/publication/374448489_Framework_to_Vectorize_Digital_Artworks_for_Physical_Fabrication_based_on_Geometric_Stylization_Techniques)
+ arXiv 2023 [Image Vectorization: a Review](https://arxiv.org/abs/2306.06441)
+ arXiv 2023 [StarVector: Generating Scalable Vector Graphics Code from Images](https://arxiv.org/abs/2312.11556)
+ arXiv 2024 [Text-Based Reasoning About Vector Graphics](https://arxiv.org/abs/2404.06479)
+ arXiv 2024 [Delving into LLMs' visual understanding ability using SVG to bridge image and text](https://openreview.net/pdf?id=pwlm6Po61I)
## How did VTracer come about?
> The following content is an excerpt from my [unpublished memoir](https://github.com/visioncortex/memoir).
At my teenage, two open source projects in the vector graphics space inspired me the most: Potrace and Anti-Grain Geometry (AGG). At my teenage, two open source projects in the vector graphics space inspired me the most: Potrace and Anti-Grain Geometry (AGG).

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "vtracer" name = "vtracer"
version = "0.6.2" version = "0.6.4"
authors = ["Chris Tsang <chris.2y3@outlook.com>"] authors = ["Chris Tsang <chris.2y3@outlook.com>"]
edition = "2021" edition = "2021"
description = "A cmd app to convert images into vector graphics." description = "A cmd app to convert images into vector graphics."
@@ -13,7 +13,7 @@ keywords = ["svg", "computer-graphics"]
[dependencies] [dependencies]
clap = "2.33.3" clap = "2.33.3"
image = "0.23.10" image = "0.23.10"
visioncortex = { version = "0.8.1" } visioncortex = { version = "0.8.8" }
fastrand = "1.8" fastrand = "1.8"
pyo3 = { version = "0.19.0", optional = true } pyo3 = { version = "0.19.0", optional = true }

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "vtracer" name = "vtracer"
version = "0.6.10" version = "0.6.11"
description = "Python bindings for the Rust Vtracer raster-to-vector library" description = "Python bindings for the Rust Vtracer raster-to-vector library"
authors = [ { name = "Chris Tsang", email = "chris.2y3@outlook.com" } ] authors = [ { name = "Chris Tsang", email = "chris.2y3@outlook.com" } ]
readme = "vtracer/README.md" readme = "vtracer/README.md"

View File

@@ -1,23 +1,27 @@
use std::str::FromStr; use std::str::FromStr;
use visioncortex::PathSimplifyMode; use visioncortex::PathSimplifyMode;
#[derive(Debug, Clone)]
pub enum Preset { pub enum Preset {
Bw, Bw,
Poster, Poster,
Photo, Photo,
} }
#[derive(Debug, Clone)]
pub enum ColorMode { pub enum ColorMode {
Color, Color,
Binary, Binary,
} }
#[derive(Debug, Clone)]
pub enum Hierarchical { pub enum Hierarchical {
Stacked, Stacked,
Cutout, Cutout,
} }
/// Converter config /// Converter config
#[derive(Debug, Clone)]
pub struct Config { pub struct Config {
pub color_mode: ColorMode, pub color_mode: ColorMode,
pub hierarchical: Hierarchical, pub hierarchical: Hierarchical,
@@ -32,6 +36,7 @@ pub struct Config {
pub path_precision: Option<u32>, pub path_precision: Option<u32>,
} }
#[derive(Debug, Clone)]
pub(crate) struct ConverterConfig { pub(crate) struct ConverterConfig {
pub color_mode: ColorMode, pub color_mode: ColorMode,
pub hierarchical: Hierarchical, pub hierarchical: Hierarchical,

View File

@@ -73,7 +73,7 @@ fn should_key_image(img: &ColorImage) -> bool {
// Check for transparency at several scanlines // Check for transparency at several scanlines
let threshold = ((img.width * 2) as f32 * KEYING_THRESHOLD) as usize; let threshold = ((img.width * 2) as f32 * KEYING_THRESHOLD) as usize;
let mut num_transparent_boundary_pixels = 0; let mut num_transparent_pixels = 0;
let y_positions = [ let y_positions = [
0, 0,
img.height / 4, img.height / 4,
@@ -84,9 +84,9 @@ fn should_key_image(img: &ColorImage) -> bool {
for y in y_positions { for y in y_positions {
for x in 0..img.width { for x in 0..img.width {
if img.get_pixel(x, y).a == 0 { if img.get_pixel(x, y).a == 0 {
num_transparent_boundary_pixels += 1; num_transparent_pixels += 1;
} }
if num_transparent_boundary_pixels >= threshold { if num_transparent_pixels >= threshold {
return true; return true;
} }
} }

View File

@@ -1,5 +1,7 @@
use crate::*; use crate::*;
use pyo3::prelude::*; use image::{io::Reader, ImageFormat};
use pyo3::{exceptions::PyException, prelude::*};
use std::io::{BufReader, Cursor};
use std::path::PathBuf; use std::path::PathBuf;
use visioncortex::PathSimplifyMode; use visioncortex::PathSimplifyMode;
@@ -23,6 +25,148 @@ fn convert_image_to_svg_py(
let input_path = PathBuf::from(image_path); let input_path = PathBuf::from(image_path);
let output_path = PathBuf::from(out_path); let output_path = PathBuf::from(out_path);
let config = construct_config(
colormode,
hierarchical,
mode,
filter_speckle,
color_precision,
layer_difference,
corner_threshold,
length_threshold,
max_iterations,
splice_threshold,
path_precision,
);
convert_image_to_svg(&input_path, &output_path, config).unwrap();
Ok(())
}
#[pyfunction]
fn convert_raw_image_to_svg(
img_bytes: Vec<u8>,
img_format: Option<&str>, // Format of the image (e.g. 'jpg', 'png'... A full list of supported formats can be found [here](https://docs.rs/image/latest/image/enum.ImageFormat.html)). If not provided, the image format will be guessed based on its contents.
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<String> {
let config = construct_config(
colormode,
hierarchical,
mode,
filter_speckle,
color_precision,
layer_difference,
corner_threshold,
length_threshold,
max_iterations,
splice_threshold,
path_precision,
);
let mut img_reader = Reader::new(BufReader::new(Cursor::new(img_bytes)));
let img_format = img_format.and_then(|ext_name| ImageFormat::from_extension(ext_name));
let img = match img_format {
Some(img_format) => {
img_reader.set_format(img_format);
img_reader.decode()
}
None => img_reader
.with_guessed_format()
.map_err(|_| PyException::new_err("Unrecognized image format. "))?
.decode(),
};
let img = match img {
Ok(img) => img.to_rgba8(),
Err(_) => return Err(PyException::new_err("Failed to decode img_bytes. ")),
};
let (width, height) = (img.width() as usize, img.height() as usize);
let img = ColorImage {
pixels: img.as_raw().to_vec(),
width,
height,
};
let svg =
convert(img, config).map_err(|_| PyException::new_err("Failed to convert the image. "))?;
Ok(format!("{}", svg))
}
#[pyfunction]
fn convert_pixels_to_svg(
rgba_pixels: Vec<(u8, u8, u8, u8)>,
size: (usize, usize),
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<String> {
let expected_pixel_count = size.0 * size.1;
if rgba_pixels.len() != expected_pixel_count {
return Err(PyException::new_err(format!(
"Length of rgba_pixels does not match given image size. Expected {} ({} * {}), got {}. ",
expected_pixel_count,
size.0,
size.1,
rgba_pixels.len()
)));
}
let config = construct_config(
colormode,
hierarchical,
mode,
filter_speckle,
color_precision,
layer_difference,
corner_threshold,
length_threshold,
max_iterations,
splice_threshold,
path_precision,
);
let mut flat_pixels: Vec<u8> = vec![];
for (r, g, b, a) in rgba_pixels {
flat_pixels.push(r);
flat_pixels.push(g);
flat_pixels.push(b);
flat_pixels.push(a);
}
let mut img = ColorImage::new();
img.pixels = flat_pixels;
(img.width, img.height) = size;
let svg =
convert(img, config).map_err(|_| PyException::new_err("Failed to convert the image. "))?;
Ok(format!("{}", svg))
}
fn construct_config(
colormode: Option<&str>,
hierarchical: Option<&str>,
mode: Option<&str>,
filter_speckle: Option<usize>,
color_precision: Option<i32>,
layer_difference: Option<i32>,
corner_threshold: Option<i32>,
length_threshold: Option<f64>,
max_iterations: Option<usize>,
splice_threshold: Option<i32>,
path_precision: Option<u32>,
) -> Config {
// TODO: enforce color mode with an enum so that we only // TODO: enforce color mode with an enum so that we only
// accept the strings 'color' or 'binary' // accept the strings 'color' or 'binary'
let color_mode = match colormode.unwrap_or("color") { let color_mode = match colormode.unwrap_or("color") {
@@ -52,7 +196,7 @@ fn convert_image_to_svg_py(
let splice_threshold = splice_threshold.unwrap_or(45); let splice_threshold = splice_threshold.unwrap_or(45);
let max_iterations = max_iterations.unwrap_or(10); let max_iterations = max_iterations.unwrap_or(10);
let config = Config { Config {
color_mode, color_mode,
hierarchical, hierarchical,
filter_speckle, filter_speckle,
@@ -65,15 +209,14 @@ fn convert_image_to_svg_py(
splice_threshold, splice_threshold,
path_precision, path_precision,
..Default::default() ..Default::default()
}; }
convert_image_to_svg(&input_path, &output_path, config).unwrap();
Ok(())
} }
/// A Python module implemented in Rust. /// A Python module implemented in Rust.
#[pymodule] #[pymodule]
fn vtracer(_py: Python, m: &PyModule) -> PyResult<()> { fn vtracer(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(convert_image_to_svg_py, m)?)?; m.add_function(wrap_pyfunction!(convert_image_to_svg_py, m)?)?;
m.add_function(wrap_pyfunction!(convert_raw_image_to_svg, m)?)?;
m.add_function(wrap_pyfunction!(convert_pixels_to_svg, m)?)?;
Ok(()) Ok(())
} }

View File

@@ -1,6 +1,7 @@
use std::fmt; use std::fmt;
use visioncortex::{Color, CompoundPath, PointF64}; use visioncortex::{Color, CompoundPath, PointF64};
#[derive(Debug, Clone)]
pub struct SvgFile { pub struct SvgFile {
pub paths: Vec<SvgPath>, pub paths: Vec<SvgPath>,
pub width: usize, pub width: usize,
@@ -8,6 +9,7 @@ pub struct SvgFile {
pub path_precision: Option<u32>, pub path_precision: Option<u32>,
} }
#[derive(Debug, Clone)]
pub struct SvgPath { pub struct SvgPath {
pub path: CompoundPath, pub path: CompoundPath,
pub color: Color, pub color: Color,

View File

@@ -52,7 +52,17 @@ vtracer.convert_image_to_svg_py(inp, out)
# Single-color example. Good for line art, and much faster than full color: # Single-color example. Good for line art, and much faster than full color:
vtracer.convert_image_to_svg_py(inp, out, colormode='binary') vtracer.convert_image_to_svg_py(inp, out, colormode='binary')
# All the bells & whistles # Convert from raw image bytes
input_img_bytes: bytes = get_bytes() # e.g. reading bytes from a file or a HTTP request body
svg_str: str = vtracer.convert_raw_image_to_svg(input_img_bytes, img_format='jpg')
# Convert from RGBA image pixels
from PIL import Image
img = Image.open(input_path).convert('RGBA')
pixels: list[tuple[int, int, int, int]] = list(img.getdata())
svg_str: str = vtracer.convert_pixels_to_svg(pixels, img.size)
# All the bells & whistles, also applicable to convert_raw_image_to_svg and convert_pixels_to_svg.
vtracer.convert_image_to_svg_py(inp, vtracer.convert_image_to_svg_py(inp,
out, out,
colormode = 'color', # ["color"] or "binary" colormode = 'color', # ["color"] or "binary"

View File

@@ -1 +1,2 @@
from .vtracer import convert_image_to_svg_py from .vtracer import (convert_image_to_svg_py, convert_pixels_to_svg,
convert_raw_image_to_svg)

View File

@@ -15,3 +15,35 @@ def convert_image_to_svg_py(image_path: str,
path_precision: Optional[int] = None, # default: 8 path_precision: Optional[int] = None, # default: 8
) -> None: ) -> None:
... ...
def convert_raw_image_to_svg(img_bytes: bytes,
img_format: Optional[str] = None, # Format of the image (e.g. 'jpg', 'png'... A full list of supported formats can be found [here](https://docs.rs/image/latest/image/enum.ImageFormat.html)). If not provided, the image format will be guessed based on its contents.
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
) -> str:
...
def convert_pixels_to_svg(rgba_pixels: list[tuple[int, int, int, int]],
size: tuple[int, int],
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
) -> str:
...

View File

@@ -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.6.0" visioncortex = "0.8.1"
# The `console_error_panic_hook` crate provides better debugging of panics by # The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires # logging them with `console.error`. This is great for development, but requires

View File

@@ -35,7 +35,11 @@ document.addEventListener('paste', function (e) {
// Download as SVG // Download as SVG
document.getElementById('export').addEventListener('click', function (e) { document.getElementById('export').addEventListener('click', function (e) {
const blob = new Blob([new XMLSerializer().serializeToString(svg)], {type: 'octet/stream'}), const blob = new Blob([
`<?xml version="1.0" encoding="UTF-8"?>\n`,
`<!-- Generator: visioncortex VTracer -->\n`,
new XMLSerializer().serializeToString(svg)
], {type: 'octet/stream'}),
url = window.URL.createObjectURL(blob); url = window.URL.createObjectURL(blob);
this.href = url; this.href = url;
@@ -444,7 +448,7 @@ class ConverterRunner {
this.converter.init(); this.converter.init();
this.stopped = false; this.stopped = false;
if (clustering_mode == 'binary') { if (clustering_mode == 'binary') {
svg.style.background = '#000'; svg.style.background = '#fff';
canvas.style.display = 'none'; canvas.style.display = 'none';
} else { } else {
svg.style.background = ''; svg.style.background = '';

View File

@@ -77,7 +77,7 @@ impl BinaryImageConverter {
self.params.max_iterations, self.params.max_iterations,
self.params.splice_threshold self.params.splice_threshold
); );
let color = Color::color(&ColorName::White); let color = Color::color(&ColorName::Black);
self.svg.prepend_path( self.svg.prepend_path(
&paths, &paths,
&color, &color,

View File

@@ -1,6 +1,6 @@
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use visioncortex::PathSimplifyMode; use visioncortex::{Color, ColorImage, PathSimplifyMode};
use visioncortex::color_clusters::{IncrementalBuilder, Clusters, Runner, RunnerConfig, HIERARCHICAL_MAX}; use visioncortex::color_clusters::{Clusters, Runner, RunnerConfig, HIERARCHICAL_MAX, IncrementalBuilder, KeyingAction};
use crate::canvas::*; use crate::canvas::*;
use crate::svg::*; use crate::svg::*;
@@ -8,6 +8,8 @@ use crate::svg::*;
use serde::Deserialize; use serde::Deserialize;
use super::util; use super::util;
const KEYING_THRESHOLD: f32 = 0.2;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct ColorImageConverterParams { pub struct ColorImageConverterParams {
pub canvas_id: String, pub canvas_id: String,
@@ -67,7 +69,26 @@ 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_color_image(0, 0, width, height); let mut image = self.canvas.get_image_data_as_color_image(0, 0, width, height);
let key_color = if Self::should_key_image(&image) {
if let Ok(key_color) = Self::find_unused_color_in_image(&image) {
for y in 0..height as usize {
for x in 0..width as usize {
if image.get_pixel(x, y).a == 0 {
image.set_pixel(x, y, &key_color);
}
}
}
key_color
} else {
Color::default()
}
} 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: self.params.layer_difference == 0, diagonal: self.params.layer_difference == 0,
hierarchical: HIERARCHICAL_MAX, hierarchical: HIERARCHICAL_MAX,
@@ -78,6 +99,12 @@ impl ColorImageConverter {
is_same_color_b: 1, is_same_color_b: 1,
deepen_diff: self.params.layer_difference, deepen_diff: self.params.layer_difference,
hollow_neighbours: 1, hollow_neighbours: 1,
key_color,
keying_action: if self.params.hierarchical == "cutout" {
KeyingAction::Keep
} else {
KeyingAction::Discard
},
}, image); }, image);
self.stage = Stage::Clustering(runner.start()); self.stage = Stage::Clustering(runner.start());
} }
@@ -108,6 +135,8 @@ impl ColorImageConverter {
is_same_color_b: 1, is_same_color_b: 1,
deepen_diff: 0, deepen_diff: 0,
hollow_neighbours: 0, hollow_neighbours: 0,
key_color: Default::default(),
keying_action: KeyingAction::Discard,
}, image); }, image);
self.stage = Stage::Reclustering(runner.start()); self.stage = Stage::Reclustering(runner.start());
}, },
@@ -167,4 +196,56 @@ impl ColorImageConverter {
}) as i32 }) as i32
} }
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),
Color::new(128, 128, 128),
]);
for color in special_colors {
if !Self::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_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_pixels += 1;
}
if num_transparent_pixels >= threshold {
return true;
}
}
}
false
}
} }

View File

@@ -1,6 +1,3 @@
mod binary_image; mod binary_image;
mod color_image; mod color_image;
mod util; mod util;
pub use binary_image::*;
pub use color_image::*;