Skip to content

Commit 85b1bd7

Browse files
committed
Support "Descriptions for embedded files" UA-2 rules
DEVSIX-9042
1 parent cd124c1 commit 85b1bd7

File tree

8 files changed

+158
-59
lines changed

8 files changed

+158
-59
lines changed

pdfua/src/main/java/com/itextpdf/pdfua/checkers/PdfUA2Checker.java

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,15 @@ This file is part of the iText (R) project.
4949
import com.itextpdf.pdfua.checkers.utils.PdfUAValidationContext;
5050
import com.itextpdf.pdfua.checkers.utils.tables.TableCheckUtil;
5151
import com.itextpdf.pdfua.checkers.utils.ua2.PdfUA2DestinationsChecker;
52+
import com.itextpdf.pdfua.checkers.utils.ua2.PdfUA2EmbeddedFilesChecker;
5253
import com.itextpdf.pdfua.checkers.utils.ua2.PdfUA2FormChecker;
5354
import com.itextpdf.pdfua.checkers.utils.ua2.PdfUA2FormulaChecker;
5455
import com.itextpdf.pdfua.checkers.utils.ua2.PdfUA2HeadingsChecker;
5556
import com.itextpdf.pdfua.checkers.utils.ua2.PdfUA2LinkChecker;
5657
import com.itextpdf.pdfua.checkers.utils.ua2.PdfUA2ListChecker;
5758
import com.itextpdf.pdfua.checkers.utils.ua2.PdfUA2NotesChecker;
5859
import com.itextpdf.pdfua.checkers.utils.ua2.PdfUA2TableOfContentsChecker;
59-
import com.itextpdf.pdfua.checkers.utils.ua2.PdfUA2XfaCheckUtil;
60+
import com.itextpdf.pdfua.checkers.utils.ua2.PdfUA2XfaChecker;
6061
import com.itextpdf.pdfua.exceptions.PdfUAConformanceException;
6162
import com.itextpdf.pdfua.exceptions.PdfUAExceptionMessageConstants;
6263
import com.itextpdf.pdfua.logs.PdfUALogMessageConstants;
@@ -98,7 +99,7 @@ public void validate(IValidationContext context) {
9899
checkStructureTreeRoot(pdfDocContext.getPdfDocument().getStructTreeRoot());
99100
checkFonts(pdfDocContext.getDocumentFonts());
100101
new PdfUA2DestinationsChecker(pdfDocument).checkDestinations();
101-
PdfUA2XfaCheckUtil.check(pdfDocContext.getPdfDocument());
102+
PdfUA2XfaChecker.check(pdfDocContext.getPdfDocument());
102103
break;
103104
case FONT:
104105
FontValidationContext fontContext = (FontValidationContext) context;
@@ -157,9 +158,6 @@ protected void checkMetadata(PdfCatalog catalog) {
157158
/**
158159
* Validates document catalog dictionary against PDF/UA-2 standard.
159160
*
160-
* <p>
161-
* For now, only {@code Metadata} and {@code ViewerPreferences} are checked.
162-
*
163161
* @param catalog {@link PdfCatalog} document catalog dictionary to check
164162
*/
165163
private void checkCatalog(PdfCatalog catalog) {
@@ -171,18 +169,16 @@ private void checkCatalog(PdfCatalog catalog) {
171169
formChecker.checkFormFields(catalog.getPdfObject().getAsDictionary(PdfName.AcroForm));
172170
formChecker.checkWidgetAnnotations(this.pdfDocument);
173171
PdfUA2LinkChecker.checkLinkAnnotations(this.pdfDocument);
172+
PdfUA2EmbeddedFilesChecker.checkEmbeddedFiles(catalog);
174173
}
175174

176175
/**
177176
* Validates structure tree root dictionary against PDF/UA-2 standard.
178177
*
179178
* <p>
180-
* Checks that within a given explicitly provided namespace, structure types are not role mapped to other structure
181-
* types in the same namespace. In the StructTreeRoot RoleMap there is no explicitly provided namespace, that's why
182-
* it is not checked.
183-
*
184-
* <p>
185-
* Besides this, only headings check is performed for now.
179+
* Additionally, checks that within a given explicitly provided namespace, structure types are not role mapped to
180+
* other structure types in the same namespace. In the StructTreeRoot RoleMap there is no explicitly provided
181+
* namespace, that's why it is not checked.
186182
*
187183
* @param structTreeRoot {@link PdfStructTreeRoot} structure tree root dictionary to check
188184
*/

pdfua/src/main/java/com/itextpdf/pdfua/checkers/utils/tables/TableStructElementIterator.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ This file is part of the iText (R) project.
2323
package com.itextpdf.pdfua.checkers.utils.tables;
2424

2525
import com.itextpdf.commons.datastructures.Tuple2;
26+
import com.itextpdf.commons.utils.MessageFormatUtil;
2627
import com.itextpdf.kernel.pdf.PdfArray;
2728
import com.itextpdf.kernel.pdf.PdfDictionary;
2829
import com.itextpdf.kernel.pdf.PdfName;
@@ -31,6 +32,8 @@ This file is part of the iText (R) project.
3132
import com.itextpdf.kernel.pdf.tagging.IStructureNode;
3233
import com.itextpdf.kernel.pdf.tagging.PdfStructElem;
3334
import com.itextpdf.pdfua.checkers.utils.PdfUAValidationContext;
35+
import com.itextpdf.pdfua.exceptions.PdfUAConformanceException;
36+
import com.itextpdf.pdfua.exceptions.PdfUAExceptionMessageConstants;
3437

3538
import java.util.ArrayList;
3639
import java.util.Collections;
@@ -204,6 +207,10 @@ private void build2DRepresentationOfTagTreeStructures(List<PdfStructElem> rows)
204207
break;
205208
}
206209
}
210+
if (firstOpenColIndex == -1) {
211+
throw new PdfUAConformanceException(MessageFormatUtil.format(
212+
PdfUAExceptionMessageConstants.ROWS_SPAN_DIFFERENT_NUMBER_OF_COLUMNS, rowIdx, rowIdx + 1));
213+
}
207214
// Set the colspan and rowspan of each cell with a placeholder
208215
for (int i = rowIdx; i < rowIdx + rowSpan; i++) {
209216
for (int j = firstOpenColIndex; j < firstOpenColIndex + colSpan; j++) {
@@ -240,9 +247,9 @@ private void setAmountOfCols(List<PdfStructElem> rows) {
240247
private List<PdfStructElem> extractCells(PdfStructElem row) {
241248
final List<PdfStructElem> elems = new ArrayList<>();
242249
for (final IStructureNode kid : row.getKids()) {
243-
if (kid instanceof PdfStructElem ) {
250+
if (kid instanceof PdfStructElem) {
244251
final PdfName kidRole = this.getRole(kid);
245-
if ((PdfName.TH.equals(kidRole) || PdfName.TD.equals(kidRole))){
252+
if ((PdfName.TH.equals(kidRole) || PdfName.TD.equals(kidRole))) {
246253
elems.add((PdfStructElem) kid);
247254
}
248255
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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.pdfua.checkers.utils.ua2;
24+
25+
import com.itextpdf.kernel.pdf.PdfCatalog;
26+
import com.itextpdf.kernel.pdf.PdfDictionary;
27+
import com.itextpdf.kernel.pdf.PdfName;
28+
import com.itextpdf.kernel.pdf.PdfNameTree;
29+
import com.itextpdf.kernel.pdf.PdfObject;
30+
import com.itextpdf.kernel.pdf.PdfString;
31+
import com.itextpdf.pdfua.exceptions.PdfUAConformanceException;
32+
import com.itextpdf.pdfua.exceptions.PdfUAExceptionMessageConstants;
33+
34+
import java.util.Map;
35+
36+
/**
37+
* Utility class which performs the EmbeddedFiles name tree check according to PDF/UA-2 specification.
38+
*/
39+
public final class PdfUA2EmbeddedFilesChecker {
40+
41+
private PdfUA2EmbeddedFilesChecker() {
42+
// Private constructor will prevent the instantiation of this class directly.
43+
}
44+
45+
/**
46+
* Verify the conformity of the EmbeddedFiles name tree.
47+
*
48+
* @param catalog {@link PdfCatalog} document catalog dictionary
49+
*/
50+
public static void checkEmbeddedFiles(PdfCatalog catalog) {
51+
PdfNameTree embeddedFiles = catalog.getNameTree(PdfName.EmbeddedFiles);
52+
Map<PdfString, PdfObject> embeddedFilesMap = embeddedFiles.getNames();
53+
for (PdfObject fileSpecObject : embeddedFilesMap.values()) {
54+
checkFileSpec(fileSpecObject);
55+
}
56+
}
57+
58+
/**
59+
* Verify the conformity of the file specification dictionary.
60+
*
61+
* @param obj the {@link PdfDictionary} containing file specification to be checked
62+
*/
63+
private static void checkFileSpec(PdfObject obj) {
64+
if (obj.getType() == PdfObject.DICTIONARY) {
65+
PdfDictionary dict = (PdfDictionary) obj;
66+
PdfName type = dict.getAsName(PdfName.Type);
67+
if (PdfName.Filespec.equals(type) && !dict.containsKey(PdfName.Desc)) {
68+
throw new PdfUAConformanceException(
69+
PdfUAExceptionMessageConstants.DESC_IS_REQUIRED_ON_ALL_FILE_SPEC_FROM_THE_EMBEDDED_FILES);
70+
}
71+
}
72+
}
73+
}

pdfua/src/main/java/com/itextpdf/pdfua/checkers/utils/ua2/PdfUA2XfaCheckUtil.java renamed to pdfua/src/main/java/com/itextpdf/pdfua/checkers/utils/ua2/PdfUA2XfaChecker.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ This file is part of the iText (R) project.
3030
/**
3131
* Utility class which performs XFA forms check according to PDF/UA-2 specification.
3232
*/
33-
public final class PdfUA2XfaCheckUtil {
33+
public final class PdfUA2XfaChecker {
3434

35-
private PdfUA2XfaCheckUtil() {
35+
private PdfUA2XfaChecker() {
3636
// Private constructor will prevent the instantiation of this class directly.
3737
}
3838

pdfua/src/main/java/com/itextpdf/pdfua/exceptions/PdfUAExceptionMessageConstants.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ public final class PdfUAExceptionMessageConstants {
4646
"Content with MCID, but MCID wasn't found in StructTreeRoot.";
4747
public static final String CT_OR_ALT_ENTRY_IS_MISSING_IN_MEDIA_CLIP = "CT or Alt entry is missing from the media " +
4848
"clip data dictionary.";
49+
public static final String DESC_IS_REQUIRED_ON_ALL_FILE_SPEC_FROM_THE_EMBEDDED_FILES = "The Desc entry " +
50+
"shall be present on all file specification dictionaries present in the EmbeddedFiles name tree " +
51+
"of a conforming file.";
4952
public static final String DESTINATION_NOT_STRUCTURE_DESTINATION =
5053
"All destinations whose target lies within the same document shall be structure destinations.";
5154
public static final String DIFFERENT_LINKS_IN_SINGLE_STRUCT_ELEM = "Link annotations that target different " +
@@ -129,6 +132,8 @@ public final class PdfUAExceptionMessageConstants {
129132
"Content marked as content may not reside in Artifact content.";
130133
public static final String REAL_CONTENT_INSIDE_ARTIFACT_OR_VICE_VERSA =
131134
"Tagged content is present inside content marked as Artifact or vice versa.";
135+
public static final String ROWS_SPAN_DIFFERENT_NUMBER_OF_COLUMNS =
136+
"Table rows {0} and {1} span different number of columns.";
132137
public static final String SAME_LINKS_IN_DIFFERENT_STRUCT_ELEMS = "Multiple link annotations targeting the same " +
133138
"location shall be included in a single Link or Reference structure element instead of separate ones.";
134139
public static final String STRUCTURE_TYPE_IS_ROLE_MAPPED_TO_OTHER_STRUCTURE_TYPE_IN_THE_SAME_NAMESPACE =

pdfua/src/test/java/com/itextpdf/pdfua/PdfUATaggedGridContainerTest.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public void simpleBorderBoxSizingTest(PdfUAConformance pdfUAConformance) throws
9393

9494
document.add(gridContainer1);
9595
});
96-
framework.assertVeraPdfValid("border", pdfUAConformance);
96+
framework.assertBothValid("border", pdfUAConformance);
9797
}
9898

9999
@ParameterizedTest
@@ -112,7 +112,7 @@ public void simpleMarginTest(PdfUAConformance pdfUAConformance) throws IOExcepti
112112
gridContainer0.setMarginRight(10);
113113
document.add(gridContainer0);
114114
});
115-
framework.assertVeraPdfValid("margin", pdfUAConformance);
115+
framework.assertBothValid("margin", pdfUAConformance);
116116
}
117117

118118
@ParameterizedTest
@@ -131,7 +131,7 @@ public void simplePaddingTest(PdfUAConformance pdfUAConformance) throws IOExcept
131131
gridContainer0.setPaddingRight(10);
132132
document.add(gridContainer0);
133133
});
134-
framework.assertVeraPdfValid("padding", pdfUAConformance);
134+
framework.assertBothValid("padding", pdfUAConformance);
135135
}
136136

137137
@ParameterizedTest
@@ -147,7 +147,7 @@ public void simpleBackgroundTest(PdfUAConformance pdfUAConformance) throws IOExc
147147
gridContainer0.setBackgroundColor(ColorConstants.RED);
148148
document.add(gridContainer0);
149149
});
150-
framework.assertVeraPdfValid("background", pdfUAConformance);
150+
framework.assertBothValid("background", pdfUAConformance);
151151
}
152152

153153
@ParameterizedTest
@@ -167,7 +167,7 @@ public void emptyGridContainerTest(PdfUAConformance pdfUAConformance) throws IOE
167167
gridContainer0.setProperty(Property.COLUMN_GAP, 12.0f);
168168
document.add(gridContainer0);
169169
});
170-
framework.assertVeraPdfValid("emptyGridContainer", pdfUAConformance);
170+
framework.assertBothValid("emptyGridContainer", pdfUAConformance);
171171
}
172172

