任何其他获取 svg 的边界矩形值的方法

问题描述 投票:0回答:1

我有必须在 JSDOM 内处理的 SVG 元素,并且需要获取它们的 getBBox() 值。

使用 getBBox() 时,由于 JSDOM 内没有执行绘制,因此无法获取值。

因为我的文件中有很多 svgs,并且需要一种像 getBBox() 一样查找 x、y、min、max 坐标并返回这些值的方法。我也尝试过像 getBoundingClientRect() 这样的方法

https://github.com/jsdom/jsdom/issues/2647 如果 getBBox() 不能在 JSDOM 内部使用,是否有其他替代方法。

javascript svg jsdom
1个回答
0
投票

如果不模拟成熟的 CSS 和 SVG 渲染引擎,就无法在无头环境中复制

getBBox()

从 x/y 极值计算 BBox

前提是,您的 svgs 仅包含您可以实际计算的几何元素 来自元素极端 x/y 坐标的边界框。


来自 w3c §8.10。边界框

这些几何元素类包括:

<path>
<polygon>
<polyline>
<circle>
<ellipse>
<rect>
<line>
元素。

显然,使用

<path>
元素会让事情变得更加复杂,因为我们首先需要将路径数据解析并规范化为可计算数据对象。
例如,我们需要将相对命令转换为绝对命令。此外,我们需要所有段都使用普通命令。

function getBBoxFromEl(el) {
  let geoEls = ['path', 'line', 'polyline', 'polygon', 'circle', 'ellipse', 'rect'];

  let xMin = Infinity
  let xMax = -Infinity
  let yMin = Infinity
  let yMax = -Infinity
  let geometryEls

  //single element
  if (geoEls.includes(el.nodeName)) {
    geometryEls = [el]
  } else {
    geometryEls = el.querySelectorAll(`${geoEls.join(', ')}`)
  }

  geometryEls.forEach(geoEl => {
    let pathData = getPathDataFromEl(geoEl, {
      toAbsolute: true,
      toLonghands: true
    })
    let {
      x,
      y,
      width,
      height
    } = getPathDataBBox(pathData)

    if (x < xMin) {
      xMin = x
    }
    if (y < yMin) {
      yMin = y
    }

    if (x + width > xMax) {
      xMax = x + width
    }
    if (y + height > yMax) {
      yMax = y + height
    }
  })
  let bbN = {
    x: xMin,
    y: yMin,
    width: xMax - xMin,
    height: yMax - yMin
  }
  return bbN

}

function getBBoxFromD(d) {
  // normalize to absolute coordinates and longhand commands
  let pathData = parsePathDataNormalized(d);
  let bb = getPathDataBBox(pathData);
  console.log('bbox');
  return bb;
}

function getPathDataBBox(pathData) {

  // save extreme values
  let xMin = Infinity;
  let xMax = -Infinity;
  let yMin = Infinity;
  let yMax = -Infinity;

  const setXYmaxMin = (pt) => {
    if (pt.x < xMin) {
      xMin = pt.x
    }
    if (pt.x > xMax) {
      xMax = pt.x
    }
    if (pt.y < yMin) {
      yMin = pt.y
    }
    if (pt.y > yMax) {
      yMax = pt.y
    }
  }

  for (let i = 0; i < pathData.length; i++) {
    let com = pathData[i]
    let {
      type,
      values
    } = com;
    let valuesL = values.length;
    let comPrev = pathData[i - 1] ? pathData[i - 1] : pathData[i];
    let valuesPrev = comPrev.values;
    let valuesPrevL = valuesPrev.length;

    if (valuesL) {
      let p0 = {
        x: valuesPrev[valuesPrevL - 2],
        y: valuesPrev[valuesPrevL - 1]
      };
      let p = {
        x: values[valuesL - 2],
        y: values[valuesL - 1]
      };
      // add final on path point
      setXYmaxMin(p)

      if (type === 'C' || type === 'Q') {
        let cp1 = {
          x: values[0],
          y: values[1]
        };
        let cp2 = type === 'C' ? {
          x: values[2],
          y: values[3]
        } : cp1;
        let pts = type === 'C' ? [p0, cp1, cp2, p] : [p0, cp1, p];

        let bezierExtremesT = getBezierExtremeT(pts)
        bezierExtremesT.forEach(t => {
          let pt = getPointAtBezierT(pts, t);
          setXYmaxMin(pt)
        })
      } else if (type === 'A') {
        let arcExtremes = getArcExtemes(p0, values)
        arcExtremes.forEach(pt => {
          setXYmaxMin(pt)
        })
      }
    }
  }

  let bbox = {
    x: xMin,
    y: yMin,
    width: xMax - xMin,
    height: yMax - yMin
  }
  return bbox
}


