mirror of
https://github.com/visioncortex/vtracer.git
synced 2026-09-19 10:35:53 -07:00
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:
+2
-3
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
*.svg
|
||||
*.png
|
||||
*.jpg
|
||||
@@ -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"]
|
||||
@@ -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.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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).
|
||||
@@ -1,2 +0,0 @@
|
||||
from .vtracer import (convert_image_to_svg_py, convert_pixels_to_svg,
|
||||
convert_raw_image_to_svg)
|
||||
@@ -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:
|
||||
...
|
||||
Reference in New Issue
Block a user