Join fragmented handwriting strokes in an SVG into continuous paths.
Handwriting fonts vectorised to SVG often emit each letter as many tiny path fragments rather than one stroke. PathMerge reconnects fragments whose endpoints touch (or very nearly touch) into single continuous paths, while leaving genuinely separate letters and strokes alone.
npm install
npx tsx src/index.ts page3.svg
# or: npm run connect -- page3.svgThe result is written next to the input: page3.svg → page3.connected.svg.
Pass several files, a glob, or any mix of the two. Each file is processed
independently and gets its own <name>.connected.svg:
npx tsx src/index.ts page1.svg page2.svg # explicit list
npx tsx src/index.ts 'page*.svg' # glob (quote it!)Quote globs so the shell passes the pattern through unexpanded — PathMerge expands it itself, so the behaviour is identical on every platform. Duplicate matches are de-duplicated, and one unreadable or path-less file is reported without aborting the rest of the batch (the process exits non-zero if any file failed).
| Flag | Default | Meaning |
|---|---|---|
--tolerance, -t |
0.5 |
Max gap between two endpoints to treat them as connected (in SVG user units). |
--decimals, -d |
2 |
Coordinate precision in the output. |
--stats |
off | Print a histogram of nearest-endpoint distances (handy for tuning tolerance). |
npx tsx src/index.ts page3.svg --tolerance 0.5 --stats- Parse every
<path>dstring into segments with absolute coordinates (relative commands,H/V/S/Tshorthands and arcs are all normalised). Working in absolute space means strokes can be reordered, reversed and joined with no relative-offset drift. - Reduce each open stroke to its two endpoints.
- Cluster endpoints that fall within
toleranceof each other into shared nodes (union-find over a uniform spatial grid, so it stays near-linear). - Build a multigraph whose nodes are those clusters and whose edges are the strokes, then decompose each connected component into trails — continuous walks that never reuse a stroke. Greedy edge-removal walks are used (not Hierholzer) because stroke crossings create junctions of degree ≥ 3 where a component isn't traceable as a single trail; a greedy walk started at an odd-degree vertex provably can't get stuck mid-stroke, so every trail is a genuine gap-free chain.
- Emit each trail as one continuous path (orienting/reversing strokes as
needed). Closed (
Z) shapes and compound paths are passed through untouched.
--stats shows that for typical exports the touching joints cluster at ≤0.05
units while real inter-letter spacing only begins around ~1.5 units, with a wide
flat gap in between. The default of 0.5 sits safely in that gap: it absorbs
both exact joints and imperceptible vectorisation near-misses without fusing
letters that should stay apart.
npm test # run the unit tests
npm run typecheck