kurviq
v1.10.0
Published
Fast, deterministic raster-to-SVG tracing and SVG authoring for Node.js. WebAssembly, zero dependencies.
Maintainers
Readme
Kurviq
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.
Install
npm i kurviqUse it immediately from the command line:
npx kurviq --input in.png --output out.svgCLI reference
A 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'.
Letting Kurviq pick
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.
Document passes
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.
Picking settings
| 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.
Downscaling oversized sources
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.
Upscaling small text and fine detail
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.
Sharpening blurry flat-color art
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.
Background removal
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.
Alpha handling
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.
Node API
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.
Authoring SVG (v1.3, kurviq/author)
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.
Verifying output (@tecsteps/kurviq-toolbelt)
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 statesDeterminism
For 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.
Limitations
- 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.
License
Kurviq is available under the MIT License. See LICENSE for the license text
and required third-party notices.
Acknowledgements
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).
Author
Fabian Wesner · [email protected] · LinkedIn
