Skip to content

Commit 5956db5

Browse files
committed
Add PDF/A-4 checks for annotations and interactive forms
DEVSIX-7744
1 parent 5c0e6e3 commit 5956db5

16 files changed

+506
-14
lines changed

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ public class PdfAConformanceLevel {
4242
public static final PdfAConformanceLevel PDF_A_3B = new PdfAConformanceLevel("3", "B");
4343
public static final PdfAConformanceLevel PDF_A_3U = new PdfAConformanceLevel("3", "U");
4444
public static final PdfAConformanceLevel PDF_A_4 = new PdfAConformanceLevel("4", null);
45+
public static final PdfAConformanceLevel PDF_A_4E = new PdfAConformanceLevel("4", "E");
46+
public static final PdfAConformanceLevel PDF_A_4F = new PdfAConformanceLevel("4", "F");
4547
public static final String PDF_A_4_REVISION = "2020";
4648

4749
private final String conformance;
@@ -65,6 +67,8 @@ public static PdfAConformanceLevel getConformanceLevel(String part, String confo
6567
boolean aLevel = "A".equals(lowLetter);
6668
boolean bLevel = "B".equals(lowLetter);
6769
boolean uLevel = "U".equals(lowLetter);
70+
boolean eLevel = "E".equals(lowLetter);
71+
boolean fLevel = "F".equals(lowLetter);
6872

6973
switch (part) {
7074
case "1":
@@ -90,6 +94,10 @@ public static PdfAConformanceLevel getConformanceLevel(String part, String confo
9094
return PdfAConformanceLevel.PDF_A_3U;
9195
break;
9296
case "4":
97+
if (eLevel)
98+
return PdfAConformanceLevel.PDF_A_4E;
99+
if (fLevel)
100+
return PdfAConformanceLevel.PDF_A_4F;
93101
return PdfAConformanceLevel.PDF_A_4;
94102
}
95103
return null;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,7 @@ public class PdfName extends PdfPrimitiveObject implements Comparable<PdfName> {
661661
public static final PdfName Private = createDirectName("Private");
662662
public static final PdfName ProcSet = createDirectName("ProcSet");
663663
public static final PdfName Producer = createDirectName("Producer");
664+
public static final PdfName Projection = createDirectName("Projection");
664665
public static final PdfName PronunciationLexicon = createDirectName("PronunciationLexicon");
665666
public static final PdfName Prop_Build = createDirectName("Prop_Build");
666667
public static final PdfName Properties = createDirectName("Properties");

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

Lines changed: 4 additions & 2 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.xmp.impl.XMPMetaImpl;
2929
import com.itextpdf.test.ExtendedITextTest;
3030
import com.itextpdf.test.annotations.type.UnitTest;
31+
3132
import org.junit.Assert;
3233
import org.junit.Test;
3334
import org.junit.experimental.categories.Category;
@@ -36,8 +37,9 @@ This file is part of the iText (R) project.
3637
public class PdfAConformanceLevelTest extends ExtendedITextTest {
3738
@Test
3839
public void getConformanceTest() {
39-
PdfAConformanceLevel level = PdfAConformanceLevel.getConformanceLevel("4", null);
40-
Assert.assertEquals(PdfAConformanceLevel.PDF_A_4, level);
40+
Assert.assertEquals(PdfAConformanceLevel.PDF_A_4, PdfAConformanceLevel.getConformanceLevel("4", null));
41+
Assert.assertEquals(PdfAConformanceLevel.PDF_A_4E, PdfAConformanceLevel.getConformanceLevel("4", "E"));
42+
Assert.assertEquals(PdfAConformanceLevel.PDF_A_4F, PdfAConformanceLevel.getConformanceLevel("4", "F"));
4143
}
4244

4345
@Test

pdfa/src/main/java/com/itextpdf/pdfa/checker/PdfA1Checker.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -584,7 +584,7 @@ protected void checkAnnotation(PdfDictionary annotDic) {
584584
if (subtype == null) {
585585
throw new PdfAConformanceException(PdfAConformanceException.ANNOTATION_TYPE_0_IS_NOT_PERMITTED).setMessageParams("null");
586586
}
587-
if (forbiddenAnnotations.contains(subtype)) {
587+
if (getForbiddenAnnotations().contains(subtype)) {
588588
throw new PdfAConformanceException(PdfAConformanceException.ANNOTATION_TYPE_0_IS_NOT_PERMITTED).setMessageParams(subtype.getValue());
589589
}
590590
PdfNumber ca = annotDic.getAsNumber(PdfName.CA);
@@ -642,6 +642,15 @@ protected void checkAnnotation(PdfDictionary annotDic) {
642642
}
643643
}
644644

645+
/**
646+
* Gets forbidden annotation types.
647+
*
648+
* @return a set of forbidden annotation types
649+
*/
650+
protected Set<PdfName> getForbiddenAnnotations() {
651+
return forbiddenAnnotations;
652+
}
653+
645654
@Override
646655
protected void checkForm(PdfDictionary form) {
647656
if (form == null)

pdfa/src/main/java/com/itextpdf/pdfa/checker/PdfA2Checker.java

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ public class PdfA2Checker extends PdfA1Checker {
8282
PdfName.Sound,
8383
PdfName.Screen,
8484
PdfName.Movie)));
85+
86+
protected static final Set<PdfName> apLessAnnotations = Collections.unmodifiableSet(
87+
new HashSet<>(Arrays.asList(PdfName.Popup, PdfName.Link)));
88+
8589
protected static final Set<PdfName> forbiddenActions = Collections
8690
.unmodifiableSet(new HashSet<>(Arrays.asList(
8791
PdfName.Launch,
@@ -423,7 +427,7 @@ protected void checkAnnotation(PdfDictionary annotDic) {
423427
if (subtype == null) {
424428
throw new PdfAConformanceException(PdfAConformanceException.ANNOTATION_TYPE_0_IS_NOT_PERMITTED).setMessageParams("null");
425429
}
426-
if (forbiddenAnnotations.contains(subtype)) {
430+
if (getForbiddenAnnotations().contains(subtype)) {
427431
throw new PdfAConformanceException(PdfAConformanceException.ANNOTATION_TYPE_0_IS_NOT_PERMITTED).setMessageParams(subtype.getValue());
428432
}
429433

@@ -447,13 +451,7 @@ protected void checkAnnotation(PdfDictionary annotDic) {
447451
}
448452
}
449453

450-
if (PdfName.Widget.equals(subtype) && (annotDic.containsKey(PdfName.AA) || annotDic.containsKey(PdfName.A))) {
451-
throw new PdfAConformanceException(PdfAConformanceException.WIDGET_ANNOTATION_DICTIONARY_OR_FIELD_DICTIONARY_SHALL_NOT_INCLUDE_A_OR_AA_ENTRY);
452-
}
453-
454-
if (annotDic.containsKey(PdfName.AA)) {
455-
throw new PdfAConformanceException(PdfAConformanceException.AN_ANNOTATION_DICTIONARY_SHALL_NOT_CONTAIN_AA_KEY);
456-
}
454+
checkAnnotationAgainstActions(annotDic);
457455

458456
if (checkStructure(conformanceLevel)) {
459457
if (contentAnnotations.contains(subtype) && !annotDic.containsKey(PdfName.Contents)) {
@@ -488,13 +486,45 @@ protected void checkAnnotation(PdfDictionary annotDic) {
488486
index0.floatValue() == index2.floatValue() && index1.floatValue() == index3.floatValue())
489487
isCorrectRect = true;
490488
}
491-
if (!PdfName.Popup.equals(subtype) &&
492-
!PdfName.Link.equals(subtype) &&
493-
!isCorrectRect)
489+
if (!getAppearanceLessAnnotations().contains(subtype) && !isCorrectRect)
494490
throw new PdfAConformanceException(PdfAConformanceException.EVERY_ANNOTATION_SHALL_HAVE_AT_LEAST_ONE_APPEARANCE_DICTIONARY);
495491
}
496492
}
497493

494+
/**
495+
* Gets annotation types which are allowed not to have appearance stream.
496+
*
497+
* @return set of annotation names.
498+
*/
499+
protected Set<PdfName> getAppearanceLessAnnotations() {
500+
return apLessAnnotations;
501+
}
502+
503+
/**
504+
* Checked annotation against actions, exception will be thrown if either {@code A}
505+
* or {@code AA} actions aren't allowed for specific type of annotation.
506+
*
507+
* @param annotDic an annotation PDF dictionary
508+
*/
509+
protected void checkAnnotationAgainstActions(PdfDictionary annotDic) {
510+
if (PdfName.Widget.equals(annotDic.getAsName(PdfName.Subtype))
511+
&& (annotDic.containsKey(PdfName.AA) || annotDic.containsKey(PdfName.A))) {
512+
513+
throw new PdfAConformanceException(PdfAConformanceException.WIDGET_ANNOTATION_DICTIONARY_OR_FIELD_DICTIONARY_SHALL_NOT_INCLUDE_A_OR_AA_ENTRY);
514+
}
515+
if (annotDic.containsKey(PdfName.AA)) {
516+
throw new PdfAConformanceException(PdfAConformanceException.AN_ANNOTATION_DICTIONARY_SHALL_NOT_CONTAIN_AA_KEY);
517+
}
518+
}
519+
520+
/**
521+
* {@inheritDoc}
522+
*/
523+
@Override
524+
protected Set<PdfName> getForbiddenAnnotations() {
525+
return forbiddenAnnotations;
526+
}
527+
498528
@Override
499529
protected void checkAppearanceStream(PdfStream appearanceStream) {
500530
if (isAlreadyChecked(appearanceStream)) {

pdfa/src/main/java/com/itextpdf/pdfa/checker/PdfA4Checker.java

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ This file is part of the iText (R) project.
3333
import com.itextpdf.pdfa.exceptions.PdfAConformanceException;
3434
import com.itextpdf.pdfa.exceptions.PdfaExceptionMessageConstant;
3535

36+
import java.util.Arrays;
37+
import java.util.Collections;
38+
import java.util.HashSet;
39+
import java.util.Set;
40+
3641

3742
/**
3843
* PdfA4Checker defines the requirements of the PDF/A-4 standard and contains a
@@ -42,6 +47,33 @@ This file is part of the iText (R) project.
4247
* The specification implemented by this class is ISO 19005-4
4348
*/
4449
public class PdfA4Checker extends PdfA3Checker {
50+
protected static final Set<PdfName> forbiddenAnnotations4 = Collections
51+
.unmodifiableSet(new HashSet<>(Arrays.asList(
52+
PdfName._3D,
53+
PdfName.RichMedia,
54+
PdfName.FileAttachment,
55+
PdfName.Sound,
56+
PdfName.Screen,
57+
PdfName.Movie)));
58+
59+
protected static final Set<PdfName> forbiddenAnnotations4E = Collections
60+
.unmodifiableSet(new HashSet<>(Arrays.asList(
61+
PdfName.FileAttachment,
62+
PdfName.Sound,
63+
PdfName.Screen,
64+
PdfName.Movie)));
65+
66+
protected static final Set<PdfName> forbiddenAnnotations4F = Collections
67+
.unmodifiableSet(new HashSet<>(Arrays.asList(
68+
PdfName._3D,
69+
PdfName.RichMedia,
70+
PdfName.Sound,
71+
PdfName.Screen,
72+
PdfName.Movie)));
73+
74+
protected static final Set<PdfName> apLessAnnotations = Collections.unmodifiableSet(
75+
new HashSet<>(Arrays.asList(PdfName.Popup, PdfName.Link, PdfName.Projection)));
76+
4577
/**
4678
* Creates a PdfA4Checker with the required conformance level
4779
*
@@ -132,4 +164,39 @@ protected int getMaxStringLength() {
132164
protected void checkNumberOfDeviceNComponents(PdfSpecialCs.DeviceN deviceN) {
133165

134166
}
167+
168+
/**
169+
* {@inheritDoc}
170+
*/
171+
@Override
172+
protected Set<PdfName> getForbiddenAnnotations() {
173+
if ("E".equals(conformanceLevel.getConformance())) {
174+
return forbiddenAnnotations4E;
175+
} else if ("F".equals(conformanceLevel.getConformance())) {
176+
return forbiddenAnnotations4F;
177+
}
178+
return forbiddenAnnotations4;
179+
}
180+
181+
/**
182+
* {@inheritDoc}
183+
*/
184+
@Override
185+
protected Set<PdfName> getAppearanceLessAnnotations() {
186+
return apLessAnnotations;
187+
}
188+
189+
/**
190+
* {@inheritDoc}
191+
*/
192+
@Override
193+
protected void checkAnnotationAgainstActions(PdfDictionary annotDic) {
194+
if (PdfName.Widget.equals(annotDic.getAsName(PdfName.Subtype)) && annotDic.containsKey(PdfName.A)) {
195+
throw new PdfAConformanceException(
196+
PdfaExceptionMessageConstant.WIDGET_ANNOTATION_DICTIONARY_OR_FIELD_DICTIONARY_SHALL_NOT_INCLUDE_A_ENTRY);
197+
}
198+
if (!PdfName.Widget.equals(annotDic.getAsName(PdfName.Subtype)) && annotDic.containsKey(PdfName.AA)) {
199+
throw new PdfAConformanceException(PdfAConformanceException.AN_ANNOTATION_DICTIONARY_SHALL_NOT_CONTAIN_AA_KEY);
200+
}
201+
}
135202
}

pdfa/src/main/java/com/itextpdf/pdfa/exceptions/PdfaExceptionMessageConstant.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public final class PdfaExceptionMessageConstant {
3535
public static final String THE_CATALOG_VERSION_SHALL_CONTAIN_RIGHT_PDF_VERSION = "The catalog version key shall begin at byte zero and shall consist of “%PDF-{0}.n”";
3636
public static final String CANNOT_FIND_PDFA_CHECKER_FOR_SPECIFIED_NAME
3737
= "Can't find an appropriate checker for a specified name.";
38+
public static final String WIDGET_ANNOTATION_DICTIONARY_OR_FIELD_DICTIONARY_SHALL_NOT_INCLUDE_A_ENTRY = "Widget annotation dictionary or field dictionary shall not include a entry";
3839

3940
private PdfaExceptionMessageConstant(){}
4041
}

0 commit comments

Comments
 (0)