TL;DR – If you would like to extract vector outlines from an SVG document in Bezier format, you can use our new library here: github.com/mattlag/SVG-to-Bezier
Bézier curves are the foundation for all modern vector graphics. If you use SVG, or any vector editor’s pen tool, you’re using Bézier curves. The “standard” notation for a single (cubic) Bézier curve only requires 4 x/y points. So, with only 8 simple numbers, a huge variety of complex curves can be described.
SVG has many tags that describe different shapes (
polyline) but the one that does the most heavy lifting is the
path tag. The
path tag only has one main attribute,
d (which stands for… ‘definition’? ‘data’?) the
d attribute itself contains a long string of single letter ‘commands’ which are followed by numbers. These commands encode a variety of ways vector lines can be drawn across a canvas:
(oh, in addition to the capital-letter commands, there are lowercase variants – capital letter commands are followed by absolute coordinate data, while lowercase commands are followed by relative coordinates. And they can be mixed together – requiring a running knowledge of the ‘current’ point in order to accurately know what the next command wants to do. fun.)
SVG is the main way Glyphr Studio imports and exports vector data to and from different file formats. In addition to regular
.svg files, this format is also how data from font files is shown to us. OpenType.js is the library we use for reading and writing font files. Many font files use a data format called Post Script to encode vector data natively… and actually Post Script looks very similar to SVG
path tag data. OpenType.js reads a font file, and has a handy
getSVG method for each glyph.
So, it’s incredibly important that Glyphr Studio can parse SVG data, namely the
d attribute, and translate it into Glyphr Studio’s native data structure. In Glyphr Studio v1, there are over 1200 lines of code dedicated to parsing and converting SVG data. It works pretty well… but over the years, SVG parsing has been one of the largest sources of bugs and unexpected behavior for the whole product. And since there are so many hops a piece of vector data has to take to get to the edit canvas, it is very hard to reliably and efficiently test the end-to-end data flow. Here’s how the current data flow works:
Font ⮞ OpenType.js ⮞ SVG ⮞ XMLtoJSON ⮞ Glyphr Studio SVG parser ⮞ Glyphr Studio Data
So what’s to be done?
Those 1200 lines of SVG parsing code were actually a deep mix of Glyphr Studio specific data and logic, combined with some generic math specific to Bézier curves. Bézier curves are relatively simple and easy to work with… but all those pesky SVG commands really cause the complexity to go through the roof. In Glyphr Studio v1 some of that complexity was handled by going straight from SVG to Glyphr Studio’s data format, with some of this Bézier math leveraged along the way implicitly.
But, ultimately, all of this ingrained functionality was the source of many bugs, and didn’t actually contribute to simplicity that much. So, the solution is to extract the standard SVG parsing code and go straight to standard Bézier data format. Leave Glyphr Studio out of it until the very end.
A pure SVG-to-Bézier step would be easier to test and reason about when bugs arise. So, just find a library that does that, right? Well, even though SVG is a vector standard, and Bézier curves are a well-understood vector area of mathematics, there wasn’t a good off the shelf solution to be found. Searching across GitHub only yielded partial or very specific-purpose libraries. SVG has a huge superset of functionality that supports Graphic Design, not just simple vector outlines. This is probably why there isn’t a lot of options for converting a whole SVG document to Bézier curves – for most things SVG is used for, converting to Bézier would leave huge amounts of other information behind (like fill color, line styles, gradients, animations, raster images, masks… to name a few).
Jumping out of a perfectly good airplane
This is an old skydiving joke, but it’s kind of how I feel as I work on Glyphr Studio v2. I had hopes that the Glyphr Studio v1 SVG logic could stay intact – after all, SVG wasn’t changing, so why should the parser code? But, for all of the reasons outlined in the previous section, I thought it was time to leave a large chunk of perfectly respectable code behind and embark on yet another ground-up rewrite. It’s time to jump out of a perfectly good airplane… with plans to land in a better airplane? Sorry, my metaphor has gotten a little out of hand.
The new SVG-to-Bezier library takes a step-by-step approach to slowly convert all the crazy complexity that can exist in a SVG
path tag, and slowly work it toward a nice simple Bézier. Here’s how we go from a long
d path attribute string and end up with a simple Bézier data format:
- Break the long single string into chunks of an individual command and it’s associated coordinate data.
- Coordinate data can be ‘chained’ – if the command doesn’t change, it’s possible just to keep adding coordinate numbers and it should be interpreted as multiple repeated commands. Step 2 is splitting any chained commands into discreet single commands.
- Convert relative coordinate data (where we have to keep track of the current x/y point) is converted to absolute coordinates.
- Symmetric notation for Cubic and Quadratic Bézier curves are just simplifications that use the previous point’s data to compute a piece of the current curve. These symmetric points are calculated and converted to standard Cubic and Quadratic Bézier curves.
- Convert any Quadratic Bézier curve to the (more standard) Cubic Bézier curve.
- Lines can be converted to a simple format of Bézier curve – technically it’s less efficient to express a line in Bézier curve format, but we’ll do it for the sake of uniformity in the resulting data.
- And, lastly, the pesky Arc-to command. Unfortunately, this format has nothing to do with Bézier math, and there is no perfect way to translate between the two. There is a large chunk of code dedicated to approximating an arc using a recursive curve fitting function. It’s not perfect, and will always quit at some given threshold. Even though it’s potentially lossy, it’s better than nothing.
And with that, we’ve worked through each SVG Command and
d format quirk to arrive at standard plain old Bézier curves.
So what now?
This library is like 1000% easier to test. In fact, in the GitHub repository there are many tests already built in – these tests load a suite of SVG files, run them through the converter, then draw the resulting Bézier curves to a canvas so they can be visually checked for irregularities. This kind of thing was not at all possible with the Glyphr Studio v1 approach.
So now this stand-alone library will replace a large chunk of Glyphr Studio v2 code. Horay! Now our overall process looks like this:
Font ⮞ OpenType.js ⮞ SVG-to-Bezier ⮞ Glyphr Studio Data
But does the new library actually handle all the relevant parts of SVG? Unfortunately, one last thing remains – a
transform attribute can exist on any SVG shape tag or grouping
g tag. In Glyphr Studio v1 we conveniently ignore all transforms, and leave it as an exercise for the user to get their SVG editor to not use them somehow. This is, admittedly, not great.
transform attribute itself has a few different functions that can change how that path or group of paths look. Many of the functions aren’t too bad, like
skew are pretty straight forward, mathematically speaking. But, the last function to consider,
matrix, allows for arbitrary matrix transforms. So I guess it’s time to dust off the old linear algebra. Then there is the additional hierarchical structure to consider with the grouping function of the
g tag. So all these transforms have to potentially combine in a larger hierarchical tree.
All of this transform stuff was basically impossible to implement in the old Glyphr Studio v1 SVG parser. But now, the good news is that the new SVG-to-Bézier approach means this transform functionality is within reach. We now have a strong foundation for less bugs and more functionality in the future. That’s how a good side quest should end up.