Skip to content

Commit 3adb38d

Browse files
SnipxiText-CI
authored andcommitted
SVG: Support floating numbers with exponent notation in path painting instructions
DEVSIX-2343
1 parent 6bf0c6b commit 3adb38d

File tree

6 files changed

+121
-24
lines changed

6 files changed

+121
-24
lines changed

styled-xml-parser/src/main/java/com/itextpdf/styledxmlparser/css/util/CssUtils.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,8 @@ private static int determinePositionBetweenValueAndUnit(String string) {
376376
if (string.charAt(pos) == '+' ||
377377
string.charAt(pos) == '-' ||
378378
string.charAt(pos) == '.' ||
379-
string.charAt(pos) >= '0' && string.charAt(pos) <= '9') {
379+
isDigit(string.charAt(pos)) ||
380+
isExponentNotation(string, pos)) {
380381
pos++;
381382
} else {
382383
break;
@@ -588,4 +589,16 @@ private static boolean addRange(RangeBuilder builder, String left, String right)
588589
builder.addRange(l, r);
589590
return true;
590591
}
592+
593+
private static boolean isDigit(char ch) {
594+
return ch >= '0' && ch <= '9';
595+
}
596+
597+
private static boolean isExponentNotation(String s, int index) {
598+
return index < s.length() && s.charAt(index) == 'e' &&
599+
// e.g. 12e5
600+
(index + 1 < s.length() && isDigit(s.charAt(index + 1)) ||
601+
// e.g. 12e-5, 12e+5
602+
index + 2 < s.length() && (s.charAt(index + 1) == '-' || s.charAt(index + 1) == '+') && isDigit(s.charAt(index + 2)));
603+
}
591604
}

styled-xml-parser/src/test/java/com/itextpdf/styledxmlparser/css/util/CssUtilTest.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ This file is part of the iText (R) project.
5858

5959
import static org.junit.Assert.assertEquals;
6060
import static org.junit.Assert.assertNull;
61-
import static org.junit.Assert.assertTrue;
6261

