mirror of
https://github.com/visioncortex/vtracer.git
synced 2026-09-28 06:51:21 -07:00
Publish desktop updater 1.0.0-alpha.3
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
use wasm_bindgen::{JsCast};
|
||||
use web_sys::{console, CanvasRenderingContext2d, HtmlCanvasElement};
|
||||
use visioncortex::{ColorImage};
|
||||
|
||||
use super::common::document;
|
||||
|
||||
pub struct Canvas {
|
||||
html_canvas: HtmlCanvasElement,
|
||||
cctx: CanvasRenderingContext2d,
|
||||
}
|
||||
|
||||
impl Canvas {
|
||||
pub fn new_from_id(canvas_id: &str) -> Canvas {
|
||||
let html_canvas = document().get_element_by_id(canvas_id).unwrap();
|
||||
let html_canvas: HtmlCanvasElement = html_canvas
|
||||
.dyn_into::<HtmlCanvasElement>()
|
||||
.map_err(|_| ())
|
||||
.unwrap();
|
||||
|
||||
let cctx = html_canvas
|
||||
.get_context("2d")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.dyn_into::<CanvasRenderingContext2d>()
|
||||
.unwrap();
|
||||
|
||||
Canvas {
|
||||
html_canvas,
|
||||
cctx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
self.html_canvas.width() as usize
|
||||
}
|
||||
|
||||
pub fn height(&self) -> usize {
|
||||
self.html_canvas.height() as usize
|
||||
}
|
||||
|
||||
pub fn get_image_data(&self, x: u32, y: u32, width: u32, height: u32) -> Vec<u8> {
|
||||
let image = self
|
||||
.cctx
|
||||
.get_image_data(x as f64, y as f64, width as f64, height as f64)
|
||||
.unwrap();
|
||||
image.data().to_vec()
|
||||
}
|
||||
|
||||
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,
|
||||
height: height as usize,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log(&self, string: &str) {
|
||||
console::log_1(&wasm_bindgen::JsValue::from_str(string));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
pub fn window() -> web_sys::Window {
|
||||
web_sys::window().unwrap()
|
||||
}
|
||||
|
||||
pub fn document() -> web_sys::Document {
|
||||
window().document().unwrap()
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
use wasm_bindgen::prelude::*;
|
||||
use visioncortex::{clusters::Clusters, Color, ColorName, PathSimplifyMode};
|
||||
|
||||
use crate::{canvas::*};
|
||||
use crate::svg::*;
|
||||
|
||||
use serde::Deserialize;
|
||||
use super::util;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct BinaryImageConverterParams {
|
||||
pub canvas_id: String,
|
||||
pub svg_id: String,
|
||||
pub mode: String,
|
||||
pub corner_threshold: f64,
|
||||
pub length_threshold: f64,
|
||||
pub max_iterations: usize,
|
||||
pub splice_threshold: f64,
|
||||
pub filter_speckle: usize,
|
||||
pub path_precision: u32,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct BinaryImageConverter {
|
||||
canvas: Canvas,
|
||||
svg: Svg,
|
||||
clusters: Clusters,
|
||||
counter: usize,
|
||||
mode: PathSimplifyMode,
|
||||
params: BinaryImageConverterParams,
|
||||
}
|
||||
|
||||
impl BinaryImageConverter {
|
||||
pub fn new(params: BinaryImageConverterParams) -> Self {
|
||||
let canvas = Canvas::new_from_id(¶ms.canvas_id);
|
||||
let svg = Svg::new_from_id(¶ms.svg_id);
|
||||
Self {
|
||||
canvas,
|
||||
svg,
|
||||
clusters: Clusters::default(),
|
||||
counter: 0,
|
||||
mode: util::path_simplify_mode(¶ms.mode),
|
||||
params,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl BinaryImageConverter {
|
||||
pub fn new_with_string(params: String) -> Self {
|
||||
let params: BinaryImageConverterParams = serde_json::from_str(params.as_str()).unwrap();
|
||||
Self::new(params)
|
||||
}
|
||||
|
||||
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_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!(
|
||||
"clusters.len() = {}, self.clusters.rect.left = {}",
|
||||
self.clusters.len(),
|
||||
self.clusters.rect.left
|
||||
));
|
||||
}
|
||||
|
||||
pub fn tick(&mut self) -> bool {
|
||||
if self.counter < self.clusters.len() {
|
||||
self.canvas.log(&format!("tick {}", self.counter));
|
||||
let cluster = self.clusters.get_cluster(self.counter);
|
||||
if cluster.size() >= self.params.filter_speckle {
|
||||
let paths = cluster.to_compound_path(
|
||||
self.mode,
|
||||
self.params.corner_threshold,
|
||||
self.params.length_threshold,
|
||||
self.params.max_iterations,
|
||||
self.params.splice_threshold
|
||||
);
|
||||
let color = Color::color(&ColorName::Black);
|
||||
self.svg.prepend_path(
|
||||
&paths,
|
||||
&color,
|
||||
Some(self.params.path_precision),
|
||||
);
|
||||
}
|
||||
self.counter += 1;
|
||||
false
|
||||
} else {
|
||||
self.canvas.log("done");
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub fn progress(&self) -> u32 {
|
||||
100 * self.counter as u32 / self.clusters.len() as u32
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
use wasm_bindgen::prelude::*;
|
||||
use visioncortex::{Color, ColorImage, PathSimplifyMode};
|
||||
use visioncortex::color_clusters::{
|
||||
Cluster, Clusters, ClustersView, IncrementalBuilder, KeyingAction, NeighbourInfo, Runner,
|
||||
RunnerConfig, HIERARCHICAL_MAX,
|
||||
};
|
||||
|
||||
use crate::canvas::*;
|
||||
use crate::svg::*;
|
||||
|
||||
use serde::Deserialize;
|
||||
use super::util;
|
||||
|
||||
const KEYING_THRESHOLD: f32 = 0.2;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
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,
|
||||
pub splice_threshold: f64,
|
||||
pub filter_speckle: usize,
|
||||
pub color_precision: i32,
|
||||
pub layer_difference: i32,
|
||||
pub path_precision: u32,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct ColorImageConverter {
|
||||
canvas: Canvas,
|
||||
svg: Svg,
|
||||
stage: Stage,
|
||||
counter: usize,
|
||||
mode: PathSimplifyMode,
|
||||
params: ColorImageConverterParams,
|
||||
}
|
||||
|
||||
pub enum Stage {
|
||||
New,
|
||||
Clustering(Box<dyn ClusterBuilder>),
|
||||
Reclustering(Box<dyn ClusterBuilder>),
|
||||
Vectorize(Clusters),
|
||||
}
|
||||
|
||||
/// visioncortex 0.9 parameterises `IncrementalBuilder` over its four closures,
|
||||
/// and `Runner::start()` returns them as anonymous `impl Fn` types, so the
|
||||
/// builder can no longer be named in a field. The stage machine only ever steps
|
||||
/// it, so erase the closures behind a trait object.
|
||||
pub trait ClusterBuilder {
|
||||
fn tick(&mut self) -> bool;
|
||||
fn progress(&self) -> u32;
|
||||
fn result(&mut self) -> Clusters;
|
||||
}
|
||||
|
||||
impl<C, D, P, H> ClusterBuilder for IncrementalBuilder<C, D, P, H>
|
||||
where
|
||||
C: Fn(Color, Color) -> bool,
|
||||
D: Fn(Color, Color) -> i32,
|
||||
P: Fn(&ClustersView, &Cluster, &[NeighbourInfo]) -> bool,
|
||||
H: Fn(&ClustersView, &Cluster, &[NeighbourInfo]) -> bool,
|
||||
{
|
||||
fn tick(&mut self) -> bool {
|
||||
IncrementalBuilder::tick(self)
|
||||
}
|
||||
|
||||
fn progress(&self) -> u32 {
|
||||
IncrementalBuilder::progress(self)
|
||||
}
|
||||
|
||||
fn result(&mut self) -> Clusters {
|
||||
IncrementalBuilder::result(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorImageConverter {
|
||||
pub fn new(params: ColorImageConverterParams) -> Self {
|
||||
let canvas = Canvas::new_from_id(¶ms.canvas_id);
|
||||
let svg = Svg::new_from_id(¶ms.svg_id);
|
||||
Self {
|
||||
canvas,
|
||||
svg,
|
||||
stage: Stage::New,
|
||||
counter: 0,
|
||||
mode: util::path_simplify_mode(¶ms.mode),
|
||||
params,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl ColorImageConverter {
|
||||
|
||||
pub fn new_with_string(params: String) -> Self {
|
||||
let params: ColorImageConverterParams = serde_json::from_str(params.as_str()).unwrap();
|
||||
Self::new(params)
|
||||
}
|
||||
|
||||
pub fn init(&mut self) {
|
||||
let width = self.canvas.width() as u32;
|
||||
let height = self.canvas.height() as u32;
|
||||
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 {
|
||||
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,
|
||||
is_same_color_a: self.params.color_precision,
|
||||
is_same_color_b: 1,
|
||||
deepen_diff: self.params.layer_difference,
|
||||
hollow_neighbours: 1,
|
||||
key_color,
|
||||
keying_action: if self.params.hierarchical == "cutout" {
|
||||
KeyingAction::Keep
|
||||
} else {
|
||||
KeyingAction::Discard
|
||||
},
|
||||
}, image);
|
||||
self.stage = Stage::Clustering(Box::new(runner.start()));
|
||||
}
|
||||
|
||||
pub fn tick(&mut self) -> bool {
|
||||
match &mut self.stage {
|
||||
Stage::New => {
|
||||
panic!("uninitialized");
|
||||
},
|
||||
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,
|
||||
key_color: Default::default(),
|
||||
keying_action: KeyingAction::Discard,
|
||||
}, image);
|
||||
self.stage = Stage::Reclustering(Box::new(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())
|
||||
}
|
||||
false
|
||||
},
|
||||
Stage::Vectorize(clusters) => {
|
||||
let view = clusters.view();
|
||||
if self.counter < view.clusters_output.len() {
|
||||
self.canvas.log("Vectorize tick");
|
||||
let cluster = view.get_cluster(view.clusters_output[self.counter]);
|
||||
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(
|
||||
&paths,
|
||||
&cluster.residue_color(),
|
||||
Some(self.params.path_precision),
|
||||
);
|
||||
self.counter += 1;
|
||||
false
|
||||
} else {
|
||||
self.canvas.log("done");
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn progress(&self) -> i32 {
|
||||
(match &self.stage {
|
||||
Stage::New => {
|
||||
0
|
||||
},
|
||||
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
|
||||
}
|
||||
}) 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mod binary_image;
|
||||
mod color_image;
|
||||
mod util;
|
||||
@@ -0,0 +1,10 @@
|
||||
use visioncortex::PathSimplifyMode;
|
||||
|
||||
pub fn path_simplify_mode(s: &str) -> PathSimplifyMode {
|
||||
match s {
|
||||
"polygon" => PathSimplifyMode::Polygon,
|
||||
"spline" => PathSimplifyMode::Spline,
|
||||
"none" => PathSimplifyMode::None,
|
||||
_ => panic!("unknown PathSimplifyMode {}", s),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright 2020 Tsang Hao Fung. See the COPYRIGHT
|
||||
// file at the top-level directory of this distribution and at
|
||||
// http://rust-lang.org/COPYRIGHT.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <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;
|
||||
mod canvas;
|
||||
mod common;
|
||||
mod svg;
|
||||
mod utils;
|
||||
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn main() {
|
||||
utils::set_panic_hook();
|
||||
console_log::init().unwrap();
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use web_sys::Element;
|
||||
use visioncortex::{Color, CompoundPath, PointF64};
|
||||
use super::common::document;
|
||||
|
||||
pub struct Svg {
|
||||
element: Element,
|
||||
}
|
||||
|
||||
impl Svg {
|
||||
pub fn new_from_id(svg_id: &str) -> Self {
|
||||
let element = document().get_element_by_id(svg_id).unwrap();
|
||||
|
||||
Self { element }
|
||||
}
|
||||
|
||||
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();
|
||||
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(),
|
||||
)
|
||||
.unwrap();
|
||||
path.set_attribute(
|
||||
"style",
|
||||
format!("fill: {};", color.to_hex_string()).as_str(),
|
||||
)
|
||||
.unwrap();
|
||||
self.element.prepend_with_node_1(&path).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
extern crate cfg_if;
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
// When the `console_error_panic_hook` feature is enabled, we can call the
|
||||
// `set_panic_hook` function to get better error messages if we ever panic.
|
||||
if #[cfg(feature = "console_error_panic_hook")] {
|
||||
extern crate console_error_panic_hook;
|
||||
pub use self::console_error_panic_hook::set_once as set_panic_hook;
|
||||
} else {
|
||||
#[inline]
|
||||
pub fn set_panic_hook() {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user