Skip to content

Commit c76aa6b

Browse files
committed
Add PDF/A-4 checks for embedded files
DEVSIX-7748
1 parent 5caaeaf commit c76aa6b

File tree

8 files changed

+350
-9
lines changed

8 files changed

+350
-9
lines changed

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -970,6 +970,17 @@ protected void checkFormXObject(PdfStream form, PdfStream contentStream) {
970970
checkContentStream(form);
971971
}
972972

973+
/**
974+
* Check optional content configuration dictionary against AS key.
975+
*
976+
* @param config a content configuration dictionary
977+
*/
978+
protected void checkContentConfigurationDictAgainstAsKey(PdfDictionary config) {
979+
if (config.containsKey(PdfName.AS)) {
980+
throw new PdfAConformanceException(PdfAConformanceException.THE_AS_KEY_SHALL_NOT_APPEAR_IN_ANY_OPTIONAL_CONTENT_CONFIGURATION_DICTIONARY);
981+
}
982+
}
983+
973984
/**
974985
* Retrieve transparency error message valid for the pdf/a standard being used.
975986
*
@@ -1128,9 +1139,8 @@ private void checkCatalogConfig(PdfDictionary config, HashSet<PdfObject> ocgs, H
11281139
if (!names.add(name.toUnicodeString())) {
11291140
throw new PdfAConformanceException(PdfAConformanceException.VALUE_OF_NAME_ENTRY_SHALL_BE_UNIQUE_AMONG_ALL_OPTIONAL_CONTENT_CONFIGURATION_DICTIONARIES);
11301141
}
1131-
if (config.containsKey(PdfName.AS)) {
1132-
throw new PdfAConformanceException(PdfAConformanceException.THE_AS_KEY_SHALL_NOT_APPEAR_IN_ANY_OPTIONAL_CONTENT_CONFIGURATION_DICTIONARY);
1133-
}
1142+
checkContentConfigurationDictAgainstAsKey(config);
1143+
11341144
PdfArray orderArray = config.getAsArray(PdfName.Order);
11351145
if (orderArray != null) {
11361146
HashSet<PdfObject> order = new HashSet<>();

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,14 @@ This file is part of the iText (R) project.
3434
import com.itextpdf.kernel.pdf.colorspace.PdfSpecialCs;
3535
import com.itextpdf.pdfa.exceptions.PdfAConformanceException;
3636
import com.itextpdf.pdfa.exceptions.PdfaExceptionMessageConstant;
37+
import com.itextpdf.pdfa.logs.PdfAConformanceLogMessageConstant;
3738

3839
import java.util.Arrays;
3940
import java.util.Collections;
4041
import java.util.HashSet;
4142
import java.util.Set;
43+
import org.slf4j.Logger;
44+
import org.slf4j.LoggerFactory;
4245

4346

4447
/**
@@ -98,6 +101,8 @@ public class PdfA4Checker extends PdfA3Checker {
98101
private static final String TRANSPARENCY_ERROR_MESSAGE =
99102
PdfaExceptionMessageConstant.THE_DOCUMENT_AND_THE_PAGE_DO_NOT_CONTAIN_A_PDFA_OUTPUTINTENT_BUT_PAGE_CONTAINS_TRANSPARENCY_AND_DOES_NOT_CONTAIN_BLENDING_COLOR_SPACE;
100103

104+
private static final Logger LOGGER = LoggerFactory.getLogger(PdfAChecker.class);
105+
101106
/**
102107
* Creates a PdfA4Checker with the required conformance level
103108
*
@@ -137,6 +142,12 @@ protected void checkCatalog(PdfCatalog catalog) {
137142
throw new PdfAConformanceException(PdfaExceptionMessageConstant.DOCUMENT_SHALL_NOT_CONTAIN_INFO_UNLESS_THERE_IS_PIECE_INFO);
138143
}
139144
}
145+
146+
if ("F".equals(conformanceLevel.getConformance())) {
147+
if (!catalog.nameTreeContainsKey(PdfName.EmbeddedFiles)) {
148+
throw new PdfAConformanceException(PdfaExceptionMessageConstant.NAME_DICTIONARY_SHALL_CONTAIN_EMBEDDED_FILES_KEY);
149+
}
150+
}
140151
}
141152

142153
/**
@@ -153,6 +164,22 @@ protected void checkCatalogValidEntries(PdfDictionary catalogDict) {
153164
}
154165
}
155166

167+
/**
168+
* {@inheritDoc}
169+
*/
170+
@Override
171+
protected void checkFileSpec(PdfDictionary fileSpec) {
172+
if (fileSpec.getAsName(PdfName.AFRelationship) == null) {
173+
throw new PdfAConformanceException(PdfaExceptionMessageConstant.FILE_SPECIFICATION_DICTIONARY_SHALL_CONTAIN_AFRELATIONSHIP_KEY);
174+
}
175+
if (!fileSpec.containsKey(PdfName.F) || !fileSpec.containsKey(PdfName.UF)) {
176+
throw new PdfAConformanceException(PdfAConformanceException.FILE_SPECIFICATION_DICTIONARY_SHALL_CONTAIN_F_KEY_AND_UF_KEY);
177+
}
178+
if (!fileSpec.containsKey(PdfName.Desc)) {
179+
LOGGER.warn(PdfAConformanceLogMessageConstant.FILE_SPECIFICATION_DICTIONARY_SHOULD_CONTAIN_DESC_KEY);
180+
}
181+
}
182+
156183
/**
157184
* {@inheritDoc}
158185
*/
@@ -263,6 +290,14 @@ protected void checkAnnotationAgainstActions(PdfDictionary annotDic) {
263290
}
264291
}
265292

