6

I'm doing a number of D3.JS operations requiring that I work with SVG paths instead of primitives/shapes (polylines, recs, etc.).

This question is general, but I'd like to know if it is possible to convert any SVG primitive to a path, either with D3 or another script/library.

For reference, here is a link which does it for polylines: https://gist.github.com/andytlr/9283541

I'd like to do this for every primitive. Any ideas? Is this possible?

ekatz
  • 1,012
  • 1
  • 11
  • 27
  • Here's a good function for polylines and polygons: http://stackoverflow.com/questions/10717190/convert-svg-polygon-to-path – ekatz Jul 22 '15 at 23:28
  • 1
    inkscape command line solutions (GUI will pop-up though): http://stackoverflow.com/questions/15203650 – alephreish Jul 22 '15 at 23:30

2 Answers2

1

I found this github site which has a set of java functions for converting shapes to paths: https://github.com/JFXtras/jfxtras-labs/blob/2.2/src/main/java/jfxtras/labs/util/ShapeConverter.java

ekatz
  • 1,012
  • 1
  • 11
  • 27
1

JavaScript solution

You can also convert all primitives using Jarek Foksa's path-data polyfill:

It's main purpose is to parse a path's d attribute to an array of commands.
But it also provides a normalize method to convert any element's geometry to path commands.

element.getPathData({normalize: true});

This method will convert all commands and coordinates to absolute values
and reduce the set of commands to these cubic bézier commands: M,L, C, Z.

Example usage: Convert all primitives and normalize path commands – like A (arc)

const svgWrp = document.querySelector('.svgWrp');
const svg = document.querySelector('svg');
const primitives = svg.querySelectorAll('path, line, polyline, polygon, circle, rect');
const svgMarkup = document.querySelector('#svgMarkup');
svgMarkup.value = svgWrp.innerHTML;

function convertPrimitives(svg, primitives) {
  primitives.forEach(function(primitive, i) {
    /**
     * get normalized path data: 
     * all coordinates are absolute; 
     * reduced set of commands: M, L, C, Z
     */
    let pathData = primitive.getPathData({
      normalize: true
    });

    //get all attributes
    let attributes = [...primitive.attributes];
    let path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    //exclude attributes not needed for paths
    let exclude = ['x', 'y', 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'r', 'rx', 'ry', 'points', 'height',
      'width'
    ];
    setAttributes(path, attributes, exclude);
    // set d attribute from rounded pathData
    path.setPathData(roundPathData(pathData, 1));
    svg.appendChild(path);
    primitive.remove();
  })
  // optional: output new svg markup
  let newSvgMarkup = svgWrp.innerHTML.
  replaceAll("></path>", "/>").
  replace(/([ |\n|\r|\t])/g, " ").
  replace(/  +/g, ' ').trim().
  replaceAll("> <", "><").
  replaceAll("><", ">\n<");
  svgMarkup.value = newSvgMarkup;
}

function roundPathData(pathData, decimals = 3) {
  pathData.forEach(function(com, c) {
    let values = com['values'];
    values.forEach(function(val, v) {
      pathData[c]['values'][v] = +val.toFixed(decimals);
    })
  })
  return pathData;
}

function setAttributes(el, attributes, exclude = []) {
  attributes.forEach(function(att, a) {
    if (exclude.indexOf(att.nodeName) === -1) {
      el.setAttribute(att.nodeName, att.nodeValue);
    }
  })
}
<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill@1.0.3/path-data-polyfill.min.js"></script>

<p><button type="button" onclick="convertPrimitives(svg, primitives)">Convert Primitives</button></p>
<div class="svgWrp">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 150 30">
        <polygon id="polygon" fill="#ccc" stroke="green" points="9,22.4 4.1,14 9,5.5 18.8,5.5 23.7,14 
        18.8,22.4 " />
        <polyline id="polyline" fill="none" stroke="red" points="43,22.4 33.3,22.4 28.4,14 33.3,5.5 43,5.5 
        47.9,14 " />
        <rect id="rect" x="57.3" y="5.5" fill="none" stroke="orange" width="16.9" height="16.9" />
        <line id="line" fill="none" stroke="purple" x1="52.6" y1="22.4" x2="52.6" y2="5.5" />
        <circle class="circle" data-att="circle" id="circle" fill="none" stroke="magenta"  cx="87.4" cy="14" r="8.5" />
        <path transform="scale(0.9) translate(110,5)" d="M 10 0 A 10 10 0 1 1 1.34 15 L 10 10 z" fill="red" class="segment segment-1 segment-class" id="segment-01"/>
    </svg>
 </div>
 <h3>Svg markup</h3>
 <textarea name="svgMarkup" id="svgMarkup" style="width:100%; height:20em;"></textarea>

The above example script will also retain all attributes like class, id, fill etc.

But it will strip attributes like r, cx, rx specific to primitives.

Do we need this polyfill?

Unfortunately, the getPathData() and setPathData() methods are still a svg 2 drafts/proposals – intended to replace the deprecated pathSegList() methods.
Hopefully we will get native browser support in the near future.
Since this polyfill is still rather lightweight (~12.5 KB uncompressed) compared to more advanced svg libraries like (snap.svg, d3 etc.) it won't increase your loading times significantly.

herrstrietzel
  • 3,355
  • 1
  • 5
  • 15