Skip to content

Commit ac9e726

Browse files
committed
SVG: support vector-effect: non-scaling-stroke
Allow PdfCanvas.roundRectangle to accept 2 radius parameters Support href in img element DEVSIX-8861, DEVSIX-1878
1 parent ce35dbc commit ac9e726

File tree

59 files changed

+903
-114
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+903
-114
lines changed

kernel/src/main/java/com/itextpdf/kernel/pdf/canvas/PdfCanvas.java

Lines changed: 85 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1227,14 +1227,34 @@ public PdfCanvas rectangle(Rectangle rectangle) {
12271227
/**
12281228
* Draws rounded rectangle.
12291229
*
1230-
* @param x x coordinate of the starting point.
1231-
* @param y y coordinate of the starting point.
1232-
* @param width width.
1233-
* @param height height.
1234-
* @param radius radius of the arc corner.
1235-
* @return current canvas.
1230+
* @param x x coordinate of the starting point
1231+
* @param y y coordinate of the starting point
1232+
* @param width width
1233+
* @param height height
1234+
* @param radius radius of the arc corner
1235+
*
1236+
* @return current canvas
12361237
*/
12371238
public PdfCanvas roundRectangle(double x, double y, double width, double height, double radius) {
1239+
return roundRectangle(x, y, width, height, radius, radius, null);
1240+
}
1241+
1242+
/**
1243+
* Draws rounded rectangle.
1244+
*
1245+
* @param x x coordinate of the starting point
1246+
* @param y y coordinate of the starting point
1247+
* @param width width
1248+
* @param height height
1249+
* @param rx x radius of the arc corner
1250+
* @param ry y radius of the arc corner
1251+
* @param transform {@link AffineTransform} to apply before drawing,
1252+
* or {@code null} in case transform shouldn't be applied
1253+
*
1254+
* @return current canvas
1255+
*/
1256+
public PdfCanvas roundRectangle(double x, double y, double width, double height, double rx, double ry,
1257+
AffineTransform transform) {
12381258
if (width < 0) {
12391259
x += width;
12401260
width = -width;
@@ -1243,18 +1263,29 @@ public PdfCanvas roundRectangle(double x, double y, double width, double height,
12431263
y += height;
12441264
height = -height;
12451265
}
1246-
if (radius < 0)
1247-
radius = -radius;
1248-
final double curv = 0.4477f;
1249-
moveTo(x + radius, y);
1250-
lineTo(x + width - radius, y);
1251-
curveTo(x + width - radius * curv, y, x + width, y + radius * curv, x + width, y + radius);
1252-
lineTo(x + width, y + height - radius);
1253-
curveTo(x + width, y + height - radius * curv, x + width - radius * curv, y + height, x + width - radius, y + height);
1254-
lineTo(x + radius, y + height);
1255-
curveTo(x + radius * curv, y + height, x, y + height - radius * curv, x, y + height - radius);
1256-
lineTo(x, y + radius);
1257-
curveTo(x, y + radius * curv, x + radius * curv, y, x + radius, y);
1266+
if (rx < 0) {
1267+
rx = -rx;
1268+
}
1269+
if (ry < 0) {
1270+
ry = -ry;
1271+
}
1272+
1273+
double[] points = getEllipseRoundedRectPoints(x, y, width, height, rx, ry);
1274+
if (transform != null) {
1275+
transform.transform(points, 0, points, 0, points.length / 2);
1276+
}
1277+
1278+
int i = 0;
1279+
this.moveTo(points[i++], points[i++])
1280+
.lineTo(points[i++], points[i++])
1281+
.curveTo(points[i++], points[i++], points[i++], points[i++], points[i++], points[i++])
1282+
.lineTo(points[i++], points[i++])
1283+
.curveTo(points[i++], points[i++], points[i++], points[i++], points[i++], points[i++])
1284+
.lineTo(points[i++], points[i++])
1285+
.curveTo(points[i++], points[i++], points[i++], points[i++], points[i++], points[i++])
1286+
.lineTo(points[i++], points[i++])
1287+
.curveTo(points[i++], points[i++], points[i++], points[i++], points[i++], points[i])
1288+
.closePath();
12581289
return this;
12591290
}
12601291

@@ -2630,6 +2661,42 @@ private static boolean isIdentityMatrix(float a, float b, float c, float d, floa
26302661
Math.abs(1 - d) < IDENTITY_MATRIX_EPS && Math.abs(e) < IDENTITY_MATRIX_EPS && Math.abs(f) < IDENTITY_MATRIX_EPS;
26312662
}
26322663

2664+
private static double[] getEllipseRoundedRectPoints(double x, double y, double width, double height,
2665+
double rx, double ry) {
2666+
/*
2667+
2668+
y+h -> ____________________________
2669+
/ \
2670+
/ \
2671+
y+h-ry -> / \
2672+
| |
2673+
| |
2674+
| |
2675+
| |
2676+
y+ry -> \ /
2677+
\ /
2678+
y -> \____________________________/
2679+
^ ^ ^ ^
2680+
x x+rx x+w-rx x+w
2681+
2682+
*/
2683+
2684+
double[] pt1 = PdfCanvas.bezierArc(x + width - 2 * rx, y, x + width, y + 2 * ry, -90, 90).get(0);
2685+
double[] pt2 = PdfCanvas.bezierArc(x + width, y + height - 2 * ry, x + width - 2 * rx, y + height, 0, 90).get(0);
2686+
double[] pt3 = PdfCanvas.bezierArc(x + 2 * rx, y + height, x, y + height - 2 * ry, 90, 90).get(0);
2687+
double[] pt4 = PdfCanvas.bezierArc(x, y + 2 * ry, x + 2 * rx, y, 180, 90).get(0);
2688+
2689+
return new double[]{x + rx, y,
2690+
x + width - rx, y,
2691+
pt1[2], pt1[3], pt1[4], pt1[5], pt1[6], pt1[7],
2692+
x + width, y + height - ry,
2693+
pt2[2], pt2[3], pt2[4], pt2[5], pt2[6], pt2[7],
2694+
x + rx, y + height,
2695+
pt3[2], pt3[3], pt3[4], pt3[5], pt3[6], pt3[7],
2696+
x, y + ry,
2697+
pt4[2], pt4[3], pt4[4], pt4[5], pt4[6], pt4[7]};
2698+
}
2699+
26332700
/**
26342701
* This method is used to traverse parent tree and begin all layers in it.
26352702
* If layer was already begun during method call, it will not be processed again.

svg/src/main/java/com/itextpdf/svg/SvgConstants.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -925,7 +925,12 @@ public static final class Attributes extends CommonAttributeConstants {
925925
public static final String Y2 = "y2";
926926

927927
/**
928-
* Attribute defining version
928+
* Attribute defining vector-effect.
929+
*/
930+
public static final String VECTOR_EFFECT = "vector-effect";
931+
932+
/**
933+
* Attribute defining version.
929934
*/
930935
public static final String VERSION = "version";
931936
}
@@ -993,10 +998,15 @@ public static final class Values {
993998
public static final String MEET = "meet";
994999

9951000
/**
996-
* Value representing the "none" value".
1001+
* Value representing the "none" value.
9971002
*/
9981003
public static final String NONE = "none";
9991004

1005+
/**
1006+
* Value representing the "non-scaling-stroke" value for vector-effect attribute.
1007+
*/
1008+
public static final String NONE_SCALING_STROKE = "non-scaling-stroke";
1009+
10001010
/**
10011011
* Value representing the units relation "objectBoundingBox".
10021012
*/

svg/src/main/java/com/itextpdf/svg/logs/SvgLogMessageConstant.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ public final class SvgLogMessageConstant {
8181
"Non-invertible transformation matrix was used in a clipping path context. Clipped elements may show "
8282
+ "undefined behavior.";
8383

84+
public static final String NON_INVERTIBLE_TRANSFORMATION_MATRIX_FOR_NON_SCALING_STROKE =
85+
"Unable to get inverse transformation matrix and thus apply non-scaling-stroke vector-effect property: " +
86+
"some of the transformation matrices, written to the document, have a determinant of zero value.";
87+
8488
public static final String UNABLE_TO_GET_INVERSE_MATRIX_DUE_TO_ZERO_DETERMINANT =
8589
"Unable to get inverse transformation matrix and thus calculate a viewport for the element because some of"
8690
+ " the transformation matrices, which are written to document, have a determinant of zero value. "

svg/src/main/java/com/itextpdf/svg/renderers/SvgDrawContext.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ This file is part of the iText (R) project.
3535
import com.itextpdf.svg.exceptions.SvgProcessingException;
3636
import com.itextpdf.svg.utils.SvgTextProperties;
3737

38+
import java.util.ArrayList;
3839
import java.util.Deque;
3940
import java.util.HashMap;
4041
import java.util.LinkedList;
42+
import java.util.List;
4143
import java.util.Map;
4244
import java.util.Stack;
4345

@@ -495,4 +497,26 @@ public AffineTransform getClippingElementTransform() {
495497
public void resetClippingElementTransform() {
496498
this.clippingElementTransform.setToIdentity();
497499
}
500+
501+
/**
502+
* Concatenates all transformations applied from the top level of the svg to the current one.
503+
*
504+
* @return {@link AffineTransform} instance
505+
*/
506+
public AffineTransform getConcatenatedTransform() {
507+
List<PdfCanvas> canvasList = new ArrayList<>();
508+
int canvasesSize = this.size();
509+
for (int i = 0; i < canvasesSize; i++) {
510+
canvasList.add(this.popCanvas());
511+
}
512+
AffineTransform transform = new AffineTransform();
513+
for (int i = canvasList.size() - 1; i >= 0; i--) {
514+
PdfCanvas pdfCanvas = canvasList.get(i);
515+
Matrix matrix = pdfCanvas.getGraphicsState().getCtm();
516+
transform.concatenate(new AffineTransform(matrix.get(0), matrix.get(1), matrix.get(3),
517+
matrix.get(4), matrix.get(6), matrix.get(7)));
518+
this.pushCanvas(pdfCanvas);
519+
}
520+
return transform;
521+
}
498522
}

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

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -292,19 +292,7 @@ private static boolean isOverflowVisible(AbstractSvgNodeRenderer currentElement)
292292
* @return the set to {@code PdfStream} bbox
293293
*/
294294
private static Rectangle getBBoxAccordingToVisibleOverflow(SvgDrawContext context) {
295-
List<PdfCanvas> canvases = new ArrayList<>();
296-
int canvasesSize = context.size();
297-
for (int i = 0; i < canvasesSize; i++) {
298-
canvases.add(context.popCanvas());
299-
}
300-
AffineTransform transform = new AffineTransform();
301-
for (int i = canvases.size() - 1; i >= 0; i--) {
302-
PdfCanvas canvas = canvases.get(i);
303-
Matrix matrix = canvas.getGraphicsState().getCtm();
304-
transform.concatenate(new AffineTransform(matrix.get(0), matrix.get(1), matrix.get(3),
305-
matrix.get(4), matrix.get(6), matrix.get(7)));
306-
context.pushCanvas(canvas);
307-
}
295+
AffineTransform transform = context.getConcatenatedTransform();
308296
try {
309297
transform = transform.createInverse();
310298
} catch (NoninvertibleTransformException e) {

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ This file is part of the iText (R) project.
2626
import com.itextpdf.kernel.colors.ColorConstants;
2727
import com.itextpdf.kernel.colors.DeviceRgb;
2828
import com.itextpdf.kernel.geom.AffineTransform;
29+
import com.itextpdf.kernel.geom.NoninvertibleTransformException;
2930
import com.itextpdf.kernel.geom.Rectangle;
3031
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
3132
import com.itextpdf.kernel.pdf.extgstate.PdfExtGState;
@@ -44,12 +45,15 @@ This file is part of the iText (R) project.
4445
import com.itextpdf.svg.css.SvgStrokeParameterConverter;
4546
import com.itextpdf.svg.css.SvgStrokeParameterConverter.PdfLineDashParameters;
4647
import com.itextpdf.svg.css.impl.SvgNodeRendererInheritanceResolver;
48+
import com.itextpdf.svg.logs.SvgLogMessageConstant;
4749
import com.itextpdf.svg.renderers.IMarkerCapable;
4850
import com.itextpdf.svg.renderers.ISvgNodeRenderer;
4951
import com.itextpdf.svg.renderers.ISvgPaintServer;
5052
import com.itextpdf.svg.renderers.SvgDrawContext;
5153
import com.itextpdf.svg.utils.SvgCssUtils;
5254
import com.itextpdf.svg.utils.TransformUtils;
55+
import org.slf4j.Logger;
56+
import org.slf4j.LoggerFactory;
5357

5458
import java.util.HashMap;
5559
import java.util.Map;
@@ -62,6 +66,8 @@ public abstract class AbstractSvgNodeRenderer implements ISvgNodeRenderer {
6266
private static final MarkerVertexType[] MARKER_VERTEX_TYPES = new MarkerVertexType[] {MarkerVertexType.MARKER_START,
6367
MarkerVertexType.MARKER_MID, MarkerVertexType.MARKER_END};
6468

69+
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractSvgNodeRenderer.class);
70+
6571
/**
6672
* Map that contains attributes and styles used for drawing operations.
6773
*/
@@ -286,6 +292,31 @@ ClipPathSvgNodeRenderer getParentClipPath() {
286292
return null;
287293
}
288294

295+
/**
296+
* Applies non-scaling-stroke vector-effect to this renderer by concatenating all transformations applied
297+
* from the top level of the svg to the current one, inverting it and applying to the current canvas.
298+
*
299+
* @param context the SVG draw context
300+
*
301+
* @return the transformation that was inverted and applied to this renderer
302+
* to achieve non-scaling-stroke vector-effect
303+
*/
304+
AffineTransform applyNonScalingStrokeTransform(SvgDrawContext context) {
305+
AffineTransform transform = null;
306+
boolean isNonScalingStroke = doStroke && SvgConstants.Values.NONE_SCALING_STROKE.equals(
307+
getAttribute(SvgConstants.Attributes.VECTOR_EFFECT));
308+
if (isNonScalingStroke) {
309+
transform = context.getConcatenatedTransform();
310+
try {
311+
context.getCurrentCanvas().concatMatrix(transform.createInverse());
312+
} catch (NoninvertibleTransformException e) {
313+
LOGGER.warn(SvgLogMessageConstant.NON_INVERTIBLE_TRANSFORMATION_MATRIX_FOR_NON_SCALING_STROKE);
314+
transform = null;
315+
}
316+
}
317+
return transform;
318+
}
319+
289320
/**
290321
* Calculate the transformation for the viewport based on the context. Only used by elements that can create
291322
* viewports

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ This file is part of the iText (R) project.
2222
*/
2323
package com.itextpdf.svg.renderers.impl;
2424

25+
import com.itextpdf.kernel.geom.AffineTransform;
2526
import com.itextpdf.kernel.geom.Rectangle;
2627
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
2728
import com.itextpdf.svg.SvgConstants;
@@ -42,9 +43,14 @@ protected void doDraw(SvgDrawContext context) {
4243
cv.writeLiteral("% ellipse\n");
4344
if (setParameters(context)) {
4445
// Use double type locally to have better precision of the result after applying arithmetic operations
45-
cv.moveTo((double) cx + (double) rx, cy);
46+
double[] startPoint = new double[]{(double) cx + (double) rx, cy};
47+
AffineTransform transform = applyNonScalingStrokeTransform(context);
48+
if (transform != null) {
49+
transform.transform(startPoint, 0, startPoint, 0, startPoint.length / 2);
50+
}
51+
cv.moveTo(startPoint[0], startPoint[1]);
4652
DrawUtils.arc((double) cx - (double) rx, (double) cy - (double) ry, (double) cx + (double) rx,
47-
(double) cy + (double) ry, 0, 360, cv);
53+
(double) cy + (double) ry, 0, 360, cv, transform);
4854
}
4955
}
5056

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,12 @@ protected void doDraw(SvgDrawContext context) {
5555
if (resourceResolver == null || this.attributesAndStyles == null) {
5656
return;
5757
}
58-
String uri = this.attributesAndStyles.get(SvgConstants.Attributes.XLINK_HREF);
58+
59+
String uri = this.attributesAndStyles.get(SvgConstants.Attributes.HREF);
60+
if (uri == null) {
61+
uri = this.attributesAndStyles.get(SvgConstants.Attributes.XLINK_HREF);
62+
}
63+
5964
PdfXObject xObject = resourceResolver.retrieveImage(uri);
6065

6166
if (xObject == null) {

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ This file is part of the iText (R) project.
2222
*/
2323
package com.itextpdf.svg.renderers.impl;
2424

25+
import com.itextpdf.kernel.geom.AffineTransform;
2526
import com.itextpdf.kernel.geom.Rectangle;
2627
import com.itextpdf.kernel.geom.Vector;
2728
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
@@ -51,7 +52,13 @@ public void doDraw(SvgDrawContext context) {
5152
canvas.writeLiteral("% line\n");
5253

5354
if (setParameters(context)) {
54-
canvas.moveTo(x1, y1).lineTo(x2, y2);
55+
float[] points = new float[]{x1, y1, x2, y2};
56+
AffineTransform transform = applyNonScalingStrokeTransform(context);
57+
if (transform != null) {
58+
transform.transform(points, 0, points, 0, points.length / 2);
59+
}
60+
int i = 0;
61+
canvas.moveTo(points[i++], points[i++]).lineTo(points[i++], points[i]);
5562
}
5663
}
5764

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ This file is part of the iText (R) project.
2222
*/
2323
package com.itextpdf.svg.renderers.impl;
2424

25+
import com.itextpdf.kernel.geom.AffineTransform;
2526
import com.itextpdf.kernel.geom.Point;
2627
import com.itextpdf.kernel.geom.Rectangle;
2728
import com.itextpdf.kernel.geom.Vector;
@@ -98,11 +99,14 @@ public class PathSvgNodeRenderer extends AbstractSvgNodeRenderer implements IMar
9899
public void doDraw(SvgDrawContext context) {
99100
PdfCanvas canvas = context.getCurrentCanvas();
100101
canvas.writeLiteral("% path\n");
102+
103+
AffineTransform transform = applyNonScalingStrokeTransform(context);
101104
for (IPathShape item : getShapes()) {
102105
if (item instanceof AbstractPathShape) {
103106
AbstractPathShape shape = (AbstractPathShape) item;
104107
shape.setParent(this);
105108
shape.setContext(context);
109+
shape.setTransform(transform);
106110
}
107111

108112
item.draw(context.getCurrentCanvas());

0 commit comments

Comments
 (0)