Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Convert SVG Path to Relative Commands

Tags:

javascript

svg

Given an SVG Path element, how can I convert all path commands into relative coordinates? For example, convert this path (which includes every command, absolute and relative, interleaved):

<path d="M3,7 L13,7 m-10,10 l10,0 V27 H23 v10 h10
         C33,43 38,47 43,47 c0,5 5,10 10,10
         S63,67 63,67       s-10,10 10,10
         Q50,50 73,57       q20,-5 0,-10
         T70,40             t0,-15
         A5,5 45 1 0 40,20  a5,5 20 0 1 -10,-10
         Z" />

into this equivalent path:

<path d="m3,7 l10,0 m-10 10 l10,0 v10 h10 v10 h10
         c0,6 5,10 10,10    c0,5 5,10 10,10
         s10,10 10,10       s-10,10 10,10
         q-23,-27 0,-20     q20,-5 0,-10
         t-3,-7             t0-15
         a5,5 45 1 0 -30,-5 a5,5 20 0 1 -10,-10
         z"/>

This question was motivated by this question.

like image 805
ecmanaut Avatar asked Jan 06 '13 04:01

ecmanaut


3 Answers

Snap.SVG has Snap.path.toRelative().

var rel = Snap.path.toRelative(abspathstring);

Fiddle

like image 118
Iktys Avatar answered Nov 18 '22 22:11

Iktys


I tweaked Phrogz' convertToAbsolute into this convertToRelative function:

function convertToRelative(path) {
  function set(type) {
    var args = [].slice.call(arguments, 1)
      , rcmd = 'createSVGPathSeg'+ type +'Rel'
      , rseg = path[rcmd].apply(path, args);
    segs.replaceItem(rseg, i);
  }
  var dx, dy, x0, y0, x1, y1, x2, y2, segs = path.pathSegList;
  for (var x = 0, y = 0, i = 0, len = segs.numberOfItems; i < len; i++) {
    var seg = segs.getItem(i)
      , c   = seg.pathSegTypeAsLetter;
    if (/[MLHVCSQTAZz]/.test(c)) {
      if ('x1' in seg) x1 = seg.x1 - x;
      if ('x2' in seg) x2 = seg.x2 - x;
      if ('y1' in seg) y1 = seg.y1 - y;
      if ('y2' in seg) y2 = seg.y2 - y;
      if ('x'  in seg) dx = -x + (x = seg.x);
      if ('y'  in seg) dy = -y + (y = seg.y);
      switch (c) {
        case 'M': set('Moveto',dx,dy);                   break;
        case 'L': set('Lineto',dx,dy);                   break;
        case 'H': set('LinetoHorizontal',dx);            break;
        case 'V': set('LinetoVertical',dy);              break;
        case 'C': set('CurvetoCubic',dx,dy,x1,y1,x2,y2); break;
        case 'S': set('CurvetoCubicSmooth',dx,dy,x2,y2); break;
        case 'Q': set('CurvetoQuadratic',dx,dy,x1,y1);   break;
        case 'T': set('CurvetoQuadraticSmooth',dx,dy);   break;
        case 'A': set('Arc',dx,dy,seg.r1,seg.r2,seg.angle,
                      seg.largeArcFlag,seg.sweepFlag);   break;
        case 'Z': case 'z': x = x0; y = y0; break;
      }
    }
    else {
      if ('x' in seg) x += seg.x;
      if ('y' in seg) y += seg.y;
    }
    // store the start of a subpath
    if (c == 'M' || c == 'm') {
      x0 = x;
      y0 = y;
    }
  }
  path.setAttribute('d', path.getAttribute('d').replace(/Z/g, 'z'));
}

Used like so with the path from the question:

var path = document.querySelector('path');
convertToRelative(path);
console.log(path.getAttribute('d'));
// m 3 7 l 10 0 m -10 10 l 10 0 v 10 h 10 v 10 h 10 c 0 6 5 10 10 10 c 0 5 5 10 10 10 s 10 10 10 10 s -10 10 10 10 q -23 -27 0 -20 q 20 -5 0 -10 t -3 -7 t 0 -15 a 5 5 45 1 0 -30 -5 a 5 5 20 0 1 -10 -10 z