173173

pdfua/src/test/java/com/itextpdf/pdfua/checkers/PdfUAEmbeddedFilesCheckTest.java

Lines changed: 52 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ This file is part of the iText (R) project.
2626
import com.itextpdf.kernel.font.PdfFont;
2727
import com.itextpdf.kernel.font.PdfFontFactory;
2828
import com.itextpdf.kernel.pdf.PdfDictionary;
29+
import com.itextpdf.kernel.pdf.PdfDocument;
2930
import com.itextpdf.kernel.pdf.PdfName;
3031
import com.itextpdf.kernel.pdf.PdfPage;
3132
import com.itextpdf.kernel.pdf.PdfUAConformance;
@@ -37,24 +38,23 @@ This file is part of the iText (R) project.
3738
import com.itextpdf.pdfua.exceptions.PdfUAExceptionMessageConstants;
3839
import com.itextpdf.test.ExtendedITextTest;
3940
import com.itextpdf.test.TestUtil;
40-
41-
import java.io.IOException;
42-
import java.util.List;
4341
import org.junit.jupiter.api.BeforeAll;
4442
import org.junit.jupiter.api.BeforeEach;
4543
import org.junit.jupiter.api.Tag;
4644
import org.junit.jupiter.params.ParameterizedTest;
4745
import org.junit.jupiter.params.provider.MethodSource;
4846

