UNPKG

svg-parse-path-normalized

Version:

Parse svg path data from string and normalize it to calculable value arrays

401 lines (297 loc) 18.5 kB
[![npm version](https://img.shields.io/npm/v/svg-parse-path-normalized.svg)](https://www.npmjs.com/package/svg-parse-path-normalized) # svg-parse-path-normalized Parses path data from string including *fine-grained* normalisation and conversion options. This library aims to provide a robust and versatile yet *quite* compact (~6KB/3KB minified; gzipped) parser – respecting all minified/shorthand notations as a basis for all kinds of custom path data manipulations. Compatible with the [w3C SVGPathData interface draft](https://svgwg.org/specs/paths/#InterfaceSVGPathData) format recommendations. ## Yet another SVG path parser? While there is no shortage of excellent parsers – unfortunately, the same applies to rather **incomplete ones** often deployed in libraries due to their appealing lightweight codebase. 1. **Minified `A` arcto commands** quite often lightweight parsers crash since they can't unravel concatenated `largeArc`, `sweep` and final on path x values. 2. You may not need to load a full fledged SVG library but just a robust **foundation for your specific SVG path manipulations**. 3. **Finegrained normalization:** Normalize as little as possible: Usually, you need at least absolute coordinates as well as "unshortened" ones. You may in some case need to convert quadratic béziers to cubics or `A` arcs to cubics – your choice – by default the parser will return the least destructive normalization. 4. **Just parsing without normalization?** Fair enough, works as well e.g if you need a to scale the path data proportinally/keeping the aspect ratio. 5. **Debugging** Sometimes you may also need some hints to what's wrong with your current path data – e.g if paths were sliced or concatenated incorrectly. The debugging option will return info about the problematic commands. ## Table of content * [1. Basic functionality and helpers](#1-basic-functionality-and-helpers) + [1.1 Parse, normalize and stringify](#11-parse-normalize-and-stringify) + [1.2 Advanced conversions](#12-advanced-conversions) * [2. Usage parser](#2-usage-parser) + [2.1 Browser](#21-browser) + [2.2 Node](#22-node) * [3. Pathdata format](#3-pathdata-format) * [4. All normalization options](#4-all-normalization-options) + [4.1 Original path data: normalization disabled](#41-original-path-data-normalization-disabled) + [4.2 Recommendations](#42-recommendations) * [5. Stringify to back to d attribute string](#5-stringify-to-back-to-d-attribute-string) * [6. More conversions via pathDataConvert.js](#6-more-conversions-via-pathdataconvertjs) + [6.1 Usage](#61-usage) + [6.2 Usage as an addon/plugin for getPathData()](#62-usage-as-an-addon-plugin-for-getpathdata) + [6.3 Convert pathdata structure](#63-convert-pathdata-structure) - [6.3.1 Array notation to pathdata](#631-array-notation-to-pathdata) - [6.3.2 pathDataToVerbose(pathData)](#632-pathdatatoverbosepathdata) * [7. Demos](#7-demos) * [8. Limitations](#8-limitations) * [Credits](#credits) ## 1. Basic functionality and helpers ### 1.1 Parse, normalize and stringify Usually parsing alone is not enough to get computable path data values – due to relative or shorthand commands or `a` arcto commands that may rather complicate further manipulations such as length or area calculations – especially when dealing with elliptical and/or rotated arcs. Normalization (admittedly a slightly ambigious term) via `parsePathDataNormalized(d)` applies by default these conversions: * (default) all commands to **absolute** * (default) decompose **implicit or repeated** commands e.g `m 0 0 .5.5.5.5` to `M 0 0 l 0.5 0.5 l 0.5 0.5` * commands to **shorthand/reflected** commands to longhand equivalents like e.g `h`, `v`, `s`, `t` to `L`, `C`, `T` * *(optional)* convert/approximate **arcs to cubics** * *(optional)* convert **quadratic béziers to cubics** * *(optional)* **debug:** detect malformed path data inputs * *(optional)* **round** coordinates * **stringify to `d` attribute** – including minification options ### 1.2 Advanced conversions Provided by `pathDataConvert.js`: Useful to convert your manipulated/processed path data to all kind of command types/structures (E.g to get a more compact or special formats like lineto-to-bezier conversions for morphing animations by converting) * all commands to **relative** (usually more concise in file size) * **apply shorthands** – if possible (also decreases filesize) * **linetos to cubic** or quadratic béziers * **cubic béziers to quadratic** * **different path data formats** e.g array based path data notations as used in snap.svg and other libraries or APIs * this scripts also includes all normalizations options such as **relative-absolute**, **shorthand-to-longhands**, **rounding** etc. * can be used as an **addon complementing `getPathData()`** or other parsers compliant with the [w3C SVGPathData interface draft](https://svgwg.org/specs/paths/#InterfaceSVGPathData) format recommendations. ## 2. Usage parser **Update:** You can now use a more convenient function name for the parsing - the more verbose/clunky function still works. Since this parser aims to parse path data into a processable data array, "parsePathDataNormalized" is semantically more accurate – I hope you understand my naming delemma =) ``` /* shorthand notation */ let pathData = parseD(d, options) ``` ``` /* original notation */ let pathData = parsePathDataNormalized(d, options) ``` Feel free to post an issue or write a post in the discussion if the recent update messed up anything. ### 2.1 Browser ``` lang-html <script src="https://www.unpkg.com/svg-parse-path-normalized@latest/js/pathDataParseNormalized.js"></script> ``` **Optional: Load minified script via jsDelivr (~6KB/3KB minified; gzipped)** ``` <!--basic parser ---> <script src="https://cdn.jsdelivr.net/npm/svg-parse-path-normalized@latest/js/pathDataParseNormalized.min.js"></script> ``` ``` <script> //parse const d ="m 0 0 .5.5.5.5a 5 10 45 1040 20" ; /* shorthand notation */ let pathData = parseD(d) /* verbose notation */ let pathData = parsePathDataNormalized(d) //stringify to pathdata d string let minify = false; /* chainable notation */ let dNew = pathData.toD(decimals, minify); /* classic function syntax */ let dNew = pathDataToD(pathData, decimals, minify); console.log(pathData); console.log(dNew); </script> ``` ### 2.2 Node ``` npm install svg-parse-path-normalized ``` ``` lang-js const parsepathData = require('svg-parse-path-normalized'); const {parsePathDataNormalized, pathDataToD} = parsepathData; //parse const d ="m 0 0 .5.5.5.5a 5 10 45 1040 20" ; let pathData = parsePathDataNormalized(d) //stringify to pathdata d string let minify = false; let dNew = pathDataToD(pathData, 1, minify); console.log(pathData); console.log(dNew); ``` ## 3. Pathdata format This library uses the pathdata format as suggested in the [w3C SVGPathData interface draft](https://svgwg.org/specs/paths/#InterfaceSVGPathData). The returned path data parsed from a stringified pathdata `d` attribute string is an array representing each command as an object like so: ``` lang-js const d ="m 0 0 .5.5.5.5a 5 1045 1040 20" parsePathDataNormalized(d) ``` ``` lang-js [ {"type":"M","values":[0,0]}, {"type":"L","values":[0.5, 0.5]}, {"type":"L","values":[1, 1]}, {"type":"A","values":[5, 10, 45, 1, 0, 41, 21]} ] ``` The above example illustrates a problem with overly "lightweight" path parsers: We need an extra check to "unravel" the `A` arcto's `largeArc` and `sweep` flags, which can be concatenated with the subsequent on-path x coordinate value. (See [basic example](https://codepen.io/herrstrietzel/pen/NWJpOYR)) ## 4. All normalization options `parsePathDataNormalized(d, options)` accepts these parameters ``` let options= { normalize: null, //shorthand for aggressive normalisation toAbsolute: true, //necessary for most calculations unshort: true, //dito arcToCubic: false, //sometimes necessary quadraticToCubic: false, //frankly, not necessary most of the time lineToCubic: false, //handy for morphing animations debug: false, //handy to detect malformed pathdata retrieved from user inputs decimals: -1 //-1=no rounding } ``` | parameter | default | effect | | -- | -- | -- | | toAbsolute | true | convert all to absolute | | unshort | true | convert all shorthands to longhands | | arcToCubic | *false* | convert arcs `A` commands to cubic béziers | | quadraticToCubic | *false* | convert quadratic to cubic béziers | | lineToCubic | *false* | convert all `L` linetos to cubic béziers (handy for morphing animations) | | decimals | *-1* | round values to floating point decimals. -1=no rounding | | debug | *false* | reports malformed path data structures via `console.log`| | normalize | *null* | shorthand to also convert arcs and quadratic béziers to cubic – similar to the W3C draft's suggested `getPathData({normalize:true})` parameter | ### 4.1 Original path data: normalization disabled Set normalize to false to get the original (not normalized) pathdata – including relative or shorthand commands. `parsePathDataNormalized(d, {normalize:false})` ### 4.2 Recommendations * Quadratic béziers usually provide much faster calculations/algorithms – think twice before converting to cubic. * `debug:true` can be handy if you need to find errors in malformed pathdata – maybe caused by manual path splitting * Arc to cubic conversion/approximation is quite complex and thus quite expensive – you may not need this conversion ## 5. Stringify to back to `d` attribute string **Options:** * decimals: rounds pathdata * minify: omits command letters for implicit or repeated commands and leading zeros You can stringify the path data to a `d` attribute (or CSS property) by a chained prototype method or the basic function like so: ``` let d = pathData.toD(decimals, minify) ``` which is just a wrapper for the actual stringifying function. ``` let d = pathDataToD(pathData, decimals, minify) ``` and eventually apply it like so: ``` path.setAttribute('d', d); ``` ---- ## 6. More conversions via pathDataConvert.js Load `pathDataConvert.js` to get more conversion methods. This script is intended to provide various conversions to optimize the path data after processing e.g for a minified path output. | parameter | default | effect | | -- | -- | -- | | toRelative | *false* | convert all to relative | | toAbsolute | true | convert all to absolute | | toShorthands | *false* | convert all to to shorthands – if applicable | | toLonghands | true | convert all shorthands to longhands | | arcToCubic | *false* | convert arcs `A` commands to cubic béziers | | lineToCubic | *false* | convert all `L` linetos to cubic béziers (handy for morphing animations) | | quadraticToCubic | *false* | convert quadratic to cubic béziers | | cubicToQuadratic | *false* | convert all cubic to quadratic | | cubicToQuadraticPrecision | 0.1 | cubic to quadratic accuracy | | decimals | *-1* | round values to floating point decimals. -1=no rounding | | normalize | *null* , true, false | shorthand to also convert arcs and quadratic béziers to cubic – similar to the W3C draft's suggested `getPathData({normalize:true})` parameter | | optimize | *false* | shorthand to convert to shorthands, relative and round to 3 decimals for a more compact output | ### 6.1 Usage ``` <script src="https://www.unpkg.com/svg-parse-path-normalized@latest/js/pathDataConvert.js"></script> ``` Load minified via jsDelivr (13KB/6KB minified) ``` <!-- optional conversions --> <script src="https://cdn.jsdelivr.net/npm/svg-parse-path-normalized@latest/js/pathDataConvert.min.js"></script> ``` ``` let options = {arcToCubic:true, toRelative:true, decimals:0} let pathDataCon = pathData.convert(options) ``` **Conversion can be applied via** * chainable prototype method `convert(options)` to apply all conversions at once * separate chainable methods like `pathData.toAbsolute()`, `pathData.toRelative()`, `pathData.toLonghands()`, `pathData.toShorthands()`, `pathData.round()`, `pathData.toQuadratic()`, `pathData.toVerbose()` * individual functions like `pathDataToAbsolute(pathData)`, `pathDataToRelative(pathData)`, `pathDataToShorthands(pathData)`, `pathDataToShorthands(pathData)`, `pathDataToQuadratic(pathData)`, `roundPathData(pathData)` ### 6.2 Usage as an addon/plugin for `getPathData()` Currently, the W3C draft for the SVGPathData interface is not supported by any major browser. Fortunately Jarek Foksa wrote a this [great polyfill library](https://github.com/jarek-foksa/path-data-polyfill) and also contributed to the potential spec outcome – most importantly that it should include geometry elements like `circle`, `rect`, `polygon`, `line` to retrieve path data. **This polyfill is a "battle-proof" parser!** Since the W3C draft doesn't include fine-grained control over the normalisation/conversion process you can use the `pathDataConvert.js` script as an addon/plugin alongside with the aforementioned polyfill script. (See Demo/getPathDataAddon.html) ### 6.3 Convert pathdata structure You may already have a set of parsed/abstracted path data retrieved from other libraries or APIs or need a more verbose notation. In this case you may use these conversion methods. #### 6.3.1 Array notation to pathdata A lot of libraries – such as snap.svg use a nested array structure for each command like so ``` [ ["M", 0, 0] , ["L", 0.5, 0.5], ["L", 1, 1], ["A", 5, 10, 45, 1, 0, 41, 21] ] ``` In case you need to convert these you can use the helper methods (included in pathDataConvert.js) to convert format in both directions * `convertArrayPathData(pathDataArray)` * `revertPathDataToArray(pathData)` #### 6.3.2 pathDataToVerbose(pathData) Besides you can use `pathDataToVerbose(pathData)` to get a more detailed data array including original and absolute point coordinates as well as parametrized arc data `rx` and `ry`, `startAngle`, `endAngle`, `deltaAngle` (in radians) ``` let data = [ { type: "M", values: [0, 0], valuesAbsolute: [0, 0], pFinal: { x: 0, y: 0 }, isRelative: false }, { type: "l", values: [0.5, 0.5], valuesAbsolute: [0.5, 0.5], pFinal: { x: 0.5, y: 0.5 }, isRelative: true, pPrev: { x: 0, y: 0 } }, { type: "l", values: [0.5, 0.5], valuesAbsolute: [1, 1], pFinal: { x: 1, y: 1 }, isRelative: true, pPrev: { x: 0.5, y: 0.5 } }, { type: "a", values: [5, 10, 45, 1, 0, 40, 20], valuesAbsolute: [5, 10, 45, 1, 0, 41, 21], pFinal: { x: 41, y: 21 }, isRelative: true, pPrev: { x: 1, y: 1 }, rx: 21.505813167606572, ry: 43.011626335213144, xAxisRotation: 45, largeArcFlag: 1, sweepFlag: 0, startAngle: 2.976443999504017, endAngle: 6.118036608390327, deltaAngle: -3.1415926982932767 } ]; ``` ## Changelog * 1.1.0 fixes a rare parsing issue where 'M' commands were omitted (e.g `z` followed by another drawing command than `M` – unfortunately valid). See updated demo with "path-from-hell3". ## 7. Demos * [parse pathdata with different normalization options](https://codepen.io/herrstrietzel/pen/NWJpOYR) (demos/index.html) * `pathDataConvert.js` as a [addon/plugin for `path.getPathData()`](https://codepen.io/herrstrietzel/pen/dyreNep) (demos/getPathDataAddon.html) * [convert commands to pretty much anything](https://codepen.io/herrstrietzel/pen/JjzvRjb) | (demos/converter.html) ## 8. Limitations This library does only support SVG path data that's actually supported by browsers. In other words: these "newer" commands are not included: * [Catmull-Rom curve commands](https://www.w3.org/TR/2015/WD-SVG2-20150409/paths.html#PathDataCatmullRomCommand) * [Bearing commands](https://www.w3.org/TR/2015/WD-SVG2-20150409/paths.html#PathDataBearingCommands) ... and this contextual closepath thing ... Seriously, there's no point in implementing these features until they're implemented (see the sad story of SVG multi-line text... we'll probably never get it). Especially the Catmull-ROM syntax extension is spooking around for more than 10 years without any results. Frankly, these features may never be included as they would bomb quite a few renderers and there are visualisation libraries to draw paths in a abstracted way. ## Credits * Jarek Foksa for his [great polyfill](https://github.com/jarek-foksa/path-data-polyfill) heavily inspring to adopt the new pathData interface methodology and for contributing to the specification * Dmitry Baranovskiy for (raphael.j/snap.svg) [pathToAbsolute/Relative functions](https://github.com/DmitryBaranovskiy/raphael/blob/master/raphael.js#L1848) * Vitaly Puzrin (fontello) for the arc to cubic conversion method [a2c.js](https://github.com/fontello/svgpath/blob/master/lib/a2c.js) and [cubic to quadratic approximation](https://github.com/fontello/cubic2quad/blob/master/test/cubic2quad.js) * Mike "POMAX" Kammermans for his great [A Primer on Bézier Curves](https://pomax.github.io/bezierinfo) ## Related Repositories/projects All of these helpers are based on the path parser described above (or by-products - although they include stripped down versions for the sake of providing a standalone lib). * [svg-getpointatlength](https://github.com/herrstrietzel/svg-getpointatlength) – Calculates a path's length or points at length based on raw pathdata * [fix-path-directions](https://github.com/herrstrietzel/fix-path-directions) – Correct sub path directions in compound path for apps that don't support fill-rules or just reverse path directions (e.g for path animations) * [svg-pathdata-getbbox](https://github.com/herrstrietzel/svg-pathdata-getbbox) – Calculates a path bounding box based on its raw pathdata * [svg-transform](https://github.com/herrstrietzel/svg-transform) – A library to transform or de-transform/flatten svg paths