diff --git a/Cargo.toml b/Cargo.toml index 1d26b57..59c5a43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,10 +5,9 @@ members = [ "crates/vtracer-cli", ] -# The pre-1.0 crates are kept in the tree for git history but are no longer -# part of the build. They are replaced by the crates/ workspace above. +# The pre-1.0 webapp is kept in the tree for now but is no longer part of the +# build. It is superseded by the crates/ workspace above. exclude = [ - "cmdapp", "webapp", ] diff --git a/cmdapp/.gitignore b/cmdapp/.gitignore deleted file mode 100644 index 18e5178..0000000 --- a/cmdapp/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.svg -*.png -*.jpg \ No newline at end of file diff --git a/cmdapp/Cargo.toml b/cmdapp/Cargo.toml deleted file mode 100644 index 0ad3636..0000000 --- a/cmdapp/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "vtracer" -version = "0.6.12" -authors = ["Chris Tsang "] -edition = "2021" -description = "A cmd app to convert images into vector graphics." -license = "MIT" -homepage = "http://www.visioncortex.org/vtracer" -repository = "https://github.com/visioncortex/vtracer/" -categories = ["graphics"] -keywords = ["svg", "computer-graphics"] - -[dependencies] -clap = "2.33.3" -image = "0.23.10" -visioncortex = { version = "0.8.8" } -fastrand = { version = "2.3" } -pyo3 = { version = "0.19.0", optional = true } - -[features] -python-binding = ["pyo3"] -wasm = ["fastrand/js"] - -[lib] -name = "vtracer" -crate-type = ["rlib", "cdylib"] \ No newline at end of file diff --git a/cmdapp/LICENSE b/cmdapp/LICENSE deleted file mode 100644 index f8fd70d..0000000 --- a/cmdapp/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -Copyright (c) 2024 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. \ No newline at end of file diff --git a/cmdapp/README.md b/cmdapp/README.md deleted file mode 100644 index 48dda5c..0000000 --- a/cmdapp/README.md +++ /dev/null @@ -1,96 +0,0 @@ -
- - -

VTracer

- -

- Raster to Vector Graphics Converter built on top of visioncortex -

- -

- Article - | - Demo - | - Download -

