Skip to content

Commit 4da833d

Browse files
Introduce new constructor Canvas(PdfPage, Rectangle)
Using this constructor allows to add link annotations and destinations via Canvas. DEVSIX-2486
1 parent 70def83 commit 4da833d

15 files changed

+314
-7
lines changed

io/src/main/java/com/itextpdf/io/LogMessageConstant.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ public final class LogMessageConstant {
121121
public static final String MAPPING_IN_STRUCT_ROOT_OVERWRITTEN = "Existing mapping for {0} in structure tree root role map was {1} and it was overwritten with {2}.";
122122
public static final String METHOD_IS_NOT_IMPLEMENTED_BY_DEFAULT_OTHER_METHOD_WILL_BE_USED = "Method {0} is not implemented by default: please, override and implement it. {1} will be used instead.";
123123
public static final String NAME_ALREADY_EXISTS_IN_THE_NAME_TREE = "Name \"{0}\" already exists in the name tree; old value will be replaced by the new one.";
124+
public static final String UNABLE_TO_APPLY_PAGE_DEPENDENT_PROP_UNKNOWN_PAGE_ON_WHICH_ELEMENT_IS_DRAWN = "Unable to apply page dependent property, because the page on which element is drawn is unknown. Usually this means that element was added to the Canvas instance that was created not with constructor taking PdfPage as argument. Not processed property: {0}";
124125
public static final String NOT_TAGGED_PAGES_IN_TAGGED_DOCUMENT = "Not tagged pages are copied to the tagged document. Destination document now may contain not tagged content.";
125126
public static final String NO_FIELDS_IN_ACROFORM = "Required AcroForm entry /Fields does not exist in the document. Empty array /Fields will be created.";
126127
public static final String NUM_TREE_SHALL_NOT_END_WITH_KEY = "Number tree ends with a key which is invalid according to the PDF specification.";
@@ -131,6 +132,7 @@ public final class LogMessageConstant {
131132
public static final String ONLY_ONE_OF_ARTBOX_OR_TRIMBOX_CAN_EXIST_IN_THE_PAGE = "Only one of artbox or trimbox can exist on the page. The trimbox will be deleted";
132133
public static final String OPENTYPE_GDEF_TABLE_ERROR = "OpenType GDEF table error: {0}";
133134
public static final String PAGE_TREE_IS_BROKEN_FAILED_TO_RETRIEVE_PAGE = "Page tree is broken. Failed to retrieve page number {0}. Null will be returned.";
135+
public static final String PASSED_PAGE_SHALL_BE_ON_WHICH_CANVAS_WILL_BE_RENDERED = "The page passed to Canvas#enableAutoTagging(PdfPage) method shall be the one on which this canvas will be rendered. However the actual passed PdfPage instance sets not such page. This might lead to creation of malformed PDF document.";
134136
public static final String PATH_KEY_IS_PRESENT_VERTICES_WILL_BE_IGNORED = "Path key is present. Vertices will be ignored";
135137
public static final String PDF_OBJECT_FLUSHING_NOT_PERFORMED = "PdfObject flushing is not performed: PdfDocument is opened in append mode and the object is not marked as modified ( see PdfObject#setModified() ).";
136138
public static final String PDF_READER_CLOSING_FAILED = "PdfReader closing failed due to the error occurred!";

layout/src/main/java/com/itextpdf/layout/Canvas.java

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ This file is part of the iText (R) project.
4343
*/
4444
package com.itextpdf.layout;
4545

46+
import com.itextpdf.io.LogMessageConstant;
47+
import com.itextpdf.kernel.PdfException;
4648
import com.itextpdf.kernel.geom.Rectangle;
4749
import com.itextpdf.kernel.pdf.PdfDocument;
4850
import com.itextpdf.kernel.pdf.PdfPage;
@@ -52,6 +54,8 @@ This file is part of the iText (R) project.
5254
import com.itextpdf.layout.renderer.CanvasRenderer;
5355
import com.itextpdf.layout.renderer.IRenderer;
5456
import com.itextpdf.layout.renderer.RootRenderer;
57+
import org.slf4j.Logger;
58+
import org.slf4j.LoggerFactory;
5559

5660
/**
5761
* This class is used for adding content directly onto a specified {@link PdfCanvas}.
@@ -71,8 +75,29 @@ public class Canvas extends RootElement<Canvas> {
7175
*/
7276
protected PdfPage page;
7377

78+
private boolean isCanvasOfPage;
79+
7480
/**
75-
* Creates a new Canvas to manipulate a specific document and page.
81+
* Creates a new Canvas to manipulate a specific page content stream. The given page shall not be flushed:
82+
* drawing on flushed pages is impossible because their content is already written to the output stream.
83+
* Use this constructor to be able to add {@link com.itextpdf.layout.element.Link} elements on it
84+
* (using any other constructor would result in inability to add PDF annotations, based on which, for example, links work).
85+
* <p>
86+
* If the {@link PdfDocument#isTagged()} is true, using this constructor would automatically enable
87+
* the tagging for the content. Regarding tagging the effect is the same as using {@link #enableAutoTagging(PdfPage)}.
88+
*
89+
* @param page the page on which this canvas will be rendered, shall not be flushed (see {@link PdfPage#isFlushed()}).
90+
* @param rootArea the maximum area that the Canvas may write upon
91+
*/
92+
public Canvas(PdfPage page, Rectangle rootArea) {
93+
this(initPdfCanvasOrThrowIfPageIsFlushed(page), page.getDocument(), rootArea);
94+
this.enableAutoTagging(page);
95+
this.isCanvasOfPage = true;
96+
}
97+
98+
/**
99+
* Creates a new Canvas to manipulate a specific document and content stream, which might be for example a page
100+
* or {@link PdfFormXObject} stream.
76101
*
77102
* @param pdfCanvas the low-level content stream writer
78103
* @param pdfDocument the document that the resulting content stream will be written to
@@ -142,8 +167,8 @@ public void setRenderer(CanvasRenderer canvasRenderer) {
142167
}
143168

144169
/**
145-
* Returned value is not null only in case when autotagging is enabled.
146-
* @return the page, on which this canvas will be rendered, or null if autotagging is not enabled.
170+
* The page on which this canvas will be rendered.
171+
* @return the specified {@link PdfPage} instance, might be null if this the page was not set.
147172
*/
148173
public PdfPage getPage() {
149174
return page;
@@ -154,6 +179,10 @@ public PdfPage getPage() {
154179
* @param page the page, on which this canvas will be rendered.
155180
*/
156181
public void enableAutoTagging(PdfPage page) {
182+
if (isCanvasOfPage() && this.page != page) {
183+
Logger logger = LoggerFactory.getLogger(Canvas.class);
184+
logger.error(LogMessageConstant.PASSED_PAGE_SHALL_BE_ON_WHICH_CANVAS_WILL_BE_RENDERED);
185+
}
157186
this.page = page;
158187
}
159188

@@ -164,6 +193,17 @@ public boolean isAutoTaggingEnabled() {
164193
return page != null;
165194
}
166195

196+
/**
197+
* Defines if the canvas is exactly the direct content of the page. This is known definitely only if
198+
* this instance was created by {@link Canvas#Canvas(PdfPage, Rectangle)} constructor overload,
199+
* otherwise this method returns false.
200+
* @return true if the canvas on which this instance performs drawing is directly the canvas of the page;
201+
* false if the instance of this class was created not with {@link Canvas#Canvas(PdfPage, Rectangle)} constructor overload.
202+
*/
203+
public boolean isCanvasOfPage() {
204+
return isCanvasOfPage;
205+
}
206+
167207
/**
168208
* Performs an entire recalculation of the element flow on the canvas,
169209
* taking into account all its current child elements. May become very
@@ -214,4 +254,11 @@ protected RootRenderer ensureRootRendererNotNull() {
214254
return rootRenderer;
215255
}
216256

257+
private static PdfCanvas initPdfCanvasOrThrowIfPageIsFlushed(PdfPage page) {
258+
if (page.isFlushed()) {
259+
throw new PdfException(PdfException.CannotDrawElementsOnAlreadyFlushedPages);
260+
}
261+
return new PdfCanvas(page);
262+
}
263+
217264
}

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1653,8 +1653,15 @@ protected void applyRelativePositioningTranslation(boolean reverse) {
16531653
protected void applyDestination(PdfDocument document) {
16541654
String destination = this.<String>getProperty(Property.DESTINATION);
16551655
if (destination != null) {
1656+
int pageNumber = occupiedArea.getPageNumber();
1657+
if (pageNumber < 1 || pageNumber > document.getNumberOfPages()) {
1658+
Logger logger = LoggerFactory.getLogger(AbstractRenderer.class);
1659+
String logMessageArg = "Property.DESTINATION, which specifies this element location as destination, see ElementPropertyContainer.setDestination.";
1660+
logger.warn(MessageFormatUtil.format(LogMessageConstant.UNABLE_TO_APPLY_PAGE_DEPENDENT_PROP_UNKNOWN_PAGE_ON_WHICH_ELEMENT_IS_DRAWN, logMessageArg));
1661+
return;
1662+
}
16561663
PdfArray array = new PdfArray();
1657-
array.add(document.getPage(occupiedArea.getPageNumber()).getPdfObject());
1664+
array.add(document.getPage(pageNumber).getPdfObject());
16581665
array.add(PdfName.XYZ);
16591666
array.add(new PdfNumber(occupiedArea.getBBox().getX()));
16601667
array.add(new PdfNumber(occupiedArea.getBBox().getY() + occupiedArea.getBBox().getHeight()));
@@ -1686,14 +1693,21 @@ protected void applyAction(PdfDocument document) {
16861693
protected void applyLinkAnnotation(PdfDocument document) {
16871694
PdfLinkAnnotation linkAnnotation = this.<PdfLinkAnnotation>getProperty(Property.LINK_ANNOTATION);
16881695
if (linkAnnotation != null) {
1696+
int pageNumber = occupiedArea.getPageNumber();
1697+
if (pageNumber < 1 || pageNumber > document.getNumberOfPages()) {
1698+
Logger logger = LoggerFactory.getLogger(AbstractRenderer.class);
1699+
String logMessageArg = "Property.LINK_ANNOTATION, which specifies a link associated with this element content area, see com.itextpdf.layout.element.Link.";
1700+
logger.warn(MessageFormatUtil.format(LogMessageConstant.UNABLE_TO_APPLY_PAGE_DEPENDENT_PROP_UNKNOWN_PAGE_ON_WHICH_ELEMENT_IS_DRAWN, logMessageArg));
1701+
return;
1702+
}
16891703
Rectangle pdfBBox = calculateAbsolutePdfBBox();
16901704
if (linkAnnotation.getPage() != null) {
16911705
PdfDictionary oldAnnotation = (PdfDictionary) linkAnnotation.getPdfObject().clone();
16921706
linkAnnotation = (PdfLinkAnnotation) PdfAnnotation.makeAnnotation(oldAnnotation);
16931707
}
16941708
linkAnnotation.setRectangle(new PdfArray(pdfBBox));
16951709

1696-
PdfPage page = document.getPage(occupiedArea.getPageNumber());
1710+
PdfPage page = document.getPage(pageNumber);
16971711
page.addAnnotation(linkAnnotation);
16981712
}
16991713
}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,13 @@ public void draw(DrawContext drawContext) {
559559

560560
if (processOverflow) {
561561
drawContext.getCanvas().saveState();
562-
Rectangle clippedArea = drawContext.getDocument().getPage(occupiedArea.getPageNumber()).getPageSize();
562+
int pageNumber = occupiedArea.getPageNumber();
563+
Rectangle clippedArea;
564+
if (pageNumber < 1 || pageNumber > drawContext.getDocument().getNumberOfPages()) {
565+
clippedArea = new Rectangle(-INF / 2 , -INF / 2, INF, INF);
566+
} else {
567+
clippedArea = drawContext.getDocument().getPage(pageNumber).getPageSize();
568+
}
563569
Rectangle area = getBorderAreaBBox();
564570
if (overflowXHidden) {
565571
clippedArea.setX(area.getX()).setWidth(area.getWidth());

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,8 @@ protected void flushSingleRenderer(IRenderer resultRenderer) {
133133
@Override
134134
protected LayoutArea updateCurrentArea(LayoutResult overflowResult) {
135135
if (currentArea == null) {
136-
currentArea = new RootLayoutArea(0, canvas.getRootArea().clone());
136+
int pageNumber = canvas.isCanvasOfPage() ? canvas.getPdfDocument().getPageNumber(canvas.getPage()) : 0;
137+
currentArea = new RootLayoutArea(pageNumber, canvas.getRootArea().clone());
137138
} else {
138139
setProperty(Property.FULL, true);
139140
currentArea = null;
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package com.itextpdf.layout;
2+
3+
import com.itextpdf.io.LogMessageConstant;
4+
import com.itextpdf.kernel.colors.ColorConstants;
5+
import com.itextpdf.kernel.geom.Rectangle;
6+
import com.itextpdf.kernel.pdf.PdfDocument;
7+
import com.itextpdf.kernel.pdf.PdfPage;
8+
import com.itextpdf.kernel.pdf.PdfWriter;
9+
import com.itextpdf.kernel.pdf.action.PdfAction;
10+
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
11+
import com.itextpdf.kernel.utils.CompareTool;
12+
import com.itextpdf.layout.element.Link;
13+
import com.itextpdf.layout.element.Paragraph;
14+
import com.itextpdf.test.ExtendedITextTest;
15+
import com.itextpdf.test.annotations.LogMessage;
16+
import com.itextpdf.test.annotations.LogMessages;
17+
import com.itextpdf.test.annotations.type.IntegrationTest;
18+
import java.io.IOException;
19+
import org.junit.Assert;
20+
import org.junit.BeforeClass;
21+
import org.junit.Test;
22+
import org.junit.experimental.categories.Category;
23+
24+
@Category(IntegrationTest.class)
25+
public class CanvasTest extends ExtendedITextTest {
26+
27+
public static final String sourceFolder = "./src/test/resources/com/itextpdf/layout/CanvasTest/";
28+
public static final String destinationFolder = "./target/test/com/itextpdf/layout/CanvasTest/";
29+
30+
@BeforeClass
31+
public static void beforeClass() {
32+
createOrClearDestinationFolder(destinationFolder);
33+
}
34+
35+
@Test
36+
@LogMessages(messages = @LogMessage(messageTemplate = LogMessageConstant.UNABLE_TO_APPLY_PAGE_DEPENDENT_PROP_UNKNOWN_PAGE_ON_WHICH_ELEMENT_IS_DRAWN))
37+
public void canvasNoPageLinkTest() throws IOException, InterruptedException {
38+
String testName = "canvasNoPageLinkTest";
39+
String out = destinationFolder + testName + ".pdf";
40+
String cmp = sourceFolder + "cmp_" + testName + ".pdf";
41+
PdfDocument pdf = new PdfDocument(new PdfWriter(out));
42+
PdfPage page = pdf.addNewPage();
43+
Rectangle pageSize = page.getPageSize();
44+
PdfCanvas pdfCanvas = new PdfCanvas(page.getLastContentStream(), page.getResources(), pdf);
45+
Rectangle rectangle = new Rectangle(
46+
pageSize.getX() + 36,
47+
pageSize.getTop() - 80,
48+
pageSize.getWidth() - 72,
49+
50);
50+
51+
Canvas canvas = new Canvas(pdfCanvas, pdf, rectangle);
52+
canvas.add(
53+
new Paragraph(
54+
new Link("Google link!", PdfAction.createURI("https://www.google.com"))
55+
.setUnderline()
56+
.setFontColor(ColorConstants.BLUE)));
57+
canvas.close();
58+
pdf.close();
59+
60+
Assert.assertNull(new CompareTool().compareByContent(out, cmp, destinationFolder));
61+
}
62+
63+
@Test
64+
public void canvasWithPageLinkTest() throws IOException, InterruptedException {
65+
String testName = "canvasWithPageLinkTest";
66+
String out = destinationFolder + testName + ".pdf";
67+
String cmp = sourceFolder + "cmp_" + testName + ".pdf";
68+
PdfDocument pdf = new PdfDocument(new PdfWriter(out));
69+
PdfPage page = pdf.addNewPage();
70+
Rectangle pageSize = page.getPageSize();
71+
Rectangle rectangle = new Rectangle(
72+
pageSize.getX() + 36,
73+
pageSize.getTop() - 80,
74+
pageSize.getWidth() - 72,
75+
50);
76+
77+
Canvas canvas = new Canvas(page, rectangle);
78+
canvas.add(
79+
new Paragraph(
80+
new Link("Google link!", PdfAction.createURI("https://www.google.com"))
81+
.setUnderline()
82+
.setFontColor(ColorConstants.BLUE)));
83+
canvas.close();
84+
pdf.close();
85+
86+
Assert.assertNull(new CompareTool().compareByContent(out, cmp, destinationFolder));
87+
}
88+
89+
@Test
90+
public void canvasWithPageEnableTaggingTest01() throws IOException, InterruptedException {
91+
String testName = "canvasWithPageEnableTaggingTest01";
92+
String out = destinationFolder + testName + ".pdf";
93+
String cmp = sourceFolder + "cmp_" + testName + ".pdf";
94+
PdfDocument pdf = new PdfDocument(new PdfWriter(out));
95+
96+
pdf.setTagged();
97+
98+
PdfPage page = pdf.addNewPage();
99+
Rectangle pageSize = page.getPageSize();
100+
Rectangle rectangle = new Rectangle(
101+
pageSize.getX() + 36,
102+
pageSize.getTop() - 80,
103+
pageSize.getWidth() - 72,
104+
50);
105+
106+
Canvas canvas = new Canvas(page, rectangle);
107+
canvas.add(
108+
new Paragraph(
109+
new Link("Google link!", PdfAction.createURI("https://www.google.com"))
110+
.setUnderline()
111+
.setFontColor(ColorConstants.BLUE)));
112+
canvas.close();
113+
pdf.close();
114+
115+
Assert.assertNull(new CompareTool().compareByContent(out, cmp, destinationFolder));
116+
}
117+
118+
@Test
119+
@LogMessages(messages = {@LogMessage(messageTemplate = LogMessageConstant.UNABLE_TO_APPLY_PAGE_DEPENDENT_PROP_UNKNOWN_PAGE_ON_WHICH_ELEMENT_IS_DRAWN),
120+
@LogMessage(messageTemplate = LogMessageConstant.PASSED_PAGE_SHALL_BE_ON_WHICH_CANVAS_WILL_BE_RENDERED)})
121+
public void canvasWithPageEnableTaggingTest02() throws IOException, InterruptedException {
122+
String testName = "canvasWithPageEnableTaggingTest02";
123+
String out = destinationFolder + testName + ".pdf";
124+
String cmp = sourceFolder + "cmp_" + testName + ".pdf";
125+
PdfDocument pdf = new PdfDocument(new PdfWriter(out));
126+
127+
pdf.setTagged();
128+
129+
PdfPage page = pdf.addNewPage();
130+
Rectangle pageSize = page.getPageSize();
131+
Rectangle rectangle = new Rectangle(
132+
pageSize.getX() + 36,
133+
pageSize.getTop() - 80,
134+
pageSize.getWidth() - 72,
135+
50);
136+
137+
Canvas canvas = new Canvas(page, rectangle);
138+
139+
// This will disable tagging and also prevent annotations addition. Created tagged document is invalid. Expected log message.
140+
canvas.enableAutoTagging(null);
141+
142+
canvas.add(
143+
new Paragraph(
144+
new Link("Google link!", PdfAction.createURI("https://www.google.com"))
145+
.setUnderline()
146+
.setFontColor(ColorConstants.BLUE)));
147+
canvas.close();
148+
pdf.close();
149+
150+
Assert.assertNull(new CompareTool().compareByContent(out, cmp, destinationFolder));
151+
}
152+
}

0 commit comments

Comments
 (0)