/**
 * based on Nikos M.'s answer
 * how-do-you-calculate-the-axis-aligned-bounding-box-of-an-ellipse
 * https://stackoverflow.com/questions/87734/#75031511
 * See also: https://github.com/foo123/Geometrize
 */
function getArcExtemes(p0, values) {
  // compute point on ellipse from angle around ellipse (theta)
  const arc = (theta, cx, cy, rx, ry, alpha) => {
    // alpha is angle of rotation of ellipse in radians
    var cos = Math.cos(alpha),
      sin = Math.sin(alpha),
      x = rx * Math.cos(theta),
      y = ry * Math.sin(theta);

    return {
      x: cx + cos * x - sin * y,
      y: cy + sin * x + cos * y
    };
  }

  //parametrize arcto data
  let arcData = svgArcToCenterParam(p0.x, p0.y, values[0], values[1], values[2], values[3], values[4], values[5], values[6]);
  let {
    rx,
    ry,
    pt,
    endAngle,
    deltaAngle
  } = arcData;

  // arc rotation
  let deg = values[2];
  let p = {
    x: values[5],
    y: values[6]
  }

  // circle/elipse center coordinates
  let [cx, cy] = [pt.x, pt.y];

  // collect extreme points – add end point
  let extremes = [p]

  // rotation to radians
  let alpha = deg * Math.PI / 180;
  let tan = Math.tan(alpha),
    p1, p2, p3, p4, theta;

  /**
   * find min/max from zeroes of directional derivative along x and y
   * along x axis
   */
  theta = Math.atan2(-ry * tan, rx);

  let angle1 = theta;
  let angle2 = theta + Math.PI;
  let angle3 = Math.atan2(ry, rx * tan);
  let angle4 = angle3 + Math.PI;

  // inner bounding box
  let xArr = [p0.x, p.x]
  let yArr = [p0.y, p.y]
  let xMin = Math.min(...xArr)
  let xMax = Math.max(...xArr)
  let yMin = Math.min(...yArr)
  let yMax = Math.max(...yArr)

  // on path point close after start
  let angleAfterStart = endAngle - deltaAngle * 0.001
  let pP2 = arc(angleAfterStart, cx, cy, rx, ry, alpha);

  // on path point close before end
  let angleBeforeEnd = endAngle - deltaAngle * 0.999
  let pP3 = arc(angleBeforeEnd, cx, cy, rx, ry, alpha);


  /**
   * expected extremes
   * if leaving inner bounding box
   * (between segment start and end point)
   * otherwise exclude elliptic extreme points
   */

  // right
  if (pP2.x > xMax || pP3.x > xMax) {
    // get point for this theta
    p1 = arc(angle1, cx, cy, rx, ry, alpha);
    extremes.push(p1)
  }

  // left
  if (pP2.x < xMin || pP3.x < xMin) {
    // get anti-symmetric point
    p2 = arc(angle2, cx, cy, rx, ry, alpha);
    extremes.push(p2)
  }

  // top
  if (pP2.y < yMin || pP3.y < yMin) {
    // get anti-symmetric point
    p4 = arc(angle4, cx, cy, rx, ry, alpha);
    extremes.push(p4)
  }

  // bottom
  if (pP2.y > yMax || pP3.y > yMax) {
    // get point for this theta
    p3 = arc(angle3, cx, cy, rx, ry, alpha);
    extremes.push(p3)
  }

  return extremes;
}


