Skip to content

plottertools/vpype-js

Repository files navigation

vpype-js

npm CI license TypeScript

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.

Contents

What vpype-js is

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.

How does it work?

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.svg

This 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

Try it in your browser

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 dev

Installation

npm install vpype-js          # library, for Node and bundlers
npm install -g vpype-js       # global CLI: vpype-js

Node ≥ 18 (≥ 22 recommended; forfile glob patterns with ** need 22).

CLI usage

# 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 show

Global 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

Command reference

Run vpype-js COMMAND --help for every option. Defaults match Python vpype.

Input / Output

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)

Plotting optimization

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

Layout and transforms

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

Layers and metadata

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

Generation, text, filters, blocks

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

Units

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 (a6a0, letter, legal, executive, tabloid, tight) or WIDTHxHEIGHT with optional units (13.5inx4cm).

Blocks

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' end

Expressions and property substitution

Any 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 proxies prop / 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 with eval '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.

Hershey text

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.svg

Options: -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" });

HPGL output

vpype-js read input.svg layout a4 write --device hp7475a output.hpgl

Bundled 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).

Plug-ins and scripts

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.svg

The 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.svg

Library usage

The 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 interleaved Float64Array [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/write with file paths are Node-only (a clear error is thrown in browsers); browser apps pass strings to readMultilayerSvg/writeSvg;
  • %expressions% need an eval-permitting CSP in browsers.

Differences from Python vpype

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)

Faithfulness to the original

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 baselines

As a bonus, the port is roughly 8× faster than Python vpype on a typical read→merge→sort→simplify pipeline.

Development

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 configs

Contributions are welcome — please make sure npm test and npm run lint pass, and add parity cases when porting behaviour from Python vpype.

Acknowledgments

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.

License

MIT — see LICENSE and LICENSE_THIRD_PARTY.

About

The plotter SVG toolkit, in TypeScript — optimize SVGs for pen plotters in the browser or Node.js. A faithful port of vpype.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages