Skip to content

Commit 0d8eb13

Browse files
committed
Support "Annotations as artifacts" related UA-2 rules
DEVSIX-9007
1 parent fec44dd commit 0d8eb13

File tree

21 files changed

+554
-127
lines changed

21 files changed

+554
-127
lines changed

kernel/src/main/java/com/itextpdf/kernel/pdf/PdfPage.java

Lines changed: 101 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ This file is part of the iText (R) project.
4444
import com.itextpdf.kernel.pdf.xobject.PdfImageXObject;
4545
import com.itextpdf.kernel.utils.ICopyFilter;
4646
import com.itextpdf.kernel.utils.NullCopyFilter;
47+
import com.itextpdf.kernel.utils.checkers.PdfCheckersUtil;
48+
import com.itextpdf.kernel.validation.context.PdfAnnotationContext;
4749
import com.itextpdf.kernel.validation.context.PdfDestinationAdditionContext;
4850
import com.itextpdf.kernel.validation.context.PdfPageValidationContext;
4951
import com.itextpdf.kernel.xmp.XMPException;
@@ -883,16 +885,7 @@ public PdfPage addAnnotation(PdfAnnotation annotation) {
883885
public PdfPage addAnnotation(int index, PdfAnnotation annotation, boolean tagAnnotation) {
884886
if (getDocument().isTagged()) {
885887
if (tagAnnotation) {
886-
TagTreePointer tagPointer = getDocument().getTagStructureContext().getAutoTaggingPointer();
887-
boolean tagAdded = addAnnotationTag(tagPointer, annotation);
888-
PdfPage prevPage = tagPointer.getCurrentPage();
889-
tagPointer.setPageForTagging(this).addAnnotationTag(annotation);
890-
if (prevPage != null) {
891-
tagPointer.setPageForTagging(prevPage);
892-
}
893-
if (tagAdded) {
894-
tagPointer.moveToParent();
895-
}
888+
tagAnnotation(annotation);
896889
}
897890
if (getTabOrder() == null) {
898891
setTabOrder(PdfName.S);
@@ -913,55 +906,11 @@ public PdfPage addAnnotation(int index, PdfAnnotation annotation, boolean tagAnn
913906
//Annots are indirect so need to be marked as modified
914907
annots.setModified();
915908
}
916-
checkIsoConformanceForDestinations(annotation);
909+
checkIsoConformanceForAnnotation(annotation);
917910

918911
return this;
919912
}
920913

921-
private void checkIsoConformanceForDestinations(PdfAnnotation annotation) {
922-
if (annotation instanceof PdfLinkAnnotation) {
923-
PdfLinkAnnotation linkAnnotation = (PdfLinkAnnotation) annotation;
924-
getDocument().checkIsoConformance(new PdfDestinationAdditionContext(linkAnnotation.getDestinationObject()));
925-
if (linkAnnotation.getAction() != null && PdfName.GoTo.equals(linkAnnotation.getAction().get(PdfName.S))) {
926-
// We only care about destinations, whose target lies within this document.
927-
// That's why GoToR and GoToE are ignored.
928-
getDocument().checkIsoConformance(
929-
new PdfDestinationAdditionContext(new PdfAction(linkAnnotation.getAction())));
930-
}
931-
}
932-
}
933-
934-
private boolean addAnnotationTag(TagTreePointer tagPointer, PdfAnnotation annotation) {
935-
if (annotation instanceof PdfLinkAnnotation) {
936-
// "Link" tag was added starting from PDF 1.4
937-
if (PdfVersion.PDF_1_3.compareTo(getDocument().getPdfVersion()) < 0) {
938-
if (!StandardRoles.LINK.equals(tagPointer.getRole())) {
939-
tagPointer.addTag(StandardRoles.LINK);
940-
return true;
941-
}
942-
}
943-
} else {
944-
if (!(annotation instanceof PdfWidgetAnnotation)
945-
&& !(annotation instanceof PdfPrinterMarkAnnotation)) {
946-
// "Annot" tag was added starting from PDF 1.5
947-
if (PdfVersion.PDF_1_4.compareTo(getDocument().getPdfVersion()) < 0) {
948-
if (!StandardRoles.ANNOT.equals(tagPointer.getRole())) {
949-
tagPointer.addTag(StandardRoles.ANNOT);
950-
return true;
951-
}
952-
}
953-
}
954-
if (annotation instanceof PdfPrinterMarkAnnotation &&
955-
PdfVersion.PDF_2_0.compareTo(getDocument().getPdfVersion()) <= 0) {
956-
if (!StandardRoles.ARTIFACT.equals(tagPointer.getRole())) {
957-
tagPointer.addTag(StandardRoles.ARTIFACT);
958-
return true;
959-
}
960-
}
961-
}
962-
return false;
963-
}
964-
965914
/**
966915
* Removes an annotation from the page.
967916
*
@@ -1327,6 +1276,103 @@ protected boolean isWrappedObjectMustBeIndirect() {
13271276
return true;
13281277
}
13291278

1279+
private static boolean isAnnotInvisible(PdfAnnotation annotation) {
1280+
PdfNumber f = annotation.getPdfObject().getAsNumber(PdfName.F);
1281+
if (f == null) {
1282+
return false;
1283+
}
1284+
int flags = f.intValue();
1285+
return PdfCheckersUtil.checkFlag(flags, PdfAnnotation.INVISIBLE) ||
1286+
(PdfCheckersUtil.checkFlag(flags, PdfAnnotation.NO_VIEW) &&
1287+
!PdfCheckersUtil.checkFlag(flags, PdfAnnotation.TOGGLE_NO_VIEW));
1288+
}
1289+
1290+
private void tagAnnotation(PdfAnnotation annotation) {
1291+
boolean tagAdded = false;
1292+
boolean presentInTagStructure = true;
1293+
boolean isUA2 = isPdfUA2Document();
1294+
TagTreePointer tagPointer = getDocument().getTagStructureContext().getAutoTaggingPointer();
1295+
if (isUA2 && isAnnotInvisible(annotation)) {
1296+
if (PdfVersion.PDF_2_0.compareTo(getDocument().getPdfVersion()) <= 0) {
1297+
if (!StandardRoles.ARTIFACT.equals(tagPointer.getRole())) {
1298+
tagPointer.addTag(StandardRoles.ARTIFACT);
1299+
tagAdded = true;
1300+
}
1301+
} else {
1302+
presentInTagStructure = false;
1303+
}
1304+
} else {
1305+
tagAdded = addAnnotationTag(tagPointer, annotation);
1306+
}
1307+
if (presentInTagStructure) {
1308+
PdfPage prevPage = tagPointer.getCurrentPage();
1309+
tagPointer.setPageForTagging(this).addAnnotationTag(annotation);
1310+
if (prevPage != null) {
1311+
tagPointer.setPageForTagging(prevPage);
1312+
}
1313+
}
1314+
if (tagAdded) {
1315+
tagPointer.moveToParent();
1316+
}
1317+
}
1318+
1319+
private boolean isPdfUA2Document() {
1320+
PdfUAConformance uaConformance = getDocument().getConformance().getUAConformance();
1321+
if (uaConformance == null) {
1322+
try {
1323+
uaConformance = PdfConformance.getConformance(getDocument().getXmpMetadata()).getUAConformance();
1324+
} catch (XMPException e) {
1325+
return false;
1326+
}
1327+
}
1328+
return PdfUAConformance.PDF_UA_2 == uaConformance;
1329+
}
1330+
1331+
private void checkIsoConformanceForAnnotation(PdfAnnotation annotation) {
1332+
getDocument().checkIsoConformance(new PdfAnnotationContext(annotation.getPdfObject()));
1333+
if (annotation instanceof PdfLinkAnnotation) {
1334+
PdfLinkAnnotation linkAnnotation = (PdfLinkAnnotation) annotation;
1335+
getDocument().checkIsoConformance(new PdfDestinationAdditionContext(linkAnnotation.getDestinationObject()));
1336+
if (linkAnnotation.getAction() != null && PdfName.GoTo.equals(linkAnnotation.getAction().get(PdfName.S))) {
1337+
// We only care about destinations, whose target lies within this document.
1338+
// That's why GoToR and GoToE are ignored.
1339+
getDocument().checkIsoConformance(
1340+
new PdfDestinationAdditionContext(new PdfAction(linkAnnotation.getAction())));
1341+
}
1342+
}
1343+
}
1344+
1345+
private boolean addAnnotationTag(TagTreePointer tagPointer, PdfAnnotation annotation) {
1346+
if (annotation instanceof PdfLinkAnnotation) {
1347+
// "Link" tag was added starting from PDF 1.4
1348+
if (PdfVersion.PDF_1_3.compareTo(getDocument().getPdfVersion()) < 0) {
1349+
if (!StandardRoles.LINK.equals(tagPointer.getRole())) {
1350+
tagPointer.addTag(StandardRoles.LINK);
1351+
return true;
1352+
}
1353+
}
1354+
} else {
1355+
if (!(annotation instanceof PdfWidgetAnnotation)
1356+
&& !(annotation instanceof PdfPrinterMarkAnnotation)) {
1357+
// "Annot" tag was added starting from PDF 1.5
1358+
if (PdfVersion.PDF_1_4.compareTo(getDocument().getPdfVersion()) < 0) {
1359+
if (!StandardRoles.ANNOT.equals(tagPointer.getRole())) {
1360+
tagPointer.addTag(StandardRoles.ANNOT);
1361+
return true;
1362+
}
1363+
}
1364+
}
1365+
if (annotation instanceof PdfPrinterMarkAnnotation &&
1366+
PdfVersion.PDF_2_0.compareTo(getDocument().getPdfVersion()) <= 0) {
1367+
if (!StandardRoles.ARTIFACT.equals(tagPointer.getRole())) {
1368+
tagPointer.addTag(StandardRoles.ARTIFACT);
1369+
return true;
1370+
}
1371+
}
1372+
}
1373+
return false;
1374+
}
1375+
13301376
private PdfPage copyTo(PdfPage page, PdfDocument toDocument, IPdfPageExtraCopier copier) {
13311377
final ICopyFilter copyFilter = new DestinationResolverCopyFilter(this.getDocument(), toDocument);
13321378
copyInheritedProperties(page, toDocument, NullCopyFilter.getInstance());
@@ -1540,6 +1586,4 @@ private void rebuildFormFieldParent(PdfDictionary field, PdfDictionary newField,
15401586
newField.put(PdfName.Parent, newParent);
15411587
}
15421588
}
1543-
1544-
15451589
}

kernel/src/main/java/com/itextpdf/kernel/utils/checkers/PdfCheckersUtil.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,16 @@ private static boolean isValidXmpRevision(String value) {
150150
}
151151
return true;
152152
}
153+
154+
/**
155+
* Checks if the specified flag is set.
156+
*
157+
* @param flags a set of flags specifying various characteristics of the PDF object
158+
* @param flag to be checked
159+
*
160+
* @return {@code true} if the specified flag is set, {@code false} otherwise
161+
*/
162+
public static boolean checkFlag(int flags, int flag) {
163+
return (flags & flag) != 0;
164+
}
153165
}

kernel/src/main/java/com/itextpdf/kernel/validation/ValidationType.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,6 @@ public enum ValidationType {
4848
CANVAS_WRITING_CONTENT,
4949
LAYOUT,
5050
DUPLICATE_ID_ENTRY,
51-
DESTINATION_ADDITION
51+
DESTINATION_ADDITION,
52+
ANNOTATION
5253
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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.kernel.validation.context;
24+
25+
import com.itextpdf.kernel.pdf.PdfDictionary;
26+
import com.itextpdf.kernel.validation.IValidationContext;
27+
import com.itextpdf.kernel.validation.ValidationType;
28+
29+
/**
30+
* Class which contains context in which annotation was added.
31+
*/
32+
public class PdfAnnotationContext implements IValidationContext {
33+
private final PdfDictionary annotation;
34+
35+
/**
36+
* Creates new {@link PdfAnnotationContext} instance.
37+
*
38+
* @param annotation {@link PdfDictionary} annotation which was added
39+
*/
40+
public PdfAnnotationContext(PdfDictionary annotation) {
41+
this.annotation = annotation;
42+
}
43+
44+
/**
45+
* {@inheritDoc}
46+
*
47+
* @return {@inheritDoc}
48+
*/
49+
@Override
50+
public ValidationType getType() {
51+
return ValidationType.ANNOTATION;
52+
}
53+
54+
/**
55+
* Gets {@link PdfDictionary} annotation instance.
56+
*
57+
* @return annotation dictionary
58+
*/
59+
public PdfDictionary getAnnotation() {
60+
return annotation;
61+
}
62+
}

kernel/src/main/java/com/itextpdf/kernel/validation/context/PdfDestinationAdditionContext.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ public PdfDestination getDestination() {
8181
return destination;
8282
}
8383

84+
/**
85+
* Gets {@link PdfAction} instance.
86+
*
87+
* @return {@link PdfAction} instance
88+
*/
8489
public PdfAction getAction() {
8590
return action;
8691
}

kernel/src/test/java/com/itextpdf/kernel/pdf/PdfStructElemTest.java

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ This file is part of the iText (R) project.
2828
import com.itextpdf.kernel.exceptions.PdfException;
2929
import com.itextpdf.kernel.font.PdfFontFactory;
3030
import com.itextpdf.kernel.geom.Rectangle;
31+
import com.itextpdf.kernel.pdf.annot.PdfAnnotation;
3132
import com.itextpdf.kernel.pdf.annot.PdfLinkAnnotation;
3233
import com.itextpdf.kernel.pdf.canvas.CanvasTag;
3334
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
@@ -43,18 +44,19 @@ This file is part of the iText (R) project.
4344
import com.itextpdf.test.TestUtil;
4445
import com.itextpdf.test.annotations.LogMessage;
4546
import com.itextpdf.test.annotations.LogMessages;
46-
47-
import java.io.ByteArrayInputStream;
48-
import java.io.ByteArrayOutputStream;
49-
import java.io.IOException;
50-
import java.util.ArrayList;
51-
import javax.xml.parsers.ParserConfigurationException;
5247
import org.junit.jupiter.api.AfterAll;
5348
import org.junit.jupiter.api.Assertions;
5449
import org.junit.jupiter.api.BeforeAll;
5550
import org.junit.jupiter.api.Tag;
5651
import org.junit.jupiter.api.Test;
5752
import org.xml.sax.SAXException;
53+
54+
import javax.xml.parsers.ParserConfigurationException;
55+
import java.io.ByteArrayInputStream;
56+
import java.io.ByteArrayOutputStream;
57+
import java.io.IOException;
58+
import java.util.ArrayList;
59+
5860
import static org.junit.jupiter.api.Assertions.assertTrue;
5961
import static org.junit.jupiter.api.Assertions.fail;
6062

@@ -746,6 +748,40 @@ public void corruptedTagStructureTest04() throws IOException {
746748
document.close();
747749
}
748750

751+
@Test
752+
public void addAnnotationTaggedAsArtifactTest() throws Exception {
753+
try (PdfDocument document = new PdfDocument(
754+
CompareTool.createTestPdfWriter(destinationFolder + "addAnnotationTaggedAsArtifact.pdf",
755+
new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0)
756+
.addPdfUaXmpMetadata(PdfUAConformance.PDF_UA_2)))) {
757+
document.setTagged();
758+
759+
PdfPage page = document.addNewPage();
760+
PdfLinkAnnotation linkAnnotation = new PdfLinkAnnotation(new Rectangle(80, 508, 40, 18));
761+
linkAnnotation.setFlag(PdfAnnotation.INVISIBLE);
762+
page.addAnnotation(linkAnnotation);
763+
}
764+
765+
compareResult("addAnnotationTaggedAsArtifact.pdf", "cmp_addAnnotationTaggedAsArtifact.pdf");
766+
}
767+
768+
@Test
769+
public void addNotTaggedAnnotationTest() throws Exception {
770+
try (PdfDocument document = new PdfDocument(
771+
CompareTool.createTestPdfWriter(destinationFolder + "addNotTaggedAnnotation.pdf",
772+
new WriterProperties().setPdfVersion(PdfVersion.PDF_1_7)
773+
.addPdfUaXmpMetadata(PdfUAConformance.PDF_UA_2)))) {
774+
document.setTagged();
775+
776+
PdfPage page = document.addNewPage();
777+
PdfLinkAnnotation linkAnnotation = new PdfLinkAnnotation(new Rectangle(80, 508, 40, 18));
778+
linkAnnotation.setFlag(PdfAnnotation.INVISIBLE);
779+
page.addAnnotation(linkAnnotation);
780+
}
781+
782+
compareResult("addNotTaggedAnnotation.pdf", "cmp_addNotTaggedAnnotation.pdf");
783+
}
784+
749785
private void compareResult(String outFileName, String cmpFileName)
750786
throws IOException, InterruptedException, ParserConfigurationException, SAXException {
751787
CompareTool compareTool = new CompareTool();

0 commit comments

Comments
 (0)