SVG - 将所有形状/基元转换为<path>

7

我正在进行一些需要使用SVG路径而不是基本图形/形状(折线、矩形等)的D3.JS操作。

这个问题比较普遍,但我想知道是否可以使用D3或其他脚本/库将任何SVG基本图形转换为路径。

参考链接中有一个将折线转换为路径的示例:https://gist.github.com/andytlr/9283541

我想对每个基本图形都进行转换。有什么想法吗?这可能吗?


这里有一个适用于折线和多边形的好函数:https://dev59.com/RWgv5IYBdhLWcg3wSe0f - ekatz
1
Inkscape 命令行解决方案(虽然 GUI 将弹出):https://dev59.com/9G_Xa4cB1Zd3GeqPyTA5 - alephreish
2个回答

3

JavaScript解决方案

你还可以使用Jarek Foksa的path-data polyfill将所有基元转换:

其主要目的是将路径的d属性解析为命令数组。

getPathData()还可以从任何SVGGeometryElement中检索pathData,包括像<rect><circle><ellipse><polygon>这样的基元。

示例1:转换所有基元

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 = new XMLSerializer().serializeToString(svg);

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();

    //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));
    primitive.replaceWith(path);
  })
  // optional: output new svg markup
  let newSvgMarkup = new XMLSerializer().serializeToString(svg);
  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 id="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" rx="2" ry="2" 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>

getPathData() 还提供了一个 normalize 方法,将任何元素的几何图形转换为一组缩小的绝对命令,只使用:
MLCZ
二次曲线 QT 命令也被转换, 弧线 A 和简写形式如 VH 也是如此。
element.getPathData({normalize: true});

示例2:转换所有基元(已标准化)

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 = new XMLSerializer().serializeToString(svg);

function convertPrimitives(svg, primitives) {
  primitives.forEach(function(primitive, i) {
    /**
     * get path data: 
     */
    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));
    primitive.replaceWith(path);
  })
  // optional: output new svg markup
  let newSvgMarkup = new XMLSerializer().serializeToString(svg);
  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 id="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" rx="2" ry="2" 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>

上述示例脚本也会保留所有属性,如classidfill等。
但它将剥离特定于基元的rcxrx等属性。
我们需要这个polyfill吗?
不幸的是,getPathData()setPathData()方法仍然是svg 2 drafts/proposals - 旨在替换已弃用的pathSegList()方法。
希望不久的将来我们能得到本地浏览器支持。
由于这个polyfill相对较轻(未压缩约12.5 KB),与更高级的svg库(如snap.svg、d3等)相比,它不会显著增加您的加载时间。
更新:独立脚本(无polyfill依赖)
这是一个概念验证 - 你可以基于相当基本的值计算,将svg原语转换为路径元素 - 无需高级框架/库 - 受到此帖子的启发: 将所有形状/原语转换为SVG路径元素
但是当我摆弄自己笨拙的转换脚本时,我很快意识到有一些挑战(Jarek Foksa的标准化实现完美地解决了这些问题),例如:
相对单位,即基于百分比的单位
<circle cx="50%" cy="50%" r="25%" />  

好的...我想我们需要根据父级svg的边界来计算这些相对值到绝对坐标,如viewBox属性所定义...也许没有可用的viewBox...或者宽度/高度值。

或者像rxry属性一样为<rect>元素应用圆角边框 - 为了进行良好的转换,我们需要添加一些弯曲命令,如acs

路径 vs. 原始图形
确实,<path>元素可以通过立体或二次样条命令绘制任何原始图形提供的形状 - 甚至更有效率,因为它具有连接多个形状的能力和相对或简写命令。
但它不支持相对单位 - 然而,您需要转换的形状可能严重依赖于相对尺寸(例如圆形仪表盘饼图等)

结论
编写自定义转换脚本并不太困难,但要注意一些棘手的细节。

const svg = document.querySelector('svg');
const svgMarkup = document.querySelector('#svgMarkup');
svgMarkup.value = new XMLSerializer().serializeToString(svg);

/**
 * example script
 **/
function getConvertedMarkup(svg, markupEl, decimals = 1) {
  convertPrimitivesNative(svg, decimals);
  markupEl.value = new XMLSerializer().serializeToString(svg);
}

/**
 * parse svg attributes and convert relative units
 **/
