mirror of
https://github.com/visioncortex/vtracer.git
synced 2026-09-26 05:51:21 -07:00
Add vtracer-py: Python bindings with a rich API
New crates/vtracer-py (pyo3 + maturin, abi3) wrapping the vtracer framework. Rather than a thin CLI-style wrapper, it exposes a mutable `Config` class with named properties and `bw`/`poster`/`photo` preset constructors, plus three input paths — `convert_file`, `convert_bytes` (encoded image, optional format), and `convert_pixels` (raw RGBA8) — available as `Config` methods and module-level functions. Palette is a list of `#rrggbb` strings; bad inputs raise ValueError. The core crate stays pure: image decoding lives here. The crate is excluded from the cargo workspace (pyo3 extension-module cdylibs don't link libpython, which breaks `cargo test` at the root) and is built with maturin. Ships a vtracer.pyi type stub. README updated.
This commit is contained in:
@@ -9,6 +9,8 @@ members = [
|
|||||||
# build. It is superseded by the crates/ workspace above.
|
# build. It is superseded by the crates/ workspace above.
|
||||||
exclude = [
|
exclude = [
|
||||||
"webapp",
|
"webapp",
|
||||||
|
# pyo3 extension-module cdylib; built with maturin, not the core workspace.
|
||||||
|
"crates/vtracer-py",
|
||||||
]
|
]
|
||||||
|
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|||||||
@@ -124,12 +124,28 @@ cargo add vtracer
|
|||||||
|
|
||||||
### Python Library
|
### Python Library
|
||||||
|
|
||||||
Since `0.6`, [`vtracer`](https://pypi.org/project/vtracer/) is also packaged as Python native extensions, thanks to the awesome [pyo3](https://github.com/PyO3/pyo3) project.
|
[`vtracer`](https://pypi.org/project/vtracer/) is also packaged as a Python native extension (built with [pyo3](https://github.com/PyO3/pyo3) + [maturin](https://www.maturin.rs), from the `crates/vtracer-py` crate).
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pip install vtracer
|
pip install vtracer
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
import vtracer
|
||||||
|
|
||||||
|
# one-liners
|
||||||
|
vtracer.convert_file("in.png", "out.svg")
|
||||||
|
svg = vtracer.convert_bytes(open("in.png", "rb").read())
|
||||||
|
|
||||||
|
# rich, reusable config + presets
|
||||||
|
cfg = vtracer.Config(mode="polygon", hierarchical="cutout")
|
||||||
|
cfg.palette = ["#1b1b1b", "#e0c088", "#5a7d3c"]
|
||||||
|
svg = cfg.convert_bytes(data)
|
||||||
|
vtracer.Config.poster().convert_file("photo.jpg", "poster.svg")
|
||||||
|
```
|
||||||
|
|
||||||
|
See [`crates/vtracer-py`](crates/vtracer-py/README.md) for the full API.
|
||||||
|
|
||||||
## Citations
|
## Citations
|
||||||
|
|
||||||
VTracer has since been cited by a few academic papers in computer graphics / vision research. Please kindly let us know if you have cited our work:
|
VTracer has since been cited by a few academic papers in computer graphics / vision research. Please kindly let us know if you have cited our work:
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
[package]
|
||||||
|
name = "vtracer-py"
|
||||||
|
description = "Python bindings for the vtracer vectorization framework."
|
||||||
|
version = "1.0.0-alpha.1"
|
||||||
|
authors = ["Chris Tsang <tyt2y7@gmail.com>"]
|
||||||
|
edition = "2021"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
homepage = "http://www.visioncortex.org/vtracer"
|
||||||
|
repository = "https://github.com/visioncortex/vtracer/"
|
||||||
|
|
||||||
|
# Excluded from the workspace: pyo3 `extension-module` cdylibs don't link
|
||||||
|
# libpython, which breaks `cargo test` at the workspace root. Built with
|
||||||
|
# maturin. Deps are declared explicitly (no workspace inheritance).
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
# Python imports this as `vtracer`.
|
||||||
|
name = "vtracer"
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
vtracer = { version = "1.0.0-alpha.1", path = "../vtracer" }
|
||||||
|
image = "0.25"
|
||||||
|
pyo3 = { version = "0.22", features = ["extension-module", "abi3-py38"] }
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# vtracer (Python)
|
||||||
|
|
||||||
|
Python bindings for the [`vtracer`](https://github.com/visioncortex/vtracer)
|
||||||
|
raster-to-vector framework. Built with [pyo3](https://pyo3.rs) +
|
||||||
|
[maturin](https://www.maturin.rs); the core Rust crate stays pure (no I/O), and
|
||||||
|
this crate adds image decoding and a Pythonic API.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pip install vtracer
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```python
|
||||||
|
import vtracer
|
||||||
|
|
||||||
|
# one-liners
|
||||||
|
vtracer.convert_file("in.png", "out.svg")
|
||||||
|
svg = vtracer.convert_bytes(open("in.png", "rb").read()) # -> str
|
||||||
|
svg = vtracer.convert_pixels(rgba_bytes, width, height) # raw RGBA8
|
||||||
|
|
||||||
|
# a rich, reusable configuration object
|
||||||
|
cfg = vtracer.Config(mode="polygon", filter_speckle=8)
|
||||||
|
cfg.hierarchical = "cutout" # seam-free mosaic
|
||||||
|
cfg.palette = ["#1b1b1b", "#e0c088", "#5a7d3c"] # snap to a fixed palette
|
||||||
|
cfg.max_colors = 8 # or auto-quantize
|
||||||
|
cfg.optimize = 2
|
||||||
|
svg = cfg.convert_bytes(data)
|
||||||
|
|
||||||
|
# presets
|
||||||
|
vtracer.Config.poster().convert_file("photo.jpg", "poster.svg")
|
||||||
|
vtracer.Config.bw().convert_file("scan.png", "lineart.svg")
|
||||||
|
```
|
||||||
|
|
||||||
|
### `Config`
|
||||||
|
|
||||||
|
Constructor keyword arguments (all optional) — also exposed as mutable
|
||||||
|
properties, plus the presets `Config.bw()`, `Config.poster()`, `Config.photo()`:
|
||||||
|
|
||||||
|
| arg | default | notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `color_mode` | `"color"` | `"color"` or `"bw"` |
|
||||||
|
| `hierarchical` | `"stacked"` | `"stacked"` or `"cutout"` (mosaic) |
|
||||||
|
| `mode` | `"spline"` | `"pixel"`, `"polygon"`, `"spline"` |
|
||||||
|
| `filter_speckle` | `4` | discard patches smaller than X px |
|
||||||
|
| `color_precision` | `6` | significant bits per channel |
|
||||||
|
| `layer_difference` | `16` | color diff between gradient layers |
|
||||||
|
| `corner_threshold` | `60` | degrees |
|
||||||
|
| `length_threshold` | `4.0` | px |
|
||||||
|
| `max_iterations` | `10` | |
|
||||||
|
| `splice_threshold` | `45` | degrees |
|
||||||
|
| `path_precision` | `2` | output decimal places |
|
||||||
|
| `palette` | `None` | list of `#rrggbb` strings |
|
||||||
|
| `max_colors` | `None` | auto-quantize target |
|
||||||
|
| `optimize` | `1` | `0` off, `1` quantize+simplify, `2` + shorthands |
|
||||||
|
|
||||||
|
Each `Config` has `convert_file(input, output)`, `convert_bytes(data, format=None) -> str`,
|
||||||
|
and `convert_pixels(rgba, width, height) -> str`.
|
||||||
|
|
||||||
|
## Build from source
|
||||||
|
|
||||||
|
```sh
|
||||||
|
maturin develop # into the active virtualenv
|
||||||
|
maturin build --release # produce a wheel
|
||||||
|
```
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["maturin>=1.5,<2.0"]
|
||||||
|
build-backend = "maturin"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "vtracer"
|
||||||
|
description = "Raster to vector graphics converter — Python bindings for the vtracer framework."
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
license = { text = "MIT OR Apache-2.0" }
|
||||||
|
authors = [{ name = "Chris Tsang", email = "tyt2y7@gmail.com" }]
|
||||||
|
keywords = ["svg", "vectorization", "raster", "computer-graphics"]
|
||||||
|
classifiers = [
|
||||||
|
"Programming Language :: Rust",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Topic :: Multimedia :: Graphics",
|
||||||
|
]
|
||||||
|
dynamic = ["version"]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = "http://www.visioncortex.org/vtracer"
|
||||||
|
Repository = "https://github.com/visioncortex/vtracer/"
|
||||||
|
|
||||||
|
[tool.maturin]
|
||||||
|
# Pure-Rust extension module; the compiled library is imported as `vtracer`.
|
||||||
|
module-name = "vtracer"
|
||||||
|
features = ["pyo3/extension-module"]
|
||||||
@@ -0,0 +1,446 @@
|
|||||||
|
//! Python bindings for the `vtracer` vectorization framework.
|
||||||
|
//!
|
||||||
|
//! The API centers on a mutable [`Config`] object with named properties and
|
||||||
|
//! preset constructors, plus three input paths — a file, encoded image bytes,
|
||||||
|
//! or a raw RGBA buffer — each returning the SVG (or writing it to disk):
|
||||||
|
//!
|
||||||
|
//! ```python
|
||||||
|
//! import vtracer
|
||||||
|
//!
|
||||||
|
//! # one-liners
|
||||||
|
//! vtracer.convert_file("in.png", "out.svg")
|
||||||
|
//! svg = vtracer.convert_bytes(open("in.png", "rb").read())
|
||||||
|
//!
|
||||||
|
//! # rich, reusable config
|
||||||
|
//! cfg = vtracer.Config(mode="polygon", hierarchical="cutout")
|
||||||
|
//! cfg.max_colors = 8
|
||||||
|
//! cfg.palette = ["#1b1b1b", "#e0c088", "#5a7d3c"]
|
||||||
|
//! svg = cfg.convert_bytes(data)
|
||||||
|
//!
|
||||||
|
//! # presets
|
||||||
|
//! vtracer.Config.poster().convert_file("photo.jpg", "poster.svg")
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use std::io::Cursor;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use pyo3::exceptions::{PyIOError, PyValueError};
|
||||||
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
|
use ::vtracer::{Color, ColorImage, ColorMode, Config as CoreConfig, FitMode, Hierarchical, Preset};
|
||||||
|
|
||||||
|
// --- string <-> enum helpers -------------------------------------------------
|
||||||
|
|
||||||
|
fn parse<T: std::str::FromStr<Err = String>>(s: &str) -> PyResult<T> {
|
||||||
|
s.parse().map_err(PyValueError::new_err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn color_mode_str(m: ColorMode) -> &'static str {
|
||||||
|
match m {
|
||||||
|
ColorMode::Color => "color",
|
||||||
|
ColorMode::Binary => "bw",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hierarchical_str(h: Hierarchical) -> &'static str {
|
||||||
|
match h {
|
||||||
|
Hierarchical::Stacked => "stacked",
|
||||||
|
Hierarchical::Cutout => "cutout",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mode_str(m: FitMode) -> &'static str {
|
||||||
|
match m {
|
||||||
|
FitMode::Pixel => "pixel",
|
||||||
|
FitMode::Polygon => "polygon",
|
||||||
|
FitMode::Spline => "spline",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_hex(token: &str) -> PyResult<Color> {
|
||||||
|
let hex = token.strip_prefix('#').unwrap_or(token);
|
||||||
|
if hex.len() != 6 {
|
||||||
|
return Err(PyValueError::new_err(format!(
|
||||||
|
"`{token}` is not a #rrggbb color"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let byte = |r: std::ops::Range<usize>| {
|
||||||
|
u8::from_str_radix(&hex[r], 16)
|
||||||
|
.map_err(|_| PyValueError::new_err(format!("`{token}` is not a #rrggbb color")))
|
||||||
|
};
|
||||||
|
Ok(Color::new(byte(0..2)?, byte(2..4)?, byte(4..6)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- image helpers -----------------------------------------------------------
|
||||||
|
|
||||||
|
fn dynimg_to_color(img: image::DynamicImage) -> ColorImage {
|
||||||
|
let img = img.to_rgba8();
|
||||||
|
let (w, h) = (img.width() as usize, img.height() as usize);
|
||||||
|
ColorImage {
|
||||||
|
pixels: img.into_raw(),
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_bytes(bytes: &[u8], format: Option<&str>) -> PyResult<ColorImage> {
|
||||||
|
let mut reader = image::ImageReader::new(Cursor::new(bytes));
|
||||||
|
match format {
|
||||||
|
Some(ext) => {
|
||||||
|
let fmt = image::ImageFormat::from_extension(ext)
|
||||||
|
.ok_or_else(|| PyValueError::new_err(format!("unknown image format `{ext}`")))?;
|
||||||
|
reader.set_format(fmt);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
reader = reader
|
||||||
|
.with_guessed_format()
|
||||||
|
.map_err(|e| PyValueError::new_err(e.to_string()))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let img = reader
|
||||||
|
.decode()
|
||||||
|
.map_err(|e| PyValueError::new_err(format!("failed to decode image: {e}")))?;
|
||||||
|
Ok(dynimg_to_color(img))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Config ------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Conversion configuration. Construct with keyword arguments or a preset,
|
||||||
|
/// mutate via properties, then call one of the `convert_*` methods.
|
||||||
|
#[pyclass(name = "Config")]
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct PyConfig {
|
||||||
|
inner: CoreConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PyConfig {
|
||||||
|
fn to_svg(&self, img: &ColorImage) -> PyResult<String> {
|
||||||
|
self.inner
|
||||||
|
.build()
|
||||||
|
.map_err(|e| PyValueError::new_err(e.to_string()))?
|
||||||
|
.to_svg(img)
|
||||||
|
.map_err(|e| PyValueError::new_err(e.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pymethods]
|
||||||
|
impl PyConfig {
|
||||||
|
#[new]
|
||||||
|
#[pyo3(signature = (
|
||||||
|
color_mode = "color",
|
||||||
|
hierarchical = "stacked",
|
||||||
|
mode = "spline",
|
||||||
|
filter_speckle = 4,
|
||||||
|
color_precision = 6,
|
||||||
|
layer_difference = 16,
|
||||||
|
corner_threshold = 60,
|
||||||
|
length_threshold = 4.0,
|
||||||
|
max_iterations = 10,
|
||||||
|
splice_threshold = 45,
|
||||||
|
path_precision = 2,
|
||||||
|
palette = None,
|
||||||
|
max_colors = None,
|
||||||
|
optimize = 1,
|
||||||
|
))]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn new(
|
||||||
|
color_mode: &str,
|
||||||
|
hierarchical: &str,
|
||||||
|
mode: &str,
|
||||||
|
filter_speckle: usize,
|
||||||
|
color_precision: i32,
|
||||||
|
layer_difference: i32,
|
||||||
|
corner_threshold: i32,
|
||||||
|
length_threshold: f64,
|
||||||
|
max_iterations: usize,
|
||||||
|
splice_threshold: i32,
|
||||||
|
path_precision: u32,
|
||||||
|
palette: Option<Vec<String>>,
|
||||||
|
max_colors: Option<usize>,
|
||||||
|
optimize: u8,
|
||||||
|
) -> PyResult<Self> {
|
||||||
|
let palette = match palette {
|
||||||
|
Some(list) => list.iter().map(|s| parse_hex(s)).collect::<PyResult<_>>()?,
|
||||||
|
None => Vec::new(),
|
||||||
|
};
|
||||||
|
Ok(Self {
|
||||||
|
inner: CoreConfig {
|
||||||
|
color_mode: parse(color_mode)?,
|
||||||
|
hierarchical: parse(hierarchical)?,
|
||||||
|
mode: parse(mode)?,
|
||||||
|
filter_speckle,
|
||||||
|
color_precision,
|
||||||
|
layer_difference,
|
||||||
|
corner_threshold,
|
||||||
|
length_threshold,
|
||||||
|
max_iterations,
|
||||||
|
splice_threshold,
|
||||||
|
path_precision: Some(path_precision),
|
||||||
|
palette,
|
||||||
|
max_colors,
|
||||||
|
optimize,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Preset for black & white line art.
|
||||||
|
#[staticmethod]
|
||||||
|
fn bw() -> Self {
|
||||||
|
Self { inner: CoreConfig::from_preset(Preset::Bw) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Preset for posterized color art.
|
||||||
|
#[staticmethod]
|
||||||
|
fn poster() -> Self {
|
||||||
|
Self { inner: CoreConfig::from_preset(Preset::Poster) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Preset tuned for photographs.
|
||||||
|
#[staticmethod]
|
||||||
|
fn photo() -> Self {
|
||||||
|
Self { inner: CoreConfig::from_preset(Preset::Photo) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- properties ---
|
||||||
|
|
||||||
|
#[getter]
|
||||||
|
fn color_mode(&self) -> &'static str {
|
||||||
|
color_mode_str(self.inner.color_mode)
|
||||||
|
}
|
||||||
|
#[setter]
|
||||||
|
fn set_color_mode(&mut self, v: &str) -> PyResult<()> {
|
||||||
|
self.inner.color_mode = parse(v)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[getter]
|
||||||
|
fn hierarchical(&self) -> &'static str {
|
||||||
|
hierarchical_str(self.inner.hierarchical)
|
||||||
|
}
|
||||||
|
#[setter]
|
||||||
|
fn set_hierarchical(&mut self, v: &str) -> PyResult<()> {
|
||||||
|
self.inner.hierarchical = parse(v)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[getter]
|
||||||
|
fn mode(&self) -> &'static str {
|
||||||
|
mode_str(self.inner.mode)
|
||||||
|
}
|
||||||
|
#[setter]
|
||||||
|
fn set_mode(&mut self, v: &str) -> PyResult<()> {
|
||||||
|
self.inner.mode = parse(v)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[getter]
|
||||||
|
fn filter_speckle(&self) -> usize {
|
||||||
|
self.inner.filter_speckle
|
||||||
|
}
|
||||||
|
#[setter]
|
||||||
|
fn set_filter_speckle(&mut self, v: usize) {
|
||||||
|
self.inner.filter_speckle = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[getter]
|
||||||
|
fn color_precision(&self) -> i32 {
|
||||||
|
self.inner.color_precision
|
||||||
|
}
|
||||||
|
#[setter]
|
||||||
|
fn set_color_precision(&mut self, v: i32) {
|
||||||
|
self.inner.color_precision = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[getter]
|
||||||
|
fn layer_difference(&self) -> i32 {
|
||||||
|
self.inner.layer_difference
|
||||||
|
}
|
||||||
|
#[setter]
|
||||||
|
fn set_layer_difference(&mut self, v: i32) {
|
||||||
|
self.inner.layer_difference = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[getter]
|
||||||
|
fn corner_threshold(&self) -> i32 {
|
||||||
|
self.inner.corner_threshold
|
||||||
|
}
|
||||||
|
#[setter]
|
||||||
|
fn set_corner_threshold(&mut self, v: i32) {
|
||||||
|
self.inner.corner_threshold = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[getter]
|
||||||
|
fn length_threshold(&self) -> f64 {
|
||||||
|
self.inner.length_threshold
|
||||||
|
}
|
||||||
|
#[setter]
|
||||||
|
fn set_length_threshold(&mut self, v: f64) {
|
||||||
|
self.inner.length_threshold = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[getter]
|
||||||
|
fn max_iterations(&self) -> usize {
|
||||||
|
self.inner.max_iterations
|
||||||
|
}
|
||||||
|
#[setter]
|
||||||
|
fn set_max_iterations(&mut self, v: usize) {
|
||||||
|
self.inner.max_iterations = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[getter]
|
||||||
|
fn splice_threshold(&self) -> i32 {
|
||||||
|
self.inner.splice_threshold
|
||||||
|
}
|
||||||
|
#[setter]
|
||||||
|
fn set_splice_threshold(&mut self, v: i32) {
|
||||||
|
self.inner.splice_threshold = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[getter]
|
||||||
|
fn path_precision(&self) -> Option<u32> {
|
||||||
|
self.inner.path_precision
|
||||||
|
}
|
||||||
|
#[setter]
|
||||||
|
fn set_path_precision(&mut self, v: Option<u32>) {
|
||||||
|
self.inner.path_precision = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[getter]
|
||||||
|
fn palette(&self) -> Vec<String> {
|
||||||
|
self.inner.palette.iter().map(Color::to_hex_string).collect()
|
||||||
|
}
|
||||||
|
#[setter]
|
||||||
|
fn set_palette(&mut self, v: Vec<String>) -> PyResult<()> {
|
||||||
|
self.inner.palette = v.iter().map(|s| parse_hex(s)).collect::<PyResult<_>>()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[getter]
|
||||||
|
fn max_colors(&self) -> Option<usize> {
|
||||||
|
self.inner.max_colors
|
||||||
|
}
|
||||||
|
#[setter]
|
||||||
|
fn set_max_colors(&mut self, v: Option<usize>) {
|
||||||
|
self.inner.max_colors = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[getter]
|
||||||
|
fn optimize(&self) -> u8 {
|
||||||
|
self.inner.optimize
|
||||||
|
}
|
||||||
|
#[setter]
|
||||||
|
fn set_optimize(&mut self, v: u8) {
|
||||||
|
self.inner.optimize = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- conversion ---
|
||||||
|
|
||||||
|
/// Trace the image at `input_path` and write the SVG to `output_path`.
|
||||||
|
fn convert_file(&self, input_path: PathBuf, output_path: PathBuf) -> PyResult<()> {
|
||||||
|
let img = image::open(&input_path)
|
||||||
|
.map_err(|e| PyIOError::new_err(format!("cannot open `{}`: {e}", input_path.display())))?;
|
||||||
|
let svg = self.to_svg(&dynimg_to_color(img))?;
|
||||||
|
std::fs::write(&output_path, svg)
|
||||||
|
.map_err(|e| PyIOError::new_err(format!("cannot write `{}`: {e}", output_path.display())))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trace encoded image `data` (png/jpg/...) and return the SVG string.
|
||||||
|
/// `format` (e.g. "png") overrides content-based format detection.
|
||||||
|
#[pyo3(signature = (data, format = None))]
|
||||||
|
fn convert_bytes(&self, data: Vec<u8>, format: Option<&str>) -> PyResult<String> {
|
||||||
|
self.to_svg(&decode_bytes(&data, format)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trace a raw RGBA8 buffer (`width * height * 4` bytes) and return the SVG.
|
||||||
|
fn convert_pixels(&self, rgba: Vec<u8>, width: usize, height: usize) -> PyResult<String> {
|
||||||
|
if rgba.len() != width * height * 4 {
|
||||||
|
return Err(PyValueError::new_err(format!(
|
||||||
|
"rgba length {} != width*height*4 ({})",
|
||||||
|
rgba.len(),
|
||||||
|
width * height * 4
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
self.to_svg(&ColorImage {
|
||||||
|
pixels: rgba,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn __repr__(&self) -> String {
|
||||||
|
let c = &self.inner;
|
||||||
|
format!(
|
||||||
|
"Config(color_mode='{}', hierarchical='{}', mode='{}', filter_speckle={}, \
|
||||||
|
color_precision={}, layer_difference={}, corner_threshold={}, length_threshold={}, \
|
||||||
|
max_iterations={}, splice_threshold={}, path_precision={:?}, palette={} colors, \
|
||||||
|
max_colors={:?}, optimize={})",
|
||||||
|
color_mode_str(c.color_mode),
|
||||||
|
hierarchical_str(c.hierarchical),
|
||||||
|
mode_str(c.mode),
|
||||||
|
c.filter_speckle,
|
||||||
|
c.color_precision,
|
||||||
|
c.layer_difference,
|
||||||
|
c.corner_threshold,
|
||||||
|
c.length_threshold,
|
||||||
|
c.max_iterations,
|
||||||
|
c.splice_threshold,
|
||||||
|
c.path_precision,
|
||||||
|
c.palette.len(),
|
||||||
|
c.max_colors,
|
||||||
|
c.optimize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- module-level convenience ------------------------------------------------
|
||||||
|
|
||||||
|
/// Convert a file to SVG on disk, using `config` (or defaults).
|
||||||
|
#[pyfunction]
|
||||||
|
#[pyo3(signature = (input_path, output_path, config = None))]
|
||||||
|
fn convert_file(
|
||||||
|
input_path: PathBuf,
|
||||||
|
output_path: PathBuf,
|
||||||
|
config: Option<PyConfig>,
|
||||||
|
) -> PyResult<()> {
|
||||||
|
config.unwrap_or_else(default_config).convert_file(input_path, output_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert encoded image bytes to an SVG string, using `config` (or defaults).
|
||||||
|
#[pyfunction]
|
||||||
|
#[pyo3(signature = (data, config = None, format = None))]
|
||||||
|
fn convert_bytes(
|
||||||
|
data: Vec<u8>,
|
||||||
|
config: Option<PyConfig>,
|
||||||
|
format: Option<&str>,
|
||||||
|
) -> PyResult<String> {
|
||||||
|
config.unwrap_or_else(default_config).convert_bytes(data, format)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a raw RGBA8 buffer to an SVG string, using `config` (or defaults).
|
||||||
|
#[pyfunction]
|
||||||
|
#[pyo3(signature = (rgba, width, height, config = None))]
|
||||||
|
fn convert_pixels(
|
||||||
|
rgba: Vec<u8>,
|
||||||
|
width: usize,
|
||||||
|
height: usize,
|
||||||
|
config: Option<PyConfig>,
|
||||||
|
) -> PyResult<String> {
|
||||||
|
config.unwrap_or_else(default_config).convert_pixels(rgba, width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_config() -> PyConfig {
|
||||||
|
PyConfig {
|
||||||
|
inner: CoreConfig::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pymodule]
|
||||||
|
#[pyo3(name = "vtracer")]
|
||||||
|
fn vtracer_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||||
|
m.add_class::<PyConfig>()?;
|
||||||
|
m.add_function(wrap_pyfunction!(convert_file, m)?)?;
|
||||||
|
m.add_function(wrap_pyfunction!(convert_bytes, m)?)?;
|
||||||
|
m.add_function(wrap_pyfunction!(convert_pixels, m)?)?;
|
||||||
|
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
__version__: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
"""Conversion configuration. Construct with keyword arguments or a preset,
|
||||||
|
mutate via properties, then call one of the ``convert_*`` methods."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
color_mode: str = "color", # "color" | "bw"
|
||||||
|
hierarchical: str = "stacked", # "stacked" | "cutout" (mosaic)
|
||||||
|
mode: str = "spline", # "pixel" | "polygon" | "spline"
|
||||||
|
filter_speckle: int = 4,
|
||||||
|
color_precision: int = 6,
|
||||||
|
layer_difference: int = 16,
|
||||||
|
corner_threshold: int = 60,
|
||||||
|
length_threshold: float = 4.0,
|
||||||
|
max_iterations: int = 10,
|
||||||
|
splice_threshold: int = 45,
|
||||||
|
path_precision: int = 2,
|
||||||
|
palette: Optional[list[str]] = None, # e.g. ["#112233", "#445566"]
|
||||||
|
max_colors: Optional[int] = None, # auto-quantize target
|
||||||
|
optimize: int = 1, # 0 | 1 | 2
|
||||||
|
) -> None: ...
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def bw() -> "Config": ...
|
||||||
|
@staticmethod
|
||||||
|
def poster() -> "Config": ...
|
||||||
|
@staticmethod
|
||||||
|
def photo() -> "Config": ...
|
||||||
|
|
||||||
|
color_mode: str
|
||||||
|
hierarchical: str
|
||||||
|
mode: str
|
||||||
|
filter_speckle: int
|
||||||
|
color_precision: int
|
||||||
|
layer_difference: int
|
||||||
|
corner_threshold: int
|
||||||
|
length_threshold: float
|
||||||
|
max_iterations: int
|
||||||
|
splice_threshold: int
|
||||||
|
path_precision: Optional[int]
|
||||||
|
palette: list[str]
|
||||||
|
max_colors: Optional[int]
|
||||||
|
optimize: int
|
||||||
|
|
||||||
|
def convert_file(self, input_path: str, output_path: str) -> None: ...
|
||||||
|
def convert_bytes(self, data: bytes, format: Optional[str] = None) -> str: ...
|
||||||
|
def convert_pixels(self, rgba: bytes, width: int, height: int) -> str: ...
|
||||||
|
|
||||||
|
def convert_file(input_path: str, output_path: str, config: Optional[Config] = None) -> None: ...
|
||||||
|
def convert_bytes(data: bytes, config: Optional[Config] = None, format: Optional[str] = None) -> str: ...
|
||||||
|
def convert_pixels(rgba: bytes, width: int, height: int, config: Optional[Config] = None) -> str: ...
|
||||||
Reference in New Issue
Block a user