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.
Snap.SVG has Snap.path.toRelative()
.
var rel = Snap.path.toRelative(abspathstring);
Fiddle
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).
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..
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With