Kurviq is a fast, deterministic raster-to-SVG tracer for Node.js. It ships its engine as WebAssembly, has zero runtime dependencies, and supports Node.js 18 or newer.
npm i kurviqUse it immediately from the command line:
npx kurviq --input in.png --output out.svgA preset is applied first; explicitly supplied tracing options override it.
| Option | Values or range | Default |
|---|---|---|
-i, --input <path> |
Input PNG, JPEG, or other supported raster path | required |
-o, --output <path> |
Output SVG path | required |
--preset |
bw, poster, photo |
none |
--colormode |
color, binary, bw |
color |
--hierarchical |
stacked, cutout |
stacked |
-m, --mode |
pixel, polygon, spline |
polygon |
-f, --filter_speckle |
integer 0–16 |
2 |
-p, --color_precision |
integer 1–8 |
6 |
-g, --gradient_step |
integer 0–255 |
16 |
-c, --corner_threshold |
integer 0–180 |
60 |
-l, --segment_length |
number 3.5–10 |
4 |
-s, --splice_threshold |
integer 0–180 |
45 |
--path_precision |
unsigned integer | 2 |
--upscale |
number 1–4 |
1 |
--max-size |
integer 64–4096 |
off |
--sharpen [colors] |
bare flag or integer 1–64 |
off; bare flag uses 12 |
--alpha-mode |
auto, keep, threshold, threshold:N (N is 0–255) |
auto; threshold cutoff 128 |
--remove-background [auto|#RRGGBB] |
bare flag, auto, or a six-digit RGB color |
off |
--background-fuzz |
integer 0–64 |
10 |
--background-channel |
integer radius 0–8 |
2 |
--background-shadows |
remove, keep |
remove |
--shadow-chroma |
integer 0–255 |
8 |
--shadow-luma-min |
integer 0–255 |
205 |
--shadow-luma-max |
integer 0–255 |
249 |
--auto |
Classify the source and pick a recipe | off |
--explain |
Print the classification and chosen flags to stderr | off |
--consolidate [colors] |
bare flag, or integer 2–256 |
off; bare uses perceptual ΔE 2.3 |
--flatten |
bare flag | off |
--snap [tolerance] |
bare flag, or number (0,0.2] |
off; bare uses 0.004 |
--structure |
off, groups, layers |
off |
--theme |
off, css-vars, classes |
off |
--animate |
none, reveal, draw |
none |
--animate-duration |
milliseconds | 1200 |
--animate-stagger |
milliseconds | 40 |
--animate-easing |
linear, ease, ease-in-out |
ease-in-out |
-h, --help |
Show usage | — |
-V, --version |
Print the installed version | — |
For command-line compatibility, --colormode binary resolves to color. Use
--colormode bw or --preset bw for binary tracing. The Node API accepts the
semantic value colormode: 'binary'.
npx kurviq --input in.png --output out.svg --auto --explain--auto traces once at defaults, classifies the result, and re-traces with a
matching recipe. --explain prints the decision and the exact flags to stderr;
those flags reproduce the run byte-identically, so pin them once you are happy
and drop --auto from your build.
Measured over 47 test images, --auto improved 33 and left 7 unchanged,
reducing mean structural distance by 37%. Seven regressed slightly — it is a
heuristic, not an oracle, which is why --explain exists. --auto never
enables a document pass below: those change the shape of the document, which is
your intent, not a property of the image.
These change the emitted document rather than the trace. All are opt-in; with none of them the output is byte-identical to previous versions.
| Flag | What it does |
|---|---|
--consolidate [colors] |
Merges perceptually equal fills. A default trace of a photographic source can carry tens of thousands of near-identical colours; this is what makes the passes below meaningful. |
--flatten |
Merges elements that share a paint. Render-identical by construction. |
--snap [tolerance] |
Emits real <circle>, <rect> and <ellipse> where the fit is provable. |
--structure groups |
Adds stable ids and palette classes so the output is addressable. |
--structure layers |
Nests the engine cluster tree as <g> where paint order allows; otherwise flat ids. |
--theme css-vars |
Emits the palette as CSS custom properties. |
--animate reveal |
CSS-only entrance animation. Requires --structure groups. |
npx kurviq -i icon.png -o icon.svg --consolidate 12 --snap --structure groupsNotes worth knowing before you use them:
- Bare
--consolidateis safe on every image tested. A hard cap such as--consolidate 12is a deliberate posterisation: good on flat artwork, destructive on photographs. --themescopes its custom-property rules under akurviq-themeclass that Kurviq never sets. Add it to the<svg>or an ancestor to activate theming. Without it the literalfillattributes paint, so rasterisers that do not support CSS custom properties are unaffected.--animaterefuses above 120 animated elements, where the CSS would dwarf the geometry. Run--consolidatefirst, or pass--animate-force.--snapleaves rings and annuli alone: splitting a shape whose hole is encoded by opposite winding would fill the hole.
| Input | Recommended settings | Why |
|---|---|---|
| Logos, icons, and large flat shapes | Defaults | Fast and compact for ordinary artwork |
| Oversized sources and AI renders | --max-size 720, then add tracing options as needed |
Caps tracing complexity without changing the SVG's displayed dimensions |
| Small text and fine detail | --upscale 2 |
Gives the tracer more pixels while preserving the original SVG display size |
| Rounded flat icons | --mode spline --filter_speckle 4 --path_precision 1 --segment_length 8, optionally with --upscale 2 |
Fits smooth contours instead of polygon facets |
| Soft-shaded icons | Add --gradient_step 8 --color_precision 8 to the spline recipe |
Retains smoother shading and gradients |
| Blurry flat-color art | Add --sharpen to the spline recipe |
Recovers a compact palette and snaps antialiased ramps |
| Icon cutouts on a uniform background | --remove-background auto --background-fuzz 3 --background-channel 3 |
Removes border-connected background and attached neutral shadows while guarding narrow channels |
| Images with meaningful soft transparency | Choose --alpha-mode keep, --alpha-mode threshold:N, or the default auto |
Controls whether partial alpha is retained or made binary |
Avoid combining --sharpen with the gradient flags: sharpening deliberately
flattens color ramps, while the gradient flags preserve them.
npx kurviq --input render.png --output render.svg --max-size 720--max-size caps the largest working-canvas dimension while preserving aspect
ratio and rounding the other dimension to the nearest pixel. It runs
immediately after decode using premultiplied-alpha Lanczos3, before alpha
handling, upscaling, and background removal. Sources already within the cap
take a byte-identical no-op path.
Like --upscale, downscaling changes tracing coordinates but not display
size: SVG width and height remain the decoded input's original dimensions,
and the viewBox describes the final working canvas. The options compose in
order, so a 1024×1024 input with --max-size 720 --upscale 2 is traced at
1440×1440 but still displays at 1024×1024.
npx kurviq --input diagram.png --output diagram.svg --upscale 2Built-in upscaling leaves the SVG's displayed width and height at the
input dimensions and enlarges its coordinate system through the viewBox.
Start with 2×; higher values cost more time and usually produce larger SVGs.
npx kurviq --input soft-icon.png --output crisp-icon.svg \
--upscale 2 --mode spline --filter_speckle 4 \
--path_precision 1 --segment_length 8 --sharpenThe bare --sharpen flag uses at most 12 recovered colors. Pass another
ceiling from 1 to 64 as --sharpen 6 or --sharpen=6. Sharpening is intended
for flat-color artwork; do not use it for photos, continuous gradients, or
intentionally soft shading.
The bare --remove-background flag is equivalent to
--remove-background auto. Auto selection uses the image border. An explicit
color is useful when the border is not representative:
npx kurviq --input icon.png --output icon.svg \
--remove-background '#f2f4f8' --background-fuzz 10Removal is deterministic and limited to border-connected pixels. The default
--background-channel 2 protects enclosed areas reached only through narrow
gaps; use --background-channel 0 for an unrestricted four-connected flood.
The default fuzz of 10 is suitable for distinctly colored uniform
backgrounds, while the conservative icon recipe uses a fuzz of 3.
Attached neutral shadows are removed by default. Use
--background-shadows keep when the shadow belongs in the artwork. The sweep
can be tuned with --shadow-chroma, --shadow-luma-min, and
--shadow-luma-max; the luma minimum must not exceed the maximum.
The default --alpha-mode auto measures partial-alpha pixels against the
decoded original's visible silhouette, before any --max-size downscale. At
the 1% gate, resizing becomes alpha-aware and the result is thresholded at
128. This prevents hidden RGB values in transparent pixels from bleeding into
the trace.
Use --alpha-mode threshold to force cutoff 128,
--alpha-mode threshold:N to choose a cutoff from 0 to 255, or
--alpha-mode keep to retain the straight-RGBA path. On auto-gated soft-alpha
inputs, Kurviq intentionally improves on and diverges from, the native binary.
The CommonJS API has exactly two functions:
const fs = require('node:fs');
const { convert, convertFile } = require('kurviq');
const svg = convert(fs.readFileSync('in.png'), {
mode: 'spline',
maxSize: 720,
upscale: 2,
sharpen: true,
alphaMode: 'auto',
removeBackground: 'auto',
backgroundFuzz: 3,
backgroundChannel: 3,
});
convertFile('in.png', 'out.svg', { preset: 'poster' });convert(imageBytes, options?) accepts a Buffer or Uint8Array and returns
an SVG string. convertFile(inputPath, outputPath, options?) reads and writes
files. Both functions are synchronous and CPU-bound; use a worker thread when
conversion must not block the event loop.
Options use the CLI names, except camel-cased additions such as maxSize,
alphaMode, removeBackground, backgroundFuzz, backgroundChannel,
backgroundShadows, shadowChroma, shadowLumaMin, and shadowLumaMax.
Both colormode and color_mode are accepted.
Kurviq also builds SVG, not just traces it. Zero dependencies, deterministic, and exposed on subpaths so the root API stays two functions.
const { createScene, shapes } = require('kurviq/author');
const { linearGradient } = require('kurviq/author-paint');
const scene = createScene({ width: 200, height: 100 });
const ramp = linearGradient(scene, {
units: 'objectBoundingBox',
stops: [{ offset: 0, color: '#3366ff' }, { offset: 1, color: '#10b981' }],
});
scene.add(shapes.rect(0, 0, 200, 100, { fill: ramp.ref }));
scene.serialize();| Subpath | Provides |
|---|---|
kurviq/author |
scenes, shapes, transforms, path data, deterministic ids |
kurviq/author-paint |
gradients, patterns, hatch, symbol/use, expandUses |
kurviq/author-stroke |
stroke attributes, markers, arrowheads |
kurviq/author-mask |
clipPath, mask, fade masks |
kurviq/author-filter |
blur, drop shadow, colour matrix, morphology, creative noise |
kurviq/author-interactive |
hover/target states, links, focus, reduced motion, safety checks |
kurviq/author-animate |
AnimationPlan → CSS or SMIL, frame sampling |
Three rules worth knowing before you start, each of which exists because guessing produced a bug:
- Paint-server and mask units are mandatory. The SVG default differs per element and silently changes the result.
- Filter regions are mandatory. The default clips blurs and shadows, and the symptom never points at the cause.
- Interactivity depends on the embedding. An
<img>-embedded SVG receives no pointer events, so:hoverthere is dead markup.assertEmbedding()turns that into an error.
Full reference: docs/MANUAL.md.
Rendering, comparison and evaluation live in a separate package, so kurviq
itself stays dependency-free:
npm i @tecsteps/kurviq-toolbeltconst { evaluate } = require('@tecsteps/kurviq-toolbelt');
evaluate({ svg, referencePng }); // optional reference
evaluate({ svg, width: 512, states: ['hover'] }); // verify interaction statesFor identical input bytes, options, Kurviq version, and platform, conversion is deterministic: repeated runs produce byte-identical SVG output. Default settings retain byte parity with the corresponding native engine pipeline.
- Conversion is synchronous, single-threaded, and CPU-bound.
- Kurviq traces pixels; it does not perform OCR or preserve a selectable text layer.
- This release targets Node.js and does not yet include a browser build.
Kurviq is available under the MIT License. See LICENSE for the license text
and required third-party notices.
Kurviq is a port of vtracer: its WebAssembly engine is built on the vtracer and visioncortex vector-tracing libraries (MIT), compiled to wasm with additional capabilities on top (input downscaling, upscaling, background removal, soft-alpha handling, sharpening).
Fabian Wesner · fabian.wesner@tecsteps.com · LinkedIn