/**
 * based on @cuixiping;
 * https://stackoverflow.com/questions/9017100/calculate-center-of-svg-arc/12329083#12329083
 */
function svgArcToCenterParam(x1, y1, rx, ry, degree, fA, fS, x2, y2) {
  const radian = (ux, uy, vx, vy) => {
    let dot = ux * vx + uy * vy;
    let mod = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy));
    let rad = Math.acos(dot / mod);
    if (ux * vy - uy * vx < 0) {
      rad = -rad;
    }
    return rad;
  };
  // degree to radian
  let phi = (degree * Math.PI) / 180;
  let cx, cy, startAngle, deltaAngle, endAngle;
  let PI = Math.PI;
  let PIx2 = PI * 2;
  if (rx < 0) {
    rx = -rx;
  }
  if (ry < 0) {
    ry = -ry;
  }
  if (rx == 0 || ry == 0) {
    // invalid arguments
    throw Error("rx and ry can not be 0");
  }
  let s_phi = Math.sin(phi);
  let c_phi = Math.cos(phi);
  let hd_x = (x1 - x2) / 2; // half diff of x
  let hd_y = (y1 - y2) / 2; // half diff of y
  let hs_x = (x1 + x2) / 2; // half sum of x
  let hs_y = (y1 + y2) / 2; // half sum of y

  let x1_ = c_phi * hd_x + s_phi * hd_y;
  let y1_ = c_phi * hd_y - s_phi * hd_x;

  //   Step 3: Ensure radii are large enough
  let lambda = (x1_ * x1_) / (rx * rx) + (y1_ * y1_) / (ry * ry);
  if (lambda > 1) {
    rx = rx * Math.sqrt(lambda);
    ry = ry * Math.sqrt(lambda);
  }
  let rxry = rx * ry;
  let rxy1_ = rx * y1_;
  let ryx1_ = ry * x1_;
  let sum_of_sq = rxy1_ * rxy1_ + ryx1_ * ryx1_; // sum of square
  if (!sum_of_sq) {
    throw Error("start point can not be same as end point");
  }
  let coe = Math.sqrt(Math.abs((rxry * rxry - sum_of_sq) / sum_of_sq));
  if (fA == fS) {
    coe = -coe;
  }
  let cx_ = (coe * rxy1_) / ry;
  let cy_ = (-coe * ryx1_) / rx;

  cx = c_phi * cx_ - s_phi * cy_ + hs_x;
  cy = s_phi * cx_ + c_phi * cy_ + hs_y;
  let xcr1 = (x1_ - cx_) / rx;
  let xcr2 = (x1_ + cx_) / rx;
  let ycr1 = (y1_ - cy_) / ry;
  let ycr2 = (y1_ + cy_) / ry;

  startAngle = radian(1, 0, xcr1, ycr1);
  deltaAngle = radian(xcr1, ycr1, -xcr2, -ycr2);
  if (deltaAngle > PIx2) {
    deltaAngle -= PIx2;
  } else if (deltaAngle < 0) {
    deltaAngle += PIx2;
  }
  if (fS == false || fS == 0) {
    deltaAngle -= PIx2;
  }
  endAngle = startAngle + deltaAngle;
  if (endAngle > PIx2) {
    endAngle -= PIx2;
  } else if (endAngle < 0) {
    endAngle += PIx2;
  }
  let toDegFactor = 180 / PI;
  let outputObj = {
    pt: {
      x: cx,
      y: cy
    },
    rx: rx,
    ry: ry,
    startAngle_deg: startAngle * toDegFactor,
    startAngle: startAngle,
    deltaAngle_deg: deltaAngle * toDegFactor,
    deltaAngle: deltaAngle,
    endAngle_deg: endAngle * toDegFactor,
    endAngle: endAngle,
    clockwise: fS == true || fS == 1
  };
  return outputObj;
}



