diff --git a/src/core/fonts.js b/src/core/fonts.js index 652138a96..729db95f7 100644 --- a/src/core/fonts.js +++ b/src/core/fonts.js @@ -14,8 +14,9 @@ */ import { - bytesToString, FONT_IDENTITY_MATRIX, FontType, FormatError, info, isNum, - isSpace, MissingDataException, readUint32, shadow, string32, unreachable, warn + assert, bytesToString, FONT_IDENTITY_MATRIX, FontType, FormatError, info, + isNum, isSpace, MissingDataException, readUint32, shadow, string32, + unreachable, warn } from '../shared/util'; import { CFF, CFFCharset, CFFCompiler, CFFHeader, CFFIndex, CFFParser, CFFPrivateDict, @@ -681,6 +682,11 @@ var Font = (function FontClosure() { return readUint32(header, 0) === 0x00010000; } + function isTrueTypeCollectionFile(file) { + let header = file.peekBytes(4); + return bytesToString(header) === 'ttcf'; + } + function isOpenTypeFile(file) { var header = file.peekBytes(4); return bytesToString(header) === 'OTTO'; @@ -1294,6 +1300,33 @@ var Font = (function FontClosure() { }, checkAndRepair: function Font_checkAndRepair(name, font, properties) { + const VALID_TABLES = ['OS/2', 'cmap', 'head', 'hhea', 'hmtx', 'maxp', + 'name', 'post', 'loca', 'glyf', 'fpgm', 'prep', 'cvt ', 'CFF ']; + + function readTables(file, numTables) { + let tables = Object.create(null); + tables['OS/2'] = null; + tables['cmap'] = null; + tables['head'] = null; + tables['hhea'] = null; + tables['hmtx'] = null; + tables['maxp'] = null; + tables['name'] = null; + tables['post'] = null; + + for (let i = 0; i < numTables; i++) { + let table = readTableEntry(font); + if (VALID_TABLES.indexOf(table.tag) < 0) { + continue; // skipping table if it's not a required or optional table + } + if (table.length === 0) { + continue; // skipping empty tables + } + tables[table.tag] = table; + } + return tables; + } + function readTableEntry(file) { var tag = bytesToString(file.getBytes(4)); @@ -1333,6 +1366,68 @@ var Font = (function FontClosure() { }; } + function readTrueTypeCollectionHeader(ttc) { + let ttcTag = bytesToString(ttc.getBytes(4)); + assert(ttcTag === 'ttcf', 'Must be a TrueType Collection font.'); + + let majorVersion = ttc.getUint16(); + let minorVersion = ttc.getUint16(); + let numFonts = ttc.getInt32() >>> 0; + let offsetTable = []; + for (let i = 0; i < numFonts; i++) { + offsetTable.push(ttc.getInt32() >>> 0); + } + + let header = { + ttcTag, + majorVersion, + minorVersion, + numFonts, + offsetTable, + }; + switch (majorVersion) { + case 1: + return header; + case 2: + header.dsigTag = ttc.getInt32() >>> 0; + header.dsigLength = ttc.getInt32() >>> 0; + header.dsigOffset = ttc.getInt32() >>> 0; + return header; + } + throw new FormatError( + `Invalid TrueType Collection majorVersion: ${majorVersion}.`); + } + + function readTrueTypeCollectionData(ttc, fontName) { + let { numFonts, offsetTable, } = readTrueTypeCollectionHeader(ttc); + + for (let i = 0; i < numFonts; i++) { + ttc.pos = (ttc.start || 0) + offsetTable[i]; + let potentialHeader = readOpenTypeHeader(ttc); + let potentialTables = readTables(ttc, potentialHeader.numTables); + + if (!potentialTables['name']) { + throw new FormatError( + 'TrueType Collection font must contain a "name" table.'); + } + let nameTable = readNameTable(potentialTables['name']); + + for (let j = 0, jj = nameTable.length; j < jj; j++) { + for (let k = 0, kk = nameTable[j].length; k < kk; k++) { + let nameEntry = nameTable[j][k]; + if (nameEntry && nameEntry.replace(/\s/g, '') === fontName) { + return { + header: potentialHeader, + tables: potentialTables, + }; + } + } + } + } + throw new FormatError( + `TrueType Collection does not contain "${fontName}" font.`); + } + /** * Read the appropriate subtable from the cmap according to 9.6.6.4 from * PDF spec @@ -2189,34 +2284,16 @@ var Font = (function FontClosure() { // The following steps modify the original font data, making copy font = new Stream(new Uint8Array(font.getBytes())); - var VALID_TABLES = ['OS/2', 'cmap', 'head', 'hhea', 'hmtx', 'maxp', - 'name', 'post', 'loca', 'glyf', 'fpgm', 'prep', 'cvt ', 'CFF ']; - - var header = readOpenTypeHeader(font); - var numTables = header.numTables; - var cff, cffFile; - - var tables = Object.create(null); - tables['OS/2'] = null; - tables['cmap'] = null; - tables['head'] = null; - tables['hhea'] = null; - tables['hmtx'] = null; - tables['maxp'] = null; - tables['name'] = null; - tables['post'] = null; - - var table; - for (var i = 0; i < numTables; i++) { - table = readTableEntry(font); - if (VALID_TABLES.indexOf(table.tag) < 0) { - continue; // skipping table if it's not a required or optional table - } - if (table.length === 0) { - continue; // skipping empty tables - } - tables[table.tag] = table; + let header, tables; + if (isTrueTypeCollectionFile(font)) { + let ttcData = readTrueTypeCollectionData(font, this.name); + header = ttcData.header; + tables = ttcData.tables; + } else { + header = readOpenTypeHeader(font); + tables = readTables(font, header.numTables); } + let cff, cffFile; var isTrueType = !tables['CFF ']; if (!isTrueType) { @@ -2444,7 +2521,7 @@ var Font = (function FontClosure() { } var found = false; - for (i = 0; i < cmapMappingsLength; ++i) { + for (let i = 0; i < cmapMappingsLength; ++i) { if (cmapMappings[i].charCode !== unicodeOrCharCode) { continue; } @@ -2467,7 +2544,7 @@ var Font = (function FontClosure() { } } else if (cmapPlatformId === 0 && cmapEncodingId === 0) { // Default Unicode semantics, use the charcodes as is. - for (i = 0; i < cmapMappingsLength; ++i) { + for (let i = 0; i < cmapMappingsLength; ++i) { charCodeToGlyphId[cmapMappings[i].charCode] = cmapMappings[i].glyphId; } @@ -2483,7 +2560,7 @@ var Font = (function FontClosure() { // special range since some PDFs have char codes outside of this range // (e.g. 0x2013) which when masked would overwrite other values in the // cmap. - for (i = 0; i < cmapMappingsLength; ++i) { + for (let i = 0; i < cmapMappingsLength; ++i) { charCode = cmapMappings[i].charCode; if (cmapPlatformId === 3 && charCode >= 0xF000 && charCode <= 0xF0FF) { diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index d74dbd5f8..5a1ac9619 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -65,6 +65,7 @@ !issue8823.pdf !issue9084.pdf !issue9105_reduced.pdf +!issue9262_reduced.pdf !issue9291.pdf !bad-PageLabels.pdf !decodeACSuccessive.pdf diff --git a/test/pdfs/issue9262_reduced.pdf b/test/pdfs/issue9262_reduced.pdf new file mode 100644 index 000000000..a65f846f7 Binary files /dev/null and b/test/pdfs/issue9262_reduced.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index 0648b967c..11f260ba2 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -734,6 +734,13 @@ "lastPage": 1, "type": "eq" }, + { "id": "issue9262", + "file": "pdfs/issue9262_reduced.pdf", + "md5": "5347ce2d7b3866625c22e115fd90e0de", + "rounds": 1, + "link": false, + "type": "eq" + }, { "id": "issue8707", "file": "pdfs/issue8707.pdf", "md5": "d3dc670adde9ec9fb82c974027033029",