PathDrawer.java
package io.brunoborges.jairosvg.draw;
import java.util.ArrayList;
import io.brunoborges.jairosvg.dom.Node;
import io.brunoborges.jairosvg.surface.Surface;
import java.awt.geom.GeneralPath;
import static io.brunoborges.jairosvg.util.Helpers.*;
/**
* SVG path command parser and drawer.
*/
public final class PathDrawer {
private static final System.Logger LOG = System.getLogger(PathDrawer.class.getName());
private PathDrawer() {
}
/** Draw a path node. */
public static void path(Surface surface, Node node) {
String d = node.get("d", "");
// Use cached path if available (helps when the same node is drawn multiple
// times via <use> or markers)
if (node.cachedPath != null) {
surface.path.append(node.cachedPath.getPathIterator(null), false);
node.vertices = node.cachedVertices;
return;
}
node.vertices = new ArrayList<>();
var sc = new PathScanner(d);
char letter = 0, lastLetter = 0;
double cx = 0, cy = 0;
double x1 = 0, y1 = 0, x2 = 0, y2 = 0, x3 = 0, y3 = 0;
double[] firstPathPoint = null;
while (sc.hasMore()) {
char cmd = sc.peekCmd();
if (cmd != 0) {
letter = cmd;
if ((lastLetter == 0 || lastLetter == 'z' || lastLetter == 'Z') && letter != 'm' && letter != 'M') {
node.vertices.add(new double[]{cx, cy});
firstPathPoint = new double[]{cx, cy};
}
} else if (letter == 'M') {
letter = 'L';
} else if (letter == 'm') {
letter = 'l';
}
if (lastLetter == 0 || lastLetter == 'm' || lastLetter == 'M' || lastLetter == 'z' || lastLetter == 'Z') {
firstPathPoint = null;
}
if (letter != 'm' && letter != 'M' && letter != 'z' && letter != 'Z' && firstPathPoint == null) {
firstPathPoint = new double[]{cx, cy};
}
try {
switch (letter) {
case 'M' -> {
double x = sc.nextDouble(), y = sc.nextDouble();
if (lastLetter != 0 && lastLetter != 'z' && lastLetter != 'Z') {
node.vertices.add(null);
}
surface.path.moveTo(x, y);
cx = x;
cy = y;
}
case 'm' -> {
double dx = sc.nextDouble(), dy = sc.nextDouble();
if (lastLetter != 0 && lastLetter != 'z' && lastLetter != 'Z') {
node.vertices.add(null);
}
cx += dx;
cy += dy;
surface.path.moveTo(cx, cy);
}
case 'L' -> {
double x = sc.nextDouble(), y = sc.nextDouble();
double angle = pointAngle(cx, cy, x, y);
node.vertices.add(new double[]{angle, angle});
surface.path.lineTo(x, y);
cx = x;
cy = y;
}
case 'l' -> {
double dx = sc.nextDouble(), dy = sc.nextDouble();
double angle = pointAngle(0, 0, dx, dy);
node.vertices.add(new double[]{angle, angle});
cx += dx;
cy += dy;
surface.path.lineTo(cx, cy);
}
case 'H' -> {
double x = sc.nextDouble();
double angle = x > cx ? 0 : Math.PI;
node.vertices.add(new double[]{angle, angle});
surface.path.lineTo(x, cy);
cx = x;
}
case 'h' -> {
double dx = sc.nextDouble();
double angle = dx > 0 ? 0 : Math.PI;
node.vertices.add(new double[]{angle, angle});
cx += dx;
surface.path.lineTo(cx, cy);
}
case 'V' -> {
double y = sc.nextDouble();
double angle = Math.copySign(Math.PI / 2, y - cy);
node.vertices.add(new double[]{angle, angle});
surface.path.lineTo(cx, y);
cy = y;
}
case 'v' -> {
double dy = sc.nextDouble();
double angle = Math.copySign(Math.PI / 2, dy);
node.vertices.add(new double[]{angle, angle});
cy += dy;
surface.path.lineTo(cx, cy);
}
case 'C' -> {
x1 = sc.nextDouble();
y1 = sc.nextDouble();
x2 = sc.nextDouble();
y2 = sc.nextDouble();
x3 = sc.nextDouble();
y3 = sc.nextDouble();
node.vertices.add(new double[]{pointAngle(cx, cy, x1, y1), pointAngle(x2, y2, x3, y3)});
surface.path.curveTo(x1, y1, x2, y2, x3, y3);
cx = x3;
cy = y3;
}
case 'c' -> {
double dx1 = sc.nextDouble(), dy1 = sc.nextDouble();
double dx2 = sc.nextDouble(), dy2 = sc.nextDouble();
double dx3 = sc.nextDouble(), dy3 = sc.nextDouble();
x1 = cx + dx1;
y1 = cy + dy1;
x2 = cx + dx2;
y2 = cy + dy2;
x3 = cx + dx3;
y3 = cy + dy3;
node.vertices.add(new double[]{pointAngle(cx, cy, x1, y1), pointAngle(x2, y2, x3, y3)});
surface.path.curveTo(x1, y1, x2, y2, x3, y3);
cx = x3;
cy = y3;
}
case 'S' -> {
double sx1 = cx, sy1 = cy;
if ("csCS".indexOf(lastLetter) >= 0) {
sx1 = x3 + (x3 - x2);
sy1 = y3 + (y3 - y2);
}
x2 = sc.nextDouble();
y2 = sc.nextDouble();
x3 = sc.nextDouble();
y3 = sc.nextDouble();
x1 = sx1;
y1 = sy1;
node.vertices.add(new double[]{pointAngle(cx, cy, x1, y1), pointAngle(x2, y2, x3, y3)});
surface.path.curveTo(x1, y1, x2, y2, x3, y3);
cx = x3;
cy = y3;
}
case 's' -> {
double sx1 = 0, sy1 = 0;
if ("csCS".indexOf(lastLetter) >= 0) {
sx1 = x3 - x2;
sy1 = y3 - y2;
}
double dx2 = sc.nextDouble(), dy2 = sc.nextDouble();
double dx3 = sc.nextDouble(), dy3 = sc.nextDouble();
x1 = cx + sx1;
y1 = cy + sy1;
x2 = cx + dx2;
y2 = cy + dy2;
x3 = cx + dx3;
y3 = cy + dy3;
node.vertices.add(new double[]{pointAngle(cx, cy, x1, y1), pointAngle(x2, y2, x3, y3)});
surface.path.curveTo(x1, y1, x2, y2, x3, y3);
cx = x3;
cy = y3;
}
case 'Q' -> {
x2 = sc.nextDouble();
y2 = sc.nextDouble();
x3 = sc.nextDouble();
y3 = sc.nextDouble();
double[] qp = quadraticPoints(cx, cy, x2, y2, x3, y3);
surface.path.curveTo(qp[0], qp[1], qp[2], qp[3], qp[4], qp[5]);
node.vertices.add(new double[]{pointAngle(cx, cy, x2, y2), pointAngle(x2, y2, x3, y3)});
cx = x3;
cy = y3;
}
case 'q' -> {
double dx2 = sc.nextDouble(), dy2 = sc.nextDouble();
double dx3 = sc.nextDouble(), dy3 = sc.nextDouble();
x2 = cx + dx2;
y2 = cy + dy2;
x3 = cx + dx3;
y3 = cy + dy3;
double[] qp = quadraticPoints(0, 0, dx2, dy2, dx3, dy3);
surface.path.curveTo(cx + qp[0], cy + qp[1], cx + qp[2], cy + qp[3], cx + qp[4], cy + qp[5]);
node.vertices.add(new double[]{pointAngle(cx, cy, x2, y2), pointAngle(x2, y2, x3, y3)});
cx = x3;
cy = y3;
}
case 'T' -> {
if ("qtQT".indexOf(lastLetter) >= 0) {
x2 = 2 * cx - x2;
y2 = 2 * cy - y2;
} else {
x2 = cx;
y2 = cy;
}
x3 = sc.nextDouble();
y3 = sc.nextDouble();
double[] qp = quadraticPoints(cx, cy, x2, y2, x3, y3);
surface.path.curveTo(qp[0], qp[1], qp[2], qp[3], qp[4], qp[5]);
node.vertices.add(new double[]{pointAngle(cx, cy, x2, y2), pointAngle(x2, y2, x3, y3)});
cx = x3;
cy = y3;
}
case 't' -> {
if ("qtQT".indexOf(lastLetter) >= 0) {
x2 = 2 * cx - x2;
y2 = 2 * cy - y2;
} else {
x2 = cx;
y2 = cy;
}
double dx3 = sc.nextDouble(), dy3 = sc.nextDouble();
x3 = cx + dx3;
y3 = cy + dy3;
double[] qp = quadraticPoints(cx, cy, x2, y2, x3, y3);
surface.path.curveTo(qp[0], qp[1], qp[2], qp[3], qp[4], qp[5]);
node.vertices.add(new double[]{pointAngle(cx, cy, x2, y2), pointAngle(x2, y2, x3, y3)});
cx = x3;
cy = y3;
}
case 'A', 'a' -> {
double rx = sc.nextDouble(), ry = sc.nextDouble();
double rotation = Math.toRadians(sc.nextDouble());
int largeArc = sc.nextFlag();
int sweepFlag = sc.nextFlag();
if ((largeArc != 0 && largeArc != 1) || (sweepFlag != 0 && sweepFlag != 1)) {
continue;
}
double ex = sc.nextDouble(), ey = sc.nextDouble();
if (letter == 'a') {
ex += cx;
ey += cy;
}
double arcAngle = pointAngle(cx, cy, ex, ey);
node.vertices.add(new double[]{arcAngle, arcAngle});
drawArc(surface, cx, cy, rx, ry, rotation, largeArc != 0, sweepFlag != 0, ex, ey);
cx = ex;
cy = ey;
}
case 'Z', 'z' -> {
node.vertices.add(null);
surface.path.closePath();
if (firstPathPoint != null) {
cx = firstPathPoint[0];
cy = firstPathPoint[1];
}
}
}
} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
LOG.log(System.Logger.Level.DEBUG, "Skipping invalid path token", e);
sc.skipToken();
continue;
}
if (letter != 'Z' && letter != 'z') {
node.vertices.add(new double[]{cx, cy});
}
lastLetter = letter;
}
// Cache the result for subsequent renders of this node
node.cachedPath = new GeneralPath(surface.path);
node.cachedVertices = node.vertices;
}
/**
* Zero-allocation, single-pass tokenizer for SVG path data. Handles implicit
* sign separators (50-30 → 50, -30), consecutive decimals (0.5.3 → 0.5, 0.3),
* comma/whitespace separators, and scientific notation. Replaces 4 regex passes
* + N split() calls.
*/
private static final class PathScanner {
private final String d;
private final int len;
private int pos;
PathScanner(String d) {
this.d = d;
this.len = d.length();
}
boolean hasMore() {
int p = pos;
while (p < len && isWsOrComma(d.charAt(p)))
p++;
return p < len;
}
/**
* If the next non-ws char is a path command letter, consume and return it; else
* return 0.
*/
char peekCmd() {
skipWs();
if (pos < len) {
char c = d.charAt(pos);
if (PATH_LETTERS.indexOf(c) >= 0) {
pos++;
return c;
}
}
return 0;
}
/** Parse the next floating-point number (handles implicit separators). */
double nextDouble() {
skipWs();
int start = pos;
if (pos < len && (d.charAt(pos) == '+' || d.charAt(pos) == '-'))
pos++;
boolean hasDot = false;
while (pos < len) {
char c = d.charAt(pos);
if (c >= '0' && c <= '9') {
pos++;
} else if (c == '.' && !hasDot) {
hasDot = true;
pos++;
} else if ((c == 'e' || c == 'E') && pos > start) {
pos++;
if (pos < len && (d.charAt(pos) == '+' || d.charAt(pos) == '-'))
pos++;
} else {
break;
}
}
return Double.parseDouble(d.substring(start, pos));
}
/** Parse a single arc flag (0 or 1), skipping leading whitespace. */
int nextFlag() {
skipWs();
int f = d.charAt(pos++) - '0';
skipOptionalComma();
return f;
}
/** Skip one token (for error recovery). */
void skipToken() {
skipWs();
while (pos < len && !isWsOrComma(d.charAt(pos)))
pos++;
}
private void skipWs() {
while (pos < len && isWsOrComma(d.charAt(pos)))
pos++;
}
private void skipOptionalComma() {
while (pos < len && d.charAt(pos) == ' ')
pos++;
if (pos < len && d.charAt(pos) == ',')
pos++;
}
private static boolean isWsOrComma(char c) {
return c == ' ' || c == ',' || c == '\t' || c == '\n' || c == '\r';
}
}
/** Draw an elliptical arc approximation using cubic Bézier curves. */
private static void drawArc(Surface surface, double x1, double y1, double rx, double ry, double rotation,
boolean largeArc, boolean sweep, double x2, double y2) {
if (rx == 0 || ry == 0) {
surface.path.lineTo(x2, y2);
return;
}
rx = Math.abs(rx);
ry = Math.abs(ry);
double cosR = Math.cos(rotation);
double sinR = Math.sin(rotation);
double dx = (x1 - x2) / 2;
double dy = (y1 - y2) / 2;
double x1p = cosR * dx + sinR * dy;
double y1p = -sinR * dx + cosR * dy;
double x1p2 = x1p * x1p;
double y1p2 = y1p * y1p;
double rx2 = rx * rx;
double ry2 = ry * ry;
// Scale radii if necessary
double lambda = x1p2 / rx2 + y1p2 / ry2;
if (lambda > 1) {
double sqrtLambda = Math.sqrt(lambda);
rx *= sqrtLambda;
ry *= sqrtLambda;
rx2 = rx * rx;
ry2 = ry * ry;
}
double num = rx2 * ry2 - rx2 * y1p2 - ry2 * x1p2;
double den = rx2 * y1p2 + ry2 * x1p2;
double sq = Math.max(0, num / den);
sq = Math.sqrt(sq) * (largeArc == sweep ? -1 : 1);
double cxp = sq * rx * y1p / ry;
double cyp = -sq * ry * x1p / rx;
double cxr = cosR * cxp - sinR * cyp + (x1 + x2) / 2;
double cyr = sinR * cxp + cosR * cyp + (y1 + y2) / 2;
double theta1 = angle(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
double dtheta = angle((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry);
if (!sweep && dtheta > 0)
dtheta -= 2 * Math.PI;
if (sweep && dtheta < 0)
dtheta += 2 * Math.PI;
// Approximate with cubic Bézier curves
int segments = (int) Math.ceil(Math.abs(dtheta) / (Math.PI / 2));
double segAngle = dtheta / segments;
for (int i = 0; i < segments; i++) {
double t1 = theta1 + i * segAngle;
double t2 = theta1 + (i + 1) * segAngle;
arcToCubic(surface, cxr, cyr, rx, ry, rotation, t1, t2);
}
}
private static void arcToCubic(Surface surface, double cx, double cy, double rx, double ry, double phi,
double theta1, double theta2) {
double dTheta = theta2 - theta1;
double alpha = Math.sin(dTheta) * (Math.sqrt(4 + 3 * Math.pow(Math.tan(dTheta / 2), 2)) - 1) / 3;
double cosT1 = Math.cos(theta1), sinT1 = Math.sin(theta1);
double cosT2 = Math.cos(theta2), sinT2 = Math.sin(theta2);
double cosPhi = Math.cos(phi), sinPhi = Math.sin(phi);
double px1 = rx * cosT1;
double py1 = ry * sinT1;
double dx1 = -rx * sinT1;
double dy1 = ry * cosT1;
double px2 = rx * cosT2;
double py2 = ry * sinT2;
double dx2 = -rx * sinT2;
double dy2 = ry * cosT2;
double cp1x = cx + cosPhi * (px1 + alpha * dx1) - sinPhi * (py1 + alpha * dy1);
double cp1y = cy + sinPhi * (px1 + alpha * dx1) + cosPhi * (py1 + alpha * dy1);
double cp2x = cx + cosPhi * (px2 - alpha * dx2) - sinPhi * (py2 - alpha * dy2);
double cp2y = cy + sinPhi * (px2 - alpha * dx2) + cosPhi * (py2 - alpha * dy2);
double ex = cx + cosPhi * px2 - sinPhi * py2;
double ey = cy + sinPhi * px2 + cosPhi * py2;
surface.path.curveTo(cp1x, cp1y, cp2x, cp2y, ex, ey);
}
private static double angle(double ux, double uy, double vx, double vy) {
double dot = ux * vx + uy * vy;
double len = Math.sqrt(ux * ux + uy * uy) * Math.sqrt(vx * vx + vy * vy);
double cos = Math.max(-1, Math.min(1, dot / len));
double angle = Math.acos(cos);
if (ux * vy - uy * vx < 0)
angle = -angle;
return angle;
}
}