// wrapper functions for quadratic or cubic bezier point calculation
function getPointAtBezierT(pts, t) {
  let pt = pts.length === 4 ? getPointAtCubicSegmentT(pts[0], pts[1], pts[2], pts[3], t) : getPointAtQuadraticSegmentT(pts[0], pts[1], pts[2], t)
  return pt
}

function getBezierExtremeT(pts) {
  let tArr = pts.length === 4 ? cubicBezierExtremeT(pts[0], pts[1], pts[2], pts[3]) : quadraticBezierExtremeT(pts[0], pts[1], pts[2]);
  return tArr;
}


function cubicBezierExtremeT(p0, cp1, cp2, p) {
  let [x0, y0, x1, y1, x2, y2, x3, y3] = [p0.x, p0.y, cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y];

  /**
   * if control points are within 
   * bounding box of start and end point 
   * we cant't have extremes
   */
  let top = Math.min(p0.y, p.y)
  let left = Math.min(p0.x, p.x)
  let right = Math.max(p0.x, p.x)
  let bottom = Math.max(p0.y, p.y)

  if (
    cp1.y >= top && cp1.y <= bottom &&
    cp2.y >= top && cp2.y <= bottom &&
    cp1.x >= left && cp1.x <= right &&
    cp2.x >= left && cp2.x <= right
  ) {
    return []
  }

  var tArr = [],
    a, b, c, t, t1, t2, b2ac, sqrt_b2ac;
  for (var i = 0; i < 2; ++i) {
    if (i == 0) {
      b = 6 * x0 - 12 * x1 + 6 * x2;
      a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3;
      c = 3 * x1 - 3 * x0;
    } else {
      b = 6 * y0 - 12 * y1 + 6 * y2;
      a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3;
      c = 3 * y1 - 3 * y0;
    }
    if (Math.abs(a) < 1e-12) {
      if (Math.abs(b) < 1e-12) {
        continue;
      }
      t = -c / b;
      if (0 < t && t < 1) {
        tArr.push(t);
      }
      continue;
    }
    b2ac = b * b - 4 * c * a;
    if (b2ac < 0) {
      if (Math.abs(b2ac) < 1e-12) {
        t = -b / (2 * a);
        if (0 < t && t < 1) {
          tArr.push(t);
        }
      }
      continue;
    }
    sqrt_b2ac = Math.sqrt(b2ac);
    t1 = (-b + sqrt_b2ac) / (2 * a);
    if (0 < t1 && t1 < 1) {
      tArr.push(t1);
    }
    t2 = (-b - sqrt_b2ac) / (2 * a);
    if (0 < t2 && t2 < 1) {
      tArr.push(t2);
    }
  }

  var j = tArr.length;
  while (j--) {
    t = tArr[j];
  }
  return tArr;

}

//For quadratic bezier.
function quadraticBezierExtremeT(p0, cp1, p) {
  /**
   * if control points are within 
   * bounding box of start and end point 
   * we cant't have extremes
   */
  let top = Math.min(p0.y, p.y)
  let left = Math.min(p0.x, p.x)
  let right = Math.max(p0.x, p.x)
  let bottom = Math.max(p0.y, p.y)

  if (
    cp1.y >= top && cp1.y <= bottom &&
    cp1.x >= left && cp1.x <= right
  ) {
    return []
  }


  let [x0, y0, x1, y1, x2, y2] = [p0.x, p0.y, cp1.x, cp1.y, p.x, p.y];
  let extemeT = [];

  for (var i = 0; i < 2; ++i) {
    a = i == 0 ? x0 - 2 * x1 + x2 : y0 - 2 * y1 + y2;
    b = i == 0 ? -2 * x0 + 2 * x1 : -2 * y0 + 2 * y1;
    c = i == 0 ? x0 : y0;
    if (Math.abs(a) > 1e-12) {
      t = -b / (2 * a);
      if (t > 0 && t < 1) {
        extemeT.push(t);
      }
    }
  }
  return extemeT
}


