Skip to content

Commit 7f5a018

Browse files
committed
Support TrueType font merging
DEVSIX-5179
1 parent 6467b68 commit 7f5a018

File tree

29 files changed

+1150
-591
lines changed

29 files changed

+1150
-591
lines changed

io/src/main/java/com/itextpdf/io/exceptions/IoExceptionMessageConstant.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ public final class IoExceptionMessageConstant {
109109
public static final String IMAGE_MAGICK_OUTPUT_IS_NULL = "ImageMagick process output is null.";
110110
public static final String IMAGE_MAGICK_PROCESS_EXECUTION_FAILED = "ImageMagick process execution finished with errors: ";
111111
public static final String IMAGE_MASK_CANNOT_CONTAIN_ANOTHER_IMAGE_MASK = "Image mask cannot contain another image mask.";
112+
public static final String INCOMPATIBLE_GLYPH_DATA_DURING_FONT_MERGING =
113+
"Incompatibility of glyph data/metrics between merged fonts";
112114
public static final String INCOMPLETE_PALETTE = "Incomplete palette.";
113115
public static final String INCORRECT_SIGNATURE = "Incorrect woff2 signature";
114116
public static final String INVALID_BMP_FILE_COMPRESSION = "Invalid BMP file compression.";
Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
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.io.font;
24+
25+
import com.itextpdf.io.exceptions.IOException;
26+
import com.itextpdf.io.exceptions.IoExceptionMessageConstant;
27+
import com.itextpdf.io.source.RandomAccessFileOrArray;
28+
29+
import java.util.Arrays;
30+
import java.util.HashMap;
31+
import java.util.Map;
32+
33+
/**
34+
* The abstract class which provides a functionality to modify TrueType fonts with returning raw data of modified font.
35+
*/
36+
abstract class AbstractTrueTypeFontModifier {
37+
// If it's a regular font subset, we should not add `name` and `post`,
38+
// because information in these tables maybe irrelevant for a subset.
39+
private static final String[] TABLE_NAMES_SUBSET =
40+
{"cvt ", "fpgm", "glyf", "head", "hhea", "hmtx", "loca", "maxp", "prep", "cmap", "OS/2"};
41+
// In case ttc (true type collection) file with subset = false (#directoryOffset > 0)
42+
// `name` and `post` shall be included, because it's actually a full font.
43+
private static final String[] TABLE_NAMES =
44+
{"cvt ", "fpgm", "glyf", "head", "hhea", "hmtx", "loca", "maxp", "prep", "cmap", "OS/2", "name", "post"};
45+
private static final int[] ENTRY_SELECTORS = {0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4};
46+
private static final int TABLE_CHECKSUM = 0;
47+
private static final int TABLE_OFFSET = 1;
48+
private static final int TABLE_LENGTH = 2;
49+
private static final int HEAD_LOCA_FORMAT_OFFSET = 51;
50+
51+
/**
52+
* Contains the location of the several tables. The key is the name of the table and the
53+
* value is an {@code int[3]} where position 0 is the checksum, position 1 is the offset
54+
* from the start of the file and position 2 is the length of the table.
55+
*/
56+
protected Map<String, int[]> tableDirectory;
57+
/**
58+
* Contains glyph data from `glyf` table. The key is the GID (glyph ID) and the value is a raw data of glyph.
59+
*/
60+
protected Map<Integer, byte[]> glyphDataMap;
61+
/**
62+
* Contains font tables which have been changed during the modification. The key is the name of table which has
63+
* been changed and the value is raw data of the modified table.
64+
*/
65+
protected final Map<String, byte[]> modifiedTables = new HashMap<>();
66+
67+
/**
68+
* Font raw data on which new modified font will be built.
69+
*/
70+
protected RandomAccessFileOrArray raf;
71+
/**
72+
* Table directory offset which corresponds to font's raw data from {@link #raf}.
73+
*/
74+
protected int directoryOffset;
75+
/**
76+
* the name of font which will be modified
77+
*/
78+
protected final String fontName;
79+
80+
private FontRawData outFont;
81+
private final String[] tableNames;
82+
83+
/**
84+
* Instantiates a new {@link AbstractTrueTypeFontModifier} instance.
85+
*
86+
* @param fontName the name of font which will be modified
87+
* @param subsetTables whether subset tables (remove `name` and `post` tables) or not. It's used in case of ttc
88+
* (true type collection) font where single "full" font is needed. Despite the value of that
89+
* flag, only used glyphs will be left in the font
90+
*/
91+
AbstractTrueTypeFontModifier(String fontName, boolean subsetTables) {
92+
// subset = false is possible with directoryOffset > 0, i.e. ttc font without subset.
93+
if (subsetTables) {
94+
tableNames = TABLE_NAMES_SUBSET;
95+
} else {
96+
tableNames = TABLE_NAMES;
97+
}
98+
this.fontName = fontName;
99+
}
100+
101+
byte[] process() throws java.io.IOException {
102+
try {
103+
createTableDirectory();
104+
mergeTables();
105+
assembleFont();
106+
return outFont.getData();
107+
} finally {
108+
try {
109+
raf.close();
110+
} catch (Exception ignore) {
111+
}
112+
}
113+
}
114+
115+
abstract void mergeTables() throws java.io.IOException;
116+
117+
protected void createNewGlyfAndLocaTables() throws java.io.IOException {
118+
int[] activeGlyphs = new int[glyphDataMap.size()];
119+
int i = 0;
120+
int glyfSize = 0;
121+
for (Map.Entry<Integer, byte[]> entry : glyphDataMap.entrySet()) {
122+
activeGlyphs[i++] = entry.getKey();
123+
glyfSize += entry.getValue().length;
124+
}
125+
Arrays.sort(activeGlyphs);
126+
// If the biggest used GID is X, size of loca should be X + 1 (array index starts from 0),
127+
// plus one extra entry to get size of X element (loca[X + 1] - loca[X]), it's why 2 added
128+
int locaSize = activeGlyphs[activeGlyphs.length - 1] + 2;
129+
boolean isLocaShortTable = isLocaShortTable();
130+
int newLocaTableSize = isLocaShortTable ? locaSize * 2 : locaSize * 4;
131+
byte[] newLoca = new byte[newLocaTableSize + 3 & ~3];
132+
byte[] newGlyf = new byte[glyfSize + 3 & ~3];
133+
int glyfPtr = 0;
134+
int listGlyf = 0;
135+
for (int k = 0; k < locaSize; ++k) {
136+
writeToLoca(newLoca, k, glyfPtr, isLocaShortTable);
137+
138+
if (listGlyf < activeGlyphs.length && activeGlyphs[listGlyf] == k) {
139+
++listGlyf;
140+
141+
byte[] glyphData = glyphDataMap.get(k);
142+
System.arraycopy(glyphData, 0, newGlyf, glyfPtr, glyphData.length);
143+
glyfPtr += glyphData.length;
144+
}
145+
}
146+
modifiedTables.put("glyf", newGlyf);
147+
modifiedTables.put("loca", newLoca);
148+
}
149+
150+
private void createTableDirectory() throws java.io.IOException {
151+
tableDirectory = new HashMap<>();
152+
raf.seek(directoryOffset);
153+
int id = raf.readInt();
154+
if (id != 0x00010000) {
155+
throw new IOException(IoExceptionMessageConstant.NOT_AT_TRUE_TYPE_FILE).setMessageParams(fontName);
156+
}
157+
int numTables = raf.readUnsignedShort();
158+
raf.skipBytes(6);
159+
for (int k = 0; k < numTables; ++k) {
160+
String tag = readTag();
161+
int[] tableLocation = new int[3];
162+
tableLocation[TABLE_CHECKSUM] = raf.readInt();
163+
tableLocation[TABLE_OFFSET] = raf.readInt();
164+
tableLocation[TABLE_LENGTH] = raf.readInt();
165+
tableDirectory.put(tag, tableLocation);
166+
}
167+
}
168+
169+
private boolean isLocaShortTable() throws java.io.IOException {
170+
int[] tableLocation = tableDirectory.get("head");
171+
if (tableLocation == null) {
172+
throw new IOException(IoExceptionMessageConstant.TABLE_DOES_NOT_EXISTS_IN).setMessageParams("head",
173+
fontName);
174+
}
175+
raf.seek(tableLocation[TABLE_OFFSET] + HEAD_LOCA_FORMAT_OFFSET);
176+
return raf.readUnsignedShort() == 0;
177+
}
178+
179+
private void assembleFont() throws java.io.IOException {
180+
int[] tableLocation;
181+
// Calculate size of the out font
182+
int fullFontSize = 0;
183+
int tablesUsed = modifiedTables.size();
184+
for (String name : tableNames) {
185+
if (modifiedTables.containsKey(name)) {
186+
continue;
187+
}
188+
tableLocation = tableDirectory.get(name);
189+
if (tableLocation == null) {
190+
continue;
191+
}
192+
tablesUsed++;
193+
fullFontSize += tableLocation[TABLE_LENGTH] + 3 & ~3;
194+
}
195+
for (byte[] table : modifiedTables.values()) {
196+
fullFontSize += table.length;
197+
}
198+
int reference = 16 * tablesUsed + 12;
199+
fullFontSize += reference;
200+
outFont = new FontRawData(fullFontSize);
201+
// Write font headers + tables directory
202+
outFont.writeFontInt(0x00010000);
203+
outFont.writeFontShort(tablesUsed);
204+
int selector = ENTRY_SELECTORS[tablesUsed];
205+
outFont.writeFontShort((1 << selector) * 16);
206+
outFont.writeFontShort(selector);
207+
outFont.writeFontShort((tablesUsed - (1 << selector)) * 16);
208+
for (String name : tableNames) {
209+
int len;
210+
tableLocation = tableDirectory.get(name);
211+
if (tableLocation == null) {
212+
continue;
213+
}
214+
outFont.writeFontString(name);
215+
if (modifiedTables.containsKey(name)) {
216+
byte[] table = modifiedTables.get(name);
217+
outFont.writeFontInt(calculateChecksum(table));
218+
len = table.length;
219+
} else {
220+
outFont.writeFontInt(tableLocation[TABLE_CHECKSUM]);
221+
len = tableLocation[TABLE_LENGTH];
222+
}
223+
outFont.writeFontInt(reference);
224+
outFont.writeFontInt(len);
225+
reference += len + 3 & ~3;
226+
}
227+
// Write tables data
228+
for (String name : tableNames) {
229+
tableLocation = tableDirectory.get(name);
230+
if (tableLocation == null) {
231+
continue;
232+
}
233+
if (modifiedTables.containsKey(name)) {
234+
byte[] table = modifiedTables.get(name);
235+
outFont.writeFontTable(table);
236+
} else {
237+
raf.seek(tableLocation[TABLE_OFFSET]);
238+
outFont.writeFontTable(raf, tableLocation[TABLE_LENGTH]);
239+
}
240+
}
241+
}
242+
243+
private String readTag() throws java.io.IOException {
244+
byte[] buf = new byte[4];
245+
raf.readFully(buf);
246+
try {
247+
return new String(buf, PdfEncodings.WINANSI);
248+
} catch (Exception e) {
249+
throw new IOException("TrueType font", e);
250+
}
251+
}
252+
253+
private static void writeToLoca(byte[] loca, int index, int location, boolean isLocaShortTable) {
254+
if (isLocaShortTable) {
255+
index *= 2;
256+
location /= 2;
257+
loca[index] = (byte) (location >> 8);
258+
loca[index + 1] = (byte) location;
259+
}
260+
else {
261+
index *= 4;
262+
loca[index] = (byte) (location >> 24);
263+
loca[index + 1] = (byte) (location >> 16);
264+
loca[index + 2] = (byte) (location >> 8);
265+
loca[index + 3] = (byte) location;
266+
}
267+
}
268+
269+
private int calculateChecksum(byte[] b) {
270+
int len = b.length / 4;
271+
int v0 = 0;
272+
int v1 = 0;
273+
int v2 = 0;
274+
int v3 = 0;
275+
int ptr = 0;
276+
for (int k = 0; k < len; ++k) {
277+
v3 += b[ptr++] & 0xff;
278+
v2 += b[ptr++] & 0xff;
279+
v1 += b[ptr++] & 0xff;
280+
v0 += b[ptr++] & 0xff;
281+
}
282+
return v0 + (v1 << 8) + (v2 << 16) + (v3 << 24);
283+
}
284+
285+
private static class FontRawData {
286+
private final byte[] data;
287+
private int ptr;
288+
289+
FontRawData(int size) {
290+
this.data = new byte[size];
291+
this.ptr = 0;
292+
}
293+
294+
public byte[] getData() {
295+
return data;
296+
}
297+
298+
void writeFontTable(RandomAccessFileOrArray raf, int tableLength) throws java.io.IOException {
299+
raf.readFully(data, ptr, tableLength);
300+
ptr += tableLength + 3 & ~3;
301+
}
302+
303+
void writeFontTable(byte[] tableData) {
304+
System.arraycopy(tableData, 0, data, ptr, tableData.length);
305+
ptr += tableData.length;
306+
}
307+
308+
void writeFontShort(int n) {
309+
data[ptr++] = (byte) (n >> 8);
310+
data[ptr++] = (byte) n;
311+
}
312+
313+
void writeFontInt(int n) {
314+
data[ptr++] = (byte) (n >> 24);
315+
data[ptr++] = (byte) (n >> 16);
316+
data[ptr++] = (byte) (n >> 8);
317+
data[ptr++] = (byte) n;
318+
}
319+
320+
void writeFontString(String s) {
321+
byte[] b = PdfEncodings.convertToBytes(s, PdfEncodings.WINANSI);
322+
System.arraycopy(b, 0, data, ptr, b.length);
323+
ptr += b.length;
324+
}
325+
}
326+
}

io/src/main/java/com/itextpdf/io/font/FontCache.java

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

25-
import com.itextpdf.io.font.cmap.CMapByteCid;
26-
import com.itextpdf.io.font.cmap.CMapCidToCodepoint;
27-
import com.itextpdf.io.font.cmap.CMapCidUni;
28-
import com.itextpdf.io.font.cmap.CMapCodepointToCid;
29-
import com.itextpdf.io.font.cmap.CMapUniCid;
30-
3125
import java.util.Map;
32-
import java.util.Set;
3326
import java.util.concurrent.ConcurrentHashMap;
3427

3528
public class FontCache {

0 commit comments

Comments
 (0)