Remove the 0.6.x cmdapp crate

Superseded by crates/vtracer (framework) + crates/vtracer-cli. Verified the
new pipeline reproduces cmdapp's geometry and colors byte-for-byte (PNG always;
JPEG once the image-crate decoder is held constant), so the old crate is
retired. Drop its now-stale workspace exclude entry. Git history preserves it.
This commit is contained in:
Chris Tsang
2026-07-23 23:08:04 +01:00
parent e46c971845
commit 660dc4ff93
15 changed files with 2 additions and 1329 deletions
+2 -3
View File
@@ -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",
]
-3
View File
@@ -1,3 +0,0 @@
*.svg
*.png
*.jpg
-26
View File
@@ -1,26 +0,0 @@
[package]
name = "vtracer"
version = "0.6.12"
authors = ["Chris Tsang <chris.2y3@outlook.com>"]
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"]
-25
View File
@@ -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.
-96
View File
@@ -1,96 +0,0 @@
<div align="center">
<img src="https://raw.githubusercontent.com/visioncortex/vtracer/master/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="https://www.visioncortex.org/vtracer-docs">Article</a>
<span> | </span>
<a href="https://www.visioncortex.org/vtracer/">Demo</a>
<span> | </span>
<a href="https://github.com/visioncortex/vtracer/releases/latest">Download</a>
</h3>
<sub>Built with 🦀 by <a href="https://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](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 <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
```
### 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
```
-29
View File
@@ -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"]
-173
View File
@@ -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<u32>,
}
#[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<u32>,
}
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<Self, Self::Err> {
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<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;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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
}
-237
View File
@@ -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<SvgFile, String> {
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<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 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<SvgFile, String> {
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<SvgFile, String> {
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<ColorImage, String> {
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(())
}
-22
View File
@@ -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 <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.
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;
-282
View File
@@ -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::<usize>().is_ok() {
// is numeric
let value = value.trim().parse::<usize>().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::<i32>().is_ok() {
// is numeric
let value = value.trim().parse::<i32>().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::<i32>().is_ok() {
// is numeric
let value = value.trim().parse::<i32>().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::<i32>().is_ok() {
// is numeric
let value = value.trim().parse::<i32>().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::<f64>().is_ok() {
// is numeric
let value = value.trim().parse::<f64>().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::<i32>().is_ok() {
// is numeric
let value = value.trim().parse::<i32>().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::<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
);
}
}
(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);
}
}
}
-222
View File
@@ -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<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);
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
// 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(())
}
-75
View File
@@ -1,75 +0,0 @@
use std::fmt;
use visioncortex::{Color, CompoundPath, PointF64};
#[derive(Debug, Clone)]
pub struct SvgFile {
pub paths: Vec<SvgPath>,
pub width: usize,
pub height: usize,
pub path_precision: Option<u32>,
}
#[derive(Debug, Clone)]
pub struct SvgPath {
pub path: CompoundPath,
pub color: Color,
}
impl SvgFile {
pub fn new(width: usize, height: usize, path_precision: Option<u32>) -> 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#"<?xml version="1.0" encoding="UTF-8"?>"#)?;
writeln!(
f,
r#"<!-- Generator: visioncortex VTracer {} -->"#,
env!("CARGO_PKG_VERSION")
)?;
writeln!(
f,
r#"<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="{}" height="{}">"#,
self.width, self.height
)?;
for path in &self.paths {
path.fmt_with_precision(f, self.path_precision)?;
}
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
)
}
}
-85
View File
@@ -1,85 +0,0 @@
<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')
# 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).
-2
View File
@@ -1,2 +0,0 @@
from .vtracer import (convert_image_to_svg_py, convert_pixels_to_svg,
convert_raw_image_to_svg)
-49
View File
@@ -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:
...