Skip to content

Commit 82c8d0b

Browse files
committed
SVG: support clipping paths on text and don't write clip-path itself to content stream
* Update TODO and test for clip-path CSS property DEVSIX-2588, DEVSIX-2827, DEVSIX-2828
1 parent 0bedf08 commit 82c8d0b

File tree

59 files changed

+677
-145
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

+677
-145
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
This file is part of the iText (R) project.
3+
Copyright (c) 1998-2025 Apryse Group NV
4+
Authors: Apryse Software.
5+
6+
This program is offered under a commercial and under the AGPL license.
7+
For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below.
8+
9+
AGPL licensing:
10+
This program is free software: you can redistribute it and/or modify
11+
it under the terms of the GNU Affero General Public License as published by
12+
the Free Software Foundation, either version 3 of the License, or
13+
(at your option) any later version.
14+
15+
This program is distributed in the hope that it will be useful,
16+
but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
GNU Affero General Public License for more details.
19+
20+
You should have received a copy of the GNU Affero General Public License
21+
along with this program. If not, see <https://www.gnu.org/licenses/>.
22+
*/
23+
package com.itextpdf.layout.properties;
24+
25+
import com.itextpdf.layout.renderer.TextRenderer;
26+
27+
/**
28+
* The interface is used to execute some logic before canvas state
29+
* restoring in {@link TextRenderer} which happens for text and underline.
30+
*/
31+
public interface IBeforeTextRestoreExecutor {
32+
/**
33+
* Execute code before canvas state restoring in {@link TextRenderer}.
34+
*/
35+
void execute();
36+
}