function parseSvgAttributes(svg, atts) {
  let calcW = 0;
  let calcH = 0;
  let calcR = 0;

  //1. check viewBox
  let viewBoxAtt = svg.getAttribute('viewBox');
  let viewBox = viewBoxAtt ? viewBoxAtt.split(' ') : [];
  [calcW, calcH] = [viewBox[2], viewBox[3]];

  //2. check width attributes
  if (!calcW || !calcH) {
    widthAtt = svg.getAttribute('width') ? parseFloat(svg.getAttribute('width')) : '';
    heightAtt = svg.getAttribute('height') ? parseFloat(svg.getAttribute('height')) : '';
    [calcW, calcH] = [widthAtt, heightAtt];
  }
  //3. calculate by getBBox()
  if (!calcW || !calcH) {
    let bb = svg.getBBox();
    [calcW, calcH] = [(calcW ? calcW : bb.width), (calcH ? calcH : bb.height)];
  }

  // calculate relative radius: needed for non square aspect ratios
  calcR = Math.sqrt(Math.pow(calcW, 2) + Math.pow(calcH, 2)) / Math.sqrt(2);

  let attArr = [...atts];
  let attObj = {};
  attArr.forEach(function(att) {
    let attName = att.nodeName;
    // convert percentages to absolute svg units
    let val = att.nodeValue;
    let percentAtts = ['x', 'y', 'x1', 'y1', 'x2', 'y2', 'r', 'rx', 'ry', 'cx', 'cy', 'width', 'height']
    if (val.toString().indexOf('%') !== -1 && percentAtts.indexOf(attName) !== -1) {
      // strip units
      val = parseFloat(val);
      switch (attName) {
        case 'cx':
        case 'rx':
        case 'width':
        case 'x':
        case 'x1':
        case 'x2':
          val = 1 / 100 * val * calcW;
          break;
        case 'cy':
        case 'ry':
        case 'height':
        case 'y':
        case 'y1':
        case 'y2':
          val = 1 / 100 * val * calcH;
          break;
        case 'r':
          val = 1 / 100 * val * calcR;
          break;
      }
    }
    attObj[att.nodeName] = val;
  });
  return attObj;
}

/**
 * convert primitive attributes to relative path commands
 */
function convertPrimitivesNative(svg, decimals = 3) {
  let primitives = svg.querySelectorAll('line, polyline, polygon, circle, ellipse, rect');

  if (primitives.length) {
    primitives.forEach(function(primitive) {
      let pathData = [];
      let type = primitive.nodeName;
      let atts = parseSvgAttributes(svg, primitive.attributes, 2);
      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'
      ];
      switch (type) {
        case 'rect':
          let [rx, ry] = [atts.rx, atts.ry];
          rx = !rx && ry ? ry : rx;
          ry = !ry && rx ? rx : ry;
          let [x, y, width, height] = [atts.x, atts.y, atts.width, atts.height];
          let [widthInner, heightInner] = [width - rx * 2, height - ry * 2];
          if (rx) {
            pathData.push({
              type: 'M',
              values: [x, (y + ry)]
            }, {
              type: 'a',
              values: [rx, ry, 0, 0, 1, rx, -ry]
            }, {
              type: 'h',
              values: [widthInner]
            }, {
              type: 'a',
              values: [rx, ry, 0, 0, 1, rx, ry]
            }, {
              type: 'v',
              values: [heightInner]
            }, {
              type: 'a',
              values: [rx, ry, 0, 0, 1, -rx, ry]
            }, {
              type: 'h',
              values: [-widthInner]
            }, {
              type: 'a',
              values: [rx, ry, 0, 0, 1, -rx, -ry]
            }, {
              type: 'z',
              values: []
            });

          } else {
            pathData.push({
              type: 'M',
              values: [x, y]
            }, {
              type: 'h',
              values: [width]
            }, {
              type: 'v',
              values: [height]
            }, {
              type: 'h',
              values: [-width]
            }, {
              type: 'z',
              values: []
            });
          }
          break;

        case 'line':
          let [x1, y1, x2, y2] = [atts.x1, atts.y1, atts.x2, atts.y2];
          pathData.push({
            type: 'M',
            values: [x1, y1]
          }, {
            type: 'l',
            values: [(x2 - x1), (y2 - y1)]
          });
          break;

        case 'circle':
        case 'ellipse':
          if (type == 'circle') {
            let r = atts.r;
            let [cX, cY] = [atts.cx, atts.cy - atts.r];
            pathData.push({
              type: 'M',
              values: [cX, cY]
            }, {
              type: 'a',
              values: [r, r, 0, 0, 1, r, r]
            }, {
              type: 'a',
              values: [r, r, 0, 0, 1, -r, r]
            }, {
              type: 'a',
              values: [r, r, 0, 0, 1, -r, -r]
            }, {
              type: 'a',
              values: [r, r, 0, 0, 1, r, -r]
            }, {
              type: 'z',
              values: []
            });

          } else {
            let rx = atts.rx;
            let ry = atts.ry;
            let [cX, cY] = [atts.cx, atts.cy - atts.ry];
            pathData.push({
              type: 'M',
              values: [cX, cY]
            }, {
              type: 'a',
              values: [rx, ry, 0, 0, 1, rx, ry]
            }, {
              type: 'a',
              values: [rx, ry, 0, 0, 1, -rx, ry]
            }, {
              type: 'a',
              values: [rx, ry, 0, 0, 1, -rx, -ry]
            }, {
              type: 'a',
              values: [rx, ry, 0, 0, 1, rx, -ry]
            }, {
              type: 'z',
              values: []
            });
          }
          break;

        case 'polygon':
        case 'polyline':
          let closePath = type == 'polygon' ? 'z' : '';
          let points = atts.points.replace(/^\s+|\s+$|\s+(?=\s)/g, "").replaceAll(",", " ");
          let pointArr = points.split(' ');
          pathData.push({
            type: 'M',
            values: [+pointArr[0], +pointArr[1]]
          });

          for (let i = 2; i < pointArr.length; i += 2) {
            let [x0, y0] = [+pointArr[i - 2], +pointArr[i - 1]];
            let [x, y] = [+pointArr[i], +pointArr[i + 1]];
            let com = {};

            if (y == y0) {
              com = {
                type: 'h',
                values: [x - x0]
              }
            } else if (x == x0) {
              com = {
                type: 'v',
                values: [y - y0]
              }
            } else {
              com = {
                type: 'l',
                values: [x - x0, y - y0]
              }
            }
            pathData.push(com);
          }
          if (closePath) {
            pathData.push({
              type: 'z',
              values: []
            });
          }
          break;

          //paths
        default:
          let dClean = atts.d.replace(/^\s+|\s+$|\s+(?=\s)/g, "").replaceAll(",", " ");
          let dArr = dClean.replace(/([a-zA-Z])/g, " | $1").split(' | ');
          dArr.shift();
          for (let i = 0; i < dArr.length; i++) {
            let command = dArr[i].trim().split(' ');
            let type = command.shift();

            command = command.map((x) => {
              return parseFloat(x);
            });
            pathData.push({
              type: type,
              values: command
            });
          }
          break;
      }

      // copy primitive's attributes to path
      setAttributes(path, atts, exclude);
      // round coordinates and replace primitive with path
      path.setPathDataOpt(pathData, decimals);
      primitive.replaceWith(path);
    })
  }
};