I also made a little phantomjs shell utility svg2rel which transforms all paths in an svg this way (there's a corresponding svg2abs in the same gist, for good measure).

like image 37
ecmanaut Avatar answered Nov 18 '22 22:11

ecmanaut


Based on the svg working draft methods getPathData() and setPathData() I came up with a port of Dmitry Baranovskiy's pathToRelative/Absolute methods used in snap.svg.

getPathData() is intended to replace the deprecated pathSegList methods but is not yet supported by any major browser.
So you need a polyfill like Jarek Foksa's pathdata polyfill..

Example usage:

let svg = document.querySelector('svg');
let path = svg.querySelector('path');
let pathData = path.getPathData();
// 2nd argument defines optional rounding: -1 == no rounding; 2 == round to 2 decimals
let pathDataRel = pathDataToRelative(pathData, 3);
path.setPathData(pathDataRel);

Example snippet based on Lea Verou's post
"Convert SVG path to all-relative or all-absolute commands" (using snap.svg)

/**
 * dependancy: Jarek Foks's pathdata polyfill
 * cdn: https://cdn.jsdelivr.net/npm/[email protected]/path-data-polyfill.min.js
 * github: https://github.com/jarek-foksa/path-data-polyfill
 **/

// convert to relative commands
function pathDataToRelative(pathData, decimals = -1) {
    let M = pathData[0].values;
    let x = M[0],
        y = M[1],
        mx = x,
        my = y;
    // loop through commands
    for (let i = 1; i < pathData.length; i++) {
        let cmd = pathData[i];
        let type = cmd.type;
        let typeRel = type.toLowerCase();
        let values = cmd.values;

        // is absolute
        if (type != typeRel) {
            type = typeRel;
            cmd.type = type;
            // check current command types
            switch (typeRel) {
                case "a":
                    values[5] = +(values[5] - x);
                    values[6] = +(values[6] - y);
                    break;
                case "v":
                    values[0] = +(values[0] - y);
                    break;
                case "m":
                    mx = values[0];
                    my = values[1];
                default:
                    // other commands
                    if (values.length) {
                        for (let v = 0; v < values.length; v++) {
                            // even value indices are y coordinates
                            values[v] = values[v] - (v % 2 ? y : x);
                        }
                    }
            }
        }
        // is already relative
        else {
            if (cmd.type == "m") {
                mx = values[0] + x;
                my = values[1] + y;
            }
        }
        let vLen = values.length;
        switch (type) {
            case "z":
                x = mx;
                y = my;
                break;
            case "h":
                x += values[vLen - 1];
                break;
            case "v":
                y += values[vLen - 1];
                break;
            default:
                x += values[vLen - 2];
                y += values[vLen - 1];
        }

        // round coordinates
        if (decimals >= 0) {
            cmd.values = values.map((val) => {
                return +val.toFixed(decimals);
            });
        }
    }
    // round M (starting point)
    if (decimals >= 0) {
        [M[0], M[1]] = [+M[0].toFixed(decimals), +M[1].toFixed(decimals)];
    }
    return pathData;
}

function pathDataToAbsolute(pathData, decimals = -1) {
    let M = pathData[0].values;
    let x = M[0],
        y = M[1],
        mx = x,
        my = y;
    // loop through commands
    for (let i = 1; i < pathData.length; i++) {
        let cmd = pathData[i];
        let type = cmd.type;
        let typeAbs = type.toUpperCase();
        let values = cmd.values;

        if (type != typeAbs) {
            type = typeAbs;
            cmd.type = type;
            // check current command types
            switch (typeAbs) {
                case "A":
                    values[5] = +(values[5] + x);
                    values[6] = +(values[6] + y);
                    break;

                case "V":
                    values[0] = +(values[0] + y);
                    break;

                case "H":
                    values[0] = +(values[0] + x);
                    break;

                case "M":
                    mx = +values[0] + x;
                    my = +values[1] + y;

                default:
                    // other commands
                    if (values.length) {
                        for (let v = 0; v < values.length; v++) {
                            // even value indices are y coordinates
                            values[v] = values[v] + (v % 2 ? y : x);
                        }
                    }
            }
        }
        // is already absolute
        let vLen = values.length;
        switch (type) {
            case "Z":
                x = +mx;
                y = +my;
                break;
            case "H":
                x = values[0];
                break;
            case "V":
                y = values[0];
                break;
            case "M":
                mx = values[vLen - 2];
                my = values[vLen - 1];

            default:
                x = values[vLen - 2];
                y = values[vLen - 1];
        }

        // round coordinates
        if (decimals >= 0) {
            cmd.values = values.map((val) => {
                return +val.toFixed(decimals);
            });
        }
    }
    // round M (starting point)
    if (decimals >= 0) {
        [M[0], M[1]] = [+M[0].toFixed(decimals), +M[1].toFixed(decimals)];
    }
    return pathData;
}

function roundPathData(pathData, decimals = -1) {
    if (decimals >= 0) {
        pathData.forEach(function (com, c) {
            let values = com["values"];
            values.forEach(function (val, v) {
                pathData[c]["values"][v] = +val.toFixed(decimals);
            });
        });
    }
    return pathData;
}

// reverse pathdata
function reversePathData(pathData) {
    let M = pathData[0];
    let newPathData = [M];
    // split subpaths
    let subPathDataArr = splitSubpaths(pathData);

    subPathDataArr.forEach((subPathData, s) => {
        let subPathDataL = subPathData.length;
        let closed = subPathData[subPathDataL - 1]["type"] == "Z" ? true : false;
        let stripZ = false;

        if (!closed) {
            subPathData.push({
                type: "Z",
                values: []
            });
            subPathDataL++;
            closed = true;
            stripZ = true;
        }

        let subM = subPathData[0]["values"];

        // insert Lineto if last path segment has created by z
        let lastCom = closed
            ? subPathData[subPathDataL - 2]
            : subPathData[subPathDataL - 1];
        let lastComL = lastCom["values"].length;
        let lastXY = [
            lastCom["values"][lastComL - 2],
            lastCom["values"][lastComL - 1]
        ];
        let diff = Math.abs(subM[0] - lastXY[0]);

        if (diff > 1 && closed) {
            subPathData.pop();
            subPathData.push({
                type: "L",
                values: [subM[0], subM[1]]
            });
            subPathData.push({
                type: "Z",
                values: []
            });
        }

        subPathData.forEach(function (com, i) {
            // reverse index
            let subpathDataL = subPathData.length;
            let indexR = subpathDataL - 1 - i;
            let comR = subPathData[indexR];
            let comF = subPathData[i];
            let [typeR, valuesR] = [comR["type"], comR["values"]];
            let [typeF, valuesF] = [comF["type"], comF["values"]];
            if (typeF == "M" && s > 0) {
                newPathData.push(comF);
            } else if (typeR != "M" && typeR != "Z") {
                indexR--;
                let prevCom =
                    i > 0 ? subPathData[indexR] : subPathData[subpathDataL - 1 - i];
                let prevVals = prevCom
                    ? prevCom["values"]
                        ? prevCom["values"]
                        : [0, 0]
                    : [];
                prevVals = prevCom["values"];
                let prevValsL = prevVals.length;
                let newCoords = [];

                if (typeR == "C") {
                    newCoords = [
                        valuesR[2],
                        valuesR[3],
                        valuesR[0],
                        valuesR[1],
                        prevVals[prevValsL - 2],
                        prevVals[prevValsL - 1]
                    ];

                    if (!closed) {
                        let nextVals =
                            i < subpathDataL - 1 ? subPathData[i + 1]["values"] : lastXY;
                        let lastCX = i < subpathDataL - 2 ? nextVals[prevValsL - 2] : subM[0];
                        let lastCY = i < subpathDataL - 2 ? nextVals[prevValsL - 1] : subM[1];
                        newCoords[4] = lastCX;
                        newCoords[5] = lastCY;
                    }
                } else {
                    newCoords = [prevVals[prevValsL - 2], prevVals[prevValsL - 1]];
                }
                newPathData.push({
                    type: typeR,
                    values: newCoords
                });
            }
        });
        if (closed) {
            newPathData.push({
                type: "Z",
                values: []
            });
        }

        //fix M
        if (diff > 1 && stripZ) {
            let firstL = newPathData[1]["values"];
            newPathData[1] = {
                type: "M",
                values: [firstL[0], firstL[1]]
            };
            newPathData.shift();
            newPathData.pop();
        }
    });
    return newPathData;
}

function splitSubpaths(pathData) {
    let pathDataL = pathData.length;
    let subPathArr = [];
    let subPathMindex = [];
    pathData.forEach(function (com, i) {
        let [type, values] = [com["type"], com["values"]];
        if (type == "M") {
            subPathMindex.push(i);
        }
    });
    //split segments after M command
    subPathMindex.forEach(function (index, i) {
        let n = subPathMindex[i + 1];
        let thisSeg = pathData.slice(index, n);
        subPathArr.push(thisSeg);
    });
    return subPathArr;
}
body {
    font: 100%/1.5 Helvetica Neue, sans-serif;
    margin: 1em;
}

svg {
    border: 1px solid #ccc;
    max-height: 10em;
}

pre {
    display: inline-block;
    background: #eee;
    margin: 0;
}

section {
    flex: 1;
    display: flex;
    flex-flow: column;
}

textarea {
    display: block;
    font: inherit;
    font-family: Consolas, monospace;
    width: 100%;
    height: 8em;
    margin: 0.1em 0;
    resize: vertical;
}

footer {
    color: gray;
}

footer a {
    color: inherit;
}

@media (min-width: 800px) {
    .flex {
        display: flex;
        width: 100%;
        gap: 1em;
    }

    svg {
        max-width: 40vw;
    }
}
<form>
    <label>Round coordinates (-1 = no rounding)</label>
    <input class="input" type="number" id="precision" min="-1" value="3">
    <div class="flex">
        <section>
            <label>
                Your path: <textarea class="input" id="origPathT">M46.8 34.9 L49.5 43.2 Q46.5 44.2 42.9 44.5 Q39.3 44.8 34.1 44.8 L34.1 44.8 Q43.4 49 43.4 58.1 L43.4 58.1 Q43.4 66 38 71 Q32.6 76 23.3 76 L23.3 76 Q19.7 76 16.6 75 L16.6 75 Q15.4 75.8 14.7 77.15 Q14 78.5 14 79.9 L14 79.9 Q14 84.2 20.9 84.2 L20.9 84.2 L29.3 84.2 Q34.6 84.2 38.7 86.1 Q42.8 88 45.05 91.3 Q47.3 94.6 47.3 98.8 L47.3 98.8 Q47.3 106.5 41 110.65 Q34.7 114.8 22.6 114.8 L22.6 114.8 Q14.1 114.8 9.15 113.05 Q4.2 111.3 2.1 107.8 Q0 104.3 0 98.8 L0 98.8 L8.3 98.8 Q8.3 102 9.5 103.85 Q10.7 105.7 13.8 106.65 Q16.9 107.6 22.6 107.6 L22.6 107.6 Q30.9 107.6 34.45 105.55 Q38 103.5 38 99.4 L38 99.4 Q38 95.7 35.2 93.8 Q32.4 91.9 27.4 91.9 L27.4 91.9 L19.1 91.9 Q12.4 91.9 8.95 89.05 Q5.5 86.2 5.5 81.9 L5.5 81.9 Q5.5 79.3 7 76.9 Q8.5 74.5 11.3 72.6 L11.3 72.6 Q6.7 70.2 4.55 66.65 Q2.4 63.1 2.4 58 L2.4 58 Q2.4 52.7 5.05 48.5 Q7.7 44.3 12.35 41.95 Q17 39.6 22.7 39.6 L22.7 39.6 Q28.9 39.7 33.1 39.15 Q37.3 38.6 40.05 37.65 Q42.8 36.7 46.8 34.9 L46.8 34.9 ZM22.7 46.2 Q17.5 46.2 14.65 49.45 Q11.8 52.7 11.8 58 L11.8 58 Q11.8 63.4 14.7 66.65 Q17.6 69.9 22.9 69.9 L22.9 69.9 Q28.3 69.9 31.15 66.75 Q34 63.6 34 57.9 L34 57.9 Q34 46.2 22.7 46.2 L22.7 46.2 Z </textarea></label>
            <svg width="100%" height="100%">
                <path id="origPath" fill="indianred" d="" />
            </svg>
            <p id="fileSizeOrig" class="fileSize"></p>

        </section>
        <section>
            <label>
                All-relative path: <textarea id="relPathT" readonly></textarea></label>
            <svg width="100%" height="100%">
                <path id="relativePath" fill="yellowgreen" d="" />
            </svg>
            <p id="fileSizeRel" class="fileSize"></p>

        </section>
        <section>
            <label>
                All-absolute path: <textarea id="absPathT" readonly></textarea></label>
            <svg width="100%" height="100%">
                <path id="absolutePath" fill="hsl(180,50%,50%)" d="" />
            </svg>
            <p id="fileSizeAbs" class="fileSize"></p>

        </section>
    </div>

</form>
<footer>
    <p>Convert path commands to relative and absolute coordinates – using <a href="https://github.com/jarek-foksa/path-data-polyfill">path data polyfill by Jarek Foksa.</a></p>
    <p>Forked from original codepen: by <a href="https://codepen.io/leaverou/pen/RmwzKv">Lea Verou</a>
    </p> Described in this article: <a href="https://lea.verou.me/2019/05/utility-convert-svg-path-to-all-relative-or-all-absolute-commands/">Utility: Convert SVG path to all-relative or all-absolute commands </a></p>
</footer>

<script src="https://cdn.jsdelivr.net/npm/[email protected]/path-data-polyfill.min.js"></script>

<script>
    window.addEventListener('DOMContentLoaded', evt => {
        let decimals = parseFloat(precision.value);
        let inputs = document.querySelectorAll('.input');
        let svgs = document.querySelectorAll('svg');
        upDateSVG()
        inputs.forEach(input => {
            input.addEventListener('input', evt => {
                upDateSVG()
            })
        })

        function upDateSVG() {
            decimals = parseFloat(precision.value);
            let d = origPathT.value;
            origPath.setAttribute('d', d);
            let pathData = origPath.getPathData();
            let sizeOrig = filesize(d);
            fileSizeOrig.textContent = sizeOrig + ' KB';
            // relative
            let pathDataRel = pathDataToRelative(pathData, decimals);
            relativePath.setPathData(pathDataRel);
            relPathT.value = relativePath.getAttribute('d');
            let sizeRel = filesize(relPathT.value);
            fileSizeRel.textContent = sizeRel + ' KB';
            // absolute
            let pathDataAbs = pathDataToAbsolute(pathData, decimals);
            absolutePath.setPathData(pathDataAbs);
            absPathT.value = absolutePath.getAttribute('d');
            let sizeAbs = filesize(absPathT.value);
            fileSizeAbs.textContent = sizeAbs + ' KB';
            // adjust viewBox
            svgs.forEach(svg => {
                adjustViewBox(svg);
            })
        }
    })
    //adjustViewBox(svg);
    function adjustViewBox(svg) {
        let bb = svg.getBBox();
        let bbVals = [bb.x, bb.y, bb.width, bb.height].map((val) => {
            return +val.toFixed(2);
        });
        let maxBB = Math.max(...bbVals);
        let [x, y, width, height] = bbVals;
        svg.setAttribute("viewBox", [x, y, width, height].join(" "));
    }
    // show file size
    function filesize(str) {
        let size = new Blob([str]).size / Math.pow(1024, 1);
        return +size.toFixed(3);
    }
</script>

Fortunately there's no shortage of svg libraries or web apps providing a conversion method.

  • yqnn's "svg-path-editor"
  • thednp's svg path commander
like image 2
herrstrietzel Avatar answered Nov 18 '22 23:11

herrstrietzel