Skip to content

Commit 704f5dd

Browse files
Benoit Lagaepavel-alay
authored andcommitted
SVG paths: Support smooth curves
Refactor multiple sets of parameters DEVSIX-2611
1 parent 795ef5e commit 704f5dd

24 files changed

+560
-285
lines changed

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

Lines changed: 60 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ This file is part of the iText (R) project.
4242
*/
4343
package com.itextpdf.svg.renderers.impl;
4444

45-
import com.itextpdf.io.util.MessageFormatUtil;
4645
import com.itextpdf.kernel.geom.Point;
4746
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
4847
import com.itextpdf.svg.SvgConstants;
@@ -54,13 +53,12 @@ This file is part of the iText (R) project.
5453
import com.itextpdf.svg.renderers.path.IPathShape;
5554
import com.itextpdf.svg.renderers.path.SvgPathShapeFactory;
5655
import com.itextpdf.svg.renderers.path.impl.ClosePath;
57-
import com.itextpdf.svg.renderers.path.impl.CurveTo;
56+
import com.itextpdf.svg.renderers.path.impl.IControlPointCurve;
5857
import com.itextpdf.svg.renderers.path.impl.MoveTo;
58+
import com.itextpdf.svg.renderers.path.impl.QuadraticSmoothCurveTo;
5959
import com.itextpdf.svg.renderers.path.impl.SmoothSCurveTo;
6060
import com.itextpdf.svg.utils.SvgCssUtils;
6161
import com.itextpdf.svg.utils.SvgRegexUtils;
62-
import org.slf4j.Logger;
63-
import org.slf4j.LoggerFactory;
6462

