Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit bfdd38e

Browse files
committedMay 5, 2025
Support Character encodings related rules in UA-2
DEVSIX-9004
1 parent eb025ca commit bfdd38e

File tree

12 files changed

+421
-33
lines changed

12 files changed

+421
-33
lines changed
 

‎commons/src/main/java/com/itextpdf/commons/datastructures/Tuple2.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ This file is part of the iText (R) project.
2222
*/
2323
package com.itextpdf.commons.datastructures;
2424

25+
import java.util.Objects;
26+
2527
/**
2628
* Simple tuple container that holds two elements.
2729
*
@@ -61,6 +63,38 @@ public T2 getSecond() {
6163
return second;
6264
}
6365

66+
/**
67+
* {@inheritDoc}
68+
*
69+
* <p>
70+
* Note, that in case current class is overridden, equals should also be overridden.
71+
*
72+
* @param obj {@inheritDoc}
73+
*
74+
* @return {@inheritDoc}
75+
*/
76+
@Override
77+
public boolean equals(Object obj) {
78+
if (this == obj) {
79+
return true;
80+
}
81+
if (obj == null || getClass() != obj.getClass()) {
82+
return false;
83+
}
84+
Tuple2<T1, T2> that = (Tuple2<T1, T2>) obj;
85+
return Objects.equals(this.first, that.first) && Objects.equals(this.second, that.second);
86+
}
87+
88+
/**
89+
* {@inheritDoc}
90+
*
91+
* @return {@inheritDoc}
92+
*/
93+
@Override
94+
public int hashCode() {
95+
return Objects.hash((Object) first, (Object) second);
96+
}
97+
6498
/**
6599
* {@inheritDoc}
66100
*/

‎commons/src/test/java/com/itextpdf/commons/datastructures/Tuple2Test.java

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ This file is part of the iText (R) project.
2323
package com.itextpdf.commons.datastructures;
2424

2525
import com.itextpdf.test.ExtendedITextTest;
26-
27-
import org.junit.jupiter.api.Test;
2826
import org.junit.jupiter.api.Tag;
27+
import org.junit.jupiter.api.Test;
28+
2929
import static org.junit.jupiter.api.Assertions.assertEquals;
30+
import static org.junit.jupiter.api.Assertions.assertNotEquals;
3031
import static org.junit.jupiter.api.Assertions.assertNull;
3132

3233
@Tag("UnitTest")
@@ -51,4 +52,48 @@ public void testTuple2_TestWithNullFirstValue() {
5152
assertNull(tuple.getFirst());
5253
assertEquals(Integer.valueOf(1), tuple.getSecond());
5354
}
55+
56+
@Test
57+
public void equalsTest() {
58+
Tuple2<String, Integer> tuple1 = new Tuple2<>("test", 1);
59+
Tuple2<String, Integer> tuple2 = new Tuple2<>("test", 1);
60+
assertEquals(tuple1, tuple2);
61+
}
62+
63+
@Test
64+
public void equalsSameTest() {
65+
Tuple2<String, Integer> tuple = new Tuple2<>("test", 1);
66+
assertEquals(tuple, tuple);
67+
}
68+
69+
@Test
70+
public void equalsNullTest() {
71+
Tuple2<String, Integer> tuple = new Tuple2<>("test", 1);
72+
assertNotEquals(tuple, null);
73+
}
74+
75+
@Test
76+
public void notEqualsTest() {
77+
Tuple2<String, Integer> tuple1 = new Tuple2<>("test", 1);
78+
Tuple2<String, Integer> tuple2 = new Tuple2<>("test", 2);
79+
Tuple2<String, Integer> tuple3 = new Tuple2<>("test2", 2);
80+
assertNotEquals(tuple1, tuple2);
81+
assertNotEquals(tuple2, tuple3);
82+
assertNotEquals(tuple1, tuple3);
83+
}
84+
85+
@Test
86+
public void equalsWithCustomTest() {
87+
Tuple2<String, Integer> tuple1 = new Tuple2<>("test", 1);
88+
Tuple2<String, Integer> tuple2 = new CustomTuple2<>("test", 1);
89+
Tuple2<String, Integer> tuple3 = new CustomTuple2<>("test", 1);
90+
assertNotEquals(tuple1, tuple2);
91+
assertEquals(tuple2, tuple3);
92+
}
93+
94+
private static class CustomTuple2<T1, T2> extends Tuple2<T1, T2> {
95+
public CustomTuple2(T1 test, T2 i) {
96+
super(test, i);
97+
}
98+
}
5499
}

‎io/src/main/java/com/itextpdf/io/font/OpenTypeParser.java

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ This file is part of the iText (R) project.
2222
*/
2323
package com.itextpdf.io.font;
2424

25+
import com.itextpdf.commons.datastructures.Tuple2;
2526
import com.itextpdf.io.exceptions.IOException;
2627
import com.itextpdf.io.exceptions.IoExceptionMessageConstant;
2728
import com.itextpdf.io.font.constants.FontStretches;
@@ -124,25 +125,34 @@ static class PostTable {
124125
}
125126

126127
static class CmapTable {
128+
/**
129+
* Collection of the pairs representing Platform ID and Encoding ID of the “cmap” subtables
130+
* present in the font program.
131+
*/
132+
List<Tuple2<Integer, Integer>> cmapEncodings = new ArrayList<>();
127133
/**
128134
* The map containing the code information for the table 'cmap', encoding 1.0.
129135
* The key is the code and the value is an {@code int[2]} where position 0
130136
* is the glyph number and position 1 is the glyph width normalized to 1000 units.
137+
*
131138
* @see TrueTypeFont#UNITS_NORMALIZATION
132139
*/
133140
Map<Integer, int[]> cmap10;
134141
/**
135142
* The map containing the code information for the table 'cmap', encoding 3.1 in Unicode.
136143
* The key is the code and the value is an {@code int[2]} where position 0
137144
* is the glyph number and position 1 is the glyph width normalized to 1000 units.
145+
*
138146
* @see TrueTypeFont#UNITS_NORMALIZATION
139147
*/
140148
Map<Integer, int[]> cmap31;
141149
Map<Integer, int[]> cmapExt;
142150
boolean fontSpecific = false;
143151
}
144152

