2

I'm trying to get the bounding box of a line (or path, polygon, etc.)

According to documentation I should be able to do, line.getBBox({"stroke": true}), but it doesn't seem to be working.

https://jsfiddle.net/3x5thjsn/

<svg width="100" height="100">
   <line x1="0" y1="40" x2="200" y2="40" style="stroke:rgb(255,0,0);stroke-width:20"/>
   Sorry, your browser does not support inline SVG.
</svg> 
const line = document.querySelector("svg line");
console.log(line.getBBox({"stroke": true}))

If I have a line with a stroke width of x, the height or width of the line should account for it. In this case I am expecting height to be 20, but it's giving me 0.

Joel Hayes
  • 23
  • 3

1 Answers1

2

As commented by Robert Longson:
Most browsers do not yet support the getBBox({"stroke": true})) function option.

Workarounds

  • if you're dealing with rectangular and non transformed elements: you might easily add some space/offsets according to stroke-width and stroke-linecap property values
  • for elements with sharp angles or transformations you could calculate a bbox by rendering the element to a canvas and search for opaque pixels as described here "Calculate bounding box of arbitrary pixel-based drawing"

Example: custom getBBox helper function

// 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>

How it works

The above helper function first checks multiple conditions to avoid more expensive canvas based calculations:

  • no stroke at all – use standard getBBox()
  • element is <rect> or <line> and not transformed: increase width/height and x/y values according to stroke properties. For instance:
/**
 * 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;
    }
}
  • if element is not rectangular or transformed: render element to <canvas> and check for opaque pixels – retrieved via getImageData()
// 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++;
        }
    }
herrstrietzel
  • 11,541
  • 2
  • 12
  • 34