layout/src/main/java/com/itextpdf/layout/properties/Property.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public final class Property {
4848
public static final int BACKGROUND = 6;
4949
public static final int BACKGROUND_IMAGE = 90;
5050
public static final int BASE_DIRECTION = 7;
51+
public static final int BEFORE_TEXT_RESTORE_EXECUTOR = 157;
5152
public static final int BOLD_SIMULATION = 8;
5253
public static final int BORDER_BOTTOM = 10;
5354
public static final int BORDER_BOTTOM_LEFT_RADIUS = 113;
@@ -242,13 +243,14 @@ public final class Property {
242243
* related to textual operations. Indicates whether or not this type of property is inheritable.
243244
*/
244245
private static final boolean[] INHERITED_PROPERTIES;
245-
private static final int MAX_INHERITED_PROPERTY_ID = 156;
246+
private static final int MAX_INHERITED_PROPERTY_ID = 157;
246247

247248
static {
248249
INHERITED_PROPERTIES = new boolean[MAX_INHERITED_PROPERTY_ID + 1];
249250

250251
INHERITED_PROPERTIES[Property.APPEARANCE_STREAM_LAYOUT] = true;
251252
INHERITED_PROPERTIES[Property.BASE_DIRECTION] = true;
253+
INHERITED_PROPERTIES[Property.BEFORE_TEXT_RESTORE_EXECUTOR] = true;
252254
INHERITED_PROPERTIES[Property.BOLD_SIMULATION] = true;
253255
INHERITED_PROPERTIES[Property.CAPTION_SIDE] = true;
254256
INHERITED_PROPERTIES[Property.CHARACTER_SPACING] = true;

layout/src/main/java/com/itextpdf/layout/renderer/TextRenderer.java

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ This file is part of the iText (R) project.
4343
import com.itextpdf.kernel.pdf.canvas.CanvasArtifact;
4444
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
4545
import com.itextpdf.kernel.pdf.canvas.PdfCanvasConstants;
46+
import com.itextpdf.kernel.pdf.canvas.PdfCanvasConstants.TextRenderingMode;
4647
import com.itextpdf.kernel.pdf.tagutils.TagTreePointer;
4748
import com.itextpdf.layout.borders.Border;
4849
import com.itextpdf.layout.element.Text;
@@ -62,6 +63,7 @@ This file is part of the iText (R) project.
6263
import com.itextpdf.layout.properties.BaseDirection;
6364
import com.itextpdf.layout.properties.FloatPropertyValue;
6465
import com.itextpdf.layout.properties.FontKerning;
66+
import com.itextpdf.layout.properties.IBeforeTextRestoreExecutor;
6567
import com.itextpdf.layout.properties.OverflowPropertyValue;
6668
import com.itextpdf.layout.properties.OverflowWrapPropertyValue;
6769
import com.itextpdf.layout.properties.Property;
@@ -1006,7 +1008,13 @@ public void draw(DrawContext drawContext) {
10061008
canvas.showText(savedWordBreakAtLineEnding);
10071009
}
10081010

1009-
canvas.endText().restoreState();
1011+
canvas.endText();
1012+
IBeforeTextRestoreExecutor beforeTextRestoreExecutor = this.<IBeforeTextRestoreExecutor>getProperty(
1013+
Property.BEFORE_TEXT_RESTORE_EXECUTOR);
1014+
if (beforeTextRestoreExecutor != null) {
1015+
beforeTextRestoreExecutor.execute();
1016+
}
1017+
canvas.restoreState();
10101018
endElementOpacityApplying(drawContext);
10111019

10121020
if (isTagged) {
@@ -1506,12 +1514,13 @@ protected void drawSingleUnderline(Underline underline, TransparentColor fontCol
15061514
TransparentColor underlineStrokeColor = underline.getStrokeColor();
15071515

15081516
boolean doStroke = underlineStrokeColor != null;
1509-
RenderingMode mode = this.<RenderingMode>getProperty(Property.RENDERING_MODE);
1510-
// In SVG mode we should always use underline color, it is not related to the font color of the current text,
1517+
boolean isClippingMode = this.<Integer>getProperty(Property.TEXT_RENDERING_MODE) > TextRenderingMode.INVISIBLE;
1518+
RenderingMode renderingMode = this.<RenderingMode>getProperty(Property.RENDERING_MODE);
1519+
// In SVG renderingMode we should always use underline color, it is not related to the font color of the current text,
15111520
// but to the font color of the text element where text-decoration has been declared. In case of none value
1512-
// for fill and stroke in SVG mode, underline shouldn't be drawn at all.
1521+
// for fill and stroke in SVG renderingMode, underline shouldn't be drawn at all.
15131522
if (underlineFillColor == null && !doStroke) {
1514-
if (RenderingMode.SVG_MODE == mode) {
1523+
if (RenderingMode.SVG_MODE == renderingMode && !isClippingMode) {
15151524
return;
15161525
}
15171526
underlineFillColor = fontColor;
@@ -1544,16 +1553,27 @@ protected void drawSingleUnderline(Underline underline, TransparentColor fontCol
15441553
Rectangle underlineBBox = new Rectangle(innerAreaBbox.getX(), underlineYPosition - underlineThickness / 2,
15451554
innerAreaBbox.getWidth() - italicWidthSubstraction, underlineThickness);
15461555
canvas.rectangle(underlineBBox);
1547-
if (doFill && doStroke) {
1548-
canvas.fillStroke();
1549-
} else if (doStroke) {
1550-
canvas.stroke();
1556+
1557+
if (isClippingMode){
1558+
canvas.clip().endPath();
15511559
} else {
1552-
// In layout/html we should use default color in case underline and fontColor are null
1553-
// and still draw underline.
1554-
canvas.fill();
1560+
if (doFill && doStroke) {
1561+
canvas.fillStroke();
1562+
} else if (doStroke) {
1563+
canvas.stroke();
1564+
} else {
1565+
// In layout/html we should use default color in case underline and fontColor are null
1566+
// and still draw underline.
1567+
canvas.fill();
1568+
}
15551569
}
15561570
}
1571+
1572+
IBeforeTextRestoreExecutor beforeTextRestoreExecutor = this.<IBeforeTextRestoreExecutor>getProperty(
1573+
Property.BEFORE_TEXT_RESTORE_EXECUTOR);
1574+
if (beforeTextRestoreExecutor != null) {
1575+
beforeTextRestoreExecutor.execute();
1576+
}
15571577
canvas.restoreState();
15581578
}
15591579

layout/src/test/java/com/itextpdf/layout/renderer/TextRendererIntegrationTest.java

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,21 @@ This file is part of the iText (R) project.
2222
*/
2323
package com.itextpdf.layout.renderer;
2424

25-
import com.itextpdf.io.logs.IoLogMessageConstant;
2625
import com.itextpdf.io.font.PdfEncodings;
2726
import com.itextpdf.io.font.otf.GlyphLine;
2827
import com.itextpdf.io.image.ImageDataFactory;
28+
import com.itextpdf.io.logs.IoLogMessageConstant;
2929
import com.itextpdf.kernel.colors.ColorConstants;
3030
import com.itextpdf.kernel.font.PdfFont;
3131
import com.itextpdf.kernel.font.PdfFontFactory;
3232
import com.itextpdf.kernel.geom.PageSize;
3333
import com.itextpdf.kernel.geom.Rectangle;
3434
import com.itextpdf.kernel.pdf.PdfDocument;
3535
import com.itextpdf.kernel.pdf.PdfWriter;
36+
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
3637
import com.itextpdf.kernel.pdf.canvas.PdfCanvasConstants.TextRenderingMode;
3738
import com.itextpdf.kernel.utils.CompareTool;
39+
import com.itextpdf.layout.Canvas;
3840
import com.itextpdf.layout.ColumnDocumentRenderer;
3941
import com.itextpdf.layout.Document;
4042
import com.itextpdf.layout.borders.DashedBorder;
@@ -45,16 +47,17 @@ This file is part of the iText (R) project.
4547
import com.itextpdf.layout.element.Paragraph;
4648
import com.itextpdf.layout.element.Table;
4749
import com.itextpdf.layout.element.Text;
50+
import com.itextpdf.layout.font.FontProvider;
4851
import com.itextpdf.layout.logs.LayoutLogMessageConstant;
4952
import com.itextpdf.layout.properties.FloatPropertyValue;
53+
import com.itextpdf.layout.properties.IBeforeTextRestoreExecutor;
5054
import com.itextpdf.layout.properties.OverflowPropertyValue;
5155
import com.itextpdf.layout.properties.OverflowWrapPropertyValue;
5256
import com.itextpdf.layout.properties.Property;
5357
import com.itextpdf.layout.properties.RenderingMode;
5458
import com.itextpdf.layout.properties.TextAlignment;
5559
import com.itextpdf.layout.properties.TransparentColor;
5660
import com.itextpdf.layout.properties.UnitValue;
57-
import com.itextpdf.layout.font.FontProvider;
5861
import com.itextpdf.test.ExtendedITextTest;
5962
import com.itextpdf.test.annotations.LogMessage;
6063
import com.itextpdf.test.annotations.LogMessages;
@@ -64,8 +67,8 @@ This file is part of the iText (R) project.
6467
import java.nio.charset.StandardCharsets;
6568
import org.junit.jupiter.api.Assertions;
6669
import org.junit.jupiter.api.BeforeAll;
67-
import org.junit.jupiter.api.Test;
6870
import org.junit.jupiter.api.Tag;
71+
import org.junit.jupiter.api.Test;
6972

7073
@Tag("IntegrationTest")
7174
public class TextRendererIntegrationTest extends ExtendedITextTest {
@@ -1024,6 +1027,81 @@ public IRenderer getNextRenderer() {
10241027
}
10251028
}
10261029

1030+
@Test
1031+
// Pure layout doesn't support text clipping and CLIPPED_BY_TEXT_ELEMENT_DRAWER was developed for SVG.
1032+
// This test just show that there is a not user-friendly way to achieve text clipping.
1033+
public void simpleClippedTextTest() throws IOException, InterruptedException {
1034+
String outFileName = destinationFolder + "simpleClippedTextTest.pdf";
1035+
String cmpFileName = sourceFolder + "cmp_simpleClippedTextTest.pdf";
1036+
1037+
PdfDocument pdfDocument = new PdfDocument(new PdfWriter(outFileName));
1038+
Document doc = new Document(pdfDocument);
1039+
1040+
doc.setFontSize(20);
1041+
PdfCanvas pdfCanvas = new PdfCanvas(pdfDocument.addNewPage());
1042+
Canvas canvas = new Canvas(pdfCanvas, pdfDocument.getPage(1).getMediaBox());
1043+
1044+
Paragraph paragraph = new Paragraph("Hello World! Some long text to the end of the page")
1045+
.setTextRenderingMode(TextRenderingMode.CLIP);
1046+
paragraph.setProperty(Property.BEFORE_TEXT_RESTORE_EXECUTOR, new IBeforeTextRestoreExecutor() {
1047+
@Override
1048+
public void execute() {
1049+
Div div = new Div();
1050+
div.setFixedPosition(0, 0, 300);
1051+
div.setHeight(1200);
1052+
div.setBackgroundColor(ColorConstants.RED);
1053+
canvas.add(div);
1054+
}
1055+
});
1056+
doc.add(paragraph);
1057+
1058+
Paragraph paragraph2 = new Paragraph("Another text")
1059+
.setTextRenderingMode(TextRenderingMode.STROKE_CLIP);
1060+
paragraph2.setStrokeColor(ColorConstants.YELLOW);
1061+
paragraph2.setStrokeWidth(5);
1062+
paragraph2.setProperty(Property.BEFORE_TEXT_RESTORE_EXECUTOR, new IBeforeTextRestoreExecutor() {
1063+
@Override
1064+
public void execute() {
1065+
Div div = new Div();
1066+
div.setFixedPosition(0, 0, 300);
1067+
div.setHeight(1200);
1068+
div.setBackgroundColor(ColorConstants.GREEN);
1069+
canvas.add(div);
1070+
}
1071+
});
1072+
doc.add(paragraph2);
1073+
1074+
doc.add(new Paragraph("Bye World!"));
1075+
1076+
doc.close();
1077+
1078+
Assertions.assertNull(new CompareTool().compareByContent(outFileName, cmpFileName, destinationFolder));
1079+
}
1080+
1081+
@Test
1082+
public void clippedTextWithoutDrawerTest() throws IOException, InterruptedException {
1083+
String outFileName = destinationFolder + "clippedTextWithoutDrawerTest.pdf";
1084+
String cmpFileName = sourceFolder + "cmp_clippedTextWithoutDrawerTest.pdf";
1085+
1086+
PdfDocument pdfDocument = new PdfDocument(new PdfWriter(outFileName));
1087+
Document doc = new Document(pdfDocument);
1088+
doc.setFontSize(20);
1089+
1090+
Paragraph paragraph = new Paragraph("Hello World! Some long text to the end of the page")
1091+
.setTextRenderingMode(TextRenderingMode.CLIP);
1092+
doc.add(paragraph);
1093+
1094+
Div div = new Div();
1095+
div.setWidth(300);
1096+
div.setHeight(400);
1097+
div.setBackgroundColor(ColorConstants.RED);
1098+
doc.add(div);
1099+
1100+
doc.close();
1101+
1102+
Assertions.assertNull(new CompareTool().compareByContent(outFileName, cmpFileName, destinationFolder));
1103+
}
1104+
10271105
private static class TextRendererWithOverriddenGetNextRenderer extends TextRenderer {
10281106
public TextRendererWithOverriddenGetNextRenderer(Text textElement) {
10291107
super(textElement);

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public class SvgDrawContext {
5959
private SvgCssContext cssContext;
6060

6161
private AffineTransform rootTransform;
62+
private AffineTransform clippingElementTransform = new AffineTransform();
6263
private float[] textMove = new float[]{0.0f, 0.0f};
6364
private float[] relativePosition;
6465

@@ -470,4 +471,28 @@ public void moveRelativePosition(float dx, float dy) {
470471
public void resetRelativePosition() {
471472
relativePosition = new float[]{0.0f, 0.0f};
472473
}
474+
475+
/**
476+
* Gets clipping element transformation matrix.
477+
*
478+
* <p>
479+
* It is used to preserve clipping element transformation matrix and before drawing clipped element revert canvas
480+
* transformation matrix into original state. After clipped element will be drawn, clipping element transformation
481+
* matrix will be used once again to return clipping element matrix for next siblings.
482+
*
483+
* @return the current clipping element transformation matrix
484+
*/
485+
public AffineTransform getClippingElementTransform() {
486+
return clippingElementTransform;
487+
}
488+
489+
/**
490+
* Resets clipping element transformation matrix.
491+
*
492+
* <p>
493+
* See {@link #getClippingElementTransform()} for more info about clipping element transformation matrix.
494+
*/
495+
public void resetClippingElementTransform() {
496+
this.clippingElementTransform.setToIdentity();
497+
}
473498
}

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

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -225,16 +225,6 @@ void postDraw(SvgDrawContext context) {
225225
@Override
226226
public abstract ISvgNodeRenderer createDeepCopy();
227227

228-
@Override
229-
void setPartOfClipPath(boolean isPart) {
230-
super.setPartOfClipPath(isPart);
231-
for (ISvgNodeRenderer child : children) {
232-
if (child instanceof AbstractSvgNodeRenderer) {
233-
((AbstractSvgNodeRenderer) child).setPartOfClipPath(isPart);
234-
}
235-
}
236-
}
237-
238228
void calculateAndApplyViewBox(SvgDrawContext context, float[] values, Rectangle currentViewPort) {
239229
// If viewBox width or height is zero we should disable rendering of the element.
240230
if (Math.abs(values[2]) < EPS || Math.abs(values[3]) < EPS) {

0 commit comments

Comments
 (0)