145-
/** The file name. */
153+
/**
154+
* The file name.
155+
*/
146156
protected String fileName;
147157
/**
148158
* The file in use.
@@ -418,7 +428,8 @@ private void initializeSfntTables() throws java.io.IOException {
418428

419429
/**
420430
* Reads the font data.
421-
* @param all if true, all tables will be read, otherwise only 'head', 'name', and 'os/2'.
431+
*
432+
* @param all if {@code true}, all tables will be read, otherwise only 'head', 'name', and 'os/2'
422433
*/
423434
protected void loadTables(boolean all) throws java.io.IOException {
424435
readNameTable();
@@ -537,8 +548,9 @@ protected IntHashtable readKerning(int unitsPerEm) throws java.io.IOException {
537548
* Read the glyf bboxes from 'glyf' table.
538549
*
539550
* @param unitsPerEm {@link HeaderTable#unitsPerEm}
551+
*
540552
* @throws IOException the font is invalid
541-
* @throws java.io.IOException the font file could not be read
553+
* @throws java.io.IOException the font file could not be read
542554
*/
543555
protected int[][] readBbox(int unitsPerEm) throws java.io.IOException {
544556
int tableLocation[];
@@ -586,7 +598,7 @@ protected int[][] readBbox(int unitsPerEm) throws java.io.IOException {
586598
int start = locaTable[glyph];
587599
if (start != locaTable[glyph + 1]) {
588600
raf.seek(tableGlyphOffset + start + 2);
589-
bboxes[glyph] = new int[] {
601+
bboxes[glyph] = new int[]{
590602
FontProgram.convertGlyphSpaceToTextSpace(raf.readShort()) / unitsPerEm,
591603
FontProgram.convertGlyphSpaceToTextSpace(raf.readShort()) / unitsPerEm,
592604
FontProgram.convertGlyphSpaceToTextSpace(raf.readShort()) / unitsPerEm,
@@ -611,7 +623,7 @@ protected int readNumGlyphs() throws java.io.IOException {
611623
* Extracts the names of the font in all the languages available.
612624
*
613625
* @throws IOException on error
614-
* @throws java.io.IOException on error
626+
* @throws java.io.IOException on error
615627
*/
616628
private void readNameTable() throws java.io.IOException {
617629
int[] table_location = tables.get("name");
@@ -661,7 +673,7 @@ private void readNameTable() throws java.io.IOException {
661673
* Read horizontal header, table 'hhea'.
662674
*
663675
* @throws IOException the font is invalid.
664-
* @throws java.io.IOException the font file could not be read.
676+
* @throws java.io.IOException the font file could not be read.
665677
*/
666678
private void readHheaTable() throws java.io.IOException {
667679
int[] table_location = tables.get("hhea");
@@ -691,7 +703,7 @@ private void readHheaTable() throws java.io.IOException {
691703
* Read font header, table 'head'.
692704
*
693705
* @throws IOException the font is invalid.
694-
* @throws java.io.IOException the font file could not be read.
706+
* @throws java.io.IOException the font file could not be read.
695707
*/
696708
private void readHeadTable() throws java.io.IOException {
697709
int[] table_location = tables.get("head");
@@ -719,7 +731,7 @@ private void readHeadTable() throws java.io.IOException {
719731
* Depends on {@link HeaderTable#unitsPerEm} property.
720732
*
721733
* @throws IOException the font is invalid.
722-
* @throws java.io.IOException the font file could not be read.
734+
* @throws java.io.IOException the font file could not be read.
723735
*/
724736
private void readOs_2Table() throws java.io.IOException {
725737
int[] table_location = tables.get("OS/2");
@@ -825,6 +837,7 @@ private void readCmapTable() throws java.io.IOException {
825837
for (int k = 0; k < num_tables; ++k) {
826838
int platId = raf.readUnsignedShort();
827839
int platSpecId = raf.readUnsignedShort();
840+
cmaps.cmapEncodings.add(new Tuple2<>(platId, platSpecId));
828841
int offset = raf.readInt();
829842
if (platId == 3 && platSpecId == 0) {
830843
cmaps.fontSpecific = true;

‎io/src/main/java/com/itextpdf/io/font/TrueTypeFont.java

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ This file is part of the iText (R) project.
2222
*/
2323
package com.itextpdf.io.font;
2424

25+
import com.itextpdf.commons.datastructures.Tuple2;
2526
import com.itextpdf.commons.utils.MessageFormatUtil;
2627
import com.itextpdf.io.exceptions.IOException;
2728
import com.itextpdf.io.exceptions.IoExceptionMessageConstant;
@@ -32,6 +33,8 @@ This file is part of the iText (R) project.
3233
import com.itextpdf.io.font.otf.OpenTypeGdefTableReader;
3334
import com.itextpdf.io.logs.IoLogMessageConstant;
3435
import com.itextpdf.io.util.IntHashtable;
36+
import org.slf4j.Logger;
37+
import org.slf4j.LoggerFactory;
3538

3639
import java.util.ArrayList;
3740
import java.util.LinkedHashMap;
@@ -41,13 +44,11 @@ This file is part of the iText (R) project.
4144
import java.util.Set;
4245
import java.util.SortedSet;
4346
import java.util.stream.Collectors;
44-
import org.slf4j.Logger;
45-
import org.slf4j.LoggerFactory;
4647

4748
public class TrueTypeFont extends FontProgram {
4849

4950

50-
private OpenTypeParser fontParser;
51+
private OpenTypeParser fontParser;
5152

5253
protected int[][] bBoxes;
5354

@@ -104,6 +105,7 @@ public boolean hasKernPairs() {
104105
*
105106
* @param first the first glyph
106107
* @param second the second glyph
108+
*
107109
* @return the kerning to be applied
108110
*/
109111
@Override
@@ -218,6 +220,37 @@ public Set<Integer> mapGlyphsCidsToGids(Set<Integer> glyphs) {
218220
.collect(Collectors.toSet());
219221
}
220222

223+
/**
224+
* Checks whether current {@link TrueTypeFont} program contains the “cmap” subtable
225+
* with provided platform ID and encoding ID.
226+
*
227+
* @param platformID platform ID
228+
* @param encodingID encoding ID
229+
*
230+
* @return {@code true} if “cmap” subtable with provided platform ID and encoding ID is present in the font program,
231+
* {@code false} otherwise
232+
*/
233+
public boolean isCmapPresent(int platformID, int encodingID) {
234+
OpenTypeParser.CmapTable cmaps = fontParser.getCmapTable();
235+
if (cmaps == null) {
236+
return false;
237+
}
238+
return cmaps.cmapEncodings.contains(new Tuple2<>(platformID, encodingID));
239+
}
240+
241+
/**
242+
* Gets the number of the “cmap” subtables for the current {@link TrueTypeFont} program.
243+
*
244+
* @return the number of the “cmap” subtables
245+
*/
246+
public int getNumberOfCmaps() {
247+
OpenTypeParser.CmapTable cmaps = fontParser.getCmapTable();
248+
if (cmaps == null) {
249+
return 0;
250+
}
251+
return cmaps.cmapEncodings.size();
252+
}
253+
221254
protected void readGdefTable() throws java.io.IOException {
222255
int[] gdef = fontParser.tables.get("GDEF");
223256
if (gdef != null) {
@@ -238,7 +271,8 @@ protected void readGsubTable() throws java.io.IOException {
238271
protected void readGposTable() throws java.io.IOException {
239272
int[] gpos = fontParser.tables.get("GPOS");
240273
if (gpos != null) {
241-
gposTable = new GlyphPositioningTableReader(fontParser.raf, gpos[0], gdefTable, codeToGlyph, fontMetrics.getUnitsPerEm());
274+
gposTable = new GlyphPositioningTableReader(fontParser.raf, gpos[0], gdefTable, codeToGlyph,
275+
fontMetrics.getUnitsPerEm());
242276
}
243277
}
244278

@@ -408,9 +442,9 @@ public void updateUsedGlyphs(SortedSet<Integer> usedGlyphs, boolean subset, List
408442
if (subsetRanges != null) {
409443
compactRange = toCompactRange(subsetRanges);
410444
} else if (!subset) {
411-
compactRange = new int[] {0, 0xFFFF};
445+
compactRange = new int[]{0, 0xFFFF};
412446
} else {
413-
compactRange = new int[] {};
447+
compactRange = new int[]{};
414448
}
415449

416450
for (int k = 0; k < compactRange.length; k += 2) {
@@ -427,9 +461,11 @@ public void updateUsedGlyphs(SortedSet<Integer> usedGlyphs, boolean subset, List
427461
/**
428462
* Normalizes given ranges by making sure that first values in pairs are lower than second values and merges overlapping
429463
* ranges in one.
464+
*
430465
* @param ranges a {@link List} of integer arrays, which are constituted by pairs of ints that denote
431-
* each range limits. Each integer array size shall be a multiple of two.
432-
* @return single merged array consisting of pairs of integers, each of them denoting a range.
466+
* each range limits. Each integer array size shall be a multiple of two
467+
*
468+
* @return single merged array consisting of pairs of integers, each of them denoting a range
433469
*/
434470
private static int[] toCompactRange(List<int[]> ranges) {
435471
List<int[]> simp = new ArrayList<>();

‎io/src/test/java/com/itextpdf/io/font/TrueTypeFontTest.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,14 @@ public void checkSxHeightTtfTest() throws IOException {
117117
Assertions.assertEquals(536, xHeight);
118118
}
119119

120+
@Test
121+
public void containsCmapTest() throws IOException {
122+
TrueTypeFont fontProgram = (TrueTypeFont) FontProgramFactory.createFont(SOURCE_FOLDER + "glyphs-fmt-6.ttf");
123+
Assertions.assertEquals(1, fontProgram.getNumberOfCmaps());
124+
Assertions.assertTrue(fontProgram.isCmapPresent(0, 3));
125+
Assertions.assertFalse(fontProgram.isCmapPresent(1, 0));
126+
}
127+
120128
private void checkCmapTableEntry(FontProgram fontProgram, char uniChar, int expectedGlyphId) {
121129

122130
Glyph glyph = fontProgram.getGlyph(uniChar);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,7 @@ protected void checkContentStream(PdfStream contentStream) {
384384
@Override
385385
protected void checkNonSymbolicTrueTypeFont(PdfTrueTypeFont trueTypeFont) {
386386
String encoding = trueTypeFont.getFontEncoding().getBaseEncoding();
387-
// non-symbolic true type font will always has an encoding entry in font dictionary in itext
387+
// non-symbolic true type font will always have an encoding entry in font dictionary in itext
388388
if (!PdfEncodings.WINANSI.equals(encoding) && !PdfEncodings.MACROMAN.equals(encoding) || trueTypeFont.getFontEncoding().hasDifferences()) {
389389
throw new PdfAConformanceException(PdfaExceptionMessageConstant.ALL_NON_SYMBOLIC_TRUE_TYPE_FONT_SHALL_SPECIFY_MAC_ROMAN_OR_WIN_ANSI_ENCODING_AS_THE_ENCODING_ENTRY, trueTypeFont);
390390
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ protected void checkNumberOfDeviceNComponents(PdfSpecialCs.DeviceN deviceN) {
422422
@Override
423423
protected void checkNonSymbolicTrueTypeFont(PdfTrueTypeFont trueTypeFont) {
424424
String encoding = trueTypeFont.getFontEncoding().getBaseEncoding();
425-
// non-symbolic true type font will always has an encoding entry in font dictionary in itext
425+
// non-symbolic true type font will always have an encoding entry in font dictionary in itext
426426
if (!PdfEncodings.WINANSI.equals(encoding) && !PdfEncodings.MACROMAN.equals(encoding)) {
427427
throw new PdfAConformanceException(PdfaExceptionMessageConstant.ALL_NON_SYMBOLIC_TRUE_TYPE_FONT_SHALL_SPECIFY_MAC_ROMAN_ENCODING_OR_WIN_ANSI_ENCODING, trueTypeFont);
428428
}

‎pdfua/src/main/java/com/itextpdf/pdfua/checkers/PdfUA1Checker.java

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

2525
import com.itextpdf.commons.datastructures.Tuple2;
2626
import com.itextpdf.commons.utils.MessageFormatUtil;
27+
import com.itextpdf.io.font.TrueTypeFont;
2728
import com.itextpdf.kernel.pdf.EncryptionConstants;
2829
import com.itextpdf.kernel.pdf.PdfArray;
2930
import com.itextpdf.kernel.pdf.PdfBoolean;
@@ -218,6 +219,38 @@ void checkLogicalStructureInBMC(Stack<Tuple2<PdfName, PdfDictionary>> stack,
218219
super.checkLogicalStructureInBMC(stack, currentBmc, document);
219220
}
220221

222+
/**
223+
* For all non-symbolic TrueType fonts used for rendering, the embedded TrueType font program shall contain one or
224+
* several non-symbolic cmap entries such that all necessary glyph lookups can be carried out.
225+
*
226+
* @param fontProgram the embedded TrueType font program to check
227+
*/
228+
@Override
229+
void checkNonSymbolicCmapSubtable(TrueTypeFont fontProgram) {
230+
if ((fontProgram.isCmapPresent(3, 0) && fontProgram.getNumberOfCmaps() == 1) ||
231+
fontProgram.getNumberOfCmaps() == 0) {
232+
throw new PdfUAConformanceException(
233+
PdfUAExceptionMessageConstants.NON_SYMBOLIC_TTF_SHALL_CONTAIN_NON_SYMBOLIC_CMAP);
234+
}
235+
}
236+
237+
/**
238+
* Checks cmap entries present in the embedded TrueType font program of the symbolic TrueType font.
239+
*
240+
* <p>
241+
* The “cmap” table in the embedded font program shall either contain exactly one encoding or it shall contain,
242+
* at least, the Microsoft Symbol (3,0 – Platform ID = 3, Encoding ID = 0) encoding.
243+
*
244+
* @param fontProgram the embedded TrueType font program to check
245+
*/
246+
@Override
247+
void checkSymbolicCmapSubtable(TrueTypeFont fontProgram) {
248+
if (!fontProgram.isCmapPresent(3, 0) && fontProgram.getNumberOfCmaps() != 1) {
249+
throw new PdfUAConformanceException(PdfUAExceptionMessageConstants.
250+
SYMBOLIC_TTF_SHALL_CONTAIN_EXACTLY_ONE_OR_AT_LEAST_MICROSOFT_SYMBOL_CMAP);
251+
}
252+
}
253+
221254
private void checkStandardRoleMapping(Tuple2<PdfName, PdfDictionary> tag) {
222255
final PdfNamespace namespace = tagStructureContext.getDocumentDefaultNamespace();
223256
final String role = tag.getFirst().getValue();

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

Lines changed: 33 additions & 0 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;
2424

2525
import com.itextpdf.commons.utils.MessageFormatUtil;
26+
import com.itextpdf.io.font.TrueTypeFont;
2627
import com.itextpdf.kernel.pdf.PdfArray;
2728
import com.itextpdf.kernel.pdf.PdfCatalog;
2829
import com.itextpdf.kernel.pdf.PdfConformance;
@@ -169,6 +170,38 @@ protected void checkMetadata(PdfCatalog catalog) {
169170
}
170171
}
171172

173+
/**
174+
* For all non-symbolic TrueType fonts used for rendering, the embedded TrueType font program shall contain
175+
* at least the Microsoft Unicode (3, 1 – Platform ID = 3, Encoding ID = 1),
176+
* or the Macintosh Roman (1, 0 – Platform ID = 1, Encoding ID = 0) “cmap” subtable.
177+
*
178+
* @param fontProgram the embedded TrueType font program to check
179+
*/
180+
@Override
181+
void checkNonSymbolicCmapSubtable(TrueTypeFont fontProgram) {
182+
if (!fontProgram.isCmapPresent(3, 1) && !fontProgram.isCmapPresent(1, 0)) {
183+
throw new PdfUAConformanceException(
184+
PdfUAExceptionMessageConstants.NON_SYMBOLIC_TTF_SHALL_CONTAIN_MAC_ROMAN_OR_MICROSOFT_UNI_CMAP);
185+
}
186+
}
187+
188+
/**
189+
* Checks cmap entries present in the embedded TrueType font program of the symbolic TrueType font.
190+
*
191+
* <p>
192+
* The “cmap” subtable in the embedded font program shall either contain the Microsoft Symbol
193+
* (3, 0 – Platform ID = 3, Encoding ID = 0) or the Mac Roman (1, 0 – Platform ID = 1, Encoding ID = 1) encoding.
194+
*
195+
* @param fontProgram the embedded TrueType font program to check
196+
*/
197+
@Override
198+
void checkSymbolicCmapSubtable(TrueTypeFont fontProgram) {
199+
if (!fontProgram.isCmapPresent(3, 0) && !fontProgram.isCmapPresent(1, 0)) {
200+
throw new PdfUAConformanceException(
201+
PdfUAExceptionMessageConstants.SYMBOLIC_TTF_SHALL_CONTAIN_MAC_ROMAN_OR_MICROSOFT_SYMBOL_CMAP);
202+
}
203+
}
204+
172205
private void checkPdfObject(PdfObject obj) {
173206
switch (obj.getType()) {
174207
case PdfObject.STRING:

‎pdfua/src/main/java/com/itextpdf/pdfua/checkers/PdfUAChecker.java

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

2525
import com.itextpdf.commons.datastructures.Tuple2;
2626
import com.itextpdf.commons.utils.MessageFormatUtil;
27+
import com.itextpdf.io.font.PdfEncodings;
28+
import com.itextpdf.io.font.TrueTypeFont;
29+
import com.itextpdf.io.font.constants.FontDescriptorFlags;
2730
import com.itextpdf.kernel.exceptions.PdfException;
2831
import com.itextpdf.kernel.font.PdfFont;
32+
import com.itextpdf.kernel.font.PdfTrueTypeFont;
2933
import com.itextpdf.kernel.pdf.PdfArray;
3034
import com.itextpdf.kernel.pdf.PdfBoolean;
3135
import com.itextpdf.kernel.pdf.PdfCatalog;
@@ -37,6 +41,7 @@ This file is part of the iText (R) project.
3741
import com.itextpdf.kernel.pdf.PdfString;
3842
import com.itextpdf.kernel.pdf.tagging.PdfMcr;
3943
import com.itextpdf.kernel.utils.checkers.FontCheckUtil;
44+
import com.itextpdf.kernel.utils.checkers.PdfCheckersUtil;
4045
import com.itextpdf.kernel.validation.IValidationChecker;
4146
import com.itextpdf.pdfua.exceptions.PdfUAConformanceException;
4247
import com.itextpdf.pdfua.exceptions.PdfUAExceptionMessageConstants;
@@ -207,13 +212,28 @@ void checkContentInCanvas(Stack<Tuple2<PdfName, PdfDictionary>> tagStack, PdfDoc
207212
* least one of its glyphs is referenced from one or more content streams, are embedded within that file, as defined
208213
* in ISO 32000-2:2020, 9.9 and ISO 32000-1:2008, 9.9.
209214
*
215+
* <p>
216+
* Checks character encodings rules as defined in ISO 14289-2, 8.4.5.7 and ISO 14289-1, 7.21.6.
217+
*
210218
* @param fontsInDocument collection of fonts used in the document
211219
*/
212220
void checkFonts(Collection<PdfFont> fontsInDocument) {
213221
Set<String> fontNamesThatAreNotEmbedded = new HashSet<>();
214222
for (PdfFont font : fontsInDocument) {
215223
if (!font.isEmbedded()) {
216224
fontNamesThatAreNotEmbedded.add(font.getFontProgram().getFontNames().getFontName());
225+
continue;
226+
}
227+
if (font instanceof PdfTrueTypeFont) {
228+
PdfTrueTypeFont trueTypeFont = (PdfTrueTypeFont) font;
229+
int flags = trueTypeFont.getFontProgram().getPdfFontFlags();
230+
boolean symbolic = PdfCheckersUtil.checkFlag(flags, FontDescriptorFlags.SYMBOLIC) &&
231+
!PdfCheckersUtil.checkFlag(flags, FontDescriptorFlags.NONSYMBOLIC);
232+
if (symbolic) {
233+
checkSymbolicTrueTypeFont(trueTypeFont);
234+
} else {
235+
checkNonSymbolicTrueTypeFont(trueTypeFont);
236+
}
217237
}
218238
}
219239
if (!fontNamesThatAreNotEmbedded.isEmpty()) {
@@ -225,6 +245,20 @@ void checkFonts(Collection<PdfFont> fontsInDocument) {
225245
}
226246
}
227247

248+
/**
249+
* Checks cmap entries present in the embedded TrueType font program of the non-symbolic TrueType font.
250+
*
251+
* @param fontProgram the embedded TrueType font program to check
252+
*/
253+
abstract void checkNonSymbolicCmapSubtable(TrueTypeFont fontProgram);
254+
255+
/**
256+
* Checks cmap entries present in the embedded TrueType font program of the symbolic TrueType font.
257+
*
258+
* @param fontProgram the embedded TrueType font program to check
259+
*/
260+
abstract void checkSymbolicCmapSubtable(TrueTypeFont fontProgram);
261+
228262
/**
229263
* Checks that embedded fonts define all glyphs referenced for rendering within the conforming file.
230264
*
@@ -299,6 +333,34 @@ private static PdfMcr mcrExists(PdfDocument document, int mcid) {
299333
return null;
300334
}
301335

336+
private void checkNonSymbolicTrueTypeFont(PdfTrueTypeFont trueTypeFont) {
337+
TrueTypeFont fontProgram = (TrueTypeFont) trueTypeFont.getFontProgram();
338+
checkNonSymbolicCmapSubtable(fontProgram);
339+
340+
String encoding = trueTypeFont.getFontEncoding().getBaseEncoding();
341+
// Non-symbolic TTF will always have the dictionary value in the Encoding key of the Font dictionary in itext.
342+
if (!PdfEncodings.WINANSI.equals(encoding) && !PdfEncodings.MACROMAN.equals(encoding)) {
343+
throw new PdfUAConformanceException(
344+
PdfUAExceptionMessageConstants.NON_SYMBOLIC_TTF_SHALL_SPECIFY_MAC_ROMAN_OR_WIN_ANSI_ENCODING);
345+
}
346+
347+
if (trueTypeFont.getFontEncoding().hasDifferences() && !fontProgram.isCmapPresent(3, 1)) {
348+
// If font has differences array, itext ensures that all the glyph names in the Differences array are listed
349+
// in the Adobe Glyph List.
350+
throw new PdfUAConformanceException(
351+
PdfUAExceptionMessageConstants.NON_SYMBOLIC_TTF_SHALL_NOT_DEFINE_DIFFERENCES);
352+
}
353+
}
354+
355+
private void checkSymbolicTrueTypeFont(PdfTrueTypeFont trueTypeFont) {
356+
if (trueTypeFont.getPdfObject().containsKey(PdfName.Encoding)) {
357+
throw new PdfUAConformanceException(PdfUAExceptionMessageConstants.SYMBOLIC_TTF_SHALL_NOT_CONTAIN_ENCODING);
358+
}
359+
360+
TrueTypeFont fontProgram = (TrueTypeFont) trueTypeFont.getFontProgram();
361+
checkSymbolicCmapSubtable(fontProgram);
362+
}
363+
302364
private static final class UaCharacterChecker implements FontCheckUtil.CharacterChecker {
303365

304366
/**

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,21 @@ public final class PdfUAExceptionMessageConstants {
126126
public static final String MORE_THAN_ONE_H_TAG = "A node contains more than one H tag.";
127127
public static final String NAME_ENTRY_IS_MISSING_OR_EMPTY_IN_OCG = "Name entry is missing or has " +
128128
"an empty string as its value in an Optional Content Configuration Dictionary.";
129+
public static final String NON_SYMBOLIC_TTF_SHALL_CONTAIN_MAC_ROMAN_OR_MICROSOFT_UNI_CMAP =
130+
"For all non-symbolic TrueType fonts used for rendering, the embedded TrueType font program shall " +
131+
"contain at least the Microsoft Unicode (3, 1 – Platform ID = 3, Encoding ID = 1), or the " +
132+
"Macintosh Roman (1, 0 – Platform ID = 1, Encoding ID = 0) “cmap” subtable.";
133+
public static final String NON_SYMBOLIC_TTF_SHALL_CONTAIN_NON_SYMBOLIC_CMAP = "For all non-symbolic TrueType " +
134+
"fonts used for rendering, the embedded TrueType font program shall contain one or several non-symbolic " +
135+
"cmap entries such that all necessary glyph lookups can be carried out.";
136+
public static final String NON_SYMBOLIC_TTF_SHALL_NOT_DEFINE_DIFFERENCES = "All non-symbolic TrueType fonts " +
137+
"shall not define a Differences array unless all the glyph names in the Differences array are listed " +
138+
"in the Adobe Glyph List and the embedded font program contains at least the Microsoft Unicode " +
139+
"(3, 1 – Platform ID = 3, Encoding ID = 1) encoding in the “cmap” subtable.";
140+
public static final String NON_SYMBOLIC_TTF_SHALL_SPECIFY_MAC_ROMAN_OR_WIN_ANSI_ENCODING = "All non-symbolic " +
141+
"TrueType fonts shall have either MacRomanEncoding or WinAnsiEncoding as the value for the Encoding key " +
142+
"in the Font dictionary, or as the value for the BaseEncoding key in the dictionary that is the value of " +
143+
"the Encoding key in the Font dictionary";
129144
public static final String NON_UNIQUE_ID_ENTRY_IN_STRUCT_TREE_ROOT =
130145
"ID entry '{0}' shall be unique among all elements in the document’s structure hierarchy";
131146
public static final String NOTE_TAG_SHALL_HAVE_ID_ENTRY = "Note tags shall include a unique ID entry.";
@@ -163,6 +178,14 @@ public final class PdfUAExceptionMessageConstants {
163178
"Structure type {0}:{1} is role mapped to other structure type in the same namespace.";
164179
public static final String SUSPECTS_ENTRY_IN_MARK_INFO_DICTIONARY_SHALL_NOT_HAVE_A_VALUE_OF_TRUE =
165180
"Suspects entry in mark info dictionary shall not have a value of true.";
181+
public static final String SYMBOLIC_TTF_SHALL_CONTAIN_EXACTLY_ONE_OR_AT_LEAST_MICROSOFT_SYMBOL_CMAP = "For " +
182+
"symbolic TrueType fonts the “cmap” table in the embedded font program shall either contain exactly one " +
183+
"encoding or it shall contain, at least, the Microsoft Symbol (Platform ID = 3, Encoding ID = 0) encoding.";
184+
public static final String SYMBOLIC_TTF_SHALL_CONTAIN_MAC_ROMAN_OR_MICROSOFT_SYMBOL_CMAP = "For symbolic TrueType" +
185+
" fonts the 'cmap' subtable in the embedded font program shall either contain the Microsoft Symbol (3,0 –" +
186+
" Platform ID=3, Encoding ID=0) or the Mac Roman (1,0 – Platform ID=1, Encoding ID=0) encoding.";
187+
public static final String SYMBOLIC_TTF_SHALL_NOT_CONTAIN_ENCODING = "Symbolic TrueType fonts shall not contain " +
188+
"an Encoding entry in the font dictionary.";
166189
public static final String TABLE_CONTAINS_EMPTY_CELLS = "Cell: row {0} ({1}) col {2} is empty, each row should "
167190
+ "have the same amount of columns when taking into account spanning.";
168191
public static final String TAG_HASNT_BEEN_ADDED_BEFORE_CONTENT_ADDING =

‎pdfua/src/test/java/com/itextpdf/pdfua/PdfUAFontsTest.java

Lines changed: 114 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@ This file is part of the iText (R) project.
2626
import com.itextpdf.io.font.FontEncoding;
2727
import com.itextpdf.io.font.FontProgramFactory;
2828
import com.itextpdf.io.font.PdfEncodings;
29+
import com.itextpdf.io.font.TrueTypeFont;
2930
import com.itextpdf.io.font.constants.StandardFonts;
3031
import com.itextpdf.kernel.font.PdfFont;
3132
import com.itextpdf.kernel.font.PdfFontFactory;
3233
import com.itextpdf.kernel.font.PdfFontFactory.EmbeddingStrategy;
34+
import com.itextpdf.kernel.pdf.PdfName;
3335
import com.itextpdf.kernel.pdf.PdfUAConformance;
3436
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
3537
import com.itextpdf.kernel.pdf.tagging.StandardRoles;
@@ -39,9 +41,6 @@ This file is part of the iText (R) project.
3941
import com.itextpdf.pdfua.exceptions.PdfUAExceptionMessageConstants;
4042
import com.itextpdf.test.ExtendedITextTest;
4143
import com.itextpdf.test.TestUtil;
42-
43-
import java.io.IOException;
44-
import java.util.List;
4544
import org.junit.jupiter.api.Assertions;
4645
import org.junit.jupiter.api.BeforeAll;
4746
import org.junit.jupiter.api.BeforeEach;
@@ -50,6 +49,8 @@ This file is part of the iText (R) project.
5049
import org.junit.jupiter.params.ParameterizedTest;
5150
import org.junit.jupiter.params.provider.MethodSource;
5251

52+
import java.io.IOException;
53+
import java.util.List;
5354

5455
@Tag("IntegrationTest")
5556
public class PdfUAFontsTest extends ExtendedITextTest {
@@ -186,9 +187,8 @@ public void trueTypeFontWithDifferencesTest(PdfUAConformance pdfUAConformance) t
186187
restoreState().closeTag();
187188
});
188189

189-
// TODO DEVSIX-9017 Support PDF/UA rules for fonts.
190-
// TODO DEVSIX-9004 Support Character encodings related rules in UA-2
191-
framework.assertOnlyVeraPdfFail("trueTypeFontWithDifferencesTest", pdfUAConformance);
190+
framework.assertBothFail("trueTypeFontWithDifferencesTest", PdfUAExceptionMessageConstants.
191+
NON_SYMBOLIC_TTF_SHALL_SPECIFY_MAC_ROMAN_OR_WIN_ANSI_ENCODING, false, pdfUAConformance);
192192
}
193193

194194
@ParameterizedTest
@@ -258,7 +258,7 @@ public void nonSymbolicTtfWithValidEncodingTest(PdfUAConformance pdfUAConformanc
258258
Paragraph paragraph = new Paragraph("ABC");
259259
document.add(paragraph);
260260
});
261-
framework.assertBothValid("nonSymbolicTtfWithIncompatibleEncoding", pdfUAConformance);
261+
framework.assertBothValid("nonSymbolicTtfWithValidEncodingTest", pdfUAConformance);
262262
}
263263

264264
@ParameterizedTest
@@ -277,8 +277,8 @@ public void nonSymbolicTtfWithIncompatibleEncodingTest(PdfUAConformance pdfUACon
277277
Paragraph paragraph = new Paragraph("ABC");
278278
document.add(paragraph);
279279
});
280-
// TODO DEVSIX-9004 Support Character encodings related rules in UA-2
281-
framework.assertOnlyVeraPdfFail("nonSymbolicTtfWithIncompatibleEncoding", pdfUAConformance);
280+
framework.assertBothFail("nonSymbolicTtfWithIncompatibleEncoding", PdfUAExceptionMessageConstants.
281+
NON_SYMBOLIC_TTF_SHALL_SPECIFY_MAC_ROMAN_OR_WIN_ANSI_ENCODING, false, pdfUAConformance);
282282
}
283283

284284
@ParameterizedTest
@@ -288,7 +288,8 @@ public void symbolicTtfTest(PdfUAConformance pdfUAConformance) throws IOExceptio
288288
Document document = new Document(pdfDoc);
289289
PdfFont font;
290290
try {
291-
font = PdfFontFactory.createFont(FONT_FOLDER + "Symbols1.ttf");
291+
font = PdfFontFactory.createFont(FONT_FOLDER + "Symbols1.ttf", PdfEncodings.MACROMAN,
292+
EmbeddingStrategy.FORCE_EMBEDDED);
292293
} catch (IOException e) {
293294
throw new RuntimeException();
294295
}
@@ -307,8 +308,58 @@ public void symbolicTtfWithEncodingTest(PdfUAConformance pdfUAConformance) throw
307308
Document document = new Document(pdfDoc);
308309
PdfFont font;
309310
try {
310-
// if we specify encoding, symbolic font is treated as non-symbolic
311-
font = PdfFontFactory.createFont(FONT_FOLDER + "Symbols1.ttf", PdfEncodings.MACROMAN, EmbeddingStrategy.FORCE_EMBEDDED);
311+
font = PdfFontFactory.createFont(FONT_FOLDER + "Symbols1.ttf", PdfEncodings.MACROMAN,
312+
EmbeddingStrategy.FORCE_EMBEDDED);
313+
} catch (IOException e) {
314+
throw new RuntimeException();
315+
}
316+
font.getPdfObject().put(PdfName.Encoding, PdfName.MacRomanEncoding);
317+
document.setFont(font);
318+
319+
Paragraph paragraph = new Paragraph("ABC");
320+
document.add(paragraph);
321+
});
322+
// VeraPDF is valid since iText fixes symbolic flag to non-symbolic on closing.
323+
framework.assertOnlyITextFail("symbolicTtfWithEncoding",
324+
PdfUAExceptionMessageConstants.SYMBOLIC_TTF_SHALL_NOT_CONTAIN_ENCODING, pdfUAConformance);
325+
}
326+
327+
@ParameterizedTest
328+
@MethodSource("data")
329+
public void symbolicTtfWithInvalidCmapTest(PdfUAConformance pdfUAConformance) throws IOException {
330+
framework.addBeforeGenerationHook(pdfDoc -> {
331+
Document document = new Document(pdfDoc);
332+
PdfFont font;
333+
try {
334+
TrueTypeFont fontProgram = new CustomSymbolicTrueTypeFont(FONT);
335+
font = PdfFontFactory.createFont(fontProgram, PdfEncodings.MACROMAN, EmbeddingStrategy.FORCE_EMBEDDED);
336+
} catch (IOException e) {
337+
throw new RuntimeException();
338+
}
339+
document.setFont(font);
340+
341+
Paragraph paragraph = new Paragraph("ABC");
342+
document.add(paragraph);
343+
});
344+
// VeraPDF is valid since iText fixes symbolic flag to non-symbolic on closing.
345+
if (PdfUAConformance.PDF_UA_1 == pdfUAConformance) {
346+
framework.assertOnlyITextFail("symbolicTtfWithInvalidCmapTest", PdfUAExceptionMessageConstants.
347+
SYMBOLIC_TTF_SHALL_CONTAIN_EXACTLY_ONE_OR_AT_LEAST_MICROSOFT_SYMBOL_CMAP, pdfUAConformance);
348+
} else if (PdfUAConformance.PDF_UA_2 == pdfUAConformance) {
349+
framework.assertOnlyITextFail("symbolicTtfWithInvalidCmapTest", PdfUAExceptionMessageConstants.
350+
SYMBOLIC_TTF_SHALL_CONTAIN_MAC_ROMAN_OR_MICROSOFT_SYMBOL_CMAP, pdfUAConformance);
351+
}
352+
}
353+
354+
@ParameterizedTest
355+
@MethodSource("data")
356+
public void nonSymbolicTtfWithInvalidCmapTest(PdfUAConformance pdfUAConformance) throws IOException {
357+
framework.addBeforeGenerationHook(pdfDoc -> {
358+
Document document = new Document(pdfDoc);
359+
PdfFont font;
360+
try {
361+
TrueTypeFont fontProgram = new CustomNonSymbolicTrueTypeFont(FONT);
362+
font = PdfFontFactory.createFont(fontProgram, PdfEncodings.MACROMAN, EmbeddingStrategy.FORCE_EMBEDDED);
312363
} catch (IOException e) {
313364
throw new RuntimeException();
314365
}
@@ -317,7 +368,14 @@ public void symbolicTtfWithEncodingTest(PdfUAConformance pdfUAConformance) throw
317368
Paragraph paragraph = new Paragraph("ABC");
318369
document.add(paragraph);
319370
});
320-
framework.assertBothValid("symbolicTtfWithEncoding", pdfUAConformance);
371+
// VeraPDF is valid since the file itself is valid, but itext code is modified for testing.
372+
if (PdfUAConformance.PDF_UA_1 == pdfUAConformance) {
373+
framework.assertOnlyITextFail("nonSymbolicTtfWithInvalidCmapTest", PdfUAExceptionMessageConstants.
374+
NON_SYMBOLIC_TTF_SHALL_CONTAIN_NON_SYMBOLIC_CMAP, pdfUAConformance);
375+
} else if (PdfUAConformance.PDF_UA_2 == pdfUAConformance) {
376+
framework.assertOnlyITextFail("nonSymbolicTtfWithInvalidCmapTest", PdfUAExceptionMessageConstants.
377+
NON_SYMBOLIC_TTF_SHALL_CONTAIN_MAC_ROMAN_OR_MICROSOFT_UNI_CMAP, pdfUAConformance);
378+
}
321379
}
322380

323381
@Test
@@ -327,4 +385,47 @@ public void symbolicTtfWithChangedCmapTest() {
327385
() -> PdfFontFactory.createFont(FONT_FOLDER + "Symbols1_changed_cmap.ttf",
328386
EmbeddingStrategy.FORCE_EMBEDDED));
329387
}
388+
389+
private static class CustomSymbolicTrueTypeFont extends TrueTypeFont {
390+
public CustomSymbolicTrueTypeFont(String path) throws IOException {
391+
super(path);
392+
}
393+
394+
@Override
395+
public int getPdfFontFlags() {
396+
return 4;
397+
}
398+
399+
@Override
400+
public boolean isCmapPresent(int platformID, int encodingID) {
401+
if (platformID == 1) {
402+
return false;
403+
}
404+
return super.isCmapPresent(platformID, encodingID);
405+
}
406+
}
407+
408+
private static class CustomNonSymbolicTrueTypeFont extends TrueTypeFont {
409+
public CustomNonSymbolicTrueTypeFont(String path) throws IOException {
410+
super(path);
411+
}
412+
413+
@Override
414+
public int getPdfFontFlags() {
415+
return 32;
416+
}
417+
418+
@Override
419+
public boolean isCmapPresent(int platformID, int encodingID) {
420+
if (platformID == 1 || encodingID == 1) {
421+
return false;
422+
}
423+
return super.isCmapPresent(platformID, encodingID);
424+
}
425+
426+
@Override
427+
public int getNumberOfCmaps() {
428+
return 0;
429+
}
430+
}
330431
}

0 commit comments

Comments
 (0)
Please sign in to comment.