Skip to content

How it works

First, the SVG is parsed and every rendered shape is normalized to absolute move, line, cubic and close segments. Arcs and quadratics become cubics, which means transforms can be applied exactly.

Then the curves are flattened to polylines, with a guaranteed maximum error.

Next, every polyline is covered with small polygons: one rectangle per segment, one join shape per vertex, one cap per open end. There are no special cases for self-intersections or for curves tighter than the stroke width. The pieces simply overlap.

After that, all of those polygons are merged with a boolean union. This is Clipper, working in integer arithmetic, so it doesn't fail on degenerate input. The pieces are built to overlap by area rather than merely touch. Otherwise a vertex that rounds onto the wrong side of a neighbouring edge would leave a hairline gap.

Then every ring of the result is fitted with lines and cubic Béziers. Sharp corners are detected by turn angle, straight runs become L, H or V, and curved runs go through Schneider's algorithm: a least-squares cubic, Newton reparameterization, then a split at the point of largest error until the fit is within tolerance. Tangents are estimated over a window rather than from the nearest edge, because union output mixes tiny edges with long ones. The error is checked along the edges too, so a curve can't bulge unnoticed between two distant vertices of a straight edge.

Finally, the result is serialized as compact absolute path data.

Why polygons and not boolean operations on curves?

Tools like Figma or Illustrator offset curves directly and run their boolean operations on curves. That skips the intermediate polygon. The trouble is that curve-curve intersection is numerically fragile, and every implementation carries a long tail of degenerate cases. We tried the alternatives on a full production set of 5130 outline icons (49 of them have filled paths that the experiments skipped):

engineapproachwrong outputtime / iconafter SVGO
Clipper 1 (this library)polygons, integer grid01.0 ms5.3 MB
Clipper2 (WASM)polygons, integer grid00.8 ms5.3 MB
Skia PathOps (CanvasKit)curves25 (7 refused, 18 silently wrong)0.4 ms8.2 MB
Paper.jscurves7 (silently wrong)7.5 ms8.7 MB

Skia is the boolean engine behind Chrome, Flutter and Figma, and it still mangles swipe, coins or whisk (a filled-in hole here, a missing wall there) and refuses asterisk or feather outright. Paper.js needed two workarounds for degenerate input before it produced anything at all, and then failed on brand-redux and home-infinity. Both also emit far more curves, because they keep every fragment the intersections produce, and Skia writes arcs as strings of quadratics.

The failures are silent, which is the worst kind for a build pipeline. Polygons with integer-grid clipping don't fail, and the curve fit on the way out keeps the output small. The price is a bounded, configurable error of roughly three times tolerance. Clipper2 would be a drop-in replacement if union speed ever mattered; it costs a WASM binary and asynchronous initialization.