Skip to content

Commit 14f989f

Browse files
committed
Support structure destinations for intra-document links
DEVSIX-7956
1 parent e144a92 commit 14f989f

File tree

11 files changed

+246
-6
lines changed

11 files changed

+246
-6
lines changed

src/main/java/com/itextpdf/html2pdf/attach/impl/LinkContext.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,13 @@ This file is part of the iText (R) project.
2424

2525
import com.itextpdf.html2pdf.html.AttributeConstants;
2626
import com.itextpdf.html2pdf.html.TagConstants;
27+
import com.itextpdf.kernel.pdf.annot.PdfLinkAnnotation;
2728
import com.itextpdf.styledxmlparser.node.IElementNode;
2829
import com.itextpdf.styledxmlparser.node.INode;
2930

31+
import java.util.HashMap;
3032
import java.util.HashSet;
33+
import java.util.Map;
3134
import java.util.Set;
3235
import java.util.Stack;
3336

@@ -45,6 +48,11 @@ public class LinkContext {
4548
* the ids currently in use as valid link destinations
4649
*/
4750
private Set<String> linkDestinations = new HashSet<>();
51+
/**
52+
* Link annotations per destination id. Used to cache link annotations to be able to pass the same annotation
53+
* to different model elements.
54+
*/
55+
private Map<String, PdfLinkAnnotation> linkAnnotations = new HashMap<>();
4856

4957
/**
5058
* Construct an (empty) LinkContext
@@ -97,4 +105,24 @@ public LinkContext scanForIds(INode root) {
97105
public boolean isUsedLinkDestination(String linkDestination) {
98106
return linkDestinations.contains(linkDestination);
99107
}
108+
109+
/**
110+
* Add link annotation to the context.
111+
*
112+
* @param id link destination.
113+
* @param annot link annotation to store.
114+
*/
115+
public void addLinkAnnotation(String id, PdfLinkAnnotation annot) {
116+
linkAnnotations.put(id, annot);
117+
}
118+
119+
/**
120+
* Get link annotation.
121+
*
122+
* @param id link destination.
123+
* @return link annotation for the given link destination.
124+
*/
125+
public PdfLinkAnnotation getLinkAnnotation(String id) {
126+
return linkAnnotations.get(id);
127+
}
100128
}

src/main/java/com/itextpdf/html2pdf/attach/impl/tags/ABlockTagWorker.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public void processEnd(IElementNode element, ProcessorContext context) {
7171
}
7272
}
7373
((Div) getElementResult()).getAccessibilityProperties().setRole(StandardRoles.LINK);
74-
LinkHelper.applyLinkAnnotation(getElementResult(), url);
74+
LinkHelper.applyLinkAnnotation(getElementResult(), url, context);
7575
}
7676

7777
if (getElementResult() != null) {

src/main/java/com/itextpdf/html2pdf/attach/impl/tags/ATagWorker.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public void processEnd(IElementNode element, ProcessorContext context) {
100100
}
101101
getAllElements().set(i, simulatedDiv);
102102
}
103-
LinkHelper.applyLinkAnnotation(getAllElements().get(i), url);
103+
LinkHelper.applyLinkAnnotation(getAllElements().get(i), url, context);
104104
}
105105
}
106106

src/main/java/com/itextpdf/html2pdf/attach/util/LinkHelper.java

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,16 @@ This file is part of the iText (R) project.
2222
*/
2323
package com.itextpdf.html2pdf.attach.util;
2424