293+
/**
294+
* {@inheritDoc}
295+
*/
296+
@Override
297+
protected void checkContentConfigurationDictAgainstAsKey(PdfDictionary config) {
298+
// Do nothing because in PDF/A-4 AS key may appear in any optional content configuration dictionary.
299+
}
300+
266301
/**
267302
* {@inheritDoc}
268303
*/

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,12 @@ public final class PdfaExceptionMessageConstant {
3131
public static final String INVALID_INLINE_IMAGE_FILTER_USAGE = "Filters that are not listed in ISO 32000-2:—, 8.9.7, Table 92 or an array containing any such value shall not be used.";
3232
public static final String DOCUMENT_INFO_DICTIONARY_SHALL_ONLY_CONTAIN_MOD_DATE = "If a document information dictionary is present, it shall only contain a ModDate entry.";
3333
public static final String DOCUMENT_SHALL_NOT_CONTAIN_INFO_UNLESS_THERE_IS_PIECE_INFO = "The Info key shall not be present in the trailer dictionary of PDF/A-4 conforming files unless there exists a PieceInfo entry in the document catalog dictionary.";
34+
public static final String NAME_DICTIONARY_SHALL_CONTAIN_EMBEDDED_FILES_KEY = "Conforming file shall contain an EmbeddedFiles key in the name dictionary of the document catalog dictionary.";
3435
public static final String THE_FILE_HEADER_SHALL_CONTAIN_RIGHT_PDF_VERSION = "The file header shall begin at byte zero and shall consist of “%PDF-{0}.n”";
3536
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”";
3637
public static final String CANNOT_FIND_PDFA_CHECKER_FOR_SPECIFIED_NAME
3738
= "Can't find an appropriate checker for a specified name.";
39+
public static final String FILE_SPECIFICATION_DICTIONARY_SHALL_CONTAIN_AFRELATIONSHIP_KEY = "Each embedded file’s file specification dictionary shall contain an AFRelationship key.";
3840
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";
3941

4042
public static final String THE_DOCUMENT_AND_THE_PAGE_DO_NOT_CONTAIN_A_PDFA_OUTPUTINTENT_BUT_PAGE_CONTAINS_TRANSPARENCY_AND_DOES_NOT_CONTAIN_BLENDING_COLOR_SPACE =

pdfa/src/test/java/com/itextpdf/pdfa/PdfA4AnnotationCheckTest.java

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ This file is part of the iText (R) project.
2929
import com.itextpdf.kernel.pdf.PdfAConformanceLevel;
3030
import com.itextpdf.kernel.pdf.PdfArray;
3131
import com.itextpdf.kernel.pdf.PdfDictionary;
32+
import com.itextpdf.kernel.pdf.PdfDocument;
3233
import com.itextpdf.kernel.pdf.PdfName;
3334
import com.itextpdf.kernel.pdf.PdfNumber;
3435
import com.itextpdf.kernel.pdf.PdfOutputIntent;
@@ -217,6 +218,8 @@ public void pdfA4fForbiddenAnnotations1Test() throws FileNotFoundException {
217218
PdfADocument doc = new PdfADocument(writer, PdfAConformanceLevel.PDF_A_4F, createOutputIntent());
218219
PdfPage page = doc.addNewPage();
219220

221+
addSimpleEmbeddedFile(doc);
222+
220223
PdfAnnotation annot = new PdfSoundAnnotation(new Rectangle(100, 100, 100, 100), new PdfStream());
221224
page.addAnnotation(annot);
222225

@@ -231,6 +234,8 @@ public void pdfA4fForbiddenAnnotations2Test() throws FileNotFoundException {
231234
PdfADocument doc = new PdfADocument(writer, PdfAConformanceLevel.PDF_A_4F, createOutputIntent());
232235
PdfPage page = doc.addNewPage();
233236

237+
addSimpleEmbeddedFile(doc);
238+
234239
PdfAnnotation annot = new Pdf3DAnnotation(new Rectangle(100, 100, 100, 100), new PdfArray());
235240
page.addAnnotation(annot);
236241

@@ -248,9 +253,7 @@ public void pdfA4fAllowedAnnotations1Test() throws IOException, InterruptedExcep
248253
try (PdfADocument doc = new PdfADocument(writer, PdfAConformanceLevel.PDF_A_4F, createOutputIntent())) {
249254
PdfPage page = doc.addNewPage();
250255

251-
PdfFileSpec fs = PdfFileSpec.createEmbeddedFileSpec(
252-
doc, "file".getBytes(), "description", "file.txt", null, null, null);
253-
doc.addFileAttachment("file.txt", fs);
256+
addSimpleEmbeddedFile(doc);
254257

255258
PdfAnnotation annot = new PdfFileAttachmentAnnotation(new Rectangle(100, 100, 100, 100));
256259
annot.setFlag(PdfAnnotation.PRINT);
@@ -270,9 +273,7 @@ public void pdfA4fAllowedAnnotations2Test() throws IOException, InterruptedExcep
270273
try (PdfADocument doc = new PdfADocument(writer, PdfAConformanceLevel.PDF_A_4F, createOutputIntent())) {
271274
PdfPage page = doc.addNewPage();
272275

273-
PdfFileSpec fs = PdfFileSpec.createEmbeddedFileSpec(
274-
doc, "file".getBytes(), "description", "file.txt", null, null, null);
275-
doc.addFileAttachment("file.txt", fs);
276+
addSimpleEmbeddedFile(doc);
276277

277278
PdfAnnotation annot = new PdfLinkAnnotation(new Rectangle(100, 100, 100, 100));
278279
annot.setFlag(PdfAnnotation.PRINT);
@@ -305,6 +306,8 @@ public void pdfA4ForbiddenAKeyWidgetAnnotationTest() throws FileNotFoundExceptio
305306
PdfADocument doc = new PdfADocument(writer, PdfAConformanceLevel.PDF_A_4F, createOutputIntent());
306307
PdfPage page = doc.addNewPage();
307308

309+
addSimpleEmbeddedFile(doc);
310+
308311
PdfAnnotation annot = new PdfWidgetAnnotation(new Rectangle(100, 100, 100, 100));
309312
annot.getPdfObject().put(PdfName.A, (new PdfAction()).getPdfObject());
310313
annot.setFlag(PdfAnnotation.PRINT);
@@ -339,6 +342,8 @@ public void pdfA4ForbiddenAAKeyAnnotationTest() throws IOException, InterruptedE
339342
PdfADocument doc = new PdfADocument(writer, PdfAConformanceLevel.PDF_A_4F, createOutputIntent());
340343
PdfPage page = doc.addNewPage();
341344

345+
addSimpleEmbeddedFile(doc);
346+
342347
PdfAnnotation annot = new PdfLinkAnnotation(new Rectangle(100, 100, 100, 100));
343348
annot.getPdfObject().put(PdfName.AA, (new PdfAction()).getPdfObject());
344349
annot.setFlag(PdfAnnotation.PRINT);
@@ -356,6 +361,12 @@ private void compareResult(String outPdf, String cmpPdf) throws IOException, Int
356361
}
357362
}
358363

364+
private void addSimpleEmbeddedFile(PdfDocument doc) {
365+
PdfFileSpec fs = PdfFileSpec.createEmbeddedFileSpec(
366+
doc, "file".getBytes(), "description", "file.txt", null, null, null);
367+
doc.addFileAttachment("file.txt", fs);
368+
}
369+
359370
private PdfOutputIntent createOutputIntent() throws FileNotFoundException {
360371
return new PdfOutputIntent("Custom", "", "http://www.color.org", "sRGB IEC61966-2.1",
361372
new FileInputStream(SOURCE_FOLDER + "sRGB Color Space Profile.icm"));
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
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.pdfa;
24+
25+
import com.itextpdf.kernel.pdf.PdfAConformanceLevel;
26+
import com.itextpdf.kernel.pdf.PdfDictionary;
27+
import com.itextpdf.kernel.pdf.PdfName;
28+
import com.itextpdf.kernel.pdf.PdfOutputIntent;
29+
import com.itextpdf.kernel.pdf.PdfVersion;
30+
import com.itextpdf.kernel.pdf.PdfWriter;
31+
import com.itextpdf.kernel.pdf.WriterProperties;
32+
import com.itextpdf.kernel.pdf.filespec.PdfFileSpec;
33+
import com.itextpdf.pdfa.exceptions.PdfAConformanceException;
34+
import com.itextpdf.pdfa.exceptions.PdfaExceptionMessageConstant;
35+
import com.itextpdf.test.ExtendedITextTest;
36+
import com.itextpdf.test.annotations.type.IntegrationTest;
37+
38+
import java.io.FileInputStream;
39+
import java.io.FileNotFoundException;
40+
import java.io.IOException;
41+
import org.junit.Assert;
42+
import org.junit.BeforeClass;
43+
import org.junit.Test;
44+
import org.junit.experimental.categories.Category;
45+
46+
@Category(IntegrationTest.class)
47+
public class PdfA4EmbeddedFilesCheckTest extends ExtendedITextTest {
48+
public static final String SOURCE_FOLDER = "./src/test/resources/com/itextpdf/pdfa/";
49+
public static final String DESTINATION_FOLDER = "./target/test/com/itextpdf/pdfa/PdfA4EmbeddedFilesCheckTest/";
50+
51+
@BeforeClass
52+
public static void beforeClass() {
53+
createOrClearDestinationFolder(DESTINATION_FOLDER);
54+
}
55+
56+
// Test with successful creation PDF/A-4F (the same for PDF/A-4E and PDF/A-4) in
57+
// the embedded files meaning can be found in other tests (e.g. PdfA4CatalogCheckTest).
58+
59+
@Test
60+
public void pdfA4fWithoutEmbeddedFilesTest() throws IOException {
61+
String outPdf = DESTINATION_FOLDER + "pdfA4fWithoutEmbeddedFilesTest.pdf";
62+
63+
PdfWriter writer = new PdfWriter(outPdf, new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0));
64+
PdfADocument doc = new PdfADocument(writer, PdfAConformanceLevel.PDF_A_4F, createOutputIntent());
65+
doc.addNewPage();
66+
67+
Exception e = Assert.assertThrows(PdfAConformanceException.class, () -> doc.close());
68+
Assert.assertEquals(PdfaExceptionMessageConstant.NAME_DICTIONARY_SHALL_CONTAIN_EMBEDDED_FILES_KEY, e.getMessage());
69+
}
70+
71+
@Test
72+
public void pdfA4fWithEmbeddedFilesWithoutFTest() throws IOException {
73+
String outPdf = DESTINATION_FOLDER + "pdfA4fWithEmbeddedFilesWithoutFTest.pdf";
74+
75+
PdfWriter writer = new PdfWriter(outPdf, new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0));
76+
PdfADocument doc = new PdfADocument(writer, PdfAConformanceLevel.PDF_A_4F, createOutputIntent());
77+
doc.addNewPage();
78+
79+
PdfFileSpec fs = PdfFileSpec.createEmbeddedFileSpec(
80+
doc, "file".getBytes(), "description", "file.txt", null, null, null);
81+
PdfDictionary fsDict = (PdfDictionary) fs.getPdfObject();
82+
fsDict.remove(PdfName.F);
83+
doc.addFileAttachment("file.txt", fs);
84+
85+
Exception e = Assert.assertThrows(PdfAConformanceException.class, () -> doc.close());
86+
Assert.assertEquals(PdfAConformanceException.FILE_SPECIFICATION_DICTIONARY_SHALL_CONTAIN_F_KEY_AND_UF_KEY, e.getMessage());
87+
}
88+
89+
@Test
90+
public void pdfA4fWithEmbeddedFilesWithoutUFTest() throws IOException {
91+
String outPdf = DESTINATION_FOLDER + "pdfA4fWithEmbeddedFilesWithoutUFTest.pdf";
92+
93+
PdfWriter writer = new PdfWriter(outPdf, new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0));
94+
PdfADocument doc = new PdfADocument(writer, PdfAConformanceLevel.PDF_A_4F, createOutputIntent());
95+
doc.addNewPage();
96+
97+
PdfFileSpec fs = PdfFileSpec.createEmbeddedFileSpec(
98+
doc, "file".getBytes(), "description", "file.txt", null, null, null);
99+
PdfDictionary fsDict = (PdfDictionary) fs.getPdfObject();
100+
fsDict.remove(PdfName.UF);
101+
doc.addFileAttachment("file.txt", fs);
102+
103+
Exception e = Assert.assertThrows(PdfAConformanceException.class, () -> doc.close());
104+
Assert.assertEquals(PdfAConformanceException.FILE_SPECIFICATION_DICTIONARY_SHALL_CONTAIN_F_KEY_AND_UF_KEY, e.getMessage());
105+
}
106+
107+
@Test
108+
public void pdfA4fWithEmbeddedFilesWithoutAFRTest() throws IOException {
109+
String outPdf = DESTINATION_FOLDER + "pdfA4fWithEmbeddedFilesWithoutAFRTest.pdf";
110+
111+
PdfWriter writer = new PdfWriter(outPdf, new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0));
112+
PdfADocument doc = new PdfADocument(writer, PdfAConformanceLevel.PDF_A_4F, createOutputIntent());
113+
doc.addNewPage();
114+
115+
PdfFileSpec fs = PdfFileSpec.createEmbeddedFileSpec(
116+
doc, "file".getBytes(), "description", "file.txt", null, null, null);
117+
PdfDictionary fsDict = (PdfDictionary) fs.getPdfObject();
118+
fsDict.remove(PdfName.AFRelationship);
119+
doc.addFileAttachment("file.txt", fs);
120+
121+
Exception e = Assert.assertThrows(PdfAConformanceException.class, () -> doc.close());
122+
Assert.assertEquals(PdfaExceptionMessageConstant.FILE_SPECIFICATION_DICTIONARY_SHALL_CONTAIN_AFRELATIONSHIP_KEY, e.getMessage());
123+
}
124+
125+
@Test
126+
public void pdfA4eWithEmbeddedFilesWithoutFTest() throws IOException {
127+
String outPdf = DESTINATION_FOLDER + "pdfA4eWithEmbeddedFilesWithoutFTest.pdf";
128+
129+
PdfWriter writer = new PdfWriter(outPdf, new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0));
130+
PdfADocument doc = new PdfADocument(writer, PdfAConformanceLevel.PDF_A_4E, createOutputIntent());
131+
doc.addNewPage();
132+
133+
PdfFileSpec fs = PdfFileSpec.createEmbeddedFileSpec(
134+
doc, "file".getBytes(), "description", "file.txt", null, null, null);
135+
PdfDictionary fsDict = (PdfDictionary) fs.getPdfObject();
136+
fsDict.remove(PdfName.F);
137+
doc.addFileAttachment("file.txt", fs);
138+
139+
Exception e = Assert.assertThrows(PdfAConformanceException.class, () -> doc.close());
140+
Assert.assertEquals(PdfAConformanceException.FILE_SPECIFICATION_DICTIONARY_SHALL_CONTAIN_F_KEY_AND_UF_KEY, e.getMessage());
141+
}
142+
143+
@Test
144+
public void pdfA4WithEmbeddedFilesWithoutAFRTest() throws IOException {
145+
String outPdf = DESTINATION_FOLDER + "pdfA4WithEmbeddedFilesWithoutAFRTest.pdf";
146+
147+
PdfWriter writer = new PdfWriter(outPdf, new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0));
148+
PdfADocument doc = new PdfADocument(writer, PdfAConformanceLevel.PDF_A_4, createOutputIntent());
149+
doc.addNewPage();
150+
151+
PdfFileSpec fs = PdfFileSpec.createEmbeddedFileSpec(
152+
doc, "file".getBytes(), "description", "file.txt", null, null, null);
153+
PdfDictionary fsDict = (PdfDictionary) fs.getPdfObject();
154+
fsDict.remove(PdfName.AFRelationship);
155+
doc.addFileAttachment("file.txt", fs);
156+
157+
Exception e = Assert.assertThrows(PdfAConformanceException.class, () -> doc.close());
158+
Assert.assertEquals(PdfaExceptionMessageConstant.FILE_SPECIFICATION_DICTIONARY_SHALL_CONTAIN_AFRELATIONSHIP_KEY, e.getMessage());
159+
}
160+
161+
private PdfOutputIntent createOutputIntent() throws FileNotFoundException {
162+
return new PdfOutputIntent("Custom", "", "http://www.color.org", "sRGB IEC61966-2.1",
163+
new FileInputStream(SOURCE_FOLDER + "sRGB Color Space Profile.icm"));
164+
}
165+
}

0 commit comments

Comments
 (0)