//svg area
let areaSVG = getSvgElsArea(svg);
//potrace area
let scale = 2;
(async() => {
let areaTraced = await getTracedArea(svg, scale);
result.textContent = `area svg: ${areaSVG} | area traced: ${areaTraced}`;
})();
async function getTracedArea(el, scale = 1) {
let filter = "brightness(0%)";
let dataUrl = await svg2PngDataUrl(el, scale, filter);
let tracingOptions = {
turnpolicy: "minority",
turdsize: 2,
optcurve: true,
alphamax: 1,
opttolerance: 0.75
};
let tracedSVG = await traceFromURL(dataUrl, tracingOptions);
let areaTraced = getSvgElsArea(tracedSVG);
document.body.append(tracedSVG);
return areaTraced / scale / scale;
}
/**
* svg to canvas
*/
async function svg2PngDataUrl(el, scale = 1, filter = "") {
/**
* clone svg to add width and height
*/
const svgEl = el.cloneNode(true);
// get dimensions
let {
width,
height
} = el.getBBox();
let w = el.viewBox.baseVal.width ?
svgEl.viewBox.baseVal.width :
el.width.baseVal.value ?
el.width.baseVal.value :
width;
let h = el.viewBox.baseVal.height ?
svgEl.viewBox.baseVal.height :
el.height.baseVal.value ?
el.height.baseVal.value :
height;
// apply scaling
[w, h] = [w * scale, h * scale];
// add width and height for firefox compatibility
svgEl.setAttribute("width", w);
svgEl.setAttribute("height", h);
// create canvas
let canvas = document.createElement("canvas");
canvas.width = w;
canvas.height = h;
let svgString = new XMLSerializer().serializeToString(svgEl);
let blob = new Blob([svgString], {
type: "image/svg+xml"
});
let blobURL = URL.createObjectURL(blob);
let tmpImg = new Image();
tmpImg.src = blobURL;
tmpImg.width = w;
tmpImg.height = h;
tmpImg.crossOrigin = "anonymous";
await tmpImg.decode();
let ctx = canvas.getContext("2d");
ctx.fillStyle = "white";
ctx.fillRect(0, 0, w, h);
// apply filter to enhance contrast
if (filter) {
ctx.filter = filter;
}
ctx.drawImage(tmpImg, 0, 0, w, h);
let dataUrl = canvas.toDataURL();
return dataUrl;
}
/**
* trace img from data URl
*/
async function traceFromURL(url, customOptions) {
let defaults = {
turnpolicy: "minority",
turdsize: 2,
optcurve: true,
alphamax: 1,
opttolerance: 0.75
};
let options = {
...defaults,
...customOptions
};
// set parameters
Potrace.setParameter(options);
// load
await Potrace.loadImageFromUrl(url);
async function processPotraceAsync() {
return new Promise((resolve) => {
Potrace.process(() => {
let svgTraced = new DOMParser()
.parseFromString(Potrace.getSVG(1), "image/svg+xml")
.querySelector("svg");
// remove attributes
svgTraced.setAttribute(
"viewBox",
`0 0 ${svgTraced.width.baseVal.value} ${svgTraced.height.baseVal.value}`
);
svgTraced.removeAttribute("id");
svgTraced.removeAttribute("version");
svgTraced.removeAttribute("width");
svgTraced.removeAttribute("height");
let path = svgTraced.querySelector("path");
path.removeAttribute("stroke");
path.removeAttribute("fill");
path.removeAttribute("fill-rule");
resolve(svgTraced);
});
});
}
let tracedSvgEl = await processPotraceAsync();
return tracedSvgEl;
}
function getSvgElsArea(svg) {
let els = svg.querySelectorAll("path, circle, ellipse, rect, polygon");
let area = 0;
els.forEach((el) => {
area += getshapeArea(el);
});
return area;
}
function getshapeArea(el, decimals = 0) {
let totalArea = 0;
let polyPoints = [];
let type = el.nodeName.toLowerCase();
switch (type) {
// 1. paths
case "path":
let pathData = el.getPathData({
normalize: true
});
//check subpaths
let subPathsData = splitSubpaths(pathData);
let isCompoundPath = subPathsData.length > 1 ? true : false;
let counterShapes = [];
// check intersections for compund paths
if (isCompoundPath) {
let bboxArr = getSubPathBBoxes(subPathsData);
bboxArr.forEach(function(bb, b) {
//let path1 = path;
for (let i = 0; i < bboxArr.length; i++) {
let bb2 = bboxArr[i];
if (bb != bb2) {
let intersects = checkBBoxIntersections(bb, bb2);
if (intersects) {
counterShapes.push(i);
}
}
}
});
}
subPathsData.forEach(function(pathData, d) {
//reset polygon points for each segment
polyPoints = [];
let bezierArea = 0;
let pathArea = 0;
let multiplier = 1;
pathData.forEach(function(com, i) {
let [type, values] = [com.type, com.values];
if (values.length) {
let prevC = i > 0 ? pathData[i - 1] : pathData[0];
let prevCVals = prevC.values;
let prevCValsL = prevCVals.length;
let [x0, y0] = [
prevCVals[prevCValsL - 2],
prevCVals[prevCValsL - 1]
];
// C commands
if (values.length == 6) {
let area = getBezierArea([
x0,
y0,
values[0],
values[1],
values[2],
values[3],
values[4],
values[5]
]);
//push points to calculate inner/remaining polygon area
polyPoints.push([x0, y0], [values[4], values[5]]);
bezierArea += area;
}
// L commands
else {
polyPoints.push([x0, y0], [values[0], values[1]]);
}
}
});
//get area of remaining polygon
let areaPoly = polygonArea(polyPoints, false);
//subtract area by negative multiplier
if (counterShapes.indexOf(d) !== -1) {
multiplier = -1;
}
//values have the same sign - subtract polygon area
if (
(areaPoly < 0 && bezierArea < 0) ||
(areaPoly > 0 && bezierArea > 0)
) {
pathArea = (Math.abs(bezierArea) - Math.abs(areaPoly)) * multiplier;
} else {
pathArea = (Math.abs(bezierArea) + Math.abs(areaPoly)) * multiplier;
}
totalArea += pathArea;
});
break;
// 2.1 circle an ellipse primitives
case "circle":
case "ellipse":
totalArea = getEllipseArea(el);
break;
// 2.2 polygons
case "polygon":
case "polyline":
totalArea = getPolygonArea(el);
break;
// 2.3 rectancle primitives
case "rect":
totalArea = getRectArea(el);
break;
}
if (decimals > 0) {
totalArea = +totalArea.toFixed(decimals);
}
return totalArea;
}
function getPathArea(pathData) {
let totalArea = 0;
let polyPoints = [];
pathData.forEach(function(com, i) {
let [type, values] = [com.type, com.values];
if (values.length) {
let prevC = i > 0 ? pathData[i - 1] : pathData[0];
let prevCVals = prevC.values;
let prevCValsL = prevCVals.length;
let [x0, y0] = [prevCVals[prevCValsL - 2], prevCVals[prevCValsL - 1]];
// C commands
if (values.length == 6) {
let area = getBezierArea([
x0,
y0,
values[0],
values[1],
values[2],
values[3],
values[4],
values[5]
]);
//push points to calculate inner/remaining polygon area
polyPoints.push([x0, y0], [values[4], values[5]]);
totalArea += area;
}
// L commands
else {
polyPoints.push([x0, y0], [values[0], values[1]]);
}
}
});
let areaPoly = polygonArea(polyPoints);
totalArea = Math.abs(areaPoly) + Math.abs(totalArea);
return totalArea;
}
function getBezierArea(coords) {
let x0 = coords[0];
let y0 = coords[1];
//if is cubic command
if (coords.length == 8) {
let x1 = coords[2];
let y1 = coords[3];
let x2 = coords[4];
let y2 = coords[5];
let x3 = coords[6];
let y3 = coords[7];
let area =
((x0 * (-2 * y1 - y2 + 3 * y3) +
x1 * (2 * y0 - y2 - y3) +
x2 * (y0 + y1 - 2 * y3) +
x3 * (-3 * y0 + y1 + 2 * y2)) *
3) /
20;
return area;
} else {
return 0;
}
}
function polygonArea(points, absolute = true) {
let area = 0;
for (let i = 0; i < points.length; i++) {
const addX = points[i][0];
const addY = points[i === points.length - 1 ? 0 : i + 1][1];
const subX = points[i === points.length - 1 ? 0 : i + 1][0];
const subY = points[i][1];
area += addX * addY * 0.5 - subX * subY * 0.5;
}
if (absolute) {
area = Math.abs(area);
}
return area;
}
function getPolygonArea(el) {
// convert point string to arra of numbers
let points = el
.getAttribute("points")
.split(/,| /)
.filter(Boolean)
.map((val) => {
return parseFloat(val);
});
let polyPoints = [];
for (let i = 0; i < points.length; i += 2) {
polyPoints.push([points[i], points[i + 1]]);
}
let area = polygonArea(polyPoints);
return area;
}
function getRectArea(el) {
let width = el.width.baseVal.value;
let height = el.height.baseVal.value;
let area = width * height;
return area;
}
function getEllipseArea(el) {
// if is circle
let r = el.getAttribute("r") ? el.r.baseVal.value : "";
let rx = el.getAttribute("rx");
let ry = el.getAttribute("ry");
//if circle – take radius
rx = rx ? el.rx.baseVal.value : r;
ry = ry ? el.ry.baseVal.value : r;
let area = Math.PI * rx * ry;
return area;
}
//path data helpers
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 subPaths
subPathMindex.forEach(function(index, i) {
let end = subPathMindex[i + 1];
let thisSeg = pathData.slice(index, end);
subPathArr.push(thisSeg);
});
return subPathArr;
}
function getSubPathBBoxes(subPaths) {
let ns = "http://www.w3.org/2000/svg";
let svgTmp = document.createElementNS(ns, "svg");
svgTmp.setAttribute("style", "position:absolute; width:0; height:0;");
document.body.appendChild(svgTmp);
let bboxArr = [];
subPaths.forEach(function(pathData) {
let pathTmp = document.createElementNS(ns, "path");
svgTmp.appendChild(pathTmp);
pathTmp.setPathData(pathData);
let bb = pathTmp.getBBox();
bboxArr.push(bb);
});
svgTmp.remove();
return bboxArr;
}
function checkBBoxIntersections(bb, bb1) {
let [x, y, width, height, right, bottom] = [
bb.x,
bb.y,
bb.width,
bb.height,
bb.x + bb.width,
bb.y + bb.height
];
let [x1, y1, width1, height1, right1, bottom1] = [
bb1.x,
bb1.y,
bb1.width,
bb1.height,
bb1.x + bb1.width,
bb1.y + bb1.height
];
let intersects = false;
if (width * height != width1 * height1) {
if (width * height > width1 * height1) {
if (x < x1 && right > right1 && y < y1 && bottom > bottom1) {
intersects = true;
}
}
}
return intersects;
}
svg {
width: 20em;
height: auto;
border: 1px solid #ccc;
overflow: visible;
}
<svg id="svg" viewBox="0 0 200 100">
<path fill="yellow" fill-rule="evenodd" d="M50 0a50 50 0 0 1 50 50a50 50 0 0 1-50 50a50 50 0 0 1-50-50a50 50 0 0 1 50-50zm0 25a25 25 0 0 1 25 25a25 25 0 0 1-25 25a25 25 0 0 1-25-25a25 25 0 0 1 25-25z"/>
<path fill="pink" fill-rule="evenodd" d="M150 0a50 50 0 0 1 50 50a50 50 0 0 1-50 50a50 50 0 0 1-50-50a50 50 0 0 1 50-50zm0 25a25 25 0 0 1 25 25a25 25 0 0 1-25 25a25 25 0 0 1-25-25a25 25 0 0 1 25-25z"/>
</svg>
<p id="result"></p>
<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill@1.0.3/path-data-polyfill.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/kilobtye/potrace@master/potrace.js"></script>