Skip to content

Commit 6ab7119

Browse files
committed
Support structure destinations for intra-document links
DEVSIX-7956
1 parent 77ea061 commit 6ab7119

File tree

7 files changed

+182
-32
lines changed

7 files changed

+182
-32
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ public final class Property {
7171
public static final int COLUMN_WIDTH = 142;
7272
public static final int COLUMN_GAP = 143;
7373
public static final int COLUMN_GAP_BORDER = 144;
74+
/**
75+
* Can be either destination name (id) as String or
76+
* a Tuple2(String, PdfDictionary) where String is destination name (id) and PdfDictionary is a dictionary of
77+
* goto PdfAction. This second variant allow to create structure destination in tagged pdf.
78+
*/
7479
public static final int DESTINATION = 17;
7580
public static final int FILL_AVAILABLE_AREA = 86;
7681
public static final int FILL_AVAILABLE_AREA_ON_SPLIT = 87;

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

Lines changed: 66 additions & 30 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.layout.renderer;
2424

25+
import com.itextpdf.commons.datastructures.Tuple2;
2526
import com.itextpdf.commons.utils.MessageFormatUtil;
2627
import com.itextpdf.io.logs.IoLogMessageConstant;
2728
import com.itextpdf.io.util.NumberUtil;
@@ -37,12 +38,17 @@ This file is part of the iText (R) project.
3738
import com.itextpdf.kernel.pdf.PdfName;
3839
import com.itextpdf.kernel.pdf.PdfNumber;
3940
import com.itextpdf.kernel.pdf.PdfPage;
41+
import com.itextpdf.kernel.pdf.PdfVersion;
4042
import com.itextpdf.kernel.pdf.action.PdfAction;
4143
import com.itextpdf.kernel.pdf.annot.PdfAnnotation;
4244
import com.itextpdf.kernel.pdf.annot.PdfLinkAnnotation;
4345
import com.itextpdf.kernel.pdf.canvas.CanvasArtifact;
4446
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
4547
import com.itextpdf.kernel.pdf.extgstate.PdfExtGState;
48+
import com.itextpdf.kernel.pdf.navigation.PdfStructureDestination;
49+
import com.itextpdf.kernel.pdf.tagging.PdfStructElem;
50+
import com.itextpdf.kernel.pdf.tagutils.TagStructureContext;
51+
import com.itextpdf.kernel.pdf.tagutils.TagTreePointer;
4652
import com.itextpdf.kernel.pdf.xobject.PdfFormXObject;
4753
import com.itextpdf.kernel.pdf.xobject.PdfXObject;
4854
import com.itextpdf.layout.Document;
@@ -135,6 +141,10 @@ public abstract class AbstractRenderer implements IRenderer {
135141

136142
private static final int ARC_QUARTER_CLOCKWISE_EXTENT = -90;
137143

144+
// For autoport
145+
private static final Tuple2<String, PdfDictionary> CHECK_TUPLE2_TYPE =
146+
new Tuple2<String, PdfDictionary>("", new PdfDictionary());
147+
138148
protected List<IRenderer> childRenderers = new ArrayList<>();
139149
protected List<IRenderer> positionedRenderers = new ArrayList<>();
140150
protected IPropertyContainer modelElement;
@@ -1918,8 +1928,22 @@ protected void applyRelativePositioningTranslation(boolean reverse) {
19181928
}
19191929

19201930
protected void applyDestination(PdfDocument document) {
1921-
String destination = this.<String>getProperty(Property.DESTINATION);
1922-
if (destination != null) {
1931+
Object destination = this.<Object>getProperty(Property.DESTINATION);
1932+
if (destination == null) {
1933+
return;
1934+
}
1935+
String destinationName = null;
1936+
PdfDictionary linkActionDict = null;
1937+
if (destination instanceof String) {
1938+
destinationName = (String)destination;
1939+
} else if (CHECK_TUPLE2_TYPE.getClass().equals(destination.getClass())) {
1940+
// 'If' above is the only autoportable way it seems
1941+
Tuple2<String, PdfDictionary> destTuple = (Tuple2<String, PdfDictionary>)destination;
1942+
destinationName = destTuple.getFirst();
1943+
linkActionDict = destTuple.getSecond();
1944+
}
1945+
1946+
if (destinationName != null) {
19231947
int pageNumber = occupiedArea.getPageNumber();
19241948
if (pageNumber < 1 || pageNumber > document.getNumberOfPages()) {
19251949
Logger logger = LoggerFactory.getLogger(AbstractRenderer.class);
@@ -1935,10 +1959,19 @@ protected void applyDestination(PdfDocument document) {
19351959
array.add(new PdfNumber(occupiedArea.getBBox().getX()));
19361960
array.add(new PdfNumber(occupiedArea.getBBox().getY() + occupiedArea.getBBox().getHeight()));
19371961
array.add(new PdfNumber(0));
1938-
document.addNamedDestination(destination, array.makeIndirect(document));
1962+
document.addNamedDestination(destinationName, array.makeIndirect(document));
1963+
}
19391964

1940-
deleteProperty(Property.DESTINATION);
1965+
final boolean isPdf20 = document.getPdfVersion().compareTo(PdfVersion.PDF_2_0) >= 0;
1966+
if (linkActionDict != null && isPdf20 && document.isTagged()) {
1967+
TagStructureContext context = document.getTagStructureContext();
1968+
TagTreePointer tagPointer = context.getAutoTaggingPointer();
1969+
PdfStructElem structElem = context.getPointerStructElem(tagPointer);
1970+
PdfStructureDestination dest = PdfStructureDestination.createFit(structElem);
1971+
linkActionDict.put(PdfName.SD, dest.getPdfObject());
19411972
}
1973+
1974+
deleteProperty(Property.DESTINATION);
19421975
}
19431976

19441977
protected void applyAction(PdfDocument document) {
@@ -1962,32 +1995,35 @@ protected void applyAction(PdfDocument document) {
19621995
protected void applyLinkAnnotation(PdfDocument document) {
19631996
Logger logger = LoggerFactory.getLogger(AbstractRenderer.class);
19641997
PdfLinkAnnotation linkAnnotation = this.<PdfLinkAnnotation>getProperty(Property.LINK_ANNOTATION);
1965-
if (linkAnnotation != null) {
1966-
int pageNumber = occupiedArea.getPageNumber();
1967-
if (pageNumber < 1 || pageNumber > document.getNumberOfPages()) {
1968-
String logMessageArg = "Property.LINK_ANNOTATION, which specifies a link associated with this element content area, see com.itextpdf.layout.element.Link.";
1969-
logger.warn(MessageFormatUtil.format(
1970-
IoLogMessageConstant.UNABLE_TO_APPLY_PAGE_DEPENDENT_PROP_UNKNOWN_PAGE_ON_WHICH_ELEMENT_IS_DRAWN,
1971-
logMessageArg));
1972-
return;
1973-
}
1974-
// If an element with a link annotation occupies more than two pages,
1975-
// then a NPE might occur, because of the annotation being partially flushed.
1976-
// That's why we create and use an annotation's copy.
1977-
PdfDictionary oldAnnotation = (PdfDictionary) linkAnnotation.getPdfObject().clone();
1978-
linkAnnotation = (PdfLinkAnnotation) PdfAnnotation.makeAnnotation(oldAnnotation);
1979-
Rectangle pdfBBox = calculateAbsolutePdfBBox();
1980-
linkAnnotation.setRectangle(new PdfArray(pdfBBox));
1981-
1982-
PdfPage page = document.getPage(pageNumber);
1983-
// TODO DEVSIX-1655 This check is necessary because, in some cases, our renderer's hierarchy may contain
1984-
// a renderer from the different page that was already flushed
1985-
if (page.isFlushed()) {
1986-
logger.error(MessageFormatUtil.format(
1987-
IoLogMessageConstant.PAGE_WAS_FLUSHED_ACTION_WILL_NOT_BE_PERFORMED, "link annotation applying"));
1988-
} else {
1989-
page.addAnnotation(linkAnnotation);
1990-
}
1998+
if (linkAnnotation == null) {
1999+
return;
2000+
}
2001+
2002+
int pageNumber = occupiedArea.getPageNumber();
2003+
if (pageNumber < 1 || pageNumber > document.getNumberOfPages()) {
2004+
String logMessageArg = "Property.LINK_ANNOTATION, which specifies a link associated with this element content area, see com.itextpdf.layout.element.Link.";
2005+
logger.warn(MessageFormatUtil.format(
2006+
IoLogMessageConstant.UNABLE_TO_APPLY_PAGE_DEPENDENT_PROP_UNKNOWN_PAGE_ON_WHICH_ELEMENT_IS_DRAWN,
2007+
logMessageArg));
2008+
return;
2009+
}
2010+
2011+
// If an element with a link annotation occupies more than two pages,
2012+
// then a NPE might occur, because of the annotation being partially flushed.
2013+
// That's why we create and use an annotation's copy.
2014+
PdfDictionary newAnnotation = (PdfDictionary) linkAnnotation.getPdfObject().clone();
2015+
linkAnnotation = (PdfLinkAnnotation) PdfAnnotation.makeAnnotation(newAnnotation);
2016+
Rectangle pdfBBox = calculateAbsolutePdfBBox();
2017+
linkAnnotation.setRectangle(new PdfArray(pdfBBox));
2018+
2019+
PdfPage page = document.getPage(pageNumber);
2020+
// TODO DEVSIX-1655 This check is necessary because, in some cases, our renderer's hierarchy may contain
2021+
// a renderer from the different page that was already flushed
2022+
if (page.isFlushed()) {
2023+
logger.error(MessageFormatUtil.format(
2024+
IoLogMessageConstant.PAGE_WAS_FLUSHED_ACTION_WILL_NOT_BE_PERFORMED, "link annotation applying"));
2025+
} else {
2026+
page.addAnnotation(linkAnnotation);
19912027
}
19922028
}
19932029

layout/src/test/java/com/itextpdf/layout/LinkTest.java

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,18 @@ This file is part of the iText (R) project.
2222
*/
2323
package com.itextpdf.layout;
2424

25+
import com.itextpdf.commons.datastructures.Tuple2;
2526
import com.itextpdf.io.logs.IoLogMessageConstant;
2627
import com.itextpdf.kernel.colors.ColorConstants;
2728
import com.itextpdf.kernel.geom.Rectangle;
2829
import com.itextpdf.kernel.pdf.PdfArray;
30+
import com.itextpdf.kernel.pdf.PdfDictionary;
2931
import com.itextpdf.kernel.pdf.PdfDocument;
3032
import com.itextpdf.kernel.pdf.PdfName;
3133
import com.itextpdf.kernel.pdf.PdfNumber;
34+
import com.itextpdf.kernel.pdf.PdfVersion;
3235
import com.itextpdf.kernel.pdf.PdfWriter;
36+
import com.itextpdf.kernel.pdf.WriterProperties;
3337
import com.itextpdf.kernel.pdf.action.PdfAction;
3438
import com.itextpdf.kernel.pdf.annot.PdfLinkAnnotation;
3539
import com.itextpdf.kernel.pdf.navigation.PdfDestination;
@@ -72,7 +76,7 @@ public class LinkTest extends ExtendedITextTest {
7276

7377
@BeforeClass
7478
public static void beforeClass() {
75-
createDestinationFolder(destinationFolder);
79+
createOrClearDestinationFolder(destinationFolder);
7680
}
7781

7882
@Test
@@ -392,4 +396,69 @@ public void linkActionOnDivSplitTest01() throws IOException, InterruptedExceptio
392396
Assert.assertNull(new CompareTool().compareByContent(outFileName, cmpFileName, destinationFolder));
393397
}
394398

399+
@Test
400+
public void intraForwardLinkTest() throws IOException, InterruptedException {
401+
String outFileName = destinationFolder + "intraForwardLink.pdf";
402+
String cmpFileName = sourceFolder + "cmp_intraForwardLink.pdf";
403+
404+
PdfDocument pdfDoc = new PdfDocument(new PdfWriter(outFileName,
405+
new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0)));
406+
pdfDoc.setTagged();
407+
Document doc = new Document(pdfDoc);
408+
409+
PdfLinkAnnotation linkAnnotation = new PdfLinkAnnotation(new Rectangle(0, 0, 0, 0))
410+
.setAction(PdfAction.createGoTo("custom"));
411+
412+
Paragraph text = new Paragraph("Link to custom text");
413+
text.setProperty(Property.LINK_ANNOTATION, linkAnnotation);
414+
doc.add(text);
415+
416+
doc.add(new AreaBreak());
417+
418+
pdfDoc.getPage(1).flush();
419+
420+
doc.add(text);
421+
422+
Paragraph customText = new Paragraph("Custom text");
423+
customText.setProperty(Property.DESTINATION, new Tuple2<String, PdfDictionary>("custom", linkAnnotation.getAction()));
424+
doc.add(customText);
425+
426+
doc.close();
427+
428+
Assert.assertNull(new CompareTool().compareByContent(outFileName, cmpFileName, destinationFolder, "diff"));
429+
}
430+
431+
@Test
432+
public void intraBackwardLinkTest() throws IOException, InterruptedException {
433+
String outFileName = destinationFolder + "intraBackwardLink.pdf";
434+
String cmpFileName = sourceFolder + "cmp_intraBackwardLink.pdf";
435+
436+
PdfDocument pdfDoc = new PdfDocument(new PdfWriter(outFileName,
437+
new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0)));
438+
pdfDoc.setTagged();
439+
Document doc = new Document(pdfDoc);
440+
441+
PdfLinkAnnotation linkAnnotation = new PdfLinkAnnotation(new Rectangle(0, 0, 0, 0))
442+
.setAction(PdfAction.createGoTo("custom"));
443+
444+
Paragraph customText = new Paragraph("Custom text");
445+
customText.setProperty(Property.DESTINATION, new Tuple2<String, PdfDictionary>("custom", linkAnnotation.getAction()));
446+
doc.add(customText);
447+
448+
doc.add(new AreaBreak());
449+
pdfDoc.getPage(1).flush();
450+
451+
Paragraph text = new Paragraph("Link to custom text");
452+
text.setProperty(Property.LINK_ANNOTATION, linkAnnotation);
453+
doc.add(text);
454+
455+
doc.add(new AreaBreak());
456+
pdfDoc.getPage(2).flush();
457+
458+
doc.add(text);
459+
460+
doc.close();
461+
462+
Assert.assertNull(new CompareTool().compareByContent(outFileName, cmpFileName, destinationFolder, "diff"));
463+
}
395464
}

layout/src/test/java/com/itextpdf/layout/PdfUA2Test.java

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,20 @@ This file is part of the iText (R) project.
4545
import com.itextpdf.kernel.pdf.action.PdfAction;
4646
import com.itextpdf.kernel.pdf.annot.PdfLinkAnnotation;
4747
import com.itextpdf.kernel.pdf.filespec.PdfFileSpec;
48+
import com.itextpdf.kernel.pdf.navigation.PdfStructureDestination;
4849
import com.itextpdf.kernel.pdf.tagging.IStructureNode;
4950
import com.itextpdf.kernel.pdf.tagging.PdfNamespace;
51+
import com.itextpdf.kernel.pdf.tagging.PdfStructElem;
5052
import com.itextpdf.kernel.pdf.tagging.PdfStructTreeRoot;
5153
import com.itextpdf.kernel.pdf.tagging.StandardNamespaces;
5254
import com.itextpdf.kernel.pdf.tagging.StandardRoles;
55+
import com.itextpdf.kernel.pdf.tagutils.TagStructureContext;
5356
import com.itextpdf.kernel.pdf.tagutils.TagTreePointer;
5457
import com.itextpdf.kernel.utils.CompareTool;
5558
import com.itextpdf.kernel.xmp.XMPException;
5659
import com.itextpdf.kernel.xmp.XMPMeta;
5760
import com.itextpdf.kernel.xmp.XMPMetaFactory;
61+
import com.itextpdf.layout.element.AreaBreak;
5862
import com.itextpdf.layout.element.Div;
5963
import com.itextpdf.layout.element.Link;
6064
import com.itextpdf.layout.element.List;
@@ -89,7 +93,6 @@ public static void beforeClass() {
8993
createOrClearDestinationFolder(DESTINATION_FOLDER);
9094
}
9195

92-
9396
@Test
9497
public void checkXmpMetadataTest() throws IOException, XMPException, InterruptedException {
9598
String outFile = DESTINATION_FOLDER + "xmpMetadataTest.pdf";
@@ -716,6 +719,43 @@ public void checkPageNumberAndLabelTest() throws IOException, XMPException, Inte
716719
compareAndValidate(outFile, cmpFile);
717720
}
718721

722+
@Test
723+
public void checkStructureDestinationTest() throws IOException, InterruptedException, XMPException {
724+
String outFile = DESTINATION_FOLDER + "structureDestination01Test.pdf";
725+
String cmpFile = SOURCE_FOLDER + "cmp_structureDestination01Test.pdf";
726+
727+
try (PdfDocument pdfDocument = new PdfDocument(new PdfWriter(outFile, new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0)))) {
728+
Document document = new Document(pdfDocument);
729+
PdfFont font = PdfFontFactory.createFont(FONT_FOLDER + "FreeSans.ttf",
730+
"WinAnsi", EmbeddingStrategy.FORCE_EMBEDDED);
731+
document.setFont(font);
732+
createSimplePdfUA2Document(pdfDocument);
733+
734+
Paragraph paragraph = new Paragraph("Some text");
735+
document.add(paragraph);
736+
737+
// Now add a link to the paragraph
738+
TagStructureContext context = pdfDocument.getTagStructureContext();
739+
TagTreePointer tagPointer = context.getAutoTaggingPointer();
740+
PdfStructElem structElem = context.getPointerStructElem(tagPointer);
741+
742+
PdfLinkAnnotation linkExplicitDest = new PdfLinkAnnotation(new Rectangle(35, 785, 160, 15));
743+
PdfStructureDestination dest = PdfStructureDestination.createFit(structElem);
744+
PdfAction gotoStructAction = PdfAction.createGoTo(dest);
745+
gotoStructAction.put(PdfName.SD, dest.getPdfObject());
746+
linkExplicitDest.setAction(gotoStructAction);
747+
748+
document.add(new AreaBreak());
749+
750+
Link linkElem = new Link("Link to paragraph", linkExplicitDest);
751+
linkElem.getAccessibilityProperties().setRole(StandardRoles.LINK);
752+
linkElem.getAccessibilityProperties().setAlternateDescription("Some text");
753+
754+
document.add(new Paragraph(linkElem));
755+
}
756+
compareAndValidate(outFile, cmpFile);
757+
}
758+
719759
private void createSimplePdfUA2Document(PdfDocument pdfDocument) throws IOException, XMPException {
720760
byte[] bytes = Files.readAllBytes(Paths.get(SOURCE_FOLDER + "simplePdfUA2.xmp"));
721761
XMPMeta xmpMeta = XMPMetaFactory.parse(new ByteArrayInputStream(bytes));
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)