6362
@Category(UnitTest.class)
6463
public class CssUtilTest extends ExtendedITextTest {
@@ -122,6 +121,24 @@ public void parseAbsoluteLengthFrom10pt() {
122121
Assert.assertEquals(expected, actual, 0);
123122
}
124123

124+
@Test
125+
public void parseAboluteLengthExponential01() {
126+
String value = "1e2pt";
127+
float actual = CssUtils.parseAbsoluteLength(value);
128+
float expected = 1e2f;
129+
130+
Assert.assertEquals(expected, actual, 0);
131+
}
132+
133+
@Test
134+
public void parseAboluteLengthExponential02() {
135+
String value = "1e2px";
136+
float actual = CssUtils.parseAbsoluteLength(value);
137+
float expected = 1e2f * 0.75f;
138+
139+
Assert.assertEquals(expected, actual, 0);
140+
}
141+
125142
@Test
126143
@LogMessages(messages = {@LogMessage(messageTemplate = LogMessageConstant.UNKNOWN_ABSOLUTE_METRIC_LENGTH_PARSED, count = 1)})
127144
public void parseAbsoluteLengthFromUnknownType() {

svg/src/main/java/com/itextpdf/svg/renderers/impl/PathSvgNodeRenderer.java

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public class PathSvgNodeRenderer extends AbstractSvgNodeRenderer {
7676
/**
7777
* The regular expression to find invalid operators in the <a href="https://www.w3.org/TR/SVG/paths.html#PathData">PathData attribute of the &ltpath&gt element</a>
7878
* <p>
79-
* Find any occurence of a letter that is not an operator
79+
* Find any occurrence of a letter that is not an operator
8080
*/
8181
private static final String INVALID_OPERATOR_REGEX = "(?:(?![mzlhvcsqtae])\\p{L})";
8282
private static Pattern invalidRegexPattern = Pattern.compile(INVALID_OPERATOR_REGEX, Pattern.CASE_INSENSITIVE);
@@ -85,9 +85,17 @@ public class PathSvgNodeRenderer extends AbstractSvgNodeRenderer {
8585
* The regular expression to split the <a href="https://www.w3.org/TR/SVG/paths.html#PathData">PathData attribute of the &ltpath&gt element</a>
8686
* <p>
8787
* Since {@link PathSvgNodeRenderer#containsInvalidAttributes(String)} is called before the use of this expression in {@link PathSvgNodeRenderer#parsePathOperations()} the attribute to be split is valid.
88-
* The regex splits at each letter.
88+
*
89+
* SVG defines 6 types of path commands, for a total of 20 commands:
90+
*
91+
* MoveTo: M, m
92+
* LineTo: L, l, H, h, V, v
93+
* Cubic Bezier Curve: C, c, S, s
94+
* Quadratic Bezier Curve: Q, q, T, t
95+
* Elliptical Arc Curve: A, a
96+
* ClosePath: Z, z
8997
*/
90-
private static final String SPLIT_REGEX = "(?=[\\p{L}])";
98+
private static final Pattern SPLIT_PATTERN = Pattern.compile("(?=[mlhvcsqtaz])", Pattern.CASE_INSENSITIVE);
9199

92100

93101
/**
@@ -136,7 +144,7 @@ private String[] getShapeCoordinates(IPathShape shape, IPathShape previousShape,
136144
String[] startingControlPoint = new String[2];
137145
if (previousShape != null) {
138146
Point previousEndPoint = previousShape.getEndingPoint();
139-
//if the previous command was a Bézier curve, use its last control point
147+
//if the previous command was a Bezier curve, use its last control point
140148
if (previousShape instanceof IControlPointCurve) {
141149
Point lastControlPoint = ((IControlPointCurve) previousShape).getLastControlPoint();
142150
float reflectedX = (float) (2 * previousEndPoint.getX() - lastControlPoint.getX());
@@ -275,9 +283,10 @@ Collection<String> parsePathOperations() {
275283
if (containsInvalidAttributes(attributes)) {
276284
throw new SvgProcessingException(SvgLogMessageConstant.INVALID_PATH_D_ATTRIBUTE_OPERATORS).setMessageParams(attributes);
277285
}
278-
String[] coordinates = attributes.split(SPLIT_REGEX);//gets an array attributesAr of {M 100 100, L 300 100, L200, 300, z}
279286

280-
for (String inst : coordinates) {
287+
String[] operators = splitPathStringIntoOperators(attributes);
288+
289+
for (String inst : operators) {
281290
String instTrim = inst.trim();
282291
if (!instTrim.isEmpty()) {
283292
char instruction = instTrim.charAt(0);
@@ -287,37 +296,56 @@ Collection<String> parsePathOperations() {
287296
result.add(temp);
288297
}
289298
}
299+
290300
return result;
291301
}
292302

293303
/**
294-
* Iterate over the input string and to seperate
304+
* Iterate over the input string and separate numbers from each other with space chars
295305
*/
296306
String separateDecimalPoints(String input) {
297307
//If a space or minus sign is found reset
298308
//If a another point is found, add an extra space on before the point
299309
StringBuilder res = new StringBuilder();
300-
//Iterate over string
301-
boolean decimalPointEncountered = false;
310+
// We are now among the digits to the right of the decimal point
311+
boolean fractionalPartAfterDecimalPoint = false;
312+
// We are now among the exponent magnitude part
313+
boolean exponentSignMagnitude = false;
302314
for (int i = 0; i < input.length(); i++) {
303315
char c = input.charAt(i);
304-
//If it's a whitespace or a minus sign and a point was previously found, reset the decimal point flag
305-
if (decimalPointEncountered && (c == '-' || Character.isWhitespace(c))) {
306-
decimalPointEncountered = false;
316+
// Resetting flags
317+
if (c == '-' || Character.isWhitespace(c)) {
318+
fractionalPartAfterDecimalPoint = false;
307319
}
308-
//If a point is found, mark and continue
309-
if (c == '.') {
310-
//If it's the second point, add an extra space
311-
if (decimalPointEncountered) {
312-
res.append(" ");
313-
} else {
314-
decimalPointEncountered = true;
315-
}
316-
} else if (c == '-') {// If a minus is found, add an extra space
320+
if (Character.isWhitespace(c)) {
321+
exponentSignMagnitude = false;
322+
}
323+
324+
// Add extra space before the next number starting from '.', or before the next number starting with '-'
325+
if (endsWithNonWhitespace(res) && (c == '.' && fractionalPartAfterDecimalPoint ||
326+
c == '-' && !exponentSignMagnitude)) {
317327
res.append(" ");
318328
}
329+
330+
if (c == '.') {
331+
fractionalPartAfterDecimalPoint = true;
332+
} else if (c == 'e') {
333+
exponentSignMagnitude = true;
334+
}
335+
319336
res.append(c);
320337
}
321338
return res.toString();
322339
}
340+
341+
/**
342+
* Gets an array of strings representing operators with their arguments, e.g. {"M 100 100", "L 300 100", "L200, 300", "z"}
343+
*/
344+
static String[] splitPathStringIntoOperators(String path) {
345+
return SPLIT_PATTERN.split(path);
346+
}
347+
348+
private static boolean endsWithNonWhitespace(StringBuilder sb) {
349+
return sb.length() > 0 && !Character.isWhitespace(sb.charAt(sb.length() - 1));
350+
}
323351
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.itextpdf.svg.renderers.impl;
2+
3+
import com.itextpdf.test.annotations.type.UnitTest;
4+
import org.junit.Assert;
5+
import org.junit.Test;
6+
import org.junit.experimental.categories.Category;
7+
8+
@Category(UnitTest.class)
9+
public class PathOperatorSplitTest {
10+
11+
@Test
12+
public void testNumbersContainingExponent01() {
13+
String path = "M10,9.999999999999972C203.33333333333334,9.999999999999972,396.6666666666667,1.4210854715202004e-14,590,1.4210854715202004e-14L590,41.666666666666686C396.6666666666667,41.666666666666686,203.33333333333334,51.66666666666664,10,51.66666666666664Z";
14+
String[] operators = new String[] {
15+
"M10,9.999999999999972",
16+
"C203.33333333333334,9.999999999999972,396.6666666666667,1.4210854715202004e-14,590,1.4210854715202004e-14",
17+
"L590,41.666666666666686",
18+
"C396.6666666666667,41.666666666666686,203.33333333333334,51.66666666666664,10,51.66666666666664",
19+
"Z"
20+
};
21+
testSplitting(path, operators);
22+
}
23+
24+
private void testSplitting(String originalStr, String[] expectedSplitting) {
25+
String[] result = PathSvgNodeRenderer.splitPathStringIntoOperators(originalStr);
26+
Assert.assertArrayEquals(expectedSplitting, result);
27+
}
28+
29+
}

svg/src/test/java/com/itextpdf/svg/renderers/impl/PathParsingTest.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,4 +210,14 @@ public void negativeAfterPositiveTest() {
210210
Assert.assertEquals(expected, actual);
211211
}
212212

213+
@Test
214+
public void exponentInNumberTest01() {
215+
PathSvgNodeRenderer path = new PathSvgNodeRenderer();
216+
String input = "C 268.88888888888886 67.97916666666663e+10 331.1111111111111 -2.842170943040401e-14 393.3333333333333 -2.842170943040401e-14";
217+
218+
String expected = "C 268.88888888888886 67.97916666666663e+10 331.1111111111111 -2.842170943040401e-14 393.3333333333333 -2.842170943040401e-14";
219+
String actual = path.separateDecimalPoints(input);
220+
Assert.assertEquals(expected, actual);
221+
}
222+
213223
}

svg/src/test/java/com/itextpdf/svg/renderers/impl/PathSvgNodeRendererTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ This file is part of the iText (R) project.
6969
import java.util.HashMap;
7070
import java.util.Map;
7171

72-
@Category( IntegrationTest.class )
72+
@Category(IntegrationTest.class)
7373
public class PathSvgNodeRendererTest extends SvgIntegrationTest {
7474

7575
public static final String sourceFolder = "./src/test/resources/com/itextpdf/svg/renderers/impl/PathSvgNodeRendererTest/";

0 commit comments

Comments
 (0)