Skip to content

Commit 746e5a2

Browse files
author
Benoit Lagae
committed
Register clip-path & basic operations
DEVSIX-2067
1 parent b25e2be commit 746e5a2

24 files changed

+769
-56
lines changed

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ public static final class Tags {
107107
/**
108108
* Tag defining a clipping path. A clipping path defines the region where can be drawn. Anything outside the path won't be drawn.
109109
*/
110-
public static final String CLIP_PATH = "clipPath";
110+
public static final String CLIP_PATH = "clippath";
111111

112112
/**
113113
* Tag defining the color profile to be used.
@@ -487,6 +487,16 @@ public static final class Tags {
487487
*/
488488
public static final class Attributes extends CommonAttributeConstants {
489489

490+
/**
491+
* Attribute defining the clipping path to be applied to a specific shape or group of shapes.
492+
*/
493+
public static final String CLIP_PATH = "clip-path";
494+
495+
/**
496+
* Attribute defining the clipping rule in a clipping path (or element thereof).
497+
*/
498+
public static final String CLIP_RULE = "clip-rule";
499+
490500
/**
491501
* Attribute defining the x value of the center of a circle or ellipse.
492502
*/

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ public final class SvgLogMessageConstant {
6868
public static final String MISSING_HEIGHT="Top Svg tag has no defined height attribute and viewbox height is not present, so browser default of 150px is used";
6969
public static final String NAMED_OBJECT_NAME_NULL_OR_EMPTY = "The name of the named object can't be null or empty.";
7070
public static final String NAMED_OBJECT_NULL = "A named object can't be null.";
71+
public static final String NONINVERTIBLE_TRANSFORMATION_MATRIX_USED_IN_CLIP_PATH = "Non-invertible transformation matrix was used in a clipping path context. Clipped elements may show undefined behavior.";
7172
public static final String NOROOT = "No root found";
7273
public static final String PARAMETER_CANNOT_BE_NULL = "Parameters for this method cannot be null.";
7374
public static final String ROOT_SVG_NO_BBOX = "The root svg tag needs to have a bounding box defined.";

svg/src/main/java/com/itextpdf/svg/renderers/factories/DefaultSvgNodeRendererMapper.java

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

4545
import com.itextpdf.svg.SvgConstants;
4646
import com.itextpdf.svg.renderers.ISvgNodeRenderer;
47+
import com.itextpdf.svg.renderers.impl.ClipPathSvgNodeRenderer;
4748
import com.itextpdf.svg.renderers.impl.GroupSvgNodeRenderer;
4849
import com.itextpdf.svg.renderers.impl.CircleSvgNodeRenderer;
4950
import com.itextpdf.svg.renderers.impl.EllipseSvgNodeRenderer;
@@ -76,6 +77,7 @@ public Map<String, Class<? extends ISvgNodeRenderer>> getMapping() {
7677
Map<String, Class<? extends ISvgNodeRenderer>> result = new HashMap<>();
7778

7879
result.put(SvgConstants.Tags.CIRCLE, CircleSvgNodeRenderer.class);
80+
result.put(SvgConstants.Tags.CLIP_PATH, ClipPathSvgNodeRenderer.class);
7981
result.put(SvgConstants.Tags.DEFS, NoDrawOperationSvgNodeRenderer.class);
8082
result.put(SvgConstants.Tags.ELLIPSE, EllipseSvgNodeRenderer.class);
8183
result.put(SvgConstants.Tags.G, GroupSvgNodeRenderer.class);
@@ -102,7 +104,6 @@ public Collection<String> getIgnoredTags() {
102104
ignored.add(SvgConstants.Tags.ALT_GLYPH_DEF);
103105
ignored.add(SvgConstants.Tags.ALT_GLYPH_ITEM);
104106

105-
ignored.add(SvgConstants.Tags.CLIP_PATH);
106107
ignored.add(SvgConstants.Tags.COLOR_PROFILE);
107108

108109
ignored.add(SvgConstants.Tags.DESC);

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,4 +311,14 @@ public boolean equals(Object other){
311311
public int hashCode() {
312312
return super.hashCode()*7 + 255 + children.hashCode();
313313
}
314+
315+
@Override
316+
void setPartOfClipPath(boolean isPart) {
317+
super.setPartOfClipPath(isPart);
318+
for (ISvgNodeRenderer child : children) {
319+
if (child instanceof AbstractSvgNodeRenderer) {
320+
((AbstractSvgNodeRenderer) child).setPartOfClipPath(isPart);
321+
}
322+
}
323+
}
314324
}

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

Lines changed: 90 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ public abstract class AbstractSvgNodeRenderer implements ISvgNodeRenderer {
7171

7272
private boolean doFill = false;
7373
private boolean doStroke = false;
74+
boolean partOfClipPath;
7475

7576
@Override
7677
public void setParent(ISvgNodeRenderer parent) {
@@ -112,15 +113,38 @@ public final void draw(SvgDrawContext context) {
112113
}
113114
}
114115

115-
preDraw(context);
116-
doDraw(context);
117-
postDraw(context);
116+
/* If a (non-empty) clipping path exists, drawing operations must be surrounded by q/Q operators
117+
and may have to be drawn multiple times
118+
*/
119+
if (!drawInClipPath(context)) {
120+
preDraw(context);
121+
doDraw(context);
122+
postDraw(context);
123+
}
118124

119125
if (attributesAndStyles.containsKey(SvgConstants.Attributes.ID)) {
120126
context.removeUsedId(attributesAndStyles.get(SvgConstants.Attributes.ID));
121127
}
122128
}
123129

130+
private boolean drawInClipPath(SvgDrawContext context) {
131+
if (attributesAndStyles.containsKey(SvgConstants.Attributes.CLIP_PATH)) {
132+
String clipPathName = attributesAndStyles.get(SvgConstants.Attributes.CLIP_PATH);
133+
ISvgNodeRenderer template = context.getNamedObject(normalizeName(clipPathName));
134+
//Clone template to avoid muddying the state
135+
if (template instanceof ClipPathSvgNodeRenderer) {
136+
ClipPathSvgNodeRenderer clipPath = (ClipPathSvgNodeRenderer) template.createDeepCopy();
137+
clipPath.setClippedRenderer(this);
138+
clipPath.draw(context);
139+
return !clipPath.getChildren().isEmpty();
140+
}
141+
}
142+
return false;
143+
}
144+
145+
private String normalizeName(String name) {
146+
return name.replace("url(#", "").replace(")", "").trim();
147+
}
124148

125149
/**
126150
* Operations to perform before drawing an element.
@@ -132,52 +156,54 @@ void preDraw(SvgDrawContext context) {
132156
if (this.attributesAndStyles != null) {
133157
PdfCanvas currentCanvas = context.getCurrentCanvas();
134158

135-
// fill
136-
{
137-
String fillRawValue = getAttribute(SvgConstants.Attributes.FILL);
159+
if (!partOfClipPath) {
160+
// fill
161+
{
162+
String fillRawValue = getAttribute(SvgConstants.Attributes.FILL);
138163

139-
this.doFill = !SvgConstants.Values.NONE.equalsIgnoreCase(fillRawValue);
164+
this.doFill = !SvgConstants.Values.NONE.equalsIgnoreCase(fillRawValue);
140165

141-
if (doFill && canElementFill()) {
142-
Color color = ColorConstants.BLACK;
166+
if (doFill && canElementFill()) {
167+
Color color = ColorConstants.BLACK;
143168

144-
if (fillRawValue != null) {
145-
color = WebColors.getRGBColor(fillRawValue);
146-
}
169+
if (fillRawValue != null) {
170+
color = WebColors.getRGBColor(fillRawValue);
171+
}
147172

148-
currentCanvas.setFillColor(color);
173+
currentCanvas.setFillColor(color);
174+
}
149175
}
150-
}
151176

152-
// stroke
153-
{
154-
String strokeRawValue = getAttribute(SvgConstants.Attributes.STROKE);
155-
if (!SvgConstants.Values.NONE.equalsIgnoreCase(strokeRawValue)) {
156-
DeviceRgb rgbColor = WebColors.getRGBColor(strokeRawValue);
177+
// stroke
178+
{
179+
String strokeRawValue = getAttribute(SvgConstants.Attributes.STROKE);
180+
if (!SvgConstants.Values.NONE.equalsIgnoreCase(strokeRawValue)) {
181+
DeviceRgb rgbColor = WebColors.getRGBColor(strokeRawValue);
157182

158-
if (strokeRawValue != null && rgbColor != null) {
159-
currentCanvas.setStrokeColor(rgbColor);
183+
if (strokeRawValue != null && rgbColor != null) {
184+
currentCanvas.setStrokeColor(rgbColor);
160185

161-
String strokeWidthRawValue = getAttribute(SvgConstants.Attributes.STROKE_WIDTH);
186+
String strokeWidthRawValue = getAttribute(SvgConstants.Attributes.STROKE_WIDTH);
162187

163-
float strokeWidth = 1f;
188+
float strokeWidth = 1f;
164189

165-
if (strokeWidthRawValue != null) {
166-
strokeWidth = CssUtils.parseAbsoluteLength(strokeWidthRawValue);
167-
}
190+
if (strokeWidthRawValue != null) {
191+
strokeWidth = CssUtils.parseAbsoluteLength(strokeWidthRawValue);
192+
}
168193

169-
currentCanvas.setLineWidth(strokeWidth);
170-
doStroke = true;
194+
currentCanvas.setLineWidth(strokeWidth);
195+
doStroke = true;
196+
}
171197
}
172198
}
173-
}
174-
// opacity
175-
{
176-
String opacityValue = getAttribute(SvgConstants.Attributes.FILL_OPACITY);
177-
if (opacityValue != null && !SvgConstants.Values.NONE.equalsIgnoreCase(opacityValue)) {
178-
PdfExtGState gs1 = new PdfExtGState();
179-
gs1.setFillOpacity(Float.valueOf(opacityValue));
180-
currentCanvas.setExtGState(gs1);
199+
// opacity
200+
{
201+
String opacityValue = getAttribute(SvgConstants.Attributes.FILL_OPACITY);
202+
if (opacityValue != null && !SvgConstants.Values.NONE.equalsIgnoreCase(opacityValue)) {
203+
PdfExtGState gs1 = new PdfExtGState();
204+
gs1.setFillOpacity(Float.valueOf(opacityValue));
205+
currentCanvas.setExtGState(gs1);
206+
}
181207
}
182208
}
183209
}
@@ -227,28 +253,36 @@ void postDraw(SvgDrawContext context) {
227253
PdfCanvas currentCanvas = context.getCurrentCanvas();
228254

229255
// fill-rule
230-
if (doFill && canElementFill()) {
231-
String fillRuleRawValue = getAttribute(SvgConstants.Attributes.FILL_RULE);
232-
233-
if (SvgConstants.Values.FILL_RULE_EVEN_ODD.equalsIgnoreCase(fillRuleRawValue)) {
234-
// TODO RND-878
235-
if (doStroke) {
236-
currentCanvas.eoFillStroke();
237-
} else {
238-
currentCanvas.eoFill();
239-
}
256+
if (partOfClipPath) {
257+
currentCanvas.closePath();
258+
if (SvgConstants.Values.FILL_RULE_EVEN_ODD.equalsIgnoreCase(this.getAttribute(SvgConstants.Attributes.CLIP_RULE))) {
259+
currentCanvas.eoClip();
240260
} else {
241-
if (doStroke) {
242-
currentCanvas.fillStroke();
261+
currentCanvas.clip();
262+
}
263+
currentCanvas.newPath();
264+
} else {
265+
if (doFill && canElementFill()) {
266+
String fillRuleRawValue = getAttribute(SvgConstants.Attributes.FILL_RULE);
267+
268+
if (SvgConstants.Values.FILL_RULE_EVEN_ODD.equalsIgnoreCase(fillRuleRawValue)) {
269+
if (doStroke) {
270+
currentCanvas.eoFillStroke();
271+
} else {
272+
currentCanvas.eoFill();
273+
}
243274
} else {
244-
currentCanvas.fill();
275+
if (doStroke) {
276+
currentCanvas.fillStroke();
277+
} else {
278+
currentCanvas.fill();
279+
}
245280
}
281+
} else if (doStroke) {
282+
currentCanvas.stroke();
246283
}
247-
} else if (doStroke) {
248-
currentCanvas.stroke();
284+
currentCanvas.closePath();
249285
}
250-
251-
currentCanvas.closePath();
252286
}
253287
}
254288

@@ -325,4 +359,7 @@ protected void deepCopyAttributesAndStyles(ISvgNodeRenderer deepCopy){
325359
}
326360
}
327361

362+
void setPartOfClipPath(boolean value) {
363+
partOfClipPath = value;
364+
}
328365
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
This file is part of the iText (R) project.
3+
Copyright (c) 1998-2018 iText Group NV
4+
Authors: iText Software.
5+
6+
This program is free software; you can redistribute it and/or modify
7+
it under the terms of the GNU Affero General Public License version 3
8+
as published by the Free Software Foundation with the addition of the
9+
following permission added to Section 15 as permitted in Section 7(a):
10+
FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
11+
ITEXT GROUP. ITEXT GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
12+
OF THIRD PARTY RIGHTS
13+
14+
This program is distributed in the hope that it will be useful, but
15+
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
16+
or FITNESS FOR A PARTICULAR PURPOSE.
17+
See the GNU Affero General Public License for more details.
18+
You should have received a copy of the GNU Affero General Public License
19+
along with this program; if not, see http://www.gnu.org/licenses or write to
20+
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21+
Boston, MA, 02110-1301 USA, or download the license from the following URL:
22+
http://itextpdf.com/terms-of-use/
23+
24+
The interactive user interfaces in modified source and object code versions
25+
of this program must display Appropriate Legal Notices, as required under
26+
Section 5 of the GNU Affero General Public License.
27+
28+
In accordance with Section 7(b) of the GNU Affero General Public License,
29+
a covered work must retain the producer line in every PDF that is created
30+
or manipulated using iText.
31+
32+
You can be released from the requirements of the license by purchasing
33+
a commercial license. Buying such a license is mandatory as soon as you
34+
develop commercial activities involving the iText software without
35+
disclosing the source code of your own applications.
36+
These activities include: offering paid services to customers as an ASP,
37+
serving PDFs on the fly in a web application, shipping iText with a closed
38+
source product.
39+
40+
For more information, please contact iText Software Corp. at this
41+
42+
*/
43+
package com.itextpdf.svg.renderers.impl;
44+
45+
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
46+
import com.itextpdf.svg.renderers.ISvgNodeRenderer;
47+
import com.itextpdf.svg.renderers.SvgDrawContext;
48+
49+
/**
50+
* This renderer represents a collection of elements (simple shapes and paths).
51+
* The elements are not drawn visibly, but the union of their shapes will be used
52+
* to only show the parts of the drawn objects that fall within the clipping path.
53+
*
54+
* In PDF, the clipping path operators use the intersection of all its elements, not the union (as in SVG);
55+
* thus, we need to draw the clipped elements multiple times if the clipping path consists of multiple elements.
56+
*/
57+
public class ClipPathSvgNodeRenderer extends AbstractBranchSvgNodeRenderer {
58+
59+
private AbstractSvgNodeRenderer clippedRenderer;
60+
61+
@Override
62+
public ISvgNodeRenderer createDeepCopy() {
63+
AbstractBranchSvgNodeRenderer copy = new ClipPathSvgNodeRenderer();
64+
deepCopyAttributesAndStyles(copy);
65+
deepCopyChildren(copy);
66+
return copy;
67+
}
68+
69+
void preDraw(SvgDrawContext context) {}
70+
void postDraw(SvgDrawContext context) {}
71+
72+
@Override
73+
protected void doDraw(SvgDrawContext context) {
74+
PdfCanvas currentCanvas = context.getCurrentCanvas();
75+
for (ISvgNodeRenderer child : getChildren()) {
76+
currentCanvas.saveState();
77+
78+
if (child instanceof AbstractSvgNodeRenderer) {
79+
((AbstractSvgNodeRenderer) child).setPartOfClipPath(true);
80+
}
81+
82+
child.draw(context);
83+
84+
if (child instanceof AbstractSvgNodeRenderer) {
85+
((AbstractSvgNodeRenderer) child).setPartOfClipPath(false);
86+
}
87+
88+
if (clippedRenderer != null) {
89+
clippedRenderer.preDraw(context);
90+
clippedRenderer.doDraw(context);
91+
clippedRenderer.postDraw(context);
92+
}
93+
94+
currentCanvas.restoreState();
95+
}
96+
97+
}
98+
99+
public void setClippedRenderer(AbstractSvgNodeRenderer clippedRenderer) {
100+
this.clippedRenderer = clippedRenderer;
101+
}
102+
}

0 commit comments

Comments
 (0)