Skip to content

Commit afd1d98

Browse files
authored
Fix ArrayIndexOutOfBoundsException with compact SVG arc notation (#1282)
* Enhance PShapeSVG to support compact arc notation and add corresponding unit tests * add suggested changes * add comments * chore: retrigger CI
1 parent 451b399 commit afd1d98

File tree

2 files changed

+207
-10
lines changed

2 files changed

+207
-10
lines changed

core/src/processing/core/PShapeSVG.java

Lines changed: 97 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -961,14 +961,47 @@ else if (lexState == LexState.EXP_HEAD) {
961961
float rx = PApplet.parseFloat(pathTokens[i + 1]);
962962
float ry = PApplet.parseFloat(pathTokens[i + 2]);
963963
float angle = PApplet.parseFloat(pathTokens[i + 3]);
964-
boolean fa = PApplet.parseFloat(pathTokens[i + 4]) != 0;
965-
boolean fs = PApplet.parseFloat(pathTokens[i + 5]) != 0;
966-
float endX = PApplet.parseFloat(pathTokens[i + 6]);
967-
float endY = PApplet.parseFloat(pathTokens[i + 7]);
964+
// In compact arc notation, flags and coordinates may be concatenated.
965+
// e.g. "013" is parsed as large-arc=0, sweep=1, x=3
966+
String token4 = pathTokens[i + 4];
967+
boolean fa;
968+
boolean fs;
969+
float endX;
970+
float endY;
971+
int tokenOffset = 0;
972+
if (isCompactArcNotation(token4)) {
973+
fa = token4.charAt(0) == '1';
974+
fs = token4.charAt(1) == '1';
975+
// Case: flags and x-coordinate are concatenated (e.g. "01100")
976+
// token4 contains flags + x, so y is at i+5.
977+
// We consume 2 fewer tokens than standard (8-2=6).
978+
if (token4.length() > 2) {
979+
endX = PApplet.parseFloat(token4.substring(2));
980+
endY = PApplet.parseFloat(pathTokens[i + 5]);
981+
tokenOffset = -2;
982+
} else {
983+
// Case: flags are concatenated but separated from x (e.g. "01 100")
984+
// token4 is flags, x is at i+5, y is at i+6.
985+
// We consume 1 fewer token than standard (8-1=7).
986+
endX = PApplet.parseFloat(pathTokens[i + 5]);
987+
endY = PApplet.parseFloat(pathTokens[i + 6]);
988+
tokenOffset = -1;
989+
}
990+
} else {
991+
// Standard notation: flags and coordinates are separate tokens.
992+
// The 'A' command takes 7 arguments:
993+
// rx, ry, x-axis-rotation, large-arc-flag, sweep-flag, x, y
994+
// Here, we've already parsed rx (i+1), ry (i+2), and angle (i+3).
995+
// token4 (i+4) is the large-arc-flag.
996+
fa = PApplet.parseFloat(token4) != 0;
997+
fs = PApplet.parseFloat(pathTokens[i + 5]) != 0; // sweep-flag
998+
endX = PApplet.parseFloat(pathTokens[i + 6]); // x
999+
endY = PApplet.parseFloat(pathTokens[i + 7]); // y
1000+
}
9681001
parsePathArcto(cx, cy, rx, ry, angle, fa, fs, endX, endY);
9691002
cx = endX;
9701003
cy = endY;
971-
i += 8;
1004+
i += 8 + tokenOffset;
9721005
prevCurve = true;
9731006
}
9741007
break;
@@ -978,14 +1011,41 @@ else if (lexState == LexState.EXP_HEAD) {
9781011
float rx = PApplet.parseFloat(pathTokens[i + 1]);
9791012
float ry = PApplet.parseFloat(pathTokens[i + 2]);
9801013
float angle = PApplet.parseFloat(pathTokens[i + 3]);
981-
boolean fa = PApplet.parseFloat(pathTokens[i + 4]) != 0;
982-
boolean fs = PApplet.parseFloat(pathTokens[i + 5]) != 0;
983-
float endX = cx + PApplet.parseFloat(pathTokens[i + 6]);
984-
float endY = cy + PApplet.parseFloat(pathTokens[i + 7]);
1014+
String token4 = pathTokens[i + 4];
1015+
boolean fa;
1016+
boolean fs;
1017+
float endX;
1018+
float endY;
1019+
int tokenOffset = 0;
1020+
if (isCompactArcNotation(token4)) {
1021+
fa = token4.charAt(0) == '1';
1022+
fs = token4.charAt(1) == '1';
1023+
// Case: flags and x-coordinate are concatenated
1024+
if (token4.length() > 2) {
1025+
endX = cx + PApplet.parseFloat(token4.substring(2));
1026+
endY = cy + PApplet.parseFloat(pathTokens[i + 5]);
1027+
tokenOffset = -2;
1028+
} else {
1029+
// Case: flags are concatenated but separated from x
1030+
endX = cx + PApplet.parseFloat(pathTokens[i + 5]);
1031+
endY = cy + PApplet.parseFloat(pathTokens[i + 6]);
1032+
tokenOffset = -1;
1033+
}
1034+
} else {
1035+
// Standard notation: flags and coordinates are separate tokens.
1036+
// The 'a' command takes 7 arguments:
1037+
// rx, ry, x-axis-rotation, large-arc-flag, sweep-flag, x, y
1038+
// Here, we've already parsed rx (i+1), ry (i+2), and angle (i+3).
1039+
// token4 (i+4) is the large-arc-flag.
1040+
fa = PApplet.parseFloat(token4) != 0;
1041+
fs = PApplet.parseFloat(pathTokens[i + 5]) != 0; // sweep-flag
1042+
endX = cx + PApplet.parseFloat(pathTokens[i + 6]); // x
1043+
endY = cy + PApplet.parseFloat(pathTokens[i + 7]); // y
1044+
}
9851045
parsePathArcto(cx, cy, rx, ry, angle, fa, fs, endX, endY);
9861046
cx = endX;
9871047
cy = endY;
988-
i += 8;
1048+
i += 8 + tokenOffset;
9891049
prevCurve = true;
9901050
}
9911051
break;
@@ -1054,6 +1114,33 @@ private void parsePathMoveto(float px, float py) {
10541114
}
10551115

10561116

1117+
/**
1118+
* Checks if a token represents compact arc notation where flags and coordinates
1119+
* are concatenated (e.g., "013" for large-arc=0, sweep=1, x=3).
1120+
*
1121+
* @param token the token to check
1122+
* @return true if the token is in compact arc notation format
1123+
*/
1124+
private boolean isCompactArcNotation(String token) {
1125+
if (token == null) {
1126+
return false;
1127+
}
1128+
return token.length() > 1 &&
1129+
// First two characters must be '0' or '1' (flags)
1130+
(token.charAt(0) == '0' || token.charAt(0) == '1') &&
1131+
(token.charAt(1) == '0' || token.charAt(1) == '1') &&
1132+
// Either it's just the flags (length 2),
1133+
(token.length() == 2 ||
1134+
// Or the flags are followed by the start of a number coordinate
1135+
// (digit, sign, or decimal point)
1136+
(token.length() > 2 && (
1137+
Character.isDigit(token.charAt(2)) ||
1138+
token.charAt(2) == '+' ||
1139+
token.charAt(2) == '-' ||
1140+
token.charAt(2) == '.')));
1141+
}
1142+
1143+
10571144
private void parsePathLineto(float px, float py) {
10581145
parsePathCode(VERTEX);
10591146
parsePathVertex(px, py);
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package processing.core;
2+
3+
import org.junit.Assert;
4+
import org.junit.Test;
5+
import processing.data.XML;
6+
7+
public class PShapeSVGPathTest {
8+
9+
@Test
10+
public void testCompactPathNotation() {
11+
String svgContent = "<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.0\" viewBox=\"0 0 29 29\">" +
12+
"<path d=\"m0 6 3-2 15 4 7-7a2 2 0 013 3l-7 7 4 15-2 3-7-13-5 5v4l-2 2-2-5-5-2 2-2h4l5-5z\"/>" +
13+
"</svg>";
14+
15+
try {
16+
XML xml = XML.parse(svgContent);
17+
PShapeSVG shape = new PShapeSVG(xml);
18+
Assert.assertNotNull(shape);
19+
Assert.assertTrue(shape.getChildCount() > 0);
20+
21+
PShape path = shape.getChild(0);
22+
Assert.assertNotNull(path);
23+
Assert.assertTrue(path.getVertexCount() > 5);
24+
} catch (Exception e) {
25+
Assert.fail("Encountered exception " + e);
26+
}
27+
}
28+
29+
@Test
30+
public void testWorkingPathNotation() {
31+
// Test the working SVG (with explicit decimal points)
32+
String svgContent = "<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.0\" viewBox=\"0 0 29 29\">" +
33+
"<path d=\"m 0,5.9994379 2.9997,-1.9998 14.9985,3.9996 6.9993,-6.99930004 a 2.1211082,2.1211082 0 0 1 2.9997,2.99970004 l -6.9993,6.9993001 3.9996,14.9985 -1.9998,2.9997 -6.9993,-12.9987 -4.9995,4.9995 v 3.9996 l -1.9998,1.9998 -1.9998,-4.9995 -4.9995,-1.9998 1.9998,-1.9998 h 3.9996 l 4.9995,-4.9995 z\"/>" +
34+
"</svg>";
35+
36+
try {
37+
XML xml = XML.parse(svgContent);
38+
PShapeSVG shape = new PShapeSVG(xml);
39+
Assert.assertNotNull(shape);
40+
} catch (Exception e) {
41+
Assert.fail("Encountered exception " + e);
42+
}
43+
}
44+
45+
@Test
46+
public void testCompactArcNotationVariations() {
47+
String svgContent1 = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\">" +
48+
"<path d=\"M10 10 A30 30 0 013 50\"/></svg>";
49+
50+
try {
51+
XML xml = XML.parse(svgContent1);
52+
PShapeSVG shape = new PShapeSVG(xml);
53+
PShape path = shape.getChild(0);
54+
int vertexCount = path.getVertexCount();
55+
PVector lastVertex = path.getVertex(vertexCount - 1);
56+
Assert.assertEquals(3.0f, lastVertex.x, 0.0001f);
57+
Assert.assertEquals(50.0f, lastVertex.y, 0.0001f);
58+
} catch (Exception e) {
59+
Assert.fail("Encountered exception " + e);
60+
}
61+
62+
String svgContent2 = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\">" +
63+
"<path d=\"M10 10 A30 30 0 0110 50\"/></svg>";
64+
65+
try {
66+
XML xml = XML.parse(svgContent2);
67+
PShapeSVG shape = new PShapeSVG(xml);
68+
PShape path = shape.getChild(0);
69+
int vertexCount = path.getVertexCount();
70+
PVector lastVertex = path.getVertex(vertexCount - 1);
71+
Assert.assertEquals(10.0f, lastVertex.x, 0.0001f);
72+
Assert.assertEquals(50.0f, lastVertex.y, 0.0001f);
73+
} catch (Exception e) {
74+
Assert.fail("Encountered exception " + e);
75+
}
76+
77+
String svgContent3 = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\">" +
78+
"<path d=\"M10 10 A30 30 0 0 1 10 50\"/></svg>";
79+
80+
try {
81+
XML xml = XML.parse(svgContent3);
82+
PShapeSVG shape = new PShapeSVG(xml);
83+
PShape path = shape.getChild(0);
84+
int vertexCount = path.getVertexCount();
85+
PVector lastVertex = path.getVertex(vertexCount - 1);
86+
Assert.assertEquals(10.0f, lastVertex.x, 0.0001f);
87+
Assert.assertEquals(50.0f, lastVertex.y, 0.0001f);
88+
} catch (Exception e) {
89+
Assert.fail("Encountered exception " + e);
90+
}
91+
}
92+
93+
@Test
94+
public void testCompactArcWithNegativeCoordinates() {
95+
String svgContent = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\">" +
96+
"<path d=\"M50 50 a20 20 0 01-10 20\"/></svg>";
97+
98+
try {
99+
XML xml = XML.parse(svgContent);
100+
PShapeSVG shape = new PShapeSVG(xml);
101+
PShape path = shape.getChild(0);
102+
int vertexCount = path.getVertexCount();
103+
PVector lastVertex = path.getVertex(vertexCount - 1);
104+
Assert.assertEquals(40.0f, lastVertex.x, 0.0001f);
105+
Assert.assertEquals(70.0f, lastVertex.y, 0.0001f);
106+
} catch (Exception e) {
107+
Assert.fail("Encountered exception " + e);
108+
}
109+
}
110+
}

0 commit comments

Comments
 (0)