BoundingBox.java
package io.brunoborges.jairosvg.dom;
import static io.brunoborges.jairosvg.util.Helpers.PATH_LETTERS;
import static io.brunoborges.jairosvg.util.Helpers.normalize;
import static io.brunoborges.jairosvg.util.Helpers.pointWithRemainder;
import static io.brunoborges.jairosvg.util.Helpers.size;
import io.brunoborges.jairosvg.surface.Surface;
import io.brunoborges.jairosvg.util.ParsedPoint;
/**
* Bounding box calculations for SVG shapes and paths. Port of CairoSVG
* bounding_box.py
*/
public final class BoundingBox {
/** Bounding box as (minX, minY, width, height). */
public record Box(double minX, double minY, double width, double height) {
}
public static final Box EMPTY = new Box(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, 0, 0);
private BoundingBox() {
}
public static Box calculate(Surface surface, Node node) {
return switch (node.tag) {
case "rect" -> rect(surface, node);
case "circle" -> circle(surface, node);
case "ellipse" -> ellipse(surface, node);
case "line" -> line(surface, node);
case "polyline", "polygon" -> polyline(surface, node);
case "path" -> path(surface, node);
case "g", "marker" -> group(surface, node);
default -> null;
};
}
public static Box rect(Surface surface, Node node) {
double x = size(surface, node.get("x"), "x");
double y = size(surface, node.get("y"), "y");
double w = size(surface, node.get("width"), "x");
double h = size(surface, node.get("height"), "y");
return new Box(x, y, w, h);
}
public static Box circle(Surface surface, Node node) {
double cx = size(surface, node.get("cx"), "x");
double cy = size(surface, node.get("cy"), "y");
double r = size(surface, node.get("r"));
return new Box(cx - r, cy - r, 2 * r, 2 * r);
}
public static Box ellipse(Surface surface, Node node) {
double rx = size(surface, node.get("rx"), "x");
double ry = size(surface, node.get("ry"), "y");
double cx = size(surface, node.get("cx"), "x");
double cy = size(surface, node.get("cy"), "y");
return new Box(cx - rx, cy - ry, 2 * rx, 2 * ry);
}
public static Box line(Surface surface, Node node) {
double x1 = size(surface, node.get("x1"), "x");
double y1 = size(surface, node.get("y1"), "y");
double x2 = size(surface, node.get("x2"), "x");
double y2 = size(surface, node.get("y2"), "y");
double x = Math.min(x1, x2), y = Math.min(y1, y2);
return new Box(x, y, Math.max(x1, x2) - x, Math.max(y1, y2) - y);
}
public static Box polyline(Surface surface, Node node) {
String points = normalize(node.get("points", ""));
Box box = EMPTY;
while (!points.isBlank()) {
ParsedPoint pt = pointWithRemainder(null, points);
box = extendBox(box, pt.x(), pt.y());
points = pt.remainder();
}
return box;
}
public static Box path(Surface surface, Node node) {
String d = node.get("d", "");
for (char c : PATH_LETTERS.toCharArray()) {
d = d.replace(String.valueOf(c), " " + c + " ");
}
d = normalize(d);
Box box = EMPTY;
double px = 0, py = 0;
String letter = "M";
while (!d.isBlank()) {
d = d.strip();
String first = d.split("\\s+", 2)[0];
if (first.length() == 1 && PATH_LETTERS.indexOf(first.charAt(0)) >= 0) {
letter = first;
d = d.substring(1).strip();
}
try {
switch (letter) {
case "M", "L", "T", "m", "l", "t" -> {
ParsedPoint pt = pointWithRemainder(null, d);
double x = pt.x(), y = pt.y();
d = pt.remainder();
if ("m".equals(letter) || "l".equals(letter) || "t".equals(letter)) {
x += px;
y += py;
}
box = extendBox(box, x, y);
px = x;
py = y;
}
case "H", "h" -> {
String[] sp = (d + " ").split("\\s+", 2);
double x = Double.parseDouble(sp[0]);
d = sp[1];
if ("h".equals(letter))
x += px;
box = extendBox(box, x, py);
px = x;
}
case "V", "v" -> {
String[] sp = (d + " ").split("\\s+", 2);
double y = Double.parseDouble(sp[0]);
d = sp[1];
if ("v".equals(letter))
y += py;
box = extendBox(box, px, y);
py = y;
}
case "C", "c" -> {
ParsedPoint p1 = pointWithRemainder(null, d);
ParsedPoint p2 = pointWithRemainder(null, p1.remainder());
ParsedPoint p3 = pointWithRemainder(null, p2.remainder());
d = p3.remainder();
double x1 = p1.x(), y1 = p1.y();
double x2 = p2.x(), y2 = p2.y();
double x = p3.x(), y = p3.y();
if ("c".equals(letter)) {
x1 += px;
y1 += py;
x2 += px;
y2 += py;
x += px;
y += py;
}
box = extendBox(box, x1, y1);
box = extendBox(box, x2, y2);
box = extendBox(box, x, y);
px = x;
py = y;
}
case "S", "s", "Q", "q" -> {
ParsedPoint p1 = pointWithRemainder(null, d);
ParsedPoint p2 = pointWithRemainder(null, p1.remainder());
d = p2.remainder();
double x1 = p1.x(), y1 = p1.y();
double x = p2.x(), y = p2.y();
if ("s".equals(letter) || "q".equals(letter)) {
x1 += px;
y1 += py;
x += px;
y += py;
}
box = extendBox(box, x1, y1);
box = extendBox(box, x, y);
px = x;
py = y;
}
case "A", "a" -> {
ParsedPoint rxy = pointWithRemainder(null, d);
d = rxy.remainder();
// Skip rotation, large-arc, sweep
String[] parts = (d + " ").split("\\s+", 4);
d = parts.length > 3 ? parts[3] : "";
ParsedPoint ep = pointWithRemainder(null, d);
double x = ep.x(), y = ep.y();
d = ep.remainder();
if ("a".equals(letter)) {
x += px;
y += py;
}
box = extendBox(box, x, y);
px = x;
py = y;
}
case "Z", "z" -> {
// close path, nothing to extend
}
default -> {
// Unknown command, try to skip
if (!d.isEmpty()) {
String[] sp = d.split("\\s+", 2);
d = sp.length > 1 ? sp[1] : "";
}
}
}
} catch (Exception e) {
break;
}
d = d.strip();
}
return box;
}
public static Box group(Surface surface, Node node) {
Box box = EMPTY;
for (Node child : node.children) {
box = combine(box, calculate(surface, child));
}
return box;
}
public static Box extendBox(Box box, double x, double y) {
double minX = Math.min(box.minX, x);
double minY = Math.min(box.minY, y);
double maxX = Double.isInfinite(box.minX) ? x : Math.max(box.minX + box.width, x);
double maxY = Double.isInfinite(box.minY) ? y : Math.max(box.minY + box.height, y);
return new Box(minX, minY, maxX - minX, maxY - minY);
}
public static Box combine(Box a, Box b) {
if (b == null || !isValid(b))
return a;
Box result = extendBox(a, b.minX, b.minY);
return extendBox(result, b.minX + b.width, b.minY + b.height);
}
public static boolean isValid(Box box) {
return box != null && !Double.isInfinite(box.minX) && !Double.isInfinite(box.minY);
}
public static boolean isNonEmpty(Box box) {
return isValid(box) && box.width != 0 && box.height != 0;
}
}