// retrieve pathdata from svg geometry elements
function getPathDataFromEl(el) {
  let pathData = [];
  let type = el.nodeName;
  let atts, attNames, d, x, y, width, height, r, rx, ry, cx, cy, x1, x2, y1, y2;

  // convert relative or absolute units 
  svgElUnitsToPixel(el)

  const getAtts = (attNames) => {
    atts = {}
    attNames.forEach(att => {
      atts[att] = +el.getAttribute(att)
    })
    return atts
  }

  switch (type) {
    case 'path':
      d = el.getAttribute("d");
      pathData = parsePathDataNormalized(d);
      break;

    case 'rect':
      attNames = ['x', 'y', 'width', 'height', 'rx', 'ry'];
      ({
        x,
        y,
        width,
        height,
        rx,
        ry
      } = getAtts(attNames));


      if (!rx && !ry) {
        pathData = [{
            type: "M",
            values: [x, y]
          },
          {
            type: "H",
            values: [x + width]
          },
          {
            type: "V",
            values: [y + height]
          },
          {
            type: "H",
            values: [x]
          },
          {
            type: "Z",
            values: []
          }
        ];
      } else {

        if (rx > width / 2) {
          rx = width / 2;
        }
        if (ry > height / 2) {
          ry = height / 2;
        }

        pathData = [{
            type: "M",
            values: [x + rx, y]
          },
          {
            type: "H",
            values: [x + width - rx]
          },
          {
            type: "A",
            values: [rx, ry, 0, 0, 1, x + width, y + ry]
          },
          {
            type: "V",
            values: [y + height - ry]
          },
          {
            type: "A",
            values: [rx, ry, 0, 0, 1, x + width - rx, y + height]
          },
          {
            type: "H",
            values: [x + rx]
          },
          {
            type: "A",
            values: [rx, ry, 0, 0, 1, x, y + height - ry]
          },
          {
            type: "V",
            values: [y + ry]
          },
          {
            type: "A",
            values: [rx, ry, 0, 0, 1, x + rx, y]
          },
          {
            type: "Z",
            values: []
          }
        ];
      }
      break;

    case 'circle':
    case 'ellipse':

      attNames = ['cx', 'cy', 'rx', 'ry', 'r'];
      ({
        cx,
        cy,
        r,
        rx,
        ry
      } = getAtts(attNames));

      if (type === 'circle') {
        r = r;
        rx = r
        ry = r
      } else {
        rx = rx ? rx : r;
        ry = ry ? ry : r;
      }

      pathData = [{
          type: "M",
          values: [cx + rx, cy]
        },
        {
          type: "A",
          values: [rx, ry, 0, 1, 1, cx - rx, cy]
        },
        {
          type: "A",
          values: [rx, ry, 0, 1, 1, cx + rx, cy]
        },
      ];

      break;
    case 'line':
      attNames = ['x1', 'y1', 'x2', 'y2'];
      ({
        x1,
        y1,
        x2,
        y2
      } = getAtts(attNames));
      pathData = [{
          type: "M",
          values: [x1, y1]
        },
        {
          type: "L",
          values: [x2, y2]
        }
      ];
      break;
    case 'polygon':
    case 'polyline':

      let points = el.getAttribute('points').replaceAll(',', ' ').split(' ').filter(Boolean)

      for (let i = 0; i < points.length; i += 2) {
        pathData.push({
          type: (i === 0 ? "M" : "L"),
          values: [+points[i], +points[i + 1]]
        });
      }
      if (type === 'polygon') {
        pathData.push({
          type: "Z",
          values: []
        });
      }
      break;
  }

  return pathData;
};

/**
 * calculate single points on segments
 */
function getPointAtCubicSegmentT(p0, cp1, cp2, p, t = 0.5) {
  let t1 = 1 - t;
  return {
    x: t1 ** 3 * p0.x +
      3 * t1 ** 2 * t * cp1.x +
      3 * t1 * t ** 2 * cp2.x +
      t ** 3 * p.x,
    y: t1 ** 3 * p0.y +
      3 * t1 ** 2 * t * cp1.y +
      3 * t1 * t ** 2 * cp2.y +
      t ** 3 * p.y
  };
}