function setAttributes(el, attributes, exclude = []) {
  for (key in attributes) {
    if (exclude.indexOf(key) === -1) {
      el.setAttribute(key, attributes[key]);
    }
  }
}

function getAttributes(el) {
  let attArr = [...el.attributes];
  let attObj = {};
  attArr.forEach(function(att) {
    attObj[att.nodeName] = att.nodeValue;
  });
  return attObj;
}


/**
 * return rounded path data 
 * based on:
 * https://github.com/jarek-foksa/path-data-polyfill/blob/master/path-data-polyfill.js
 */
if (!SVGPathElement.prototype.setPathDataOpt) {
  SVGPathElement.prototype.setPathDataOpt = function(pathData, decimals = 3) {
    let d = "";
    if (pathData.length) {
      for (let i = 0; i < pathData.length; i++) {
        let seg = pathData[i];
        let [type, values] = [seg.type, seg.values];
        let valArr = [];
        if (values.length) {
          for (let v = 0; v < values.length; v++) {
            val = parseFloat(values[v]);
            valArr.push(+val.toFixed(decimals));
          }
        }
        d += type;
        if (valArr.length) {
          d += valArr.join(" ").trim();
        }
      }
      d = d.
      replaceAll(' -', '-').
      replaceAll(' 0.', ' .').
      replaceAll(' z', 'z');
      this.setAttribute("d", d);
    }
  };
}
<p><button type="button" onclick="getConvertedMarkup(svg, svgMarkup, 2)">Convert Primitives</button></p>
<svg id="svg" xmlns="http://www.w3.org/2000/svg" data-width="150px" data-height="30px" viewBox="0 0 150 30">
<polygon id="polygon" fill="#CCCCCC" stroke="#E3000F" points="7.9,22.8 3,14.3 7.9,5.8 17.6,5.8 22.5,14.3 17.6,22.8 " />
<polyline id="polyline" fill="none" stroke="#E3000F" points="40.9,22.8 31.1,22.8 26.2,14.3 31.1,5.8 
 40.9,5.8 45.8,14.3 " />
<rect id="rect" x="37.5%" y="20%" rx="2%" ry="5%" fill="none" stroke="#E3000F" width="6%" height="56%" />
<line id="line" fill="none" stroke="#E3000F" x1="50.5" y1="22.8" x2="52.5" y2="5.8" />
<circle id="circle" fill="none" stroke="#E3000F" cx="52%" cy="49%" r="8%" />
<ellipse id="ellipse" fill="none" stroke="#E3000F" cx="68%" cy="49%" rx="7%" ry="25%" />
<path id="piechart" transform="scale(0.9) translate(130, 6)" 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>
<h3>Output</h3>
<textarea name="svgMarkup" id="svgMarkup" style="width:100%; height:20em;"></textarea>

Codepen转换器示例


1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接