Files
vtracer/nodejs/src/lib.rs
T
Chris Tsang f4fe428038 Expose simplify in the Python and Node bindings
Config(simplify=...) / property in Python; simplify option in Node,
with the tolerance documented in .pyi, index.d.ts, and both READMEs.
The Node README's option list also catches up with the clustering
rename and the binary/watershed options, and test.js drops the stale
colorMode key (silently ignored since the rename, so its bw assertion
was testing the default path) and asserts simplify shrinks output.
2026-07-27 23:19:42 +01:00

193 lines
6.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! WebAssembly core for the vtracer Node package.
//!
//! Exposes vectorization over encoded image bytes or a raw RGBA buffer. Image
//! decoding happens here (in wasm), so the JS layer only needs `fs` — no
//! native dependency. Options are a plain JS object matching [`Options`].
use std::io::Cursor;
use serde::Deserialize;
use vtracer::{Color, ColorImage, Config};
use wasm_bindgen::prelude::*;
/// Conversion options; a subset may be provided from JS (camelCase). Anything
/// omitted uses the framework default.
#[derive(Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
struct Options {
/// Region forming: "color-cluster" | "bw" | "watershed".
clustering: Option<String>,
hierarchical: Option<String>,
mode: Option<String>,
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>,
/// Curve simplification tolerance in px (omit = off).
simplify: Option<f64>,
path_precision: Option<u32>,
palette: Option<Vec<String>>,
max_colors: Option<usize>,
optimize: Option<u8>,
/// Binary-mode fixed threshold (0..=255).
binary_threshold: Option<u8>,
/// Binary mode: use BradleyRoth adaptive thresholding.
adaptive: Option<bool>,
/// Adaptive window side length in px (0 = auto).
adaptive_window: Option<u32>,
/// Adaptive sensitivity: percent below the local mean (default 15).
adaptive_t: Option<f64>,
/// Watershed clustering: hierarchy cut level (0..=255).
watershed_detail: Option<u8>,
/// One of "bw" | "poster" | "photo"; applied before the other fields.
preset: Option<String>,
}
fn err(msg: impl std::fmt::Display) -> JsValue {
JsValue::from_str(&msg.to_string())
}
fn parse_hex(token: &str) -> Result<Color, JsValue> {
let hex = token.strip_prefix('#').unwrap_or(token);
if hex.len() != 6 {
return Err(err(format!("`{token}` is not a #rrggbb color")));
}
let b = |r: std::ops::Range<usize>| {
u8::from_str_radix(&hex[r], 16).map_err(|_| err(format!("`{token}` is not a #rrggbb color")))
};
Ok(Color::new(b(0..2)?, b(2..4)?, b(4..6)?))
}
fn config_from(options: JsValue) -> Result<Config, JsValue> {
let opts: Options = if options.is_undefined() || options.is_null() {
Options::default()
} else {
serde_wasm_bindgen::from_value(options).map_err(err)?
};
let mut config = match opts.preset.as_deref() {
Some("bw") => Config::from_preset(vtracer::Preset::Bw),
Some("poster") => Config::from_preset(vtracer::Preset::Poster),
Some("photo") => Config::from_preset(vtracer::Preset::Photo),
Some(other) => return Err(err(format!("unknown preset `{other}`"))),
None => Config::default(),
};
if let Some(v) = opts.clustering {
config.clustering = v.parse().map_err(err)?;
}
if let Some(v) = opts.hierarchical {
config.hierarchical = v.parse().map_err(err)?;
}
if let Some(v) = opts.mode {
config.mode = v.parse().map_err(err)?;
}
if let Some(v) = opts.filter_speckle {
config.filter_speckle = v;
}
if let Some(v) = opts.color_precision {
config.color_precision = v;
}
if let Some(v) = opts.layer_difference {
config.layer_difference = v;
}
if let Some(v) = opts.corner_threshold {
config.corner_threshold = v;
}
if let Some(v) = opts.length_threshold {
config.length_threshold = v;
}
if let Some(v) = opts.max_iterations {
config.max_iterations = v;
}
if let Some(v) = opts.splice_threshold {
config.splice_threshold = v;
}
if let Some(v) = opts.simplify {
config.simplify = Some(v);
}
if let Some(v) = opts.path_precision {
config.path_precision = Some(v);
}
if let Some(list) = opts.palette {
config.palette = list.iter().map(|s| parse_hex(s)).collect::<Result<_, _>>()?;
}
if let Some(v) = opts.max_colors {
config.max_colors = Some(v);
}
if let Some(v) = opts.optimize {
config.optimize = v;
}
if let Some(v) = opts.binary_threshold {
config.binary_threshold = v;
}
// Any adaptive tuning field (or `adaptive: true`) switches on BradleyRoth.
if opts.adaptive == Some(true) || opts.adaptive_window.is_some() || opts.adaptive_t.is_some() {
config.binary_adaptive = true;
}
if let Some(v) = opts.adaptive_window {
config.binary_adaptive_window = v;
}
if let Some(v) = opts.adaptive_t {
config.binary_adaptive_t = v;
}
if let Some(v) = opts.watershed_detail {
config.watershed_detail = v;
}
Ok(config)
}
fn to_svg(config: Config, img: ColorImage) -> Result<String, JsValue> {
config.build().map_err(err)?.to_svg(&img).map_err(err)
}
/// Vectorize encoded image bytes (PNG/JPEG/GIF/BMP). Returns the SVG string.
#[wasm_bindgen]
pub fn vectorize_bytes(data: &[u8], options: JsValue) -> Result<String, JsValue> {
let config = config_from(options)?;
let img = image::ImageReader::new(Cursor::new(data))
.with_guessed_format()
.map_err(err)?
.decode()
.map_err(|e| err(format!("failed to decode image: {e}")))?
.to_rgba8();
let (width, height) = (img.width() as usize, img.height() as usize);
to_svg(
config,
ColorImage {
pixels: img.into_raw(),
width,
height,
},
)
}
/// Vectorize a raw RGBA8 buffer (`width * height * 4` bytes). Returns the SVG.
#[wasm_bindgen]
pub fn vectorize_rgba(
data: Vec<u8>,
width: usize,
height: usize,
options: JsValue,
) -> Result<String, JsValue> {
if data.len() != width * height * 4 {
return Err(err(format!(
"rgba length {} != width*height*4 ({})",
data.len(),
width * height * 4
)));
}
let config = config_from(options)?;
to_svg(
config,
ColorImage {
pixels: data,
width,
height,
},
)
}