function getPointAtQuadraticSegmentT(p0, cp1, p, t = 0.5) {
  let t1 = 1 - t;
  return {
    x: t1 * t1 * p0.x + 2 * t1 * t * cp1.x + t ** 2 * p.x,
    y: t1 * t1 * p0.y + 2 * t1 * t * cp1.y + t ** 2 * p.y
  };
}

function svgElUnitsToPixel(el, decimals = 5) {
  //console.log(this);
  const svg = el.nodeName !== "svg" ? el.closest("svg") : el;

  // convert real life units to pixels
  const translateUnitToPixel = (value) => {

    if (value === null) {
      return 0
    }
    //default dpi = 96
    let dpi = 96;
    let unit = value.match(/([a-z]+)/gi);
    unit = unit ? unit[0] : "";
    let val = parseFloat(value);
    let rat;

    // no unit - already pixes/user unit
    if (!unit) {
      return val;
    }

    switch (unit) {
      case "in":
        rat = dpi;
        break;
      case "pt":
        rat = (1 / 72) * 96;
        break;
      case "cm":
        rat = (1 / 2.54) * 96;
        break;
      case "mm":
        rat = ((1 / 2.54) * 96) / 10;
        break;
        // just a default approximation
      case "em":
        rat = 16;
        break;
      default:
        rat = 1;
    }
    let valuePx = val * rat;
    return +valuePx.toFixed(decimals);
  };

  // svg width and height attributes
  let width = svg.getAttribute("width");
  width = width ? translateUnitToPixel(width) : 300;
  let height = svg.getAttribute("height");
  height = width ? translateUnitToPixel(height) : 150;

  //prefer viewBox values
  let vB = svg.getAttribute("viewBox");
  vB = vB ?
    vB
    .replace(/,/g, " ")
    .split(" ")
    .filter(Boolean)
    .map((val) => {
      return +val;
    }) : [];

  let w = vB.length ? vB[2] : width;
  let h = vB.length ? vB[3] : height;
  let scaleX = 0.01 * w;
  let scaleY = 0.01 * h;
  let scalRoot = Math.sqrt((Math.pow(scaleX, 2) + Math.pow(scaleY, 2)) / 2);
  let attsH = ["x", "width", "x1", "x2", "rx", "cx", "r"];
  let attsV = ["y", "height", "y1", "y2", "ry", "cy"];


  let atts = el.getAttributeNames();
  atts.forEach((att) => {
    let val = el.getAttribute(att);
    let valAbs = val;
    if (attsH.includes(att) || attsV.includes(att)) {
      let scale = attsH.includes(att) ? scaleX : scaleY;
      scale = att === "r" && w != h ? scalRoot : scale;
      let unit = val.match(/([a-z|%]+)/gi);
      unit = unit ? unit[0] : "";
      if (val.includes("%")) {
        valAbs = parseFloat(val) * scale;
      }
      //absolute units
      else {
        valAbs = translateUnitToPixel(val);
      }
      el.setAttribute(att, +valAbs);
    }
  });
}
body {
  font-family: sans-serif;
}

svg {
  width: 100%;
  overflow: visible;
  border: 1px solid red;
}

.grd {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 2em;
  height: 95vh;
}

.col {
  height: 100%;
  display: flex;
  flex-direction: column;
  gap: 0;
  justify-content: flex-start;
}

.col * {
  margin-top: 0;
  margin-bottom: 1rem;
}

textarea {
  width: 100%;
  display: block;
  min-height: 10em;
  height: 50%;
  flex: 1 1 auto;
}

input[type="range"] {
  width: 100%;
  display: block
}

#valLength {
  display: block;
}

code {
  background: #eee;
  display: block;
  padding: 0.5em;
}
<div class="grd">
  <div class="col">
    <h3>Input you pathdata</h3>
    <p>Svg gets cropped to the bounding box</p>
    <h3>Bounding box (calculated/native)</h3>
    <pre><code id="bbOut"></code></pre>
    <textarea id="svgInput">
                <svg id="svg" viewBox="0 0 100 100">
                    <path fill="none" stroke="black" d="