6563
import java.util.ArrayList;
6664
import java.util.Arrays;
@@ -75,9 +73,6 @@ public class PathSvgNodeRenderer extends AbstractSvgNodeRenderer {
7573

7674
private static final String SPACE_CHAR = " ";
7775

78-
private static final Logger LOGGER = LoggerFactory.getLogger(PathSvgNodeRenderer.class);
79-
private static final int MOVETOARGUMENTNR = 2;
80-
8176
/**
8277
* 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>
8378
* <p>
@@ -103,7 +98,7 @@ public class PathSvgNodeRenderer extends AbstractSvgNodeRenderer {
10398

10499
/**
105100
* The {@link ClosePath} shape keeping track of the initial point set by a {@link MoveTo} operation.
106-
* The original value is {@code null}, and must be set via a {@link MoveTo} operation before it may drawn.
101+
* The original value is {@code null}, and must be set via a {@link MoveTo} operation before it may be drawn.
107102
*/
108103
private ClosePath zOperator = null;
109104

@@ -137,14 +132,13 @@ private String[] getShapeCoordinates(IPathShape shape, IPathShape previousShape,
137132
return null;
138133
}
139134
String[] shapeCoordinates = null;
140-
String[] operatorArgs = Arrays.copyOfRange(pathProperties, 1, pathProperties.length);
141-
if (shape instanceof SmoothSCurveTo) {
135+
if (shape instanceof SmoothSCurveTo || shape instanceof QuadraticSmoothCurveTo) {
142136
String[] startingControlPoint = new String[2];
143137
if (previousShape != null) {
144138
Point previousEndPoint = previousShape.getEndingPoint();
145-
//if the previous command was a C or S use its last control point
146-
if (((previousShape instanceof CurveTo))) {
147-
Point lastControlPoint = ((CurveTo) previousShape).getLastControlPoint();
139+
//if the previous command was a Bézier curve, use its last control point
140+
if (previousShape instanceof IControlPointCurve) {
141+
Point lastControlPoint = ((IControlPointCurve) previousShape).getLastControlPoint();
148142
float reflectedX = (float) (2 * previousEndPoint.getX() - lastControlPoint.getX());
149143
float reflectedY = (float) (2 * previousEndPoint.getY() - lastControlPoint.getY());
150144

@@ -156,13 +150,13 @@ private String[] getShapeCoordinates(IPathShape shape, IPathShape previousShape,
156150
}
157151
} else {
158152
// TODO RND-951
159-
startingControlPoint[0] = pathProperties[1];
160-
startingControlPoint[1] = pathProperties[2];
153+
startingControlPoint[0] = pathProperties[0];
154+
startingControlPoint[1] = pathProperties[1];
161155
}
162-
shapeCoordinates = concatenate(startingControlPoint, operatorArgs);
156+
shapeCoordinates = concatenate(startingControlPoint, pathProperties);
163157
}
164158
if (shapeCoordinates == null) {
165-
shapeCoordinates = operatorArgs;
159+
shapeCoordinates = pathProperties;
166160
}
167161
return shapeCoordinates;
168162
}
@@ -178,34 +172,63 @@ private String[] getShapeCoordinates(IPathShape shape, IPathShape previousShape,
178172
*/
179173
private List<IPathShape> processPathOperator(String[] pathProperties, IPathShape previousShape) {
180174
List<IPathShape> shapes = new ArrayList<>();
181-
if (pathProperties.length == 0 || pathProperties[0].isEmpty()) {
175+
if (pathProperties.length == 0 || pathProperties[0].isEmpty() || SvgPathShapeFactory.getArgumentCount(pathProperties[0]) < 0) {
182176
return shapes;
183177
}
184-
//Implements (absolute) command value only
185-
//TODO implement relative values e. C(absolute), c(relative)
186-
IPathShape pathShape = SvgPathShapeFactory.createPathShape(pathProperties[0]);
187-
String[] shapeCoordinates = getShapeCoordinates(pathShape, previousShape, pathProperties);
188-
if (pathShape instanceof ClosePath) {
189-
if (previousShape != null) {
190-
pathShape = zOperator;
191-
} else {
178+
179+
int argumentCount = SvgPathShapeFactory.getArgumentCount(pathProperties[0]);
180+
if (argumentCount == 0) { // closePath operator
181+
if (previousShape == null) {
192182
throw new SvgProcessingException(SvgLogMessageConstant.INVALID_CLOSEPATH_OPERATOR_USE);
193183
}
194-
} else if (pathShape instanceof MoveTo) {
195-
zOperator = new ClosePath(pathShape.isRelative());
196-
if (shapeCoordinates != null && shapeCoordinates.length != MOVETOARGUMENTNR) {
197-
LOGGER.warn(MessageFormatUtil.format(SvgLogMessageConstant.PATH_WRONG_NUMBER_OF_ARGUMENTS, pathProperties[0], shapeCoordinates.length, MOVETOARGUMENTNR, MOVETOARGUMENTNR));
184+
shapes.add(zOperator);
185+
currentPoint = zOperator.getEndingPoint();
186+
return shapes;
187+
}
188+
for (int index = 1; index < pathProperties.length; index += argumentCount) {
189+
if (index + argumentCount > pathProperties.length) {
190+
break;
191+
}
192+
IPathShape pathShape = SvgPathShapeFactory.createPathShape(pathProperties[0]);
193+
if (pathShape instanceof MoveTo) {
194+
shapes.addAll(addMoveToShapes(pathShape, pathProperties));
195+
return shapes;
198196
}
199-
zOperator.setCoordinates(shapeCoordinates, currentPoint);
197+
198+
String[] shapeCoordinates = getShapeCoordinates(pathShape, previousShape, Arrays.copyOfRange(pathProperties, index, index+argumentCount));
199+
if (pathShape != null) {
200+
if (shapeCoordinates != null) {
201+
pathShape.setCoordinates(shapeCoordinates, currentPoint);
202+
}
203+
currentPoint = pathShape.getEndingPoint(); // unsupported operators are ignored.
204+
shapes.add(pathShape);
205+
}
206+
previousShape = pathShape;
200207
}
208+
return shapes;
209+
}
201210

202-
if (pathShape != null) {
203-
if (shapeCoordinates != null) {
204-
// Cast will be removed when the method is introduced in the interface
205-
pathShape.setCoordinates(shapeCoordinates, currentPoint);
211+
private List<IPathShape> addMoveToShapes(IPathShape pathShape, String[] pathProperties) {
212+
List<IPathShape> shapes = new ArrayList<>();
213+
int argumentCount = 2;
214+
String[] shapeCoordinates = getShapeCoordinates(pathShape, null, Arrays.copyOfRange(pathProperties, 1, 3));
215+
zOperator = new ClosePath(pathShape.isRelative());
216+
zOperator.setCoordinates(shapeCoordinates, currentPoint);
217+
pathShape.setCoordinates(shapeCoordinates, currentPoint);
218+
currentPoint = pathShape.getEndingPoint();
219+
shapes.add(pathShape);
220+
IPathShape previousShape = pathShape;
221+
if (pathProperties.length > 3) {
222+
for (int index = 3; index < pathProperties.length; index += argumentCount) {
223+
if (index + 2 > pathProperties.length) {
224+
break;
225+
}
226+
pathShape = pathShape.isRelative() ? SvgPathShapeFactory.createPathShape("l") : SvgPathShapeFactory.createPathShape("L");
227+
shapeCoordinates = getShapeCoordinates(pathShape, previousShape, Arrays.copyOfRange(pathProperties, index, index + 2));
228+
pathShape.setCoordinates(shapeCoordinates, previousShape.getEndingPoint());
229+
shapes.add(pathShape);
230+
previousShape = pathShape;
206231
}
207-
currentPoint = pathShape.getEndingPoint(); // unsupported operators are ignored.
208-
shapes.add(pathShape);
209232
}
210233
return shapes;
211234
}

svg/src/main/java/com/itextpdf/svg/renderers/path/IPathShape.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,11 @@ public interface IPathShape {
5959
/**
6060
* This method sets the coordinates for the path painting operator and does internal
6161
* preprocessing, if necessary
62-
* @param coordinates an array containing point values for path coordinates
62+
* @param inputCoordinates an array containing point values for path coordinates
6363
* @param startPoint the ending point of the previous operator, or, in broader terms,
6464
* the point that the coordinates should be absolutized against, for relative operators
6565
*/
66-
void setCoordinates(String[] coordinates, Point startPoint);
66+
void setCoordinates(String[] inputCoordinates, Point startPoint);
6767

6868
/**
6969
* Gets the ending point on the canvas after the path shape has been drawn

svg/src/main/java/com/itextpdf/svg/renderers/path/IPathShapeMapper.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,5 @@ public interface IPathShapeMapper {
5858
*/
5959
Map<String, IPathShape> getMapping();
6060

61+
Map<String, Integer> getArgumentCount();
6162
}

svg/src/main/java/com/itextpdf/svg/renderers/path/SvgPathShapeFactory.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ This file is part of the iText (R) project.
4444

4545
import com.itextpdf.svg.renderers.path.impl.PathShapeMapper;
4646

47+
import java.util.Map;
48+
4749
/**
4850
* A factory for creating {@link IPathShape} objects.
4951
*/
@@ -61,4 +63,18 @@ private SvgPathShapeFactory() {
6163
public static IPathShape createPathShape(String name) {
6264
return new PathShapeMapper().getMapping().get(name);
6365
}
66+
67+
/**
68+
* Finds the appropriate number of arguments for a path command, based on the passed Svg path data instruction tag.
69+
*
70+
* @param name svg path element's path-data instruction name.
71+
* @return an integer value with the required number of arguments or null if there is no mapping for the given value
72+
*/
73+
public static int getArgumentCount(String name) {
74+
Map<String, Integer> map = new PathShapeMapper().getArgumentCount();
75+
if (map.containsKey(name.toUpperCase())) {
76+
return (int) map.get(name.toUpperCase());
77+
}
78+
return -1;
79+
}
6480
}

svg/src/main/java/com/itextpdf/svg/renderers/path/impl/AbstractPathShape.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,22 @@ public abstract class AbstractPathShape implements IPathShape {
6262
* Whether this is a relative operator or not.
6363
*/
6464
protected boolean relative;
65+
protected final IOperatorConverter copier;
66+
// Original coordinates from path instruction, according to the (x1 y1 x2 y2 x y)+ spec
67+
protected String[] coordinates;
68+
69+
public AbstractPathShape() {
70+
this(false);
71+
}
72+
73+
public AbstractPathShape(boolean relative) {
74+
this(relative, new DefaultOperatorConverter());
75+
}
76+
77+
public AbstractPathShape(boolean relative, IOperatorConverter copier) {
78+
this.relative = relative;
79+
this.copier = copier;
80+
}
6581

6682
@Override
6783
public boolean isRelative() {
@@ -72,4 +88,9 @@ protected Point createPoint(String coordX, String coordY) {
7288
return new Point((float)CssUtils.parseFloat(coordX), (float)CssUtils.parseFloat(coordY));
7389
}
7490

91+
@Override
92+
public Point getEndingPoint() {
93+
return createPoint(coordinates[coordinates.length - 2], coordinates[coordinates.length - 1]);
94+
}
95+
7596
}

svg/src/main/java/com/itextpdf/svg/renderers/path/impl/ClosePath.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ This file is part of the iText (R) project.
4646
* Implements closePath(Z) attribute of SVG's path element
4747
* */
4848
public class ClosePath extends LineTo {
49+
static final int ARGUMENT_SIZE = 0;
4950

5051
public ClosePath() {
5152
this(false);

svg/src/main/java/com/itextpdf/svg/renderers/path/impl/CurveTo.java

Lines changed: 24 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -47,70 +47,55 @@ This file is part of the iText (R) project.
4747
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
4848
import com.itextpdf.styledxmlparser.css.util.CssUtils;
4949
import com.itextpdf.svg.exceptions.SvgExceptionMessageConstant;
50-
import com.itextpdf.svg.utils.SvgCoordinateUtils;
5150

5251
import java.util.Arrays;
5352

5453
/***
55-
* Implements curveTo(L) attribute of SVG's path element
54+
* Implements curveTo(C) attribute of SVG's path element
5655
* */
57-
public class CurveTo extends AbstractPathShape {
56+
public class CurveTo extends AbstractPathShape implements IControlPointCurve {
5857

59-
// Original coordinates from path instruction, according to the (x1 y1 x2 y2 x y)+ spec
60-
private String[][] coordinates;
58+
static final int ARGUMENT_SIZE = 6;
6159

6260
public CurveTo() {
6361
this(false);
6462
}
6563

6664
public CurveTo(boolean relative) {
67-
this.relative = relative;
65+
this(relative, new DefaultOperatorConverter());
66+
}
67+
68+
public CurveTo(boolean relative, IOperatorConverter copier) {
69+
super(relative, copier);
6870
}
6971

7072
@Override
7173
public void draw(PdfCanvas canvas) {
72-
for (int i = 0; i < coordinates.length; i++) {
73-
float x1 = CssUtils.parseAbsoluteLength(coordinates[i][0]);
74-
float y1 = CssUtils.parseAbsoluteLength(coordinates[i][1]);
75-
float x2 = CssUtils.parseAbsoluteLength(coordinates[i][2]);
76-
float y2 = CssUtils.parseAbsoluteLength(coordinates[i][3]);
77-
float x = CssUtils.parseAbsoluteLength(coordinates[i][4]);
78-
float y = CssUtils.parseAbsoluteLength(coordinates[i][5]);
79-
canvas.curveTo(x1, y1, x2, y2, x, y);
80-
}
74+
float x1 = CssUtils.parseAbsoluteLength(coordinates[0]);
75+
float y1 = CssUtils.parseAbsoluteLength(coordinates[1]);
76+
float x2 = CssUtils.parseAbsoluteLength(coordinates[2]);
77+
float y2 = CssUtils.parseAbsoluteLength(coordinates[3]);
78+
float x = CssUtils.parseAbsoluteLength(coordinates[4]);
79+
float y = CssUtils.parseAbsoluteLength(coordinates[5]);
80+
canvas.curveTo(x1, y1, x2, y2, x, y);
8181
}
8282

8383
@Override
84-
public void setCoordinates(String[] coordinates, Point startPoint) {
85-
if (coordinates.length == 0 || coordinates.length % 6 != 0) {
86-
throw new IllegalArgumentException(MessageFormatUtil.format(SvgExceptionMessageConstant.CURVE_TO_EXPECTS_FOLLOWING_PARAMETERS_GOT_0, Arrays.toString(coordinates)));
84+
public void setCoordinates(String[] inputCoordinates, Point startPoint) {
85+
if (inputCoordinates.length < ARGUMENT_SIZE) {
86+
throw new IllegalArgumentException(MessageFormatUtil.format(SvgExceptionMessageConstant.CURVE_TO_EXPECTS_FOLLOWING_PARAMETERS_GOT_0, Arrays.toString(inputCoordinates)));
8787
}
88-
this.coordinates = new String[coordinates.length / 6][];
88+
coordinates = new String[ARGUMENT_SIZE];
89+
System.arraycopy(inputCoordinates, 0, coordinates, 0, ARGUMENT_SIZE);
8990
double[] initialPoint = new double[] {startPoint.getX(), startPoint.getY()};
90-
for (int i = 0; i < coordinates.length; i += 6) {
91-
String[] curCoordinates = new String[]{coordinates[i], coordinates[i + 1], coordinates[i + 2],
92-
coordinates[i + 3], coordinates[i + 4], coordinates[i + 5]};
93-
if (isRelative()) {
94-
curCoordinates = SvgCoordinateUtils.makeRelativeOperatorCoordinatesAbsolute(curCoordinates, initialPoint);
95-
initialPoint[0] = (float)CssUtils.parseFloat(curCoordinates[4]);
96-
initialPoint[1] = (float)CssUtils.parseFloat(curCoordinates[5]);
97-
}
98-
this.coordinates[i / 6] = curCoordinates;
91+
if (isRelative()) {
92+
coordinates = copier.makeCoordinatesAbsolute(coordinates, initialPoint);
9993
}
10094
}
10195

102-
/**
103-
* Returns coordinates of the last control point (the one closer to the ending point)
104-
* in the series of Bezier curves (possibly, one curve), in SVG space coordinates
105-
* @return coordinates of the last control points in SVG space coordinates
106-
*/
107-
public Point getLastControlPoint() {
108-
return createPoint(coordinates[coordinates.length - 1][2], coordinates[coordinates.length - 1][3]);
109-
}
110-
11196
@Override
112-
public Point getEndingPoint() {
113-
return createPoint(coordinates[coordinates.length - 1][4], coordinates[coordinates.length - 1][5]);
97+
public Point getLastControlPoint() {
98+
return createPoint(coordinates[2], coordinates[3]);
11499
}
115100

116101
}

0 commit comments

Comments
 (0)