Skip to content

Commit

Permalink
account for various advanced SVG <path> features
Browse files Browse the repository at this point in the history
this adds support for the S and T Bezier shortcuts and commands with multiple sets of arguments, and also has better error handling for when the number of arguments is not what we expect.  I also refactord it a bit to be much cleaner in general, because wow was this poorly written.
  • Loading branch information
jkunimune committed Sep 14, 2024
1 parent dd48177 commit 2efaee9
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 32 deletions.
14 changes: 7 additions & 7 deletions src/image/Path.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public static List<Command> parse(String d) {
argStrings = argString.trim().split("[\\s,]+");

// parse the arguments
final double[] args = new double[argStrings.length]; // TODO: break up command with too many arguments
final double[] args = new double[argStrings.length];
for (int j = 0; j < args.length; j++) {
args[j] = parseDouble(argStrings[j]); //parse the coordinate
if (!isFinite(args[j]))
Expand Down Expand Up @@ -81,11 +81,11 @@ public static List<Command> translated(double xShift, double yShift, List<Comman
case 'V':
newArgs[0] += yShift;
break;
case 'L': case 'M':
case 'L': case 'M': case 'T':
newArgs[0] += xShift;
newArgs[1] += yShift;
break;
case 'Q':
case 'Q': case 'S':
newArgs[0] += xShift;
newArgs[1] += yShift;
newArgs[2] += xShift;
Expand Down Expand Up @@ -127,11 +127,11 @@ public static List<Command> scaled(double xScale, double yScale, List<Command> p
case 'V': case 'v':
newArgs[0] *= yScale;
break;
case 'L': case 'l': case 'M': case 'm':
case 'L': case 'l': case 'M': case 'm': case 'T': case 't':
newArgs[0] *= xScale;
newArgs[1] *= yScale;
break;
case 'Q': case 'q':
case 'Q': case 'q': case 'S': case 's':
newArgs[0] *= xScale;
newArgs[1] *= yScale;
newArgs[2] *= xScale;
Expand Down Expand Up @@ -187,7 +187,7 @@ public static List<Command> rotated(double rotation, List<Command> path) {
-old.args[0]*sin(rotation),
old.args[0]*cos(rotation)};
break;
case 'L': case 'l': case 'M': case 'm': case 'Q': case 'q': case 'C': case 'c':
case 'L': case 'l': case 'M': case 'm': case 'Q': case 'q': case 'C': case 'c': case 'T': case 't': case 'S': case 's':
for (int i = 0; i < newArgs.length; i += 2) {
newArgs[i ] = old.args[i]*cos(rotation) - old.args[i + 1]*sin(rotation);
newArgs[i + 1] = old.args[i]*sin(rotation) + old.args[i + 1]*cos(rotation);
Expand Down Expand Up @@ -230,7 +230,7 @@ public static double[][] asArray(List<Command> commands) {
double[][] points = new double[commands.size()][];
for (int i = 0; i < commands.size(); i ++) {
Command command = commands.get(i);
if (command.args.length >= 2) // just ignore 'L', 'V', and 'Z' commands
if (command.args.length >= 2) // just ignore 'H', 'V', and 'Z' commands
points[i] = new double[] {
command.args[command.args.length - 2],
command.args[command.args.length - 1]}; // use the last two arguments as the coordinates
Expand Down
119 changes: 94 additions & 25 deletions src/image/SVGMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -298,46 +298,115 @@ private GeographicPath parsePath(AttributesImpl attributes) {

// then parse the d and simplify the result
List<Path.Command> path = Path.parse(d);

// start by breaking up any commands with multiple sets of arguments
for (int i = path.size() - 1; i >= 0; i --) {
char type = path.get(i).type;
int expectedArgsLength;
if (type == 'Z' || type == 'z')
expectedArgsLength = 0;
else if (type == 'A' || type == 'a')
expectedArgsLength = 7;
else if (type == 'C' || type == 'c')
expectedArgsLength = 6;
else if (type == 'Q' || type == 'q' || type == 'S' || type == 's')
expectedArgsLength = 4;
else if (type == 'H' || type == 'h' || type == 'V' || type == 'v')
expectedArgsLength = 1;
else
expectedArgsLength = 2;
double[] args = path.get(i).args;
if (args.length < expectedArgsLength)
throw new IllegalArgumentException(format(
"this '%s' command only has %d elements, but I think '%s's should have %d.",
type, args.length, type, expectedArgsLength));
if (args.length > expectedArgsLength) {
if (expectedArgsLength == 0 || args.length%expectedArgsLength != 0)
throw new IllegalArgumentException(format(
"this '%s' command has %d elements, which is not a whole multiple of %d like it should be.",
type, args.length, expectedArgsLength));
path.remove(i);
for (int j = 0; j < args.length/expectedArgsLength; j ++) {
if (j > 0 && type == 'M')
type = 'L'; //when multiple pairs of args come after 'M', the subsequent commands are 'L'
double[] subArgs = Arrays.copyOfRange(
args, j*expectedArgsLength, (j + 1)*expectedArgsLength);
path.add(i + j, new Path.Command(type, subArgs));
}
}
}

// then convert fancy commands into simpler commands
double[] lastMove = {0, 0}; //for closepaths
double[] last = {0, 0}; //for relative coordinates
double[] lastPoint = {0, 0}; //for H, V, and relative coordinates
double[] lastControlPoint = {0, 0}; // for T and S commands
for (int i = 0; i < path.size(); i ++) {
char type = path.get(i).type;
char type = path.get(i).type; //get the type
double[] args = path.get(i).args;

// replace relative commands with absolute commands
if (type >= 'a') {
if (type == 'h') //for h, it's relative to the last x coordinate
args[0] += lastPoint[0];
else if (type == 'v') //for v, it's relative to the last y coordinate
args[0] += lastPoint[1];
else if (type == 'a') { //for arcs, only the last two args are relative
args[5] += lastPoint[0];
args[6] += lastPoint[1];
}
else { //for everything else, shift all the args in pairs
for (int j = 0; j < args.length; j += 2) {
args[j] += lastPoint[0];
args[j + 1] += lastPoint[1];
}
}
type -= 32; //and then make it a capital letter to indicate that it is now absolute
}
// replace arcs with lines because I don't want to deal with those
if (type == 'a' || type == 'A') {
type = (type == 'a') ? 'l' : 'L';
if (type == 'A') {
type = 'L';
args = new double[] {args[5], args[6]};
}
// convert horizontal and vertical lines to general lines to keep things simple
if (type == 'h' || type == 'H' || type == 'v' || type == 'V') {
final int direcIdx = (type%32 == 8) ? 0 : 1;
double[] newArgs = new double[] {last[0], last[1]};
if (type <= 'Z')
newArgs[direcIdx] = args[0]; //uppercase (absolute)
else
newArgs[direcIdx] += args[0]; //lowercase (relative)
args = newArgs;
last[direcIdx] = args[direcIdx];
if (type == 'H') {
args = new double[] {args[0], lastPoint[1]};
type = 'L';
}
if (type == 'V') {
args = new double[] {lastPoint[0], args[0]};
type = 'L';
}
// also replace closepath with an explicit line to the last moveto
else if (type == 'z' || type == 'Z') {
else if (type == 'Z') {
args = new double[] {lastMove[0], lastMove[1]};
type = 'L';
}
else {
for (int j = 0; j < args.length; j ++) {
if (type >= 'a')
args[j] += last[j%2]; //account for relative commands
last[j%2] = args[j];
}
if (type >= 'a') //make all letters uppercase (because it's all absolute from now on)
type -= 32;
// also convert 'T' Bezier curves to explicit 'Q' Bezier curves
else if (type == 'T') {
args = new double[] {
2*lastPoint[0] - lastControlPoint[0],
2*lastPoint[1] - lastControlPoint[1],
args[0], args[1],
};
type = 'Q';
}
if (type == 'M') { //make note of the last moveto, so we can interpret closepaths properly
lastMove[0] = args[args.length-2];
lastMove[1] = args[args.length-1];
// also convert 'S' Bezier curves to explicit 'C' Bezier curves
else if (type == 'S') {
args = new double[] {
2*lastPoint[0] - lastControlPoint[0],
2*lastPoint[1] - lastControlPoint[1],
args[0], args[1],
args[2], args[3],
};
type = 'C';
}
lastPoint = Arrays.copyOfRange(args, args.length - 2, args.length); //make note of the endpoint of this command
if (type == 'M') //make note of the last moveto, so we can interpret closepaths properly
lastMove = lastPoint;
if (type == 'Q' || type == 'C') // make note of the last control point, if there were any
lastControlPoint = Arrays.copyOfRange(args, args.length - 4, args.length - 2);
else
lastControlPoint = lastPoint;
path.set(i, new Path.Command(type, args));
}

Expand Down

0 comments on commit 2efaee9

Please sign in to comment.