47+
import java.io.IOException;
48+
import java.util.List;
49+
4950
@Tag("IntegrationTest")
50-
public class PdfUAEmbeddedFilesCheckTest extends ExtendedITextTest {
51+
public class PdfUAEmbeddedFilesCheckTest extends ExtendedITextTest {
5152

5253
private static final String DESTINATION_FOLDER = TestUtil.getOutputPath() + "/pdfua/PdfUAFormulaTest/";
5354
private static final String FONT = "./src/test/resources/com/itextpdf/pdfua/font/FreeSans.ttf";
5455

5556
private UaValidationTestFramework framework;
5657

57-
5858
@BeforeAll
5959
public static void before() {
6060
createOrClearDestinationFolder(DESTINATION_FOLDER);
@@ -69,7 +69,6 @@ public static List<PdfUAConformance> data() {
6969
return UaValidationTestFramework.getConformanceList();
7070
}
7171

72-
7372
@ParameterizedTest
7473
@MethodSource("data")
7574
public void pdfuaWithEmbeddedFilesWithoutFTest(PdfUAConformance pdfUAConformance) throws IOException {
@@ -81,12 +80,11 @@ public void pdfuaWithEmbeddedFilesWithoutFTest(PdfUAConformance pdfUAConformance
8180
pdfDocument.addFileAttachment("file.txt", fs);
8281
});
8382

84-
8583
if (pdfUAConformance == PdfUAConformance.PDF_UA_1) {
8684
framework.assertBothFail("pdfuaWithEmbeddedFilesWithoutF",
8785
PdfUAExceptionMessageConstants.FILE_SPECIFICATION_DICTIONARY_SHALL_CONTAIN_F_KEY_AND_UF_KEY, pdfUAConformance);
8886
} else if (pdfUAConformance == PdfUAConformance.PDF_UA_2) {
89-
framework.assertVeraPdfValid("pdfuaWithEmbeddedFilesWithoutF", pdfUAConformance);
87+
framework.assertBothValid("pdfuaWithEmbeddedFilesWithoutF", pdfUAConformance);
9088
}
9189
}
9290

@@ -106,44 +104,61 @@ public void pdfuaWithEmbeddedFilesWithoutUFTest(PdfUAConformance pdfUAConformanc
106104
framework.assertBothFail("pdfuaWithEmbeddedFilesWithoutUF",
107105
PdfUAExceptionMessageConstants.FILE_SPECIFICATION_DICTIONARY_SHALL_CONTAIN_F_KEY_AND_UF_KEY, pdfUAConformance);
108106
} else if (pdfUAConformance == PdfUAConformance.PDF_UA_2) {
109-
framework.assertVeraPdfValid("pdfuaWithEmbeddedFilesWithoutUF", pdfUAConformance);
107+
framework.assertBothValid("pdfuaWithEmbeddedFilesWithoutUF", pdfUAConformance);
110108
}
111109
}
112110

113111
@ParameterizedTest
114112
@MethodSource("data")
115113
public void pdfuaWithValidEmbeddedFileTest(PdfUAConformance pdfUAConformance) throws IOException {
116114
framework.addBeforeGenerationHook((pdfDocument -> {
117-
PdfFont font;
118-
try {
119-
font = PdfFontFactory.createFont(FONT, PdfEncodings.WINANSI, PdfFontFactory.EmbeddingStrategy.FORCE_EMBEDDED);
120-
} catch (IOException e) {
121-
//rethrow as unchecked to fail the test
122-
throw new RuntimeException();
123-
}
124-
PdfPage page1 = pdfDocument.addNewPage();
125-
PdfCanvas canvas = new PdfCanvas(page1);
126-
127-
TagTreePointer tagPointer = new TagTreePointer(pdfDocument)
128-
.setPageForTagging(page1)
129-
.addTag(StandardRoles.P);
130-
131-
canvas.openTag(tagPointer.getTagReference())
132-
.saveState()
133-
.beginText()
134-
.setFontAndSize(font, 12)
135-
.moveText(100, 100)
136-
.showText("Test text.")
137-
.endText()
138-
.restoreState()
139-
.closeTag();
140-
141-
byte[] somePdf = new byte[35];
142-
pdfDocument.addAssociatedFile("some test pdf file",
143-
PdfFileSpec.createEmbeddedFileSpec(pdfDocument, somePdf, "some test pdf file", "foo.pdf",
144-
PdfName.ApplicationPdf, null, new PdfName("Data")));
115+
addEmbeddedFile(pdfDocument, "some test pdf file");
145116
}));
146117
framework.assertBothValid("pdfuaWithValidEmbeddedFile", pdfUAConformance);
147118
}
148119

120+
@ParameterizedTest
121+
@MethodSource("data")
122+
public void embeddedFilesWithFileSpecWithoutDescTest(PdfUAConformance pdfUAConformance) throws IOException {
123+
framework.addBeforeGenerationHook((pdfDocument -> {
124+
addEmbeddedFile(pdfDocument, null);
125+
}));
126+
if (pdfUAConformance == PdfUAConformance.PDF_UA_1) {
127+
framework.assertBothValid("embeddedFilesWithFileSpecWithoutDesc", pdfUAConformance);
128+
} else if (pdfUAConformance == PdfUAConformance.PDF_UA_2) {
129+
framework.assertBothFail("embeddedFilesWithFileSpecWithoutDesc", PdfUAExceptionMessageConstants.
130+
DESC_IS_REQUIRED_ON_ALL_FILE_SPEC_FROM_THE_EMBEDDED_FILES, pdfUAConformance);
131+
}
132+
}
133+
134+
private static void addEmbeddedFile(PdfDocument pdfDocument, String description) {
135+
PdfFont font;
136+
try {
137+
font = PdfFontFactory.createFont(FONT, PdfEncodings.WINANSI, PdfFontFactory.EmbeddingStrategy.FORCE_EMBEDDED);
138+
} catch (IOException e) {
139+
// Rethrow as unchecked to fail the test.
140+
throw new RuntimeException();
141+
}
142+
PdfPage page = pdfDocument.addNewPage();
143+
PdfCanvas canvas = new PdfCanvas(page);
144+
145+
TagTreePointer tagPointer = new TagTreePointer(pdfDocument)
146+
.setPageForTagging(page)
147+
.addTag(StandardRoles.P);
148+
149+
canvas.openTag(tagPointer.getTagReference())
150+
.saveState()
151+
.beginText()
152+
.setFontAndSize(font, 12)
153+
.moveText(100, 100)
154+
.showText("Test text.")
155+
.endText()
156+
.restoreState()
157+
.closeTag();
158+
159+
byte[] somePdf = new byte[35];
160+
pdfDocument.addAssociatedFile("some test pdf file",
161+
PdfFileSpec.createEmbeddedFileSpec(pdfDocument, somePdf, description, "foo.pdf", PdfName.ApplicationPdf,
162+
null, new PdfName("Data")));
163+
}
149164
}

0 commit comments

Comments
 (0)