| 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 | */ |
| 9 | const 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 | */ |
| 26 | const 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 | */ |
| 62 | const sizeof = { |
| 63 | sfntTableEntry: 16, |
| 64 | sfntHeader: 12, |
| 65 | sfntNametable: 6, |
| 66 | sfntNametableEntry: 12, |
| 67 | eotPrefix: 82, |
| 68 | }; |
| 69 | |
| 70 | /** |
| 71 | * Magic numbers |
| 72 | */ |
| 73 | const magic = { |
| 74 | version: 0x00020001, |
| 75 | magic: 0x504c, |
| 76 | charset: 1, |
| 77 | english: 0x0409, |
| 78 | }; |
| 79 | |
| 80 | interface SfntTableEntry { |
| 81 | tag: string; |
| 82 | offset: number; |
| 83 | length: number; |
| 84 | } |
| 85 | |
| 86 | interface NameTable { |
| 87 | format: number; |
| 88 | count: number; |
| 89 | stringOffset: number; |
| 90 | } |
| 91 | |
| 92 | interface NameRecord { |
| 93 | platformID: number; |
| 94 | encodingID: number; |
| 95 | languageID: number; |
| 96 | nameID: number; |
| 97 | length: number; |
| 98 | offset: number; |
| 99 | } |
| 100 | |
| 101 | function dataView(bytes: Uint8Array): DataView { |
| 102 | return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); |
| 103 | } |
| 104 | |
| 105 | function readUint8(bytes: Uint8Array, offset = 0): number { |
| 106 | return dataView(bytes).getUint8(offset); |
| 107 | } |
| 108 | |
| 109 | function readUint16BE(bytes: Uint8Array, offset = 0): number { |
| 110 | return dataView(bytes).getUint16(offset, false); |
| 111 | } |
| 112 | |
| 113 | function readUint32BE(bytes: Uint8Array, offset = 0): number { |
| 114 | return dataView(bytes).getUint32(offset, false); |
| 115 | } |
| 116 | |
| 117 | function writeUint8(bytes: Uint8Array, value: number, offset: number): void { |
| 118 | dataView(bytes).setUint8(offset, value); |
| 119 | } |
| 120 | |
| 121 | function writeUint16LE(bytes: Uint8Array, value: number, offset: number): void { |
| 122 | dataView(bytes).setUint16(offset, value, true); |
| 123 | } |
| 124 | |
| 125 | function writeUint32LE(bytes: Uint8Array, value: number, offset: number): void { |
| 126 | dataView(bytes).setUint32(offset, value, true); |
| 127 | } |
| 128 | |
| 129 | function 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 | |
| 142 | function 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 | */ |
| 155 | function 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 | */ |
| 175 | export 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 | } |