mirror of
https://github.com/processing/processing4.git
synced 2026-06-16 04:26:26 +02:00
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
This commit is contained in:
@@ -961,14 +961,47 @@ public class PShapeSVG extends PShape {
|
||||
float rx = PApplet.parseFloat(pathTokens[i + 1]);
|
||||
float ry = PApplet.parseFloat(pathTokens[i + 2]);
|
||||
float angle = PApplet.parseFloat(pathTokens[i + 3]);
|
||||
boolean fa = PApplet.parseFloat(pathTokens[i + 4]) != 0;
|
||||
boolean fs = PApplet.parseFloat(pathTokens[i + 5]) != 0;
|
||||
float endX = PApplet.parseFloat(pathTokens[i + 6]);
|
||||
float endY = PApplet.parseFloat(pathTokens[i + 7]);
|
||||
// In compact arc notation, flags and coordinates may be concatenated.
|
||||
// e.g. "013" is parsed as large-arc=0, sweep=1, x=3
|
||||
String token4 = pathTokens[i + 4];
|
||||
boolean fa;
|
||||
boolean fs;
|
||||
float endX;
|
||||
float endY;
|
||||
int tokenOffset = 0;
|
||||
if (isCompactArcNotation(token4)) {
|
||||
fa = token4.charAt(0) == '1';
|
||||
fs = token4.charAt(1) == '1';
|
||||
// Case: flags and x-coordinate are concatenated (e.g. "01100")
|
||||
// token4 contains flags + x, so y is at i+5.
|
||||
// We consume 2 fewer tokens than standard (8-2=6).
|
||||
if (token4.length() > 2) {
|
||||
endX = PApplet.parseFloat(token4.substring(2));
|
||||
endY = PApplet.parseFloat(pathTokens[i + 5]);
|
||||
tokenOffset = -2;
|
||||
} else {
|
||||
// Case: flags are concatenated but separated from x (e.g. "01 100")
|
||||
// token4 is flags, x is at i+5, y is at i+6.
|
||||
// We consume 1 fewer token than standard (8-1=7).
|
||||
endX = PApplet.parseFloat(pathTokens[i + 5]);
|
||||
endY = PApplet.parseFloat(pathTokens[i + 6]);
|
||||
tokenOffset = -1;
|
||||
}
|
||||
} else {
|
||||
// Standard notation: flags and coordinates are separate tokens.
|
||||
// The 'A' command takes 7 arguments:
|
||||
// rx, ry, x-axis-rotation, large-arc-flag, sweep-flag, x, y
|
||||
// Here, we've already parsed rx (i+1), ry (i+2), and angle (i+3).
|
||||
// token4 (i+4) is the large-arc-flag.
|
||||
fa = PApplet.parseFloat(token4) != 0;
|
||||
fs = PApplet.parseFloat(pathTokens[i + 5]) != 0; // sweep-flag
|
||||
endX = PApplet.parseFloat(pathTokens[i + 6]); // x
|
||||
endY = PApplet.parseFloat(pathTokens[i + 7]); // y
|
||||
}
|
||||
parsePathArcto(cx, cy, rx, ry, angle, fa, fs, endX, endY);
|
||||
cx = endX;
|
||||
cy = endY;
|
||||
i += 8;
|
||||
i += 8 + tokenOffset;
|
||||
prevCurve = true;
|
||||
}
|
||||
break;
|
||||
@@ -978,14 +1011,41 @@ public class PShapeSVG extends PShape {
|
||||
float rx = PApplet.parseFloat(pathTokens[i + 1]);
|
||||
float ry = PApplet.parseFloat(pathTokens[i + 2]);
|
||||
float angle = PApplet.parseFloat(pathTokens[i + 3]);
|
||||
boolean fa = PApplet.parseFloat(pathTokens[i + 4]) != 0;
|
||||
boolean fs = PApplet.parseFloat(pathTokens[i + 5]) != 0;
|
||||
float endX = cx + PApplet.parseFloat(pathTokens[i + 6]);
|
||||
float endY = cy + PApplet.parseFloat(pathTokens[i + 7]);
|
||||
String token4 = pathTokens[i + 4];
|
||||
boolean fa;
|
||||
boolean fs;
|
||||
float endX;
|
||||
float endY;
|
||||
int tokenOffset = 0;
|
||||
if (isCompactArcNotation(token4)) {
|
||||
fa = token4.charAt(0) == '1';
|
||||
fs = token4.charAt(1) == '1';
|
||||
// Case: flags and x-coordinate are concatenated
|
||||
if (token4.length() > 2) {
|
||||
endX = cx + PApplet.parseFloat(token4.substring(2));
|
||||
endY = cy + PApplet.parseFloat(pathTokens[i + 5]);
|
||||
tokenOffset = -2;
|
||||
} else {
|
||||
// Case: flags are concatenated but separated from x
|
||||
endX = cx + PApplet.parseFloat(pathTokens[i + 5]);
|
||||
endY = cy + PApplet.parseFloat(pathTokens[i + 6]);
|
||||
tokenOffset = -1;
|
||||
}
|
||||
} else {
|
||||
// Standard notation: flags and coordinates are separate tokens.
|
||||
// The 'a' command takes 7 arguments:
|
||||
// rx, ry, x-axis-rotation, large-arc-flag, sweep-flag, x, y
|
||||
// Here, we've already parsed rx (i+1), ry (i+2), and angle (i+3).
|
||||
// token4 (i+4) is the large-arc-flag.
|
||||
fa = PApplet.parseFloat(token4) != 0;
|
||||
fs = PApplet.parseFloat(pathTokens[i + 5]) != 0; // sweep-flag
|
||||
endX = cx + PApplet.parseFloat(pathTokens[i + 6]); // x
|
||||
endY = cy + PApplet.parseFloat(pathTokens[i + 7]); // y
|
||||
}
|
||||
parsePathArcto(cx, cy, rx, ry, angle, fa, fs, endX, endY);
|
||||
cx = endX;
|
||||
cy = endY;
|
||||
i += 8;
|
||||
i += 8 + tokenOffset;
|
||||
prevCurve = true;
|
||||
}
|
||||
break;
|
||||
@@ -1054,6 +1114,33 @@ public class PShapeSVG extends PShape {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if a token represents compact arc notation where flags and coordinates
|
||||
* are concatenated (e.g., "013" for large-arc=0, sweep=1, x=3).
|
||||
*
|
||||
* @param token the token to check
|
||||
* @return true if the token is in compact arc notation format
|
||||
*/
|
||||
private boolean isCompactArcNotation(String token) {
|
||||
if (token == null) {
|
||||
return false;
|
||||
}
|
||||
return token.length() > 1 &&
|
||||
// First two characters must be '0' or '1' (flags)
|
||||
(token.charAt(0) == '0' || token.charAt(0) == '1') &&
|
||||
(token.charAt(1) == '0' || token.charAt(1) == '1') &&
|
||||
// Either it's just the flags (length 2),
|
||||
(token.length() == 2 ||
|
||||
// Or the flags are followed by the start of a number coordinate
|
||||
// (digit, sign, or decimal point)
|
||||
(token.length() > 2 && (
|
||||
Character.isDigit(token.charAt(2)) ||
|
||||
token.charAt(2) == '+' ||
|
||||
token.charAt(2) == '-' ||
|
||||
token.charAt(2) == '.')));
|
||||
}
|
||||
|
||||
|
||||
private void parsePathLineto(float px, float py) {
|
||||
parsePathCode(VERTEX);
|
||||
parsePathVertex(px, py);
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package processing.core;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import processing.data.XML;
|
||||
|
||||
public class PShapeSVGPathTest {
|
||||
|
||||
@Test
|
||||
public void testCompactPathNotation() {
|
||||
String svgContent = "<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.0\" viewBox=\"0 0 29 29\">" +
|
||||
"<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\"/>" +
|
||||
"</svg>";
|
||||
|
||||
try {
|
||||
XML xml = XML.parse(svgContent);
|
||||
PShapeSVG shape = new PShapeSVG(xml);
|
||||
Assert.assertNotNull(shape);
|
||||
Assert.assertTrue(shape.getChildCount() > 0);
|
||||
|
||||
PShape path = shape.getChild(0);
|
||||
Assert.assertNotNull(path);
|
||||
Assert.assertTrue(path.getVertexCount() > 5);
|
||||
} catch (Exception e) {
|
||||
Assert.fail("Encountered exception " + e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWorkingPathNotation() {
|
||||
// Test the working SVG (with explicit decimal points)
|
||||
String svgContent = "<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.0\" viewBox=\"0 0 29 29\">" +
|
||||
"<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\"/>" +
|
||||
"</svg>";
|
||||
|
||||
try {
|
||||
XML xml = XML.parse(svgContent);
|
||||
PShapeSVG shape = new PShapeSVG(xml);
|
||||
Assert.assertNotNull(shape);
|
||||
} catch (Exception e) {
|
||||
Assert.fail("Encountered exception " + e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompactArcNotationVariations() {
|
||||
String svgContent1 = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\">" +
|
||||
"<path d=\"M10 10 A30 30 0 013 50\"/></svg>";
|
||||
|
||||
try {
|
||||
XML xml = XML.parse(svgContent1);
|
||||
PShapeSVG shape = new PShapeSVG(xml);
|
||||
PShape path = shape.getChild(0);
|
||||
int vertexCount = path.getVertexCount();
|
||||
PVector lastVertex = path.getVertex(vertexCount - 1);
|
||||
Assert.assertEquals(3.0f, lastVertex.x, 0.0001f);
|
||||
Assert.assertEquals(50.0f, lastVertex.y, 0.0001f);
|
||||
} catch (Exception e) {
|
||||
Assert.fail("Encountered exception " + e);
|
||||
}
|
||||
|
||||
String svgContent2 = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\">" +
|
||||
"<path d=\"M10 10 A30 30 0 0110 50\"/></svg>";
|
||||
|
||||
try {
|
||||
XML xml = XML.parse(svgContent2);
|
||||
PShapeSVG shape = new PShapeSVG(xml);
|
||||
PShape path = shape.getChild(0);
|
||||
int vertexCount = path.getVertexCount();
|
||||
PVector lastVertex = path.getVertex(vertexCount - 1);
|
||||
Assert.assertEquals(10.0f, lastVertex.x, 0.0001f);
|
||||
Assert.assertEquals(50.0f, lastVertex.y, 0.0001f);
|
||||
} catch (Exception e) {
|
||||
Assert.fail("Encountered exception " + e);
|
||||
}
|
||||
|
||||
String svgContent3 = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\">" +
|
||||
"<path d=\"M10 10 A30 30 0 0 1 10 50\"/></svg>";
|
||||
|
||||
try {
|
||||
XML xml = XML.parse(svgContent3);
|
||||
PShapeSVG shape = new PShapeSVG(xml);
|
||||
PShape path = shape.getChild(0);
|
||||
int vertexCount = path.getVertexCount();
|
||||
PVector lastVertex = path.getVertex(vertexCount - 1);
|
||||
Assert.assertEquals(10.0f, lastVertex.x, 0.0001f);
|
||||
Assert.assertEquals(50.0f, lastVertex.y, 0.0001f);
|
||||
} catch (Exception e) {
|
||||
Assert.fail("Encountered exception " + e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompactArcWithNegativeCoordinates() {
|
||||
String svgContent = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\">" +
|
||||
"<path d=\"M50 50 a20 20 0 01-10 20\"/></svg>";
|
||||
|
||||
try {
|
||||
XML xml = XML.parse(svgContent);
|
||||
PShapeSVG shape = new PShapeSVG(xml);
|
||||
PShape path = shape.getChild(0);
|
||||
int vertexCount = path.getVertexCount();
|
||||
PVector lastVertex = path.getVertex(vertexCount - 1);
|
||||
Assert.assertEquals(40.0f, lastVertex.x, 0.0001f);
|
||||
Assert.assertEquals(70.0f, lastVertex.y, 0.0001f);
|
||||
} catch (Exception e) {
|
||||
Assert.fail("Encountered exception " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user