vpype-js is the Swiss-Army-knife toolkit for plotter vector graphics, in TypeScript. It is a faithful port of vpype (Python) that runs both in the browser and as a Node.js CLI — verified against the original implementation down to individual path coordinates.
- What vpype-js is
- How does it work?
- Try it in your browser
- Installation
- CLI usage
- Command reference
- Units
- Blocks
- Expressions and property substitution
- Hershey text
- HPGL output
- Plug-ins and scripts
- Library usage
- Differences from Python vpype
- Faithfulness to the original
- Development
- Acknowledgments
- License
Here is what vpype-js can do:
- optimize SVG files for faster, cleaner plots — merging, sorting (greedy and two-opt), simplifying and re-looping paths routinely cuts pen-up travel by 80–95%;
- layout vector files with precise control of position, scale and page format;
- produce HPGL output for vintage pen plotters, with bundled device configurations;
- create generative artwork from scratch with built-in primitives,
block processors (
grid,repeat,forlayer,forfile) and inline JavaScript expressions; - render single-stroke Hershey text (32 bundled fonts) with word wrap and justification;
- create, modify and process multi-layer files for multi-colour plots;
- run anywhere: the same package powers the Node CLI and the browser — the core library has zero DOM or Node dependencies.
Like the original, vpype-js builds pipelines of commands, where each
command's output feeds the next command's input. Some commands load geometry
(read, circle, text, …), others transform it (linemerge, linesort,
crop, rotate, …), and others write it out (write, stat, show).
vpype-js read input.svg linemerge --tolerance 0.1mm linesort write output.svgThis reads input.svg, merges paths whose endpoints nearly touch, reorders
everything to minimize pen-up travel, and writes a multilayer, Inkscape-ready
SVG.
Commands come in four flavors:
| Kind | Behaviour | Examples |
|---|---|---|
| generators | add new geometry to a layer (-l/--layer to target one) |
read, line, circle, text, random, script |
| layer processors | transform each selected layer independently (-l/--layer, default all) |
linemerge, linesort, translate, crop, squiggles |
| global processors | operate on the whole document | layout, pagesize, lmove, write, trim |
| block processors | repeat their enclosed commands | grid, repeat, forlayer, forfile |
The demo/ folder contains a single-page app where you can drop your
own SVG and optimize it (with before/after stats and HPGL export), experiment
with pipelines, and browse a gallery of live-plotted examples:
npm run build # build the library first
cd demo && npm install && npm run devnpm install vpype-js # library, for Node and bundlers
npm install -g vpype-js # global CLI: vpype-jsNode ≥ 18 (≥ 22 recommended; forfile glob patterns with ** need 22).
# the classic optimization pipeline
vpype-js read input.svg \
linemerge --tolerance 0.1mm \
linesort --two-opt \
linesimplify --tolerance 0.05mm \
reloop \
write --page-size a4 --center output.svg
# scale and lay out a drawing on A4 with 2cm margins
vpype-js read input.svg layout --fit-to-margins 2cm a4 write output.svg
# inspect a file's statistics (lengths, pen-up travel, layers, metadata)
vpype-js read input.svg stat
# multi-layer generative art with blocks and expressions
vpype-js grid -o 3cm 3cm 3 4 random -l '%_i+1%' end pens cmyk write grid.svg
# preview: render to a temp SVG and open the default viewer
vpype-js circle 5cm 5cm 3cm showGlobal options come before the first command:
| Option | Description |
|---|---|
-s, --seed N |
seed the RNG (random, reloop, squiggles, probabilistic layer ops) |
-c, --config FILE |
load a TOML config (HPGL devices, pen configs — same schema as ~/.vpype.toml) |
--plugin MODULE |
load a plug-in (repeatable; also VPYPE_JS_PLUGINS env var) |
-h, --help |
global help; COMMAND --help for any command |
Run vpype-js COMMAND --help for every option. Defaults match Python vpype.
| Command | Description |
|---|---|
read FILE |
import an SVG (- for stdin). Top-level groups become layers (inkscape:label/id digits pick the layer ID); -m/--single-layer, -a/--attr stroke (group by attribute), -q/--quantization (curve flattening, default 0.1mm), --no-crop, -ds/--display-size, --no-fail |
write FILE |
export SVG or HPGL (- for stdout; format from extension or -f). SVG: -p/--page-size, -l/--landscape, -c/--center, --layer-label, --color-mode default|none|layer|path, -pu/--pen-up, -r/--restore-attribs, -D/--dont-set-date. HPGL: see HPGL output |
show |
write a temp SVG and open it with the system viewer (-p pen-up, -c colorful) |
| Command | Description |
|---|---|
linemerge |
join paths whose endings overlap or nearly touch; -t/--tolerance (0.05mm), -f/--no-flip |
linesort |
greedy nearest-neighbour reordering to minimize pen-up travel; -f/--no-flip, -t/--two-opt, -p/--passes N (250) |
linesimplify |
reduce point count (Douglas-Peucker); -t/--tolerance (0.05mm) |
reloop |
randomize the seam of closed paths (hides pen start/stop marks); -t/--tolerance |
multipass |
retrace each path -n/--count times (2) for inky lines |
splitall |
split paths into individual segments (combine with linemerge for dense meshes) |
filter |
keep/reject paths by --min-length, --max-length, --closed, --not-closed |
snap PITCH |
snap every point to a grid, deduplicating collapsed points |
reverse |
reverse path order (-f also flips each path's direction) |
lineshuffle |
randomize path order |
splitdist DIST |
split layers every DIST of cumulative drawing distance |
| Command | Description |
|---|---|
layout SIZE |
fit/align geometry on a page (a4, letter, 10x15cm, tight…); -m/--fit-to-margins, -h/--align, -v/--valign, -l/--landscape, -b/--no-bbox |
pagesize SIZE |
set the page size without touching geometry; -l/--landscape |
pagerotate |
rotate the page 90°; -cw/--clockwise, -o/--orientation portrait|landscape |
translate DX DY |
offset geometry (-- before negative values) |
scale SX SY / scaleto W H |
scale by factors / to dimensions (-f/--fit-dimensions distorts); -o/--origin, -l/--layer |
rotate ANGLE / skew AX AY |
rotate (clockwise-positive) / shear; -o/--origin, -l/--layer |
crop X Y W H / circlecrop X Y R / trim MX MY |
crop to a rectangle / circle / by margins |
| Command | Description |
|---|---|
lcopy SRC DEST / lmove SRC DEST |
copy/move layers (all, comma lists, new); -p/--prob for random splits |
ldelete LAYERS / lswap A B / lreverse LAYERS |
delete (-k/--keep inverts) / swap / reverse path order |
color, alpha, name, penwidth |
set layer colour / opacity / name / pen width |
pens CONF |
apply a pen configuration (cmyk, rgb, or one from --config) |
propset/propget/proplist/propdel/propclear |
manage global (-g) or layer (-l) properties |
| Command | Description |
|---|---|
line, rect, circle, ellipse, arc |
geometric primitives (rect -r/--radii rounds corners; -q/--quantization 1mm) |
random |
random lines: -n/--count (10), -a/--area W H (10mm 10mm) |
frame |
single-line frame around the current geometry (-o/--offset) |
text STRING |
Hershey text — see Hershey text |
script FILE.mjs |
run a JS module's generate() to produce geometry |
squiggles |
Perlin-noise displacement (-a 0.5mm, -p 3mm) for a hand-drawn look |
grid, repeat, forlayer, forfile, begin, end |
block processors — see Blocks |
eval EXPR |
evaluate an expression (e.g. set variables for later commands) |
stat |
print detailed statistics |
Like vpype, every length option/argument understands units; the internal unit
is the CSS pixel (96 px/inch): px in ft yd mi mm cm m km pc pt — e.g.
0.5mm, 3cm, .25in. Angles accept deg (default), grad, rad, turn.
Page sizes accept names (a6–a0, letter, legal, executive, tabloid,
tight) or WIDTHxHEIGHT with optional units (13.5inx4cm).
Block processors repeat the commands enclosed between them and end
(a leading begin is optional). Each iteration sets variables usable in
expressions:
| Block | Variables | Notes |
|---|---|---|
repeat N |
_n, _i |
stack N copies |
grid NX NY |
_nx, _ny, _n, _x, _y, _i |
-o DX DY cell offset (10mm); sets page size to the grid unless -k. NX NY are required |
forlayer |
_lid, _i, _n, _name, _color, _pen_width, _prop |
runs once per layer, layer isolated |
forfile PATTERN |
_path, _name, _parent, _ext, _stem, _i, _n |
glob with ~/$VAR expansion (Node only) |
# 8x12 grid with increasing disorder (Georg Nees' "Schotter")
vpype-js grid -o 1.2cm 1.2cm 8 12 \
rect 0 0 1.1cm 1.1cm \
rotate '%(Math.random()-0.5)*_y*9%' \
end layout -m 1.5cm a5 write schotter.svg
# one output file per layer
vpype-js read input.svg forlayer write 'layer_%_lid%.svg' endAny option or argument may contain %expression% and {property} patterns,
evaluated when the command runs (so block variables work):
%…%evaluates a JavaScript expression (note: Python vpype uses Python here). The scope provides the units as variables (%2*cm%→ pixels),Math,Color,convertLength/convertAngle/convertPageSize, bare helpers (min,max,round,sin,pi, …), the property proxiesprop/lprop/gprop(layer-then-global / layer / global, readable and assignable), block variables, and — in the CLI — path helpers (basename,dirname,exists,glob, …). Variables assigned in one expression persist for the rest of the pipeline; initialize them witheval 'm = 1*cm'.{name}substitutes a property (current layer first, then global) and supports the common Python format specs:{vp_pen_width:.2f},{n:03d}.- Escape literal characters by doubling:
%%,{{,}}.
vpype-js eval 'm = 5*mm' rect %m% %m% '%prop.vp_page_size[0]-2*m%' 10cm …In browsers, expressions use new Function and therefore require an
eval-permitting CSP; expression-free pipelines have no such requirement.
vpype-js text -f futural -s 24 'Hello plotters' write hello.svg
vpype-js text -w 10cm -j -a left 'a wrapped and justified paragraph…' write block.svgOptions: -f/--font (32 single-stroke fonts: futural default, futuram,
scripts, gothiceng, timesib, …), -s/--size (18), -w/--wrap WIDTH,
-j/--justify, -p/--position X Y, -a/--align left|right|center.
Hyphenation (-h LANG) is accepted but not supported (it warns and wraps
without hyphenating).
In the library, fonts are a separate lazily-loaded export so they never weigh down your bundle:
import { loadFont } from "vpype-js/fonts";
import { textLine, textBlock } from "vpype-js";
const font = await loadFont("futural"); // its own chunk, cached
const lc = textLine("Hello", font, { size: 24, align: "center" });vpype-js read input.svg layout a4 write --device hp7475a output.hpglBundled devices: hp7475a, hp7440a, hp7550, artisan (Calcomp),
dmp_161 (Houston Instrument), dxy (Roland DXY), sketchmate. The paper
is inferred from the page size, or set with -p/--page-size NAME. Other
options: -l/--landscape, -c/--center, -a/--absolute, -vs/--velocity,
-q/--quiet. Output is byte-identical to Python vpype.
Custom devices and pen configs use vpype's TOML schema, loaded with
-c/--config my.toml (see the
vpype docs
for the format). From the library: writeHpgl(doc, {device, pageSize, …})
and loadConfig(parsedToml).
A plug-in is a JS module that registers new commands:
// my-plugin.mjs
export default function (api) {
api.registerCommand({
name: "star", group: "Plugins", kind: "generator",
help: "Generate a star.",
args: [{ name: "size", type: api.optionTypes.LengthType }],
execute: (opts) => new api.LineCollection([/* … */]),
});
}vpype-js --plugin ./my-plugin.mjs star 5cm write out.svgThe script command is the quickest way to inject generated geometry:
// gen.mjs
export function generate() {
return [[[0, 0], [50, 0], [25, 40], [0, 0]]]; // arrays of points, px
}vpype-js script gen.mjs linemerge write out.svgThe core library is browser-safe: SVG in/out as strings, no DOM, no filesystem. Everything below works identically in Node.
import {
readMultilayerSvg, writeSvg, writeHpgl,
linemerge, linesort, linesimplify,
Document, LineCollection, convertLength, execute,
} from "vpype-js";
// parse an SVG string into a Document (Map of layer ID -> LineCollection)
const doc = readMultilayerSvg(svgString, convertLength("0.1mm"));
// option A: call operations directly
for (const [, layer] of doc.layers) {
linemerge(layer, { tolerance: convertLength("0.1mm") });
linesort(layer, { twoOpt: true });
}
// option B: run a CLI pipeline on the document
const optimized = await execute("linemerge -t 0.1mm linesort --two-opt", doc);
const outSvg = writeSvg(optimized, { colorMode: "layer" });Data model (mirrors vpype's):
Line— a polyline as an interleavedFloat64Array[x0, y0, x1, y1, …](the equivalent of vpype's complex numpy arrays);LineCollection— one layer: a list of lines plus metadata (vp_color,vp_pen_width,vp_name, …), with mutating geometry methods (translate,scale,rotate,crop,merge,bounds,penUpLength…);Document— layer ID →LineCollection, plus page size and global metadata.
Also exported: every operation as a pure function (crop, layout,
squiggles, lmove, …), geometry helpers (lineLength, interpolate,
simplifyLine, LineIndex…), primitives (line/rect/circle/ellipse/arc),
textLine/textBlock, Color, units (convertLength, PAGE_SIZES, …),
HPGL (writeHpgl, getPlotterList, getPlotterConfig, loadConfig) and
setSeed for reproducible randomness.
Notes:
execute()mutates the document you pass in — re-read/clone your source if you need the original;read/writewith file paths are Node-only (a clear error is thrown in browsers); browser apps pass strings toreadMultilayerSvg/writeSvg;%expressions%need an eval-permitting CSP in browsers.
| Area | vpype (Python) | vpype-js |
|---|---|---|
| Expressions | Python (asteval) | JavaScript (%_i * 2%, %Math.sin(_i)%) |
GUI viewer (show) |
OpenGL/matplotlib viewer | renders a temp SVG, opens the system viewer |
Hyphenation (text -h) |
pyphen | not supported (warns) |
grid arguments |
optional (defaults 2 2) | required |
Plug-ins / script |
Python entry points / Python scripts | JS modules (--plugin, script file.mjs) |
designmate HPGL device |
present but broken (config typo crashes) | omitted |
forfile _n variable |
buggy (returns pattern string length) | actual file count |
| Random sequences | numpy RNG | own seeded RNG (reproducible within vpype-js, not across implementations) |
A parity harness (tests/parity/) runs 69 pipelines through both
implementations and compares results:
- geometry without curves matches Python coordinate-for-coordinate (tolerance 1e-6) — including block pipelines with expressions and Hershey text down to individual glyph strokes;
- curved geometry (Béziers, arcs, circles) matches within quantization tolerance, since curve-length estimation differs slightly;
- HPGL output is compared byte-for-byte.
The Python implementation is not part of this repository. Baselines are
checked in, so npm test verifies parity without Python. To regenerate them
against upstream vpype (requires uv):
npm run parity:setup # shallow-clones abey79/vpype into ./vpype-master (gitignored)
npm run parity:baselines # runs every pipeline through Python vpype, stores the outputs
npm test # compares vpype-js against the fresh baselinesAs a bonus, the port is roughly 8× faster than Python vpype on a typical read→merge→sort→simplify pipeline.
git clone https://github.com/plottertools/vpype-js.git && cd vpype-js
npm install
npm test # 310 unit tests + 69 parity cases
npm run typecheck
npm run lint # includes the browser-safety import guard
npm run build # tsup -> dist/ (ESM + CJS + types, fonts as lazy chunks)The generated sources (src/data/hpgl-devices.ts, src/fonts/data/*) and
the parity baselines are derived from Python vpype, which is fetched on
demand rather than vendored:
npm run parity:setup # fetch abey79/vpype into ./vpype-master (gitignored)
npm run parity:baselines
uvx --from ./vpype-master python scripts/convert-fonts.py # font data
uvx --from ./vpype-master python scripts/generate-hpgl-config.py # device configsContributions are welcome — please make sure npm test and npm run lint
pass, and add parity cases when porting behaviour from Python vpype.
vpype-js is a port of vpype by Antoine Beyeler — the architecture, algorithms, command set and test corpus are his work. The Hershey text rendering is adapted (via vpype) from axi by Michael Fogleman, using fonts by Dr. Allen V. Hershey.
MIT — see LICENSE and LICENSE_THIRD_PARTY.