- - Built with 🦀 by The Vision Cortex Research Group -
- -## 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](https://www.visioncortex.org/vtracer-docs). - -## Cmd App - -```sh -visioncortex VTracer 0.6.0 -A cmd app to convert images into vector graphics. - -USAGE: - vtracer [OPTIONS] --input --output - -FLAGS: - -h, --help Prints help information - -V, --version Prints version information - -OPTIONS: - --colormode True color image `color` (default) or Binary image `bw` - -p, --color_precision Number of significant bits to use in an RGB channel - -c, --corner_threshold Minimum momentary angle (degree) to be considered a corner - -f, --filter_speckle Discard patches smaller than X px in size - -g, --gradient_step Color difference between gradient layers - --hierarchical - Hierarchical clustering `stacked` (default) or non-stacked `cutout`. Only applies to color mode. - - -i, --input Path to input raster image - -m, --mode Curver fitting mode `pixel`, `polygon`, `spline` - -o, --output Path to output vector graphics - --path_precision Number of decimal places to use in path string - --preset Use one of the preset configs `bw`, `poster`, `photo` - -l, --segment_length - Perform iterative subdivide smooth until all segments are shorter than this length - - -s, --splice_threshold Minimum angle displacement (degree) to splice a spline -``` - -### Install - -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): - -```sh -cargo install vtracer -``` - -### Usage - -```sh -./vtracer --input input.jpg --output output.svg -``` - -## Rust Library - -You can install [`vtracer`](https://crates.io/crates/vtracer) as a Rust library. - -```sh -cargo add vtracer -``` - -## 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. - -```sh -pip install vtracer -``` diff --git a/cmdapp/pyproject.toml b/cmdapp/pyproject.toml deleted file mode 100644 index de93781..0000000 --- a/cmdapp/pyproject.toml +++ /dev/null @@ -1,29 +0,0 @@ -[project] -name = "vtracer" -version = "0.6.15" -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" -license-files = ["LICENSE"] -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", "python-binding"] -compatibility = "manylinux2014" -include = ["LICENSE"] diff --git a/cmdapp/src/config.rs b/cmdapp/src/config.rs deleted file mode 100644 index d20a4d2..0000000 --- a/cmdapp/src/config.rs +++ /dev/null @@ -1,173 +0,0 @@ -use std::str::FromStr; -use visioncortex::PathSimplifyMode; - -#[derive(Debug, Clone)] -pub enum Preset { - Bw, - Poster, - Photo, -} - -#[derive(Debug, Clone)] -pub enum ColorMode { - Color, - Binary, -} - -#[derive(Debug, Clone)] -pub enum Hierarchical { - Stacked, - Cutout, -} - -/// Converter config -#[derive(Debug, Clone)] -pub struct Config { - pub color_mode: ColorMode, - pub hierarchical: Hierarchical, - pub filter_speckle: usize, - pub color_precision: i32, - pub layer_difference: i32, - pub mode: PathSimplifyMode, - pub corner_threshold: i32, - pub length_threshold: f64, - pub max_iterations: usize, - pub splice_threshold: i32, - pub path_precision: Option, -} - -#[derive(Debug, Clone)] -pub(crate) struct ConverterConfig { - pub color_mode: ColorMode, - pub hierarchical: Hierarchical, - pub filter_speckle_area: usize, - pub color_precision_loss: i32, - pub layer_difference: i32, - pub mode: PathSimplifyMode, - pub corner_threshold: f64, - pub length_threshold: f64, - pub max_iterations: usize, - pub splice_threshold: f64, - pub path_precision: Option, -} - -impl Default for Config { - fn default() -> Self { - Self { - color_mode: ColorMode::Color, - hierarchical: Hierarchical::Stacked, - mode: PathSimplifyMode::Spline, - filter_speckle: 4, - color_precision: 6, - layer_difference: 16, - corner_threshold: 60, - length_threshold: 4.0, - splice_threshold: 45, - max_iterations: 10, - path_precision: Some(2), - } - } -} - -impl FromStr for ColorMode { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "color" => Ok(Self::Color), - "binary" => Ok(Self::Binary), - _ => Err(format!("unknown ColorMode {}", s)), - } - } -} - -impl FromStr for Hierarchical { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "stacked" => Ok(Self::Stacked), - "cutout" => Ok(Self::Cutout), - _ => Err(format!("unknown Hierarchical {}", s)), - } - } -} - -impl FromStr for Preset { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "bw" => Ok(Self::Bw), - "poster" => Ok(Self::Poster), - "photo" => Ok(Self::Photo), - _ => Err(format!("unknown Preset {}", s)), - } - } -} - -impl Config { - pub fn from_preset(preset: Preset) -> Self { - match preset { - Preset::Bw => Self { - color_mode: ColorMode::Binary, - hierarchical: Hierarchical::Stacked, - filter_speckle: 4, - color_precision: 6, - layer_difference: 16, - mode: PathSimplifyMode::Spline, - corner_threshold: 60, - length_threshold: 4.0, - max_iterations: 10, - splice_threshold: 45, - path_precision: Some(2), - }, - Preset::Poster => Self { - color_mode: ColorMode::Color, - hierarchical: Hierarchical::Stacked, - filter_speckle: 4, - color_precision: 8, - layer_difference: 16, - mode: PathSimplifyMode::Spline, - corner_threshold: 60, - length_threshold: 4.0, - max_iterations: 10, - splice_threshold: 45, - path_precision: Some(2), - }, - Preset::Photo => Self { - color_mode: ColorMode::Color, - hierarchical: Hierarchical::Stacked, - filter_speckle: 10, - color_precision: 8, - layer_difference: 48, - mode: PathSimplifyMode::Spline, - corner_threshold: 180, - length_threshold: 4.0, - max_iterations: 10, - splice_threshold: 45, - path_precision: Some(2), - }, - } - } - - pub(crate) fn into_converter_config(self) -> ConverterConfig { - ConverterConfig { - 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, - mode: self.mode, - corner_threshold: deg2rad(self.corner_threshold), - length_threshold: self.length_threshold, - max_iterations: self.max_iterations, - splice_threshold: deg2rad(self.splice_threshold), - path_precision: self.path_precision, - } - } -} - -fn deg2rad(deg: i32) -> f64 { - deg as f64 / 180.0 * std::f64::consts::PI -} diff --git a/cmdapp/src/converter.rs b/cmdapp/src/converter.rs deleted file mode 100644 index 448b5cd..0000000 --- a/cmdapp/src/converter.rs +++ /dev/null @@ -1,237 +0,0 @@ -use std::path::Path; -use std::{fs::File, io::Write}; - -use super::config::{ColorMode, Config, ConverterConfig, Hierarchical}; -use super::svg::SvgFile; -use fastrand::Rng; -use visioncortex::color_clusters::{KeyingAction, Runner, RunnerConfig, HIERARCHICAL_MAX}; -use visioncortex::{Color, ColorImage, ColorName}; - -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 in-memory image into an in-memory SVG -pub fn convert(img: ColorImage, config: Config) -> Result { - let config = config.into_converter_config(); - match config.color_mode { - ColorMode::Color => color_image_to_svg(img, config), - ColorMode::Binary => binary_image_to_svg(img, config), - } -} - -/// Convert an image file into svg file -pub fn convert_image_to_svg( - input_path: &Path, - output_path: &Path, - config: Config, -) -> Result<(), String> { - let img = read_image(input_path)?; - let svg = convert(img, config)?; - write_svg(svg, output_path) -} - -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 { - 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 mut 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_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 -} - -fn color_image_to_svg(mut img: ColorImage, config: ConverterConfig) -> Result { - let width = img.width; - let height = img.height; - - 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), - is_same_color_a: config.color_precision_loss, - 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 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, config.path_precision); - for &cluster_index in view.clusters_output.iter().rev() { - let cluster = view.get_cluster(cluster_index); - let paths = cluster.to_compound_path( - &view, - false, - config.mode, - config.corner_threshold, - config.length_threshold, - config.max_iterations, - config.splice_threshold, - ); - svg.add_path(paths, cluster.residue_color()); - } - - Ok(svg) -} - -fn binary_image_to_svg(img: ColorImage, config: ConverterConfig) -> Result { - let img = img.to_binary_image(|x| x.r < 128); - let width = img.width; - let height = img.height; - - let clusters = img.to_clusters(false); - - 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 paths = cluster.to_compound_path( - config.mode, - config.corner_threshold, - config.length_threshold, - config.max_iterations, - config.splice_threshold, - ); - svg.add_path(paths, Color::color(&ColorName::Black)); - } - } - - Ok(svg) -} - -fn read_image(input_path: &Path) -> Result { - let img = image::open(input_path); - let img = match img { - Ok(file) => file.to_rgba8(), - Err(_) => return Err(String::from("No image file found at specified input path")), - }; - - let (width, height) = (img.width() as usize, img.height() as usize); - let img = ColorImage { - pixels: img.as_raw().to_vec(), - width, - height, - }; - - Ok(img) -} - -fn write_svg(svg: SvgFile, output_path: &Path) -> Result<(), String> { - let out_file = File::create(output_path); - let mut out_file = match out_file { - Ok(file) => file, - Err(_) => return Err(String::from("Cannot create output file.")), - }; - - write!(&mut out_file, "{}", svg).expect("failed to write file."); - - Ok(()) -} diff --git a/cmdapp/src/lib.rs b/cmdapp/src/lib.rs deleted file mode 100644 index bd1e0c3..0000000 --- a/cmdapp/src/lib.rs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2023 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 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -mod config; -mod converter; -#[cfg(feature = "python-binding")] -mod python; -mod svg; - -pub use config::*; -pub use converter::*; -#[cfg(feature = "python-binding")] -pub use python::*; -pub use svg::*; -pub use visioncortex::ColorImage; diff --git a/cmdapp/src/main.rs b/cmdapp/src/main.rs deleted file mode 100644 index b33bdf2..0000000 --- a/cmdapp/src/main.rs +++ /dev/null @@ -1,282 +0,0 @@ -mod config; -mod converter; -mod svg; - -use clap::{App, Arg}; -use config::{ColorMode, Config, Hierarchical, Preset}; -use std::path::PathBuf; -use std::str::FromStr; -use visioncortex::PathSimplifyMode; - -fn path_simplify_mode_from_str(s: &str) -> PathSimplifyMode { - match s { - "polygon" => PathSimplifyMode::Polygon, - "spline" => PathSimplifyMode::Spline, - "none" => PathSimplifyMode::None, - _ => panic!("unknown PathSimplifyMode {}", s), - } -} - -pub fn config_from_args() -> (PathBuf, PathBuf, Config) { - 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") - .short("i") - .takes_value(true) - .help("Path to input raster image") - .required(true), - ); - - let app = app.arg( - Arg::with_name("output") - .long("output") - .short("o") - .takes_value(true) - .help("Path to output vector graphics") - .required(true), - ); - - let app = app.arg( - Arg::with_name("color_mode") - .long("colormode") - .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) - .help("Use one of the preset configs `bw`, `poster`, `photo`"), - ); - - let app = app.arg( - Arg::with_name("filter_speckle") - .long("filter_speckle") - .short("f") - .takes_value(true) - .help("Discard patches smaller than X px in size"), - ); - - let app = app.arg( - Arg::with_name("color_precision") - .long("color_precision") - .short("p") - .takes_value(true) - .help("Number of significant bits to use in an RGB channel"), - ); - - let app = app.arg( - Arg::with_name("gradient_step") - .long("gradient_step") - .short("g") - .takes_value(true) - .help("Color difference between gradient layers"), - ); - - let app = app.arg( - Arg::with_name("corner_threshold") - .long("corner_threshold") - .short("c") - .takes_value(true) - .help("Minimum momentary angle (degree) to be considered a corner"), - ); - - let app = app.arg(Arg::with_name("segment_length") - .long("segment_length") - .short("l") - .takes_value(true) - .help("Perform iterative subdivide smooth until all segments are shorter than this length")); - - let app = app.arg( - Arg::with_name("splice_threshold") - .long("splice_threshold") - .short("s") - .takes_value(true) - .help("Minimum angle displacement (degree) to splice a spline"), - ); - - let app = app.arg( - Arg::with_name("mode") - .long("mode") - .short("m") - .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(); - - let mut config = Config::default(); - let input_path = matches - .value_of("input") - .expect("Input path is required, please specify it by --input or -i."); - let output_path = matches - .value_of("output") - .expect("Output path is required, please specify it by --output or -o."); - - let input_path = PathBuf::from(input_path); - let output_path = PathBuf::from(output_path); - - if let Some(value) = matches.value_of("preset") { - config = Config::from_preset(Preset::from_str(value).unwrap()); - } - - if let Some(value) = matches.value_of("color_mode") { - 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" { - "none" - } else if value == "polygon" { - "polygon" - } else if value == "spline" { - "spline" - } else { - panic!("Parser Error: Curve fitting mode is invalid: {}", value); - }); - } - - if let Some(value) = matches.value_of("filter_speckle") { - if value.trim().parse::().is_ok() { - // is numeric - let value = value.trim().parse::().unwrap(); - 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: {}.", - value - ); - } - } - - if let Some(value) = matches.value_of("color_precision") { - if value.trim().parse::().is_ok() { - // is numeric - let value = value.trim().parse::().unwrap(); - if value < 1 || value > 8 { - panic!("Out of Range Error: Color precision is invalid at {}. It must be within [1,8].", value); - } - config.color_precision = value; - } else { - panic!( - "Parser Error: Color precision is not an integer: {}.", - value - ); - } - } - - if let Some(value) = matches.value_of("gradient_step") { - if value.trim().parse::().is_ok() { - // is numeric - let value = value.trim().parse::().unwrap(); - if value < 0 || value > 255 { - panic!("Out of Range Error: Gradient step is invalid at {}. It must be within [0,255].", value); - } - config.layer_difference = value; - } else { - panic!("Parser Error: Gradient step is not an integer: {}.", value); - } - } - - if let Some(value) = matches.value_of("corner_threshold") { - if value.trim().parse::().is_ok() { - // is numeric - let value = value.trim().parse::().unwrap(); - if value < 0 || value > 180 { - panic!("Out of Range Error: Corner threshold is invalid at {}. It must be within [0,180].", value); - } - config.corner_threshold = value - } else { - panic!("Parser Error: Corner threshold is not numeric: {}.", value); - } - } - - if let Some(value) = matches.value_of("segment_length") { - if value.trim().parse::().is_ok() { - // is numeric - let value = value.trim().parse::().unwrap(); - if value < 3.5 || value > 10.0 { - panic!("Out of Range Error: Segment length is invalid at {}. It must be within [3.5,10].", value); - } - config.length_threshold = value; - } else { - panic!("Parser Error: Segment length is not numeric: {}.", value); - } - } - - if let Some(value) = matches.value_of("splice_threshold") { - if value.trim().parse::().is_ok() { - // is numeric - let value = value.trim().parse::().unwrap(); - if value < 0 || value > 180 { - panic!("Out of Range Error: Segment length is invalid at {}. It must be within [0,180].", value); - } - config.splice_threshold = value; - } else { - panic!("Parser Error: Segment length is not numeric: {}.", value); - } - } - - if let Some(value) = matches.value_of("path_precision") { - if value.trim().parse::().is_ok() { - // is numeric - let value = value.trim().parse::().ok(); - config.path_precision = value; - } else { - panic!( - "Parser Error: Path precision is not an unsigned integer: {}.", - value - ); - } - } - - (input_path, output_path, config) -} - -fn main() { - let (input_path, output_path, config) = config_from_args(); - let result = converter::convert_image_to_svg(&input_path, &output_path, config); - match result { - Ok(()) => { - println!("Conversion successful."); - } - Err(msg) => { - panic!("Conversion failed with error message: {}", msg); - } - } -} diff --git a/cmdapp/src/python.rs b/cmdapp/src/python.rs deleted file mode 100644 index cb83b97..0000000 --- a/cmdapp/src/python.rs +++ /dev/null @@ -1,222 +0,0 @@ -use crate::*; -use image::{io::Reader, ImageFormat}; -use pyo3::{exceptions::PyException, prelude::*}; -use std::io::{BufReader, Cursor}; -use std::path::PathBuf; -use visioncortex::PathSimplifyMode; - -/// 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, // default: 4 - color_precision: Option, // default: 6 - layer_difference: Option, // default: 16 - corner_threshold: Option, // default: 60 - length_threshold: Option, // in [3.5, 10] default: 4.0 - max_iterations: Option, // default: 10 - splice_threshold: Option, // default: 45 - path_precision: Option, // default: 8 -) -> PyResult<()> { - let input_path = PathBuf::from(image_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, - 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, // default: 4 - color_precision: Option, // default: 6 - layer_difference: Option, // default: 16 - corner_threshold: Option, // default: 60 - length_threshold: Option, // in [3.5, 10] default: 4.0 - max_iterations: Option, // default: 10 - splice_threshold: Option, // default: 45 - path_precision: Option, // default: 8 -) -> PyResult { - 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, // default: 4 - color_precision: Option, // default: 6 - layer_difference: Option, // default: 16 - corner_threshold: Option, // default: 60 - length_threshold: Option, // in [3.5, 10] default: 4.0 - max_iterations: Option, // default: 10 - splice_threshold: Option, // default: 45 - path_precision: Option, // default: 8 -) -> PyResult { - 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 = 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, - color_precision: Option, - layer_difference: Option, - corner_threshold: Option, - length_threshold: Option, - max_iterations: Option, - splice_threshold: Option, - path_precision: Option, -) -> Config { - // 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); - - Config { - color_mode, - hierarchical, - filter_speckle, - color_precision, - layer_difference, - mode, - corner_threshold, - length_threshold, - max_iterations, - splice_threshold, - path_precision, - ..Default::default() - } -} - -/// 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)?)?; - m.add_function(wrap_pyfunction!(convert_raw_image_to_svg, m)?)?; - m.add_function(wrap_pyfunction!(convert_pixels_to_svg, m)?)?; - Ok(()) -} diff --git a/cmdapp/src/svg.rs b/cmdapp/src/svg.rs deleted file mode 100644 index a9135ce..0000000 --- a/cmdapp/src/svg.rs +++ /dev/null @@ -1,75 +0,0 @@ -use std::fmt; -use visioncortex::{Color, CompoundPath, PointF64}; - -#[derive(Debug, Clone)] -pub struct SvgFile { - pub paths: Vec, - pub width: usize, - pub height: usize, - pub path_precision: Option, -} - -#[derive(Debug, Clone)] -pub struct SvgPath { - pub path: CompoundPath, - pub color: Color, -} - -impl SvgFile { - pub fn new(width: usize, height: usize, path_precision: Option) -> Self { - SvgFile { - paths: vec![], - width, - height, - path_precision, - } - } - - pub fn add_path(&mut self, path: CompoundPath, color: Color) { - self.paths.push(SvgPath { path, color }) - } -} - -impl fmt::Display for SvgFile { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, r#""#)?; - writeln!( - f, - r#""#, - env!("CARGO_PKG_VERSION") - )?; - writeln!( - f, - r#""#, - self.width, self.height - )?; - - for path in &self.paths { - path.fmt_with_precision(f, self.path_precision)?; - } - - writeln!(f, "") - } -} - -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) -> fmt::Result { - let (string, offset) = self - .path - .to_svg_string(true, PointF64::default(), precision); - writeln!( - f, - "", - string, - self.color.to_hex_string(), - offset.x, - offset.y - ) - } -} diff --git a/cmdapp/vtracer/README.md b/cmdapp/vtracer/README.md deleted file mode 100644 index 7617cfd..0000000 --- a/cmdapp/vtracer/README.md +++ /dev/null @@ -1,85 +0,0 @@ -
- - - -

VTracer: Python Binding

- -

- Raster to Vector Graphics Converter built on top of visioncortex -

- -

- Article - | - Demo - | - Download -

- -Built with 🦀 by The Vision Cortex Research Group - -
- -## 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') - -# 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, - 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). diff --git a/cmdapp/vtracer/__init__.py b/cmdapp/vtracer/__init__.py deleted file mode 100644 index 624f5b6..0000000 --- a/cmdapp/vtracer/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .vtracer import (convert_image_to_svg_py, convert_pixels_to_svg, - convert_raw_image_to_svg) diff --git a/cmdapp/vtracer/vtracer.pyi b/cmdapp/vtracer/vtracer.pyi deleted file mode 100644 index 25fac77..0000000 --- a/cmdapp/vtracer/vtracer.pyi +++ /dev/null @@ -1,49 +0,0 @@ -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: - ... - -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: - ... \ No newline at end of file