1/**
2 * ESM port of https://github.com/fontello/ttf2eot
3 * @module
4 */
5
6/**
7 * Offsets in EOT file structure. Refer to EOTPrefix in OpenTypeUtilities.cpp
8 */
9const offsets = {
10 length: 0,
11 fontLength: 4,
12 version: 8,
13 charset: 26,
14 magic: 34,
15 fontPanose: 16,
16 italic: 27,
17 weight: 28,
18 unicodeRange: 36,
19 codepageRange: 52,
20 checksumAdjustment: 60,
21};
22
23/**
24 * Offsets in different SFNT (TTF) structures. See OpenTypeUtilities.cpp
25 */
26const sfntOffsets = {
27 // sfntHeader:
28 numTables: 4,
29
30 // TableDirectoryEntry
31 tableTag: 0,
32 tableOffset: 8,
33 tableLength: 12,
34
35 // OS2Table
36 os2Weight: 4,
37 os2FontPanose: 32,
38 os2UnicodeRange: 42,
39 os2FsSelection: 62,
40 os2CodepageRange: 78,
41
42 // headTable
43 headChecksumAdjustment: 8,
44
45 // nameTable
46 nametableFormat: 0,
47 nametableCount: 2,
48 nametableStringOffset: 4,
49
50 // nameRecord
51 namePlatformId: 0,
52 nameEncodingId: 2,
53 nameLanguageId: 4,
54 nameNameId: 6,
55 nameLength: 8,
56 nameOffset: 10,
57};
58
59/**
60 * Sizes of structures
61 */
62const sizeof = {
63 sfntTableEntry: 16,
64 sfntHeader: 12,
65 sfntNametable: 6,
66 sfntNametableEntry: 12,
67 eotPrefix: 82,
68};
69
70/**
71 * Magic numbers
72 */
73const magic = {
74 version: 0x00020001,
75 magic: 0x504c,
76 charset: 1,
77 english: 0x0409,
78};
79
80interface SfntTableEntry {
81 tag: string;
82 offset: number;
83 length: number;
84}
85
86interface NameTable {
87 format: number;
88 count: number;
89 stringOffset: number;
90}
91
92interface NameRecord {
93 platformID: number;
94 encodingID: number;
95 languageID: number;
96 nameID: number;
97 length: number;
98 offset: number;
99}
100
101function dataView(bytes: Uint8Array): DataView {
102 return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
103}
104
105function readUint8(bytes: Uint8Array, offset = 0): number {
106 return dataView(bytes).getUint8(offset);
107}
108
109function readUint16BE(bytes: Uint8Array, offset = 0): number {
110 return dataView(bytes).getUint16(offset, false);
111}
112
113function readUint32BE(bytes: Uint8Array, offset = 0): number {
114 return dataView(bytes).getUint32(offset, false);
115}
116
117function writeUint8(bytes: Uint8Array, value: number, offset: number): void {
118 dataView(bytes).setUint8(offset, value);
119}
120
121function writeUint16LE(bytes: Uint8Array, value: number, offset: number): void {
122 dataView(bytes).setUint16(offset, value, true);
123}
124
125function writeUint32LE(bytes: Uint8Array, value: number, offset: number): void {
126 dataView(bytes).setUint32(offset, value, true);
127}
128
129function concat(parts: Uint8Array[]): Uint8Array {
130 let length = 0;
131 for (const part of parts) length += part.length;
132
133 const out = new Uint8Array(length);
134 let offset = 0;
135 for (const part of parts) {
136 out.set(part, offset);
137 offset += part.length;
138 }
139 return out;
140}
141
142function readTag(bytes: Uint8Array, offset: number): string {
143 return String.fromCharCode(
144 bytes[offset]!,
145 bytes[offset + 1]!,
146 bytes[offset + 2]!,
147 bytes[offset + 3]!,
148 );
149}
150
151/**
152 * Utility function to convert buffer of utf16be chars to buffer of utf16le
153 * chars prefixed with length and suffixed with zero
154 */
155function strbuf(str: Uint8Array): Uint8Array {
156 const arr = new Uint8Array(str.length + 4);
157
158 writeUint16LE(arr, str.length, 0);
159
160 for (let i = 0; i < str.length; i += 2) {
161 writeUint16LE(arr, readUint16BE(str, i), i + 2);
162 }
163
164 writeUint16LE(arr, 0, arr.length - 2);
165
166 return arr;
167}
168
169/**
170 * Convert a TTF font -> EOT font for use in older Internet Explorer versions.
171 *
172 * NOTE: The font's FullName MUST begin with FamilyName. For example, if
173 * FamilyName is `fontello`, then FullName should be `fontello regular` and so on.
174 */
175export function ttf2eot(arr: Uint8Array): Uint8Array {
176 arr = arr.slice();
177
178 const out = new Uint8Array(sizeof.eotPrefix);
179
180 writeUint32LE(out, arr.length, offsets.fontLength);
181 writeUint32LE(out, magic.version, offsets.version);
182 writeUint8(out, magic.charset, offsets.charset);
183 writeUint16LE(out, magic.magic, offsets.magic);
184
185 let familyName: Uint8Array = new Uint8Array([0]);
186 let subfamilyName: Uint8Array = new Uint8Array([0]);
187 let fullName: Uint8Array = new Uint8Array([0]);
188 let versionString: Uint8Array = new Uint8Array([0]);
189
190 let haveOs2 = false;
191 let haveName = false;
192 let haveHead = false;
193
194 const numTables = readUint16BE(arr, sfntOffsets.numTables);
195
196 for (let i = 0; i < numTables; ++i) {
197 const data = arr.subarray(sizeof.sfntHeader + i * sizeof.sfntTableEntry);
198 const tableEntry: SfntTableEntry = {
199 tag: readTag(data, sfntOffsets.tableTag),
200 offset: readUint32BE(data, sfntOffsets.tableOffset),
201 length: readUint32BE(data, sfntOffsets.tableLength),
202 };
203
204 const table = arr.subarray(tableEntry.offset, tableEntry.offset + tableEntry.length);
205
206 if (tableEntry.tag === "OS/2") {
207 haveOs2 = true;
208 const os2Version = readUint16BE(table);
209
210 for (let j = 0; j < 10; ++j) {
211 writeUint8(out, readUint8(table, sfntOffsets.os2FontPanose + j), offsets.fontPanose + j);
212 }
213
214 writeUint8(out, readUint16BE(table, sfntOffsets.os2FsSelection) & 0x01, offsets.italic);
215 writeUint32LE(out, readUint16BE(table, sfntOffsets.os2Weight), offsets.weight);
216
217 for (let j = 0; j < 4; ++j) {
218 writeUint32LE(
219 out,
220 readUint32BE(table, sfntOffsets.os2UnicodeRange + j * 4),
221 offsets.unicodeRange + j * 4,
222 );
223 }
224
225 if (os2Version >= 1) {
226 for (let j = 0; j < 2; ++j) {
227 writeUint32LE(
228 out,
229 readUint32BE(table, sfntOffsets.os2CodepageRange + j * 4),
230 offsets.codepageRange + j * 4,
231 );
232 }
233 }
234 } else if (tableEntry.tag === "head") {
235 haveHead = true;
236 writeUint32LE(out, readUint32BE(table, sfntOffsets.headChecksumAdjustment), offsets.checksumAdjustment);
237 } else if (tableEntry.tag === "name") {
238 haveName = true;
239
240 const nameTable: NameTable = {
241 format: readUint16BE(table, sfntOffsets.nametableFormat),
242 count: readUint16BE(table, sfntOffsets.nametableCount),
243 stringOffset: readUint16BE(table, sfntOffsets.nametableStringOffset),
244 };
245
246 for (let j = 0; j < nameTable.count; ++j) {
247 const nameRecordBytes = table.subarray(sizeof.sfntNametable + j * sizeof.sfntNametableEntry);
248 const name: NameRecord = {
249 platformID: readUint16BE(nameRecordBytes, sfntOffsets.namePlatformId),
250 encodingID: readUint16BE(nameRecordBytes, sfntOffsets.nameEncodingId),
251 languageID: readUint16BE(nameRecordBytes, sfntOffsets.nameLanguageId),
252 nameID: readUint16BE(nameRecordBytes, sfntOffsets.nameNameId),
253 length: readUint16BE(nameRecordBytes, sfntOffsets.nameLength),
254 offset: readUint16BE(nameRecordBytes, sfntOffsets.nameOffset),
255 };
256
257 if (name.platformID === 3 && name.encodingID === 1 && name.languageID === magic.english) {
258 const s = strbuf(table.subarray(
259 nameTable.stringOffset + name.offset,
260 nameTable.stringOffset + name.offset + name.length,
261 ));
262
263 switch (name.nameID) {
264 case 1:
265 familyName = s;
266 break;
267 case 2:
268 subfamilyName = s;
269 break;
270 case 4:
271 fullName = s;
272 break;
273 case 5:
274 versionString = s;
275 break;
276 }
277 }
278 }
279 }
280 if (haveOs2 && haveName && haveHead) break;
281 }
282
283 if (!(haveOs2 && haveName && haveHead)) {
284 throw new Error("Required section not found");
285 }
286
287 // Create final buffer with the the same array type as input one.
288 const eot = concat([
289 out,
290 familyName,
291 subfamilyName,
292 versionString,
293 fullName,
294 new Uint8Array([0, 0]),
295 arr,
296 ]);
297
298 writeUint32LE(eot, eot.length, offsets.length); // Calculate overall length
299
300 return eot;
301}