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.
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.
Share Improve this question edited May 23, 2017 at 12:10 CommunityBot 11 silver badge asked Jan 6, 2013 at 4:35 ecmanautecmanaut 5,1502 gold badges47 silver badges67 bronze badges 1- 1 One way of doing that non programatically (in case it works for you) is by using the service from this website: lea.verou.me/2019/05/… – Adriel Jr Commented Nov 18, 2021 at 0:28
3 Answers
Reset to default 7Snap.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..
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;
}
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