// usage:
(async() => {
let bb1 = await getBBoxCustom(line1);
//draw bbox
drawBBox(svg, bb1)
console.log(bb1)
let bb2 = await getBBoxCustom(line2);
drawBBox(svg, bb2)
let bb3 = await getBBoxCustom(line3);
drawBBox(svg, bb3)
let bb4 = await getBBoxCustom(path);
drawBBox(svg, bb4)
let bb5 = await getBBoxCustom(rect);
drawBBox(svg, bb5)
let bb6 = await getBBoxCustom(rect2);
drawBBox(svg, bb6)
})();
async function getBBoxCustom(el) {
let bb = el.getBBox();
const ns = 'http://www.w3.org/2000/svg';
/**
* check element type
* stroke properties and transformations
*/
let type = el.nodeName;
let parent = el.farthestViewportElement;
let {
a,
b,
c,
d,
e,
f
} = parent.getScreenCTM().inverse().multiply(el.getScreenCTM());
let matrixStr = [a, b, c, d, e, f].map(val => {
return +val.toFixed(3)
}).join(' ');
let hasTransforms = matrixStr === '1 0 0 1 0 0' ? false : true;
let style = window.getComputedStyle(el);
let hasStroke = style.strokeWidth && style.stroke ? true : false;
let hasStrokeLinecap = hasStroke && style.strokeLinecap !== 'butt' ? true : false;
let strokeWidth = parseFloat(style.strokeWidth);
/**
* no stroke - no transforms:
* return standard bBox
*/
if (!hasStroke && hasTransforms) {
console.log('standard bbox');
return bb;
}
/**
* no transforms and straight angles:
* return standard bBox + stroke widths
*/
if (
(type === 'rect' || type === 'line') &&
!hasTransforms
) {
if (type === 'line') {
let isVertical = +el.getAttribute('x1') === +el.getAttribute('x2') ? true : false;
let isHorizontal = +el.getAttribute('y1') === +el.getAttribute('y2') ? true : false;
if (hasStrokeLinecap || isHorizontal) {
bb.y -= strokeWidth / 2;
bb.height += strokeWidth;
}
if (hasStrokeLinecap || isVertical) {
bb.x -= strokeWidth / 2;
bb.width += strokeWidth;
}
return bb;
}
if (type === 'rect') {
bb.x -= strokeWidth / 2;
bb.width += strokeWidth;
bb.y -= strokeWidth / 2;
bb.height += strokeWidth;
return bb;
}
}
let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d");
//create temporary svg
let svgTmp = document.createElementNS(ns, 'svg');
let elCloned = el.cloneNode(true);
document.body.append(svgTmp)
//document.body.append(canvas)
// if transformed - wrap in group recalculate bbox
if (hasTransforms) {
let g = document.createElementNS(ns, 'g');
g.append(elCloned)
svgTmp.append(g);
// update bbox
bb = g.getBBox();
} else {
svgTmp.append(elCloned);
}
// crop viewBox to element to reduce pixel checks
let strokeOffset = strokeWidth * 10;
let {
x,
y,
width,
height
} = bb;
let viewBoxNew = [
Math.ceil(x - strokeOffset / 2),
Math.ceil(y - strokeOffset / 2),
Math.ceil(width + strokeOffset),
Math.ceil(height + strokeOffset)
];
svgTmp.setAttribute("viewBox", viewBoxNew.join(" "));
let w = viewBoxNew[2];
let h = viewBoxNew[3];
/**
* set height and width for Firefox
*/
svgTmp.setAttribute("width", w);
svgTmp.setAttribute("height", h);
let svgURL = await new XMLSerializer().serializeToString(svgTmp);
// draw to canvas
canvas.width = w;
canvas.height = h;
let img = new Image();
img.src = "data:image/svg+xml; charset=utf8, " + encodeURIComponent(svgURL);
await img.decode();
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
/**
* get bbox from
* opaque pixels
*/
let data = ctx.getImageData(0, 0, w, h).data;
let currentX = 0;
let currentY = 0;
let xMin = x + w;
let xMax = 0;
let yMin = y + h;
let yMax = 0;
/**
* loop through color array
* every 4. item is new pixel
*/
for (let i = 0; i < data.length; i += 4) {
// separate array chunk into rgba values
let [r, g, b, a] = [data[i], data[i + 1], data[i + 2], data[i + 3]];
if (a > 0) {
xMin = currentX < xMin ? currentX : xMin;
xMax = currentX > xMax ? currentX : xMax;
yMin = currentY < yMin ? currentY : yMin;
yMax = currentY > yMax ? currentY : yMax;
}
// is new "pixel row" – increment y value, reset x to "0"
if (currentX + 1 >= w) {
currentX = 0;
currentY++
} else {
currentX++;
}
}
// recalculate viewBox according to previous offset
let bboxCanvas = {
x: xMin + viewBoxNew[0],
y: yMin + viewBoxNew[1],
width: xMax - xMin + 1,
height: yMax - yMin + 1
}
// remove tmp svg
svgTmp.remove();
return bboxCanvas;
}
function drawBBox(svg, bb) {
svg.insertAdjacentHTML('beforeend', `
<rect x="${bb.x}" y="${bb.y}" width="${bb.width}" height="${bb.height}" class="bbox" stroke="green" stroke-width="0.75" fill="none"></rect>`);
}
svg {
border: 1px solid #000;
width: 100%;
height: auto;
overflow: visible;
}
<svg id="svg" viewBox="0 0 700 200" xmlns="http://www.w3.org/2000/svg">
<line id="line1" x1="50" y1="40" x2="200" y2="40" style="stroke:rgb(255,0,0);stroke-width:20px" />
<line id="line2" x1="100" y1="150" x2="200" y2="150" stroke-linecap="square"
style="stroke:orange;stroke-width:20px" />
<rect x="500" y="50" width="60" height="20" id="rect" stroke="gray" stroke-width="3" />
<rect x="500" y="20" width="60" height="20" id="rect2" stroke="gray" stroke-width="3" transform="rotate(45 400 100)"/>
<line id="line3" x1="400" y1="20" x2="400" y2="150" stroke-linecap="square"
style="stroke:gray;stroke-width:20px" />
<path id="path" d="M600 150 l5 -50 l5 50z" stroke-width="5" stroke="red" stroke-linejoin="miter"
stroke-miterlimit="20" fill="#fff" paint-order="stroke" />
</svg>