PathDrawer.java
package io.brunoborges.jairosvg.draw;
import java.util.ArrayList;
import io.brunoborges.jairosvg.dom.Node;
import io.brunoborges.jairosvg.surface.Surface;
import io.brunoborges.jairosvg.util.Helpers;
import io.brunoborges.jairosvg.util.ParsedPoint;
import static io.brunoborges.jairosvg.util.Helpers.*;
/**
* SVG path command parser and drawer. Port of CairoSVG path.py
*/
public final class PathDrawer {
private static final java.util.regex.Pattern WHITESPACE = java.util.regex.Pattern.compile("\\s+");
private static final java.util.regex.Pattern PATH_LETTER_PATTERN = java.util.regex.Pattern
.compile("([achlmqstvzACHLMQSTVZ])");
private PathDrawer() {
}
/** Draw a path node. */
public static void path(Surface surface, Node node) {
String d = node.get("d", "");
node.vertices = new ArrayList<>();
d = PATH_LETTER_PATTERN.matcher(d).replaceAll(" $1 ");
String lastLetter = null;
d = normalize(d);
double cx = 0, cy = 0; // current point
double x1 = 0, y1 = 0, x2 = 0, y2 = 0, x3 = 0, y3 = 0;
double[] firstPathPoint = null;
String letter = null;
while (!d.isBlank()) {
d = d.strip();
int spaceIdx = d.indexOf(' ');
String first = spaceIdx > 0 ? d.substring(0, spaceIdx) : d;
if (first.length() == 1 && PATH_LETTERS.indexOf(first.charAt(0)) >= 0) {
letter = first;
d = d.length() > 1 ? d.substring(2).strip() : "";
if ((lastLetter == null || lastLetter.charAt(0) == 'z' || lastLetter.charAt(0) == 'Z')
&& (letter == null || (letter.charAt(0) != 'm' && letter.charAt(0) != 'M'))) {
node.vertices.add(new double[]{cx, cy});
firstPathPoint = new double[]{cx, cy};
}
} else if (letter != null && letter.charAt(0) == 'M') {
letter = "L";
} else if (letter != null && letter.charAt(0) == 'm') {
letter = "l";
}
if (lastLetter == null || lastLetter.charAt(0) == 'm' || lastLetter.charAt(0) == 'M'
|| lastLetter.charAt(0) == 'z' || lastLetter.charAt(0) == 'Z') {
firstPathPoint = null;
}
if (letter != null && letter.charAt(0) != 'm' && letter.charAt(0) != 'M' && letter.charAt(0) != 'z'
&& letter.charAt(0) != 'Z' && firstPathPoint == null) {
firstPathPoint = new double[]{cx, cy};
}
try {
switch (letter) {
case "M" -> {
ParsedPoint pt = pointWithRemainder(surface, d);
double x = pt.x(), y = pt.y();
d = pt.remainder();
if (lastLetter != null && lastLetter.charAt(0) != 'z' && lastLetter.charAt(0) != 'Z') {
node.vertices.add(null);
}
surface.path.moveTo(x, y);
cx = x;
cy = y;
}
case "m" -> {
ParsedPoint pt = pointWithRemainder(surface, d);
double dx = pt.x(), dy = pt.y();
d = pt.remainder();
if (lastLetter != null && lastLetter.charAt(0) != 'z' && lastLetter.charAt(0) != 'Z') {
node.vertices.add(null);
}
cx += dx;
cy += dy;
surface.path.moveTo(cx, cy);
}
case "L" -> {
ParsedPoint pt = pointWithRemainder(surface, d);
double x = pt.x(), y = pt.y();
d = pt.remainder();
double angle = pointAngle(cx, cy, x, y);
node.vertices.add(new double[]{Math.PI - angle, angle});
surface.path.lineTo(x, y);
cx = x;
cy = y;
}
case "l" -> {
ParsedPoint pt = pointWithRemainder(surface, d);
double dx = pt.x(), dy = pt.y();
d = pt.remainder();
double angle = pointAngle(0, 0, dx, dy);
node.vertices.add(new double[]{Math.PI - angle, angle});
cx += dx;
cy += dy;
surface.path.lineTo(cx, cy);
}
case "H" -> {
String[] sp = WHITESPACE.split(d + " ", 2);
double x = size(surface, sp[0], "x");
d = sp[1];
double angle = x > cx ? 0 : Math.PI;
node.vertices.add(new double[]{Math.PI - angle, angle});
surface.path.lineTo(x, cy);
cx = x;
}
case "h" -> {
String[] sp = WHITESPACE.split(d + " ", 2);
double dx = size(surface, sp[0], "x");
d = sp[1];
double angle = dx > 0 ? 0 : Math.PI;
node.vertices.add(new double[]{Math.PI - angle, angle});
cx += dx;
surface.path.lineTo(cx, cy);
}
case "V" -> {
String[] sp = WHITESPACE.split(d + " ", 2);
double y = size(surface, sp[0], "y");
d = sp[1];
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" -> {
String[] sp = WHITESPACE.split(d + " ", 2);
double dy = size(surface, sp[0], "y");
d = sp[1];
double angle = Math.copySign(Math.PI / 2, dy);
node.vertices.add(new double[]{-angle, angle});
cy += dy;
surface.path.lineTo(cx, cy);
}
case "C" -> {
ParsedPoint p1 = pointWithRemainder(surface, d);
ParsedPoint p2 = pointWithRemainder(surface, p1.remainder());
ParsedPoint p3 = pointWithRemainder(surface, p2.remainder());
x1 = p1.x();
y1 = p1.y();
x2 = p2.x();
y2 = p2.y();
x3 = p3.x();
y3 = p3.y();
d = p3.remainder();
node.vertices.add(new double[]{pointAngle(x2, y2, x1, y1), pointAngle(x2, y2, x3, y3)});
surface.path.curveTo(x1, y1, x2, y2, x3, y3);
cx = x3;
cy = y3;
}
case "c" -> {
double ox = cx, oy = cy;
ParsedPoint p1 = pointWithRemainder(surface, d);
ParsedPoint p2 = pointWithRemainder(surface, p1.remainder());
ParsedPoint p3 = pointWithRemainder(surface, p2.remainder());
double dx1 = p1.x(), dy1 = p1.y();
double dx2 = p2.x(), dy2 = p2.y();
double dx3 = p3.x(), dy3 = p3.y();
d = p3.remainder();
x1 = ox + dx1;
y1 = oy + dy1;
x2 = ox + dx2;
y2 = oy + dy2;
x3 = ox + dx3;
y3 = oy + dy3;
node.vertices.add(new double[]{pointAngle(x2, y2, 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 (lastLetter != null && "csCS".indexOf(lastLetter.charAt(0)) >= 0) {
sx1 = x3 + (x3 - x2);
sy1 = y3 + (y3 - y2);
}
ParsedPoint p2 = pointWithRemainder(surface, d);
ParsedPoint p3 = pointWithRemainder(surface, p2.remainder());
x2 = p2.x();
y2 = p2.y();
x3 = p3.x();
y3 = p3.y();
d = p3.remainder();
x1 = sx1;
y1 = sy1;
node.vertices.add(new double[]{pointAngle(x2, y2, x1, y1), pointAngle(x2, y2, x3, y3)});
surface.path.curveTo(x1, y1, x2, y2, x3, y3);
cx = x3;
cy = y3;
}
case "s" -> {
double ox = cx, oy = cy;
double sx1 = 0, sy1 = 0;
if (lastLetter != null && "csCS".indexOf(lastLetter.charAt(0)) >= 0) {
sx1 = x3 - x2;
sy1 = y3 - y2;
}
ParsedPoint p2 = pointWithRemainder(surface, d);
ParsedPoint p3 = pointWithRemainder(surface, p2.remainder());
double dx2 = p2.x(), dy2 = p2.y();
double dx3 = p3.x(), dy3 = p3.y();
d = p3.remainder();
x1 = ox + sx1;
y1 = oy + sy1;
x2 = ox + dx2;
y2 = oy + dy2;
x3 = ox + dx3;
y3 = oy + dy3;
node.vertices.add(new double[]{pointAngle(x2, y2, x1, y1), pointAngle(x2, y2, x3, y3)});
surface.path.curveTo(x1, y1, x2, y2, x3, y3);
cx = x3;
cy = y3;
}
case "Q" -> {
ParsedPoint p2 = pointWithRemainder(surface, d);
ParsedPoint p3 = pointWithRemainder(surface, p2.remainder());
x2 = p2.x();
y2 = p2.y();
x3 = p3.x();
y3 = p3.y();
d = p3.remainder();
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[]{0, 0});
cx = x3;
cy = y3;
}
case "q" -> {
ParsedPoint p2 = pointWithRemainder(surface, d);
ParsedPoint p3 = pointWithRemainder(surface, p2.remainder());
double dx2 = p2.x(), dy2 = p2.y();
double dx3 = p3.x(), dy3 = p3.y();
d = p3.remainder();
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[]{0, 0});
cx = x3;
cy = y3;
}
case "T" -> {
if (lastLetter != null && "qtQT".indexOf(lastLetter.charAt(0)) >= 0) {
x2 = 2 * cx - x2;
y2 = 2 * cy - y2;
} else {
x2 = cx;
y2 = cy;
}
ParsedPoint p3 = pointWithRemainder(surface, d);
x3 = p3.x();
y3 = p3.y();
d = p3.remainder();
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[]{0, 0});
cx = x3;
cy = y3;
}
case "t" -> {
if (lastLetter != null && "qtQT".indexOf(lastLetter.charAt(0)) >= 0) {
x2 = 2 * cx - x2;
y2 = 2 * cy - y2;
} else {
x2 = cx;
y2 = cy;
}
ParsedPoint p3 = pointWithRemainder(surface, d);
double dx3 = p3.x(), dy3 = p3.y();
d = p3.remainder();
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[]{0, 0});
cx = x3;
cy = y3;
}
case "A", "a" -> {
ParsedPoint rxy = pointWithRemainder(surface, d);
double rx = rxy.x(), ry = rxy.y();
d = rxy.remainder().strip();
String[] sp = WHITESPACE.split(d + " ", 2);
double rotation = Math.toRadians(Double.parseDouble(sp[0]));
d = sp[1].strip();
char large = d.charAt(0);
d = d.substring(1).strip();
char sweep = d.charAt(0);
d = d.substring(1).strip();
int largeArc = large - '0';
int sweepFlag = sweep - '0';
if ((largeArc != 0 && largeArc != 1) || (sweepFlag != 0 && sweepFlag != 1)) {
continue;
}
ParsedPoint ep = pointWithRemainder(surface, d);
double ex = ep.x(), ey = ep.y();
d = ep.remainder();
if (letter.charAt(0) == 'a') {
ex += cx;
ey += cy;
}
// Draw arc using approximation
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 (Helpers.PointError e) {
break;
} catch (Exception e) {
// Skip malformed data
if (!d.isEmpty()) {
String[] sp = WHITESPACE.split(d, 2);
d = sp.length > 1 ? sp[1] : "";
}
continue;
}
if (letter != null && letter.charAt(0) != 'Z' && letter.charAt(0) != 'z') {
node.vertices.add(new double[]{cx, cy});
}
d = d.strip();
lastLetter = letter;
}
}
/** 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;
}
}