Most image-to-SVG converters are upload forms: your file goes to a server, gets traced, and comes back. PrivaSVG does the whole job in the browser tab. There is no backend. Here's how it's built, with the parts that were actually tricky.
The pipeline
Raster image -> AI background removal -> Manual retouch -> Upscale (2x / 4x) -> Vectorize (Rust -> WASM) -> SVG Every stage is optional except the last, so you can jump straight to vectorization. A bulk page runs up to 50 images through a fixed chain and downloads a single ZIP.
- Vectorization: a Rust engine compiled to WebAssembly with
wasm-bindgen. - Background removal:
@imgly/background-removal, which runs theisnet_fp16segmentation model on ONNX Runtime Web. The model is fetched once, cached, and then works offline. - Upscaling: 2x or 4x canvas resampling to recover detail before tracing.
- Retouch: a brush-based remove/restore tool to fix edges the model missed.
Credit where it's due
The tracing core is not from scratch. The Rust engine is built on VTracer and the visioncortex crate (the app depends on visioncortex 0.8.1). What PrivaSVG puts around it is the browser pipeline: the background-removal and retouch stages, upscaling and bulk processing. The keying approach described below follows the same idea as the upstream VTracer web app. The project is dual-licensed MIT OR Apache-2.0.
Tracing as an incremental state machine
Tracing a large image is slow work, so the converter is an incremental state machine that the UI drives with repeated tick() calls:
pub enum Stage { New, Clustering(IncrementalBuilder), Reclustering(IncrementalBuilder), Vectorize(Clusters), } Each tick() does a small slice of work and returns whether it's finished. Clustering groups pixels into color clusters. In cutout mode a second Reclustering pass runs on the cluster view. Vectorize then converts one cluster per tick into a compound path and prepends it to the target <svg>. A progress() method maps the stages onto 0 to 100 (clustering is the first half, vectorizing the second), so the UI can show a real progress bar. Because the work is split into small slices, control returns to the caller between ticks.
Parameters arrive as JSON (new_with_string) and are deserialized with serde. They are the same knobs the settings panel exposes: curve-fitting mode (pixel, polygon, spline), speckle filter, color precision, layer difference, corner threshold, splice threshold and path precision, plus a stacked or cutout hierarchy.
The transparency problem
Background removal produces transparent pixels, but a clustering tracer wants colors. If you let transparent pixels through as black, you get a black blob behind your subject in the SVG.
The converter handles this with a key color. In init() it finds a color that isn't used anywhere in the image, replaces transparent pixels with it (including near-transparent black pixels, which the canvas API can produce when it flattens alpha), and tells the clustering runner to Discard that color. The key color never appears in the output, so transparent regions stay transparent. If no unused color can be found, it falls back to a fixed, very unlikely cyan (1, 254, 253). The source comments also record a lesson learned: red as the first-choice key color caused visible red backgrounds, so less common colors are tried first.
Privacy is the architecture, not a policy
There's no server to trust because there's no server. Images never leave the tab, and there are no accounts or tracking. The WASM engine and the ONNX model both run on your device. That's a stronger guarantee than a privacy policy, and it's why the project shares its visual language with my other privacy project, PrivaSearch.
Honest limits
- The repo is archived, so it is read-only.
- The tracing quality is the visioncortex/VTracer algorithm's. It works well for logos, illustrations and cut-out subjects, and it will not turn a photograph into a faithful vector.
- The first background removal downloads the model, so it needs the network once.
- The cutout-mode transparency handling is heuristic, as the "fallback key color" above shows.
Try it
Live demo: https://haidaresber.github.io/privasvg/
Source (Rust, WASM, webpack): https://github.com/HaidarESBER/privasvg