25+
import com.itextpdf.commons.datastructures.Tuple2;
26+
import com.itextpdf.html2pdf.ConverterProperties;
2527
import com.itextpdf.html2pdf.logs.Html2PdfLogMessageConstant;
2628
import com.itextpdf.html2pdf.attach.ITagWorker;
2729
import com.itextpdf.html2pdf.attach.ProcessorContext;
2830
import com.itextpdf.html2pdf.attach.impl.tags.SpanTagWorker;
2931
import com.itextpdf.commons.utils.MessageFormatUtil;
3032
import com.itextpdf.kernel.geom.Rectangle;
3133
import com.itextpdf.kernel.pdf.PdfArray;
34+
import com.itextpdf.kernel.pdf.PdfDictionary;
3235
import com.itextpdf.kernel.pdf.action.PdfAction;
3336
import com.itextpdf.kernel.pdf.annot.PdfAnnotation;
3437
import com.itextpdf.kernel.pdf.annot.PdfLinkAnnotation;
@@ -61,13 +64,33 @@ private LinkHelper() {
6164
*
6265
* @param container the containing object
6366
* @param url the destination
67+
* @deprecated in favour of
68+
* {@code applyLinkAnnotation(IPropertyContainer container, String url, ProcessorContext context)}
6469
*/
70+
@Deprecated
6571
public static void applyLinkAnnotation(IPropertyContainer container, String url) {
72+
// Fake context here
73+
applyLinkAnnotation(container, url, new ProcessorContext(new ConverterProperties()));
74+
}
75+
76+
/**
77+
* Applies a link annotation.
78+
*
79+
* @param container the containing object.
80+
* @param url the destination.
81+
* @param context the processor context.
82+
*/
83+
public static void applyLinkAnnotation(IPropertyContainer container, String url, ProcessorContext context) {
6684
if (container != null) {
6785
PdfLinkAnnotation linkAnnotation;
6886
if (url.startsWith("#")) {
69-
String name = url.substring(1);
70-
linkAnnotation = (PdfLinkAnnotation) new PdfLinkAnnotation(new Rectangle(0, 0, 0, 0)).setAction(PdfAction.createGoTo(name)).setFlags(PdfAnnotation.PRINT);
87+
String id = url.substring(1);
88+
linkAnnotation = context.getLinkContext().getLinkAnnotation(id);
89+
if (linkAnnotation == null) {
90+
linkAnnotation = (PdfLinkAnnotation) new PdfLinkAnnotation(new Rectangle(0, 0, 0, 0))
91+
.setAction(PdfAction.createGoTo(id)).setFlags(PdfAnnotation.PRINT);
92+
context.getLinkContext().addLinkAnnotation(id, linkAnnotation);
93+
}
7194
} else {
7295
linkAnnotation = (PdfLinkAnnotation) new PdfLinkAnnotation(new Rectangle(0, 0, 0, 0)).setAction(PdfAction.createURI(url)).setFlags(PdfAnnotation.PRINT);
7396
}
@@ -100,7 +123,16 @@ public static void createDestination(ITagWorker tagWorker, IElementNode element,
100123
Html2PdfLogMessageConstant.ANCHOR_LINK_NOT_HANDLED, element.name(), id, tagWorkerClassName));
101124
return;
102125
}
103-
propertyContainer.setProperty(Property.DESTINATION, id);
126+
127+
PdfLinkAnnotation linkAnnotation = context.getLinkContext().getLinkAnnotation(id);
128+
if (linkAnnotation == null) {
129+
linkAnnotation = (PdfLinkAnnotation) new PdfLinkAnnotation(new Rectangle(0, 0, 0, 0))
130+
.setAction(PdfAction.createGoTo(id)).setFlags(PdfAnnotation.PRINT);
131+
context.getLinkContext().addLinkAnnotation(id, linkAnnotation);
132+
}
133+
134+
propertyContainer.setProperty(Property.DESTINATION,
135+
new Tuple2<String, PdfDictionary>(id, linkAnnotation.getAction()));
104136
}
105137
if (propertyContainer != null) {
106138
propertyContainer.setProperty(Property.ID, id);
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
This file is part of the iText (R) project.
3+
Copyright (c) 1998-2023 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.html2pdf;
24+
25+
import com.itextpdf.html2pdf.resolver.font.DefaultFontProvider;
26+
import com.itextpdf.kernel.pdf.PdfDocument;
27+
import com.itextpdf.kernel.pdf.PdfDocumentInfo;
28+
import com.itextpdf.kernel.pdf.PdfString;
29+
import com.itextpdf.kernel.pdf.PdfVersion;
30+
import com.itextpdf.kernel.pdf.PdfViewerPreferences;
31+
import com.itextpdf.kernel.pdf.PdfWriter;
32+
import com.itextpdf.kernel.pdf.WriterProperties;
33+
import com.itextpdf.kernel.utils.CompareTool;
34+
import com.itextpdf.kernel.xmp.XMPException;
35+
import com.itextpdf.kernel.xmp.XMPMeta;
36+
import com.itextpdf.kernel.xmp.XMPMetaFactory;
37+
import com.itextpdf.layout.font.FontProvider;
38+
import com.itextpdf.test.ExtendedITextTest;
39+
import com.itextpdf.test.annotations.type.IntegrationTest;
40+
import com.itextpdf.test.pdfa.VeraPdfValidator;
41+
42+
import java.io.ByteArrayInputStream;
43+
import java.io.FileInputStream;
44+
import java.io.IOException;
45+
import java.nio.file.Files;
46+
import java.nio.file.Paths;
47+
import org.junit.Assert;
48+
import org.junit.BeforeClass;
49+
import org.junit.Test;
50+
import org.junit.experimental.categories.Category;
51+
52+
@Category(IntegrationTest.class)
53+
public class HtmlConverterPdfUA2Test extends ExtendedITextTest {
54+
55+
private static final String SOURCE_FOLDER = "./src/test/resources/com/itextpdf/html2pdf/HtmlConverterPdfUA2Test/";
56+
private static final String DESTINATION_FOLDER = "./target/test/com/itextpdf/html2pdf/HtmlConverterPdfUA2Test/";
57+
58+
@BeforeClass
59+
public static void beforeClass() {
60+
createOrClearDestinationFolder(DESTINATION_FOLDER);
61+
}
62+
63+
@Test
64+
public void simpleLinkTest() throws IOException, InterruptedException, XMPException {
65+
String sourceHtml = SOURCE_FOLDER + "simpleLink.html";
66+
String cmpPdf = SOURCE_FOLDER + "cmp_simpleLink.pdf";
67+
String destinationPdf = DESTINATION_FOLDER + "simpleLink.pdf";
68+
69+
PdfDocument pdfDocument = new PdfDocument(new PdfWriter(destinationPdf, new WriterProperties().setPdfVersion(
70+
PdfVersion.PDF_2_0)));
71+
createSimplePdfUA2Document(pdfDocument);
72+
73+
ConverterProperties converterProperties = new ConverterProperties();
74+
FontProvider fontProvider = new DefaultFontProvider(false, true, false);
75+
converterProperties.setFontProvider(fontProvider);
76+
HtmlConverter.convertToPdf(new FileInputStream(sourceHtml), pdfDocument, converterProperties);
77+
78+
/* TODO: DEVSIX-7996 - Links created from html2pdf are not ua-2 compliant
79+
* Two verapdf errors are generated here:
80+
* 1. clause="8.9.4.1", Link annotation neither has a Contents entry nor alternate description.
81+
* 2. clause="8.5.1", Real content that does not possess the semantics of text objects and does not have
82+
* an alternate textual representation is not enclosed within Figure or Formula structure elements.
83+
*/
84+
compareAndCheckCompliance(destinationPdf, cmpPdf, false);
85+
}
86+
87+
@Test
88+
public void backwardLinkTest() throws IOException, InterruptedException, XMPException {
89+
String sourceHtml = SOURCE_FOLDER + "backwardLink.html";
90+
String cmpPdf = SOURCE_FOLDER + "cmp_backwardLink.pdf";
91+
String destinationPdf = DESTINATION_FOLDER + "backwardLink.pdf";
92+
93+
PdfDocument pdfDocument = new PdfDocument(new PdfWriter(destinationPdf, new WriterProperties().setPdfVersion(
94+
PdfVersion.PDF_2_0)));
95+
createSimplePdfUA2Document(pdfDocument);
96+
97+
ConverterProperties converterProperties = new ConverterProperties();
98+
FontProvider fontProvider = new DefaultFontProvider(false, true, false);
99+
converterProperties.setFontProvider(fontProvider);
100+
HtmlConverter.convertToPdf(new FileInputStream(sourceHtml), pdfDocument, converterProperties);
101+
102+
/* TODO: DEVSIX-7996 - Links created from html2pdf are not ua-2 compliant
103+
* Two verapdf errors are generated here:
104+
* 1. clause="8.9.4.1", Link annotation neither has a Contents entry nor alternate description.
105+
* 2. clause="8.5.1", Real content that does not possess the semantics of text objects and does not have
106+
* an alternate textual representation is not enclosed within Figure or Formula structure elements.
107+
*/
108+
compareAndCheckCompliance(destinationPdf, cmpPdf, false);
109+
}
110+
111+
private void createSimplePdfUA2Document(PdfDocument pdfDocument) throws IOException, XMPException {
112+
byte[] bytes = Files.readAllBytes(Paths.get(SOURCE_FOLDER + "simplePdfUA2.xmp"));
113+
XMPMeta xmpMeta = XMPMetaFactory.parse(new ByteArrayInputStream(bytes));
114+
pdfDocument.setXmpMetadata(xmpMeta);
115+
pdfDocument.setTagged();
116+
pdfDocument.getCatalog().setViewerPreferences(new PdfViewerPreferences().setDisplayDocTitle(true));
117+
pdfDocument.getCatalog().setLang(new PdfString("en-US"));
118+
PdfDocumentInfo info = pdfDocument.getDocumentInfo();
119+
info.setTitle("PdfUA2 Title");
120+
}
121+
122+
private static void compareAndCheckCompliance(String destinationPdf, String cmpPdf, boolean isExpectedOk)
123+
throws IOException, InterruptedException {
124+
if (isExpectedOk) {
125+
Assert.assertNull(new VeraPdfValidator().validate(destinationPdf));
126+
} else {
127+
Assert.assertNotNull(new VeraPdfValidator().validate(destinationPdf));
128+
}
129+
Assert.assertNull(new CompareTool().compareByContent(destinationPdf, cmpPdf, DESTINATION_FOLDER,
130+
"diff_simple_"));
131+
}
132+
}

src/test/java/com/itextpdf/html2pdf/attach/util/LinkHelperTest.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,16 @@ This file is part of the iText (R) project.
2222
*/
2323
package com.itextpdf.html2pdf.attach.util;
2424

25+
import com.itextpdf.commons.datastructures.Tuple2;
2526
import com.itextpdf.html2pdf.ConverterProperties;
2627
import com.itextpdf.html2pdf.attach.ITagWorker;
2728
import com.itextpdf.html2pdf.attach.ProcessorContext;
2829
import com.itextpdf.html2pdf.attach.impl.tags.DivTagWorker;
2930
import com.itextpdf.html2pdf.css.resolve.func.counter.PageTargetCountElementNode;
3031
import com.itextpdf.html2pdf.html.AttributeConstants;
32+
import com.itextpdf.kernel.pdf.PdfDictionary;
33+
import com.itextpdf.kernel.pdf.PdfName;
34+
import com.itextpdf.kernel.pdf.PdfString;
3135
import com.itextpdf.layout.properties.Property;
3236
import com.itextpdf.styledxmlparser.jsoup.nodes.Attributes;
3337
import com.itextpdf.styledxmlparser.jsoup.nodes.Element;
@@ -52,7 +56,10 @@ public void createDestinationDestinationTest() {
5256
ProcessorContext context = new ProcessorContext(new ConverterProperties());
5357
context.getLinkContext().scanForIds(elementNode);
5458
LinkHelper.createDestination(worker, elementNode, context);
55-
Assert.assertEquals("some_id", worker.getElementResult().<String>getProperty(Property.DESTINATION));
59+
Object destination = worker.getElementResult().<Object>getProperty(Property.DESTINATION);
60+
Tuple2<String, PdfDictionary> destTuple = (Tuple2<String, PdfDictionary>)destination;
61+
Assert.assertEquals("some_id", destTuple.getFirst());
62+
Assert.assertEquals(new PdfString("some_id"), destTuple.getSecond().get(PdfName.D));
5663
}
5764

5865
@Test
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
5+
</head>
6+
<body>
7+
8+
<div id="15=15">Target div</div>
9+
<div style="height:1000px">Some content</div>
10+
<a href="#15=15">Long link text to bottom div. Long link text to bottom div. Long link text to bottom div.
11+
Long link text to bottom div. Long link text to bottom div. Long link text to bottom div.</a>
12+
13+
</body>
14+
</html>
Binary file not shown.
Binary file not shown.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
5+
</head>
6+
<body>
7+
8+
<a href="#15=15">Long link text to bottom div. Long link text to bottom div. Long link text to bottom div.
9+
Long link text to bottom div. Long link text to bottom div. Long link text to bottom div.</a>
10+
<div style="height:1000px">Some content</div>
11+
<div id="15=15">Target div</div>
12+
13+
</body>
14+
</html>

0 commit comments

Comments
 (0)