M3 7 13 7m-10 10 10 0V27H23v10h10C33 43 38 47 43 47c0 5 5 10 10 10S63 67 63 67s-10 10 10 10Q50 50 73 57q20-5 0-10T70 40t0-15A5 10 45 1040 20a5 5 20 01-10-10Z" />
                    <path fill="none" stroke="black" d="
                      M20 90a 5 10 66  1 0 10 -8" />
                    <ellipse id="ellipse" fill="none" stroke="#E3000F" cx="50%" cy="50%" rx="90" ry="30" />
                </svg>
          </textarea>
  </div>
  <div class="col" id="preview">
  </div>
</div>

<!-- parse pathdata -->
<script src="https://cdn.jsdelivr.net/npm/svg-parse-path-normalized@latest/js/pathDataParseNormalized.js"></script>


<script>
  window.addEventListener('DOMContentLoaded', e => {
    updateSVG();
  });
  svgInput.addEventListener('input', e => {
    updateSVG();
  })


  function updateSVG() {
    // reset preview
    preview.innerHTML = ''
    let ns = 'http://www.w3.org/2000/svg';
    let svgPreview
    let input = svgInput.value;

    //is svg
    if (input.includes('<svg')) {
      svgPreview = new DOMParser().parseFromString(input, 'text/html').querySelector('svg')
    }
    // is single path d string
    else {
      svgPreview = document.createElementNS(ns, 'svg')
      path = document.createElementNS(ns, 'path')
      d = svgInput.value
      path.setAttribute('d', d)
      svgPreview.append(path)
    }


    preview.append(svgPreview)

    // adjust viewBox
    let bb = getBBoxFromEl(svgPreview)
    let bbString = [bb.x, bb.y, bb.width, bb.height].join(' ')

    //native
    let bbN = svgPreview.getBBox()
    let bbNString = [bbN.x, bbN.y, bbN.width, bbN.height].join(' ')

    bbOut.textContent = `${bbString} \n${bbNString} // native`

    svgPreview.setAttribute('viewBox', bbString)
    let {
      x,
      y,
      width,
      height
    } = bb;
  }
</script>

node.js/jsDom 中的 getBBox

您会在 npm 上找到很多库。查看 “svg 边界框” 的搜索结果。

上面的例子是基于我自己的实验库/存储库你可以像这样通过npm安装

npm install svg-pathdata-getbbox

结合jsDom你可以得到这样的bbox

var pathDataBB = require("svg-pathdata-getbbox");
var { getBBoxFromEl, getBBoxFromD, getPathDataBBox, } = pathDataBB;
const jsdom = require("jsdom");
const { JSDOM } = jsdom;

let svgMarkup = `<svg id="svg" viewBox="0 0 100 100">
<path fill="none" stroke="black" d="M3 7 13 7m-10 10 10 0V27H23v10h10C33 43 38 47 43 47c0 5 5 10 10 10S63 67 63 67s-10 10 10 10Q50 50 73 57q20-5 0-10T70 40t0-15A5 10 45 1040 20a5 5 20 01-10-10Z"/>
</svg>`;

const dom = new JSDOM(svgMarkup);
let svg = dom.window.document.querySelector('svg')

let bb = getBBoxFromEl(svg)
console.log(bb);

限制/不支持

1.
<text>
元素

计算文本元素的 bbox 需要检索和解析所使用的字体系列(这已经很重要了,因为该字体需要可用于加载..或者您需要一个例程来自动检测路径、外部 URL 等)。

然后您需要计算总边界,例如通过将文本转换为路径并计算所有字符 bbox。请参阅相关 SO 帖子 “D3js:如何将 svg 文本转换为路径?”

您很可能无法近似所有类型的文本对象(文本路径、使用可变或彩色字体的文本)

2.由 CSS 设计的元素

由于我们还可以通过 CSS 定义很多“表示属性”,例如

d
(路径数据),我们最终需要一个成熟的 CSS 解析器来计算来自 CSS 规则的边界框。不知道有一个节点库能够做到这一点:(

© www.soinside.com 2019 - 2024. All rights reserved.