From ce53b1b01816cb7f2018189f5a0bf20256b21449 Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Sat, 10 Mar 2012 19:12:33 -0800 Subject: [PATCH] CFF Parser and Compiler. --- src/fonts.js | 1495 +++++++++++++++++++++++++++------------ test/pdfs/.gitignore | 1 + test/pdfs/issue1002.pdf | Bin 0 -> 19172 bytes test/test_manifest.json | 7 + test/unit/font_spec.js | 223 ++++++ 5 files changed, 1292 insertions(+), 434 deletions(-) create mode 100644 test/pdfs/issue1002.pdf create mode 100644 test/unit/font_spec.js diff --git a/src/fonts.js b/src/fonts.js index 542c33f55..547eae77f 100644 --- a/src/fonts.js +++ b/src/fonts.js @@ -829,6 +829,8 @@ var Font = (function FontClosure() { return; } + this.loadedName = getUniqueName(); + properties.id = this.loadedName; var data; switch (type) { case 'Type1': @@ -837,7 +839,7 @@ var Font = (function FontClosure() { var subtype = properties.subtype; var cff = (subtype == 'Type1C' || subtype == 'CIDFontType0C') ? - new Type2CFF(file, properties) : new CFF(name, file, properties); + new CFF(file, properties) : new Type1Font(name, file, properties); // Wrap the CFF data inside an OTF font file data = this.convert(name, cff, properties); @@ -862,7 +864,6 @@ var Font = (function FontClosure() { this.widthMultiplier = !properties.fontMatrix ? 1.0 : 1.0 / properties.fontMatrix[0]; this.encoding = properties.baseEncoding; - this.loadedName = getUniqueName(); this.loading = true; }; @@ -2931,7 +2932,7 @@ var Type1Parser = function type1Parser() { * The CFF class takes a Type1 file and wrap it into a * 'Compact Font Format' which itself embed Type2 charstrings. */ -var CFFStrings = [ +var CFFStandardStrings = [ '.notdef', 'space', 'exclam', 'quotedbl', 'numbersign', 'dollar', 'percent', 'ampersand', 'quoteright', 'parenleft', 'parenright', 'asterisk', 'plus', 'comma', 'hyphen', 'period', 'slash', 'zero', 'one', 'two', 'three', 'four', @@ -3001,7 +3002,8 @@ var CFFStrings = [ var type1Parser = new Type1Parser(); -var CFF = function cffCFF(name, file, properties) { +// Type1Font is also a CIDFontType0. +var Type1Font = function Type1Font(name, file, properties) { // Get the data block containing glyphs and subrs informations var headerBlock = file.getBytes(properties.length1); type1Parser.extractFontHeader(headerBlock, properties); @@ -3021,8 +3023,8 @@ var CFF = function cffCFF(name, file, properties) { subrs, properties); }; -CFF.prototype = { - createCFFIndexHeader: function cff_createCFFIndexHeader(objects, isByte) { +Type1Font.prototype = { + createCFFIndexHeader: function createCFFIndexHeader(objects, isByte) { // First 2 bytes contains the number of objects contained into this index var count = objects.length; @@ -3058,7 +3060,7 @@ CFF.prototype = { return data; }, - encodeNumber: function cff_encodeNumber(value) { + encodeNumber: function encodeNumber(value) { // some of the fonts has ouf-of-range values // they are just arithmetic overflows // make sanitizer happy @@ -3076,7 +3078,7 @@ CFF.prototype = { } }, - getOrderedCharStrings: function cff_getOrderedCharStrings(glyphs, + getOrderedCharStrings: function type1Font_getOrderedCharStrings(glyphs, properties) { var charstrings = []; var i, length, glyphName; @@ -3102,7 +3104,7 @@ CFF.prototype = { return charstrings; }, - getType2Charstrings: function cff_getType2Charstrings(type1Charstrings) { + getType2Charstrings: function getType2Charstrings(type1Charstrings) { var type2Charstrings = []; var count = type1Charstrings.length; for (var i = 0; i < count; i++) { @@ -3113,7 +3115,7 @@ CFF.prototype = { return type2Charstrings; }, - getType2Subrs: function cff_getType2Subrs(type1Subrs) { + getType2Subrs: function getType2Subrs(type1Subrs) { var bias = 0; var count = type1Subrs.length; if (count < 1240) @@ -3261,7 +3263,7 @@ CFF.prototype = { var count = glyphs.length; for (var i = 0; i < count; i++) { - var index = CFFStrings.indexOf(charstrings[i].glyph); + var index = CFFStandardStrings.indexOf(charstrings[i].glyph); // Some characters like asterikmath && circlecopyrt are // missing from the original strings, for the moment let's // map them to .notdef and see later if it cause any @@ -3330,106 +3332,31 @@ CFF.prototype = { } }; -var Type2CFF = (function Type2CFFClosure() { - // TODO: replace parsing code with the Type2Parser in font_utils.js - function Type2CFF(file, properties) { - var bytes = file.getBytes(); - this.bytes = bytes; +var CFF = (function CFFClosure() { + function CFF(file, properties) { this.properties = properties; - this.data = this.parse(); + var parser = new CFFParser(file, properties); + var cff = parser.parse(); + var compiler = new CFFCompiler(cff); + this.readExtra(cff); + try { + this.data = compiler.compile(); + } catch (e) { + warn('Failed to compile font ' + properties.loadedName); + // There may have just been an issue with the compiler, set the data + // anyway and hope the font loaded. + this.data = file; + } } - Type2CFF.prototype = { - parse: function cff_parse() { - var header = this.parseHeader(); - var properties = this.properties; - - var nameIndex = this.parseIndex(header.endPos); - this.sanitizeName(nameIndex); - - var dictIndex = this.parseIndex(nameIndex.endPos); - if (dictIndex.length != 1) - error('CFF contains more than 1 font'); - - var stringIndex = this.parseIndex(dictIndex.endPos); - var gsubrIndex = this.parseIndex(stringIndex.endPos); - - var strings = this.getStrings(stringIndex); - - var baseDict = this.parseDict(dictIndex.get(0).data); - var topDict = this.getTopDict(baseDict, strings); - - var bytes = this.bytes; - - var privateDict = {}; - var privateInfo = topDict.Private; - if (privateInfo) { - var privOffset = privateInfo[1], privLength = privateInfo[0]; - var privBytes = bytes.subarray(privOffset, privOffset + privLength); - baseDict = this.parseDict(privBytes); - privateDict = this.getPrivDict(baseDict, strings); - } else { - privateDict.defaultWidthX = properties.defaultWidth; - } - - var charStrings = this.parseIndex(topDict.CharStrings); - - var charset, encoding; - var isCIDFont = properties.subtype == 'CIDFontType0C'; - if (isCIDFont) { - charset = ['.notdef']; - for (var i = 1, ii = charStrings.length; i < ii; ++i) - charset.push('glyph' + i); - - encoding = this.parseCidMap(topDict.charset, - charStrings.length); - } else { - charset = this.parseCharsets(topDict.charset, - charStrings.length, strings); - encoding = this.parseEncoding(topDict.Encoding, properties, - strings, charset); - } - - // The font sanitizer does not support CFF encoding with a - // supplement, since the encoding is not really use to map - // between gid to glyph, let's overwrite what is declared in - // the top dictionary to let the sanitizer think the font use - // StandardEncoding, that's a lie but that's ok. - if (encoding.hasSupplement) - bytes[topDict.Encoding] &= 0x7F; - - // The CFF specification state that the 'dotsection' command - // (12, 0) is deprecated and treated as a no-op, but all Type2 - // charstrings processors should support them. Unfortunately - // the font sanitizer don't. As a workaround the sequence (12, 0) - // is replaced by a useless (0, hmoveto). - var count = charStrings.length; - for (var i = 0; i < count; i++) { - var charstring = charStrings.get(i); - - var start = charstring.start; - var data = charstring.data; - var length = data.length; - for (var j = 0; j <= length; j) { - var value = data[j++]; - if (value == 12 && data[j++] == 0) { - bytes[start + j - 2] = 139; - bytes[start + j - 1] = 22; - } else if (value === 28) { - j += 2; - } else if (value >= 247 && value <= 254) { - j++; - } else if (value == 255) { - j += 4; - } - } - } - + CFF.prototype = { + readExtra: function readExtra(cff) { // charstrings contains info about glyphs (one element per glyph // containing mappings for {unicode, width}) - var charstrings = this.getCharStrings(charset, encoding.encoding, - privateDict, this.properties); + var charset = cff.charset.charset; + var encoding = cff.encoding ? cff.encoding.encoding : null; + var charstrings = this.getCharStrings(charset, encoding); // create the mapping between charstring and glyph id var glyphIds = []; @@ -3438,21 +3365,18 @@ var Type2CFF = (function Type2CFFClosure() { this.charstrings = charstrings; this.glyphIds = glyphIds; - - var data = []; - for (var i = 0, ii = bytes.length; i < ii; ++i) - data.push(bytes[i]); - return data; }, - - getCharStrings: function cff_charstrings(charsets, encoding, - privateDict, properties) { + getCharStrings: function getCharStrings(charsets, encoding) { var charstrings = []; var unicodeUsed = []; var unassignedUnicodeItems = []; var inverseEncoding = []; - for (var charcode in encoding) - inverseEncoding[encoding[charcode]] = charcode | 0; + // CID fonts don't have an encoding. + if (encoding !== null) + for (var charcode in encoding) + inverseEncoding[encoding[charcode]] = charcode | 0; + else + inverseEncoding = charsets; for (var i = 0, ii = charsets.length; i < ii; i++) { var glyph = charsets[i]; if (glyph == '.notdef') @@ -3488,284 +3412,80 @@ var Type2CFF = (function Type2CFFClosure() { } // sort the array by the unicode value (again) - charstrings.sort(function type2CFFGetCharStringsSort(a, b) { + charstrings.sort(function getCharStringsSort(a, b) { return a.unicode - b.unicode; }); return charstrings; - }, + } + }; - parseEncoding: function cff_parseencoding(pos, properties, strings, - charset) { - var encoding = {}; - var bytes = this.bytes; - var result = { - encoding: encoding, - hasSupplement: false - }; + return CFF; +})(); - function readSupplement() { - var supplementsCount = bytes[pos++]; - for (var i = 0; i < supplementsCount; i++) { - var code = bytes[pos++]; - var sid = (bytes[pos++] << 8) + (bytes[pos++] & 0xff); - encoding[code] = properties.differences.indexOf(strings[sid]); - } - } - - if (pos == 0 || pos == 1) { - var gid = 1; - var baseEncoding = pos ? Encodings.ExpertEncoding : - Encodings.StandardEncoding; - for (var i = 0, ii = charset.length; i < ii; i++) { - var index = baseEncoding.indexOf(charset[i]); - if (index != -1) - encoding[index] = gid++; +var CFFParser = (function CFFParserClosure() { + function CFFParser(file, properties) { + this.bytes = file.getBytes(); + this.properties = properties; + } + CFFParser.prototype = { + parse: function parse() { + var properties = this.properties; + var cff = new CFFTable(); + this.cff = cff; + + // The first five sections must be in order, all the others are reached + // via offsets contained in one of the below. + var header = this.parseHeader(); + var nameIndex = this.parseIndex(header.endPos); + var topDictIndex = this.parseIndex(nameIndex.endPos); + var stringIndex = this.parseIndex(topDictIndex.endPos); + var globalSubrIndex = this.parseIndex(stringIndex.endPos); + + var topDictParsed = this.parseDict(topDictIndex.obj.get(0)); + var topDict = this.createDict(CFFTopDict, topDictParsed, cff.strings); + + cff.header = header.obj; + cff.names = this.parseNameIndex(nameIndex.obj); + cff.strings = this.parseStringIndex(stringIndex.obj); + cff.topDict = topDict; + cff.globalSubrIndex = globalSubrIndex.obj; + + this.parsePrivateDict(cff.topDict); + + cff.isCIDFont = topDict.hasName('ROS'); + + var charStringOffset = topDict.getByName('CharStrings'); + cff.charStrings = this.parseCharStrings(charStringOffset); + + var charset, encoding; + if (cff.isCIDFont) { + var fdArrayIndex = this.parseIndex(topDict.getByName('FDArray')).obj; + for (var i = 0, ii = fdArrayIndex.count; i < ii; ++i) { + var dictRaw = fdArrayIndex.get(i); + var fontDict = this.createDict(CFFTopDict, this.parseDict(dictRaw), + cff.strings); + this.parsePrivateDict(fontDict); + cff.fdArray.push(fontDict); } + // cid fonts don't have an encoding + encoding = null; + charset = this.parseCharsets(topDict.getByName('charset'), + cff.charStrings.count, cff.strings, true); + cff.fdSelect = this.parseFDSelect(topDict.getByName('FDSelect'), + cff.charStrings.count); } else { - var format = bytes[pos++]; - switch (format & 0x7f) { - case 0: - var glyphsCount = bytes[pos++]; - for (var i = 1; i <= glyphsCount; i++) - encoding[bytes[pos++]] = i; - break; - - case 1: - var rangesCount = bytes[pos++]; - var gid = 1; - for (var i = 0; i < rangesCount; i++) { - var start = bytes[pos++]; - var left = bytes[pos++]; - for (var j = start; j <= start + left; j++) - encoding[j] = gid++; - } - break; - - default: - error('Unknow encoding format: ' + format + ' in CFF'); - break; - } - if (format & 0x80) { - readSupplement(); - result.hasSupplement = true; - } + charset = this.parseCharsets(topDict.getByName('charset'), + cff.charStrings.count, cff.strings, false); + encoding = this.parseEncoding(topDict.getByName('Encoding'), + properties, + cff.strings, charset.charset); } - return result; + cff.charset = charset; + cff.encoding = encoding; + + return cff; }, - - parseCharsets: function cff_parsecharsets(pos, length, strings) { - if (pos == 0) { - return ISOAdobeCharset.slice(); - } else if (pos == 1) { - return ExpertCharset.slice(); - } else if (pos == 2) { - return ExpertSubsetCharset.slice(); - } - - var bytes = this.bytes; - var format = bytes[pos++]; - var charset = ['.notdef']; - - // subtract 1 for the .notdef glyph - length -= 1; - - switch (format) { - case 0: - for (var i = 0; i < length; i++) { - var sid = (bytes[pos++] << 8) | bytes[pos++]; - charset.push(strings[sid]); - } - break; - case 1: - while (charset.length <= length) { - var sid = (bytes[pos++] << 8) | bytes[pos++]; - var count = bytes[pos++]; - for (var i = 0; i <= count; i++) - charset.push(strings[sid++]); - } - break; - case 2: - while (charset.length <= length) { - var sid = (bytes[pos++] << 8) | bytes[pos++]; - var count = (bytes[pos++] << 8) | bytes[pos++]; - for (var i = 0; i <= count; i++) - charset.push(strings[sid++]); - } - break; - default: - error('Unknown charset format'); - } - return charset; - }, - - parseCidMap: function cff_parsecharsets(pos, length) { - var bytes = this.bytes; - var format = bytes[pos++]; - - var encoding = {}; - var map = {encoding: encoding}; - - encoding[0] = 0; - - var gid = 1; - switch (format) { - case 0: - while (gid < length) { - var cid = (bytes[pos++] << 8) | bytes[pos++]; - encoding[cid] = gid++; - } - break; - case 1: - while (gid < length) { - var cid = (bytes[pos++] << 8) | bytes[pos++]; - var count = bytes[pos++]; - for (var i = 0; i <= count; i++) - encoding[cid++] = gid++; - } - break; - case 2: - while (gid < length) { - var cid = (bytes[pos++] << 8) | bytes[pos++]; - var count = (bytes[pos++] << 8) | bytes[pos++]; - for (var i = 0; i <= count; i++) - encoding[cid++] = gid++; - } - break; - default: - error('Unknown charset format'); - } - return map; - }, - - getPrivDict: function cff_getprivdict(baseDict, strings) { - var dict = {}; - - // default values - dict['defaultWidthX'] = 0; - dict['nominalWidthX'] = 0; - - for (var i = 0, ii = baseDict.length; i < ii; ++i) { - var pair = baseDict[i]; - var key = pair[0]; - var value = pair[1]; - switch (key) { - case 20: - dict['defaultWidthX'] = value[0]; - case 21: - dict['nominalWidthX'] = value[0]; - default: - TODO('interpret top dict key: ' + key); - } - } - return dict; - }, - getTopDict: function cff_gettopdict(baseDict, strings) { - var dict = {}; - - // default values - dict['Encoding'] = 0; - dict['charset'] = 0; - - for (var i = 0, ii = baseDict.length; i < ii; ++i) { - var pair = baseDict[i]; - var key = pair[0]; - var value = pair[1]; - switch (key) { - case 1: - dict['Notice'] = strings[value[0]]; - break; - case 4: - dict['Weight'] = strings[value[0]]; - break; - case 3094: - dict['BaseFontName'] = strings[value[0]]; - break; - case 5: - dict['FontBBox'] = value; - break; - case 13: - dict['UniqueID'] = value[0]; - break; - case 15: - dict['charset'] = value[0]; - break; - case 16: - dict['Encoding'] = value[0]; - break; - case 17: - dict['CharStrings'] = value[0]; - break; - case 18: - dict['Private'] = value; - break; - case 3102: - case 3103: - case 3104: - case 3105: - case 3106: - case 3107: - case 3108: - case 3109: - case 3110: - dict['cidOperatorPresent'] = true; - break; - default: - TODO('interpret top dict key: ' + key); - } - } - return dict; - }, - sanitizeName: function cff_sanitizeName(nameIndex) { - // There should really only be one font, but loop to make sure. - for (var i = 0, ii = nameIndex.length; i < ii; ++i) { - var data = nameIndex.get(i).data; - var length = data.length; - if (length > 127) - warn('Font had name longer than 127 chars, will be rejected.'); - // Only certain chars are permitted in the font name. - for (var j = 0; j < length; ++j) { - var c = data[j]; - if (j === 0 && c === 0) - continue; - if (c < 33 || c > 126) { - data[j] = 95; - continue; - } - switch (c) { - case 91: // [ - case 93: // ] - case 40: // ( - case 41: // ) - case 123: // { - case 125: // } - case 60: // < - case 62: // > - case 47: // / - case 37: // % - data[j] = 95; - break; - } - } - } - }, - getStrings: function cff_getStrings(stringIndex) { - function bytesToString(bytesArray) { - var str = ''; - for (var i = 0, ii = bytesArray.length; i < ii; i++) - str += String.fromCharCode(bytesArray[i]); - return str; - } - - var stringArray = []; - for (var i = 0, ii = CFFStrings.length; i < ii; i++) - stringArray.push(CFFStrings[i]); - - for (var i = 0, ii = stringIndex.length; i < ii; i++) - stringArray.push(bytesToString(stringIndex.get(i).data)); - - return stringArray; - }, - parseHeader: function cff_parseHeader() { + parseHeader: function parseHeader() { var bytes = this.bytes; var offset = 0; @@ -3773,17 +3493,18 @@ var Type2CFF = (function Type2CFFClosure() { ++offset; if (offset != 0) { - warning('cff data is shifted'); + warn('cff data is shifted'); bytes = bytes.subarray(offset); this.bytes = bytes; } - - return { - endPos: bytes[2], - offsetSize: bytes[3] - }; + var major = bytes[0]; + var minor = bytes[1]; + var hdrSize = bytes[2]; + var offSize = bytes[3]; + var header = new CFFHeader(major, minor, hdrSize, offSize); + return {obj: header, endPos: hdrSize}; }, - parseDict: function cff_parseDict(dict) { + parseDict: function parseDict(dict) { var pos = 0; function parseOperand() { @@ -3800,11 +3521,11 @@ var Type2CFF = (function Type2CFFClosure() { value = (value << 8) | dict[pos++]; value = (value << 8) | dict[pos++]; return value; - } else if (value <= 246) { + } else if (value >= 32 && value <= 246) { return value - 139; - } else if (value <= 250) { + } else if (value >= 247 && value <= 250) { return ((value - 247) * 256) + dict[pos++] + 108; - } else if (value <= 254) { + } else if (value >= 251 && value <= 254) { return -((value - 251) * 256) - dict[pos++] - 108; } else { error('255 is not a valid DICT command'); @@ -3842,27 +3563,8 @@ var Type2CFF = (function Type2CFFClosure() { while (pos < end) { var b = dict[pos]; if (b <= 21) { - if (b === 12) { - ++pos; - var op = dict[pos]; - if ((op > 14 && op < 17) || - (op > 23 && op < 30) || op > 38) { - warn('Invalid CFF dictionary key: ' + op); - // trying to replace it with initialRandomSeed - // to pass sanitizer - dict[pos] = 19; - } - var b = (b << 8) | op; - } - if (!operands.length && b == 8 && - dict[pos + 1] == 9) { - // no operands for FamilyBlues, removing the key - // and next one is FamilyOtherBlues - skipping them - // also replacing FamilyBlues to pass sanitizer - dict[pos] = 139; - pos += 2; - continue; - } + if (b === 12) + b = (b << 8) | dict[++pos]; entries.push([b, operands]); operands = []; ++pos; @@ -3872,10 +3574,12 @@ var Type2CFF = (function Type2CFFClosure() { } return entries; }, - parseIndex: function cff_parseIndex(pos) { + parseIndex: function parseIndex(pos) { + var cffIndex = new CFFIndex(); var bytes = this.bytes; - var count = bytes[pos++] << 8 | bytes[pos++]; + var count = (bytes[pos++] << 8) | bytes[pos++]; var offsets = []; + var start = pos; var end = pos; if (count != 0) { @@ -3893,26 +3597,949 @@ var Type2CFF = (function Type2CFFClosure() { } end = offsets[count]; } + for (var i = 0, ii = offsets.length - 1; i < ii; ++i) { + var offsetStart = offsets[i]; + var offsetEnd = offsets[i + 1]; + cffIndex.add(bytes.subarray(offsetStart, offsetEnd)); + } + return {obj: cffIndex, endPos: end}; + }, + parseNameIndex: function parseNameIndex(index) { + var names = []; + for (var i = 0, ii = index.count; i < ii; ++i) { + var name = index.get(i); + // OTS doesn't allow names to be over 127 characters. + var length = Math.min(name.length, 127); + var data = new Array(length); + // OTS also only permits certain characters in the name. + for (var j = 0; j < length; ++j) { + var c = name[j]; + if (j === 0 && c === 0) { + data[j] = c; + continue; + } + if ((c < 33 || c > 126) || c === 91 /* [ */ || c === 93 /* ] */ || + c === 40 /* ( */ || c === 41 /* ) */ || c === 123 /* { */ || + c === 125 /* } */ || c === 60 /* < */ || c === 62 /* > */ || + c === 47 /* / */ || c === 37 /* % */) { + data[j] = 95; + continue; + } + data[j] = c; + } + names.push(String.fromCharCode.apply(null, data)); + } + return names; + }, + parseStringIndex: function parseStringIndex(index) { + var strings = new CFFStrings(); + for (var i = 0, ii = index.count; i < ii; ++i) { + var data = index.get(i); + strings.add(String.fromCharCode.apply(null, data)); + } + return strings; + }, + createDict: function createDict(type, dict, strings) { + var cffDict = new type(strings); + var types = cffDict.types; - return { - get: function index_get(index) { - if (index >= count) - return null; + for (var i = 0, ii = dict.length; i < ii; ++i) { + var pair = dict[i]; + var key = pair[0]; + var value = pair[1]; + cffDict.setByKey(key, value); + } + return cffDict; + }, + parseCharStrings: function parseCharStrings(charStringOffset) { + var charStrings = this.parseIndex(charStringOffset).obj; + // The CFF specification state that the 'dotsection' command + // (12, 0) is deprecated and treated as a no-op, but all Type2 + // charstrings processors should support them. Unfortunately + // the font sanitizer don't. As a workaround the sequence (12, 0) + // is replaced by a useless (0, hmoveto). + var count = charStrings.count; + for (var i = 0; i < count; i++) { + var charstring = charStrings.get(i); - var start = offsets[index]; - var end = offsets[index + 1]; - return { - start: start, - end: end, - data: bytes.subarray(start, end) - }; - }, - length: count, - endPos: end - }; + var data = charstring; + var length = data.length; + for (var j = 0; j <= length; j) { + var value = data[j++]; + if (value == 12 && data[j++] == 0) { + data[j - 2] = 139; + data[j - 1] = 22; + } else if (value === 28) { + j += 2; + } else if (value >= 247 && value <= 254) { + j++; + } else if (value == 255) { + j += 4; + } + } + } + return charStrings; + }, + parsePrivateDict: function parsePrivateDict(parentDict) { + // no private dict, do nothing + if (!parentDict.hasName('Private')) + return; + var privateOffset = parentDict.getByName('Private'); + // make sure the params are formatted correctly + if (!isArray(privateOffset) || privateOffset.length !== 2) { + parentDict.removeByName('Private'); + return; + } + var size = privateOffset[0]; + var offset = privateOffset[1]; + // remove empty dicts or ones that refer to invalid location + if (size === 0 || offset >= this.bytes.length) { + parentDict.removeByName('Private'); + return; + } + + var privateDictEnd = offset + size; + var dictData = this.bytes.subarray(offset, privateDictEnd); + var dict = this.parseDict(dictData); + var privateDict = this.createDict(CFFPrivateDict, dict, + parentDict.strings); + parentDict.privateDict = privateDict; + + // Parse the Subrs index also since it's relative to the private dict. + if (!privateDict.getByName('Subrs')) + return; + var subrsOffset = privateDict.getByName('Subrs'); + var relativeOffset = offset + subrsOffset; + // Validate the offset. + if (subrsOffset === 0 || relativeOffset >= this.bytes.length) { + privateDict.removeByName('Subrs'); + return; + } + var subrsIndex = this.parseIndex(relativeOffset); + privateDict.subrsIndex = subrsIndex.obj; + }, + parseCharsets: function parsecharsets(pos, length, strings, cid) { + if (pos == 0) { + return new CFFCharset(true, CFFCharsetPredefinedTypes.ISO_ADOBE, + ISOAdobeCharset.slice()); + } else if (pos == 1) { + return new CFFCharset(true, CFFCharsetPredefinedTypes.EXPERT, + ExpertCharset.slice()); + } else if (pos == 2) { + return new CFFCharset(true, CFFCharsetPredefinedTypes.EXPERT_SUBSET, + ExpertSubsetCharset.slice()); + } + + var bytes = this.bytes; + var start = pos; + var format = bytes[pos++]; + var charset = ['.notdef']; + + // subtract 1 for the .notdef glyph + length -= 1; + + switch (format) { + case 0: + for (var i = 0; i < length; i++) { + var id = (bytes[pos++] << 8) | bytes[pos++]; + charset.push(cid ? id : strings.get(id)); + } + break; + case 1: + while (charset.length <= length) { + var id = (bytes[pos++] << 8) | bytes[pos++]; + var count = bytes[pos++]; + for (var i = 0; i <= count; i++) + charset.push(cid ? id++ : strings.get(id++)); + } + break; + case 2: + while (charset.length <= length) { + var id = (bytes[pos++] << 8) | bytes[pos++]; + var count = (bytes[pos++] << 8) | bytes[pos++]; + for (var i = 0; i <= count; i++) + charset.push(cid ? id++ : strings.get(id++)); + } + break; + default: + error('Unknown charset format'); + } + // Raw won't be needed if we actually compile the charset. + var end = pos; + var raw = bytes.subarray(start, end); + + return new CFFCharset(false, format, charset, raw); + }, + parseEncoding: function parseEncoding(pos, properties, strings, charset) { + var encoding = {}; + var bytes = this.bytes; + var predefined = false; + var hasSupplement = false; + var format; + var raw = null; + + function readSupplement() { + var supplementsCount = bytes[pos++]; + for (var i = 0; i < supplementsCount; i++) { + var code = bytes[pos++]; + var sid = (bytes[pos++] << 8) + (bytes[pos++] & 0xff); + encoding[code] = properties.differences.indexOf(strings.get(sid)); + } + } + + if (pos == 0 || pos == 1) { + predefined = true; + format = pos; + var gid = 1; + var baseEncoding = pos ? Encodings.ExpertEncoding : + Encodings.StandardEncoding; + for (var i = 0, ii = charset.length; i < ii; i++) { + var index = baseEncoding.indexOf(charset[i]); + if (index != -1) + encoding[index] = gid++; + } + } else { + var dataStart = pos; + var format = bytes[pos++]; + switch (format & 0x7f) { + case 0: + var glyphsCount = bytes[pos++]; + for (var i = 1; i <= glyphsCount; i++) + encoding[bytes[pos++]] = i; + break; + + case 1: + var rangesCount = bytes[pos++]; + var gid = 1; + for (var i = 0; i < rangesCount; i++) { + var start = bytes[pos++]; + var left = bytes[pos++]; + for (var j = start; j <= start + left; j++) + encoding[j] = gid++; + } + break; + + default: + error('Unknow encoding format: ' + format + ' in CFF'); + break; + } + var dataEnd = pos; + if (format & 0x80) { + // The font sanitizer does not support CFF encoding with a + // supplement, since the encoding is not really used to map + // between gid to glyph, let's overwrite what is declared in + // the top dictionary to let the sanitizer think the font use + // StandardEncoding, that's a lie but that's ok. + bytes[dataStart] &= 0x7f; + readSupplement(); + hasSupplement = true; + } + raw = bytes.subarray(dataStart, dataEnd); + } + format = format & 0x7f; + return new CFFEncoding(predefined, format, encoding, raw); + }, + parseFDSelect: function parseFDSelect(pos, length) { + var start = pos; + var bytes = this.bytes; + var format = bytes[pos++]; + var fdSelect = []; + switch (format) { + case 0: + for (var i = 0; i < length; ++i) { + var id = bytes[pos++]; + fdSelect.push(id); + } + break; + case 3: + var rangesCount = (bytes[pos++] << 8) | bytes[pos++]; + for (var i = 0; i < rangesCount; ++i) { + var first = (bytes[pos++] << 8) | bytes[pos++]; + var fdIndex = bytes[pos++]; + var next = (bytes[pos] << 8) | bytes[pos + 1]; + for (var j = first; j < next; ++j) + fdSelect.push(fdIndex); + } + // Advance past the sentinel(next). + pos += 2; + break; + default: + error('Unknown fdselect format ' + format); + break; + } + var end = pos; + return new CFFFDSelect(fdSelect, bytes.subarray(start, end)); } }; - - return Type2CFF; + return CFFParser; +})(); + +// Compact Font Format +var CFFTable = (function CFFTableClosure() { + function CFFTable() { + this.header = null; + this.names = []; + this.topDict = null; + this.strings = new CFFStrings(); + this.globalSubrIndex = null; + + // The following could really be per font, but since we only have one font + // store them here. + this.encoding = null; + this.charset = null; + this.charStrings = null; + this.fdArray = []; + this.fdSelect = null; + + this.isCIDFont = false; + } + return CFFTable; +})(); + +var CFFHeader = (function CFFHeader() { + function CFFHeader(major, minor, hdrSize, offSize) { + this.major = major; + this.minor = minor; + this.hdrSize = hdrSize; + this.offSize = offSize; + } + return CFFHeader; +})(); + +var CFFStrings = (function CFFStrings() { + function CFFStrings() { + this.strings = []; + } + CFFStrings.prototype = { + get: function get(index) { + if (index >= 0 && index <= 390) + return CFFStandardStrings[index]; + if (index - 391 <= this.strings.length) + return this.strings[index - 391]; + return CFFStandardStrings[0]; + }, + add: function add(value) { + this.strings.push(value); + }, + get count() { + return this.strings.length; + } + }; + return CFFStrings; +})(); + +var CFFIndex = (function() { + function CFFIndex() { + this.objects = []; + this.length = 0; + } + CFFIndex.prototype = { + add: function add(data) { + this.length += data.length; + this.objects.push(data); + }, + get: function get(index) { + return this.objects[index]; + }, + get count() { + return this.objects.length; + } + }; + return CFFIndex; +})(); + +var CFFDict = (function CFFDictClosure() { + function CFFDict(tables, strings) { + this.keyToNameMap = tables.keyToNameMap; + this.nameToKeyMap = tables.nameToKeyMap; + this.defaults = tables.defaults; + this.types = tables.types; + this.opcodes = tables.opcodes; + this.order = tables.order; + this.strings = strings; + this.values = {}; + } + CFFDict.prototype = { + // value should always be an array + setByKey: function setByKey(key, value) { + if (!(key in this.keyToNameMap)) + return false; + // ignore empty values + if (value.length === 0) + return true; + var type = this.types[key]; + // remove the array wrapping these types of values + if (type === 'num' || type === 'sid' || type === 'offset') + value = value[0]; + this.values[key] = value; + return true; + }, + hasName: function hasName(name) { + return this.nameToKeyMap[name] in this.values; + }, + getByName: function getByName(name) { + if (!(name in this.nameToKeyMap)) + error('Invalid dictionary name "' + name + '"'); + var key = this.nameToKeyMap[name]; + if (!(key in this.values)) + return this.defaults[key]; + return this.values[key]; + }, + removeByName: function removeByName(name) { + delete this.values[this.nameToKeyMap[name]]; + }, + dump: function dump(title) { + console.log('----' + title + ' Dictionary Dump------'); + for (var key in this.values) { + if (key in this.keyToNameMap) + console.log(this.keyToNameMap[key] + '(' + key + '): ' + + this.values[key]); + else + console.log('Unknown(' + key + '): ' + this.values[key]); + } + } + }; + CFFDict.createTables = function createTables(layout) { + var tables = { + keyToNameMap: {}, + nameToKeyMap: {}, + defaults: {}, + types: {}, + opcodes: {}, + order: [] + }; + for (var i = 0, ii = layout.length; i < ii; ++i) { + var entry = layout[i]; + var key = isArray(entry[0]) ? (entry[0][0] << 8) + entry[0][1] : entry[0]; + tables.keyToNameMap[key] = entry[1]; + tables.nameToKeyMap[entry[1]] = key; + tables.types[key] = entry[2]; + tables.defaults[key] = entry[3]; + tables.opcodes[key] = isArray(entry[0]) ? entry[0] : [entry[0]]; + tables.order.push(key); + } + return tables; + }; + return CFFDict; +})(); + +var CFFTopDict = (function CFFTopDictClosure() { + var layout = [ + [[12, 30], 'ROS', ['sid', 'sid', 'num'], null], + [[12, 20], 'SyntheticBase', 'num', null], + [0, 'version', 'sid', null], + [1, 'Notice', 'sid', null], + [[12, 0], 'Copyright', 'sid', null], + [2, 'FullName', 'sid', null], + [3, 'FamilyName', 'sid', null], + [4, 'Weight', 'sid', null], + [[12, 1], 'isFixedPitch', 'num', 0], + [[12, 2], 'ItalicAngle', 'num', 0], + [[12, 3], 'UnderlinePosition', 'num', -100], + [[12, 4], 'UnderlineThickness', 'num', 50], + [[12, 5], 'PaintType', 'num', 0], + [[12, 6], 'CharstringType', 'num', 2], + [[12, 7], 'FontMatrix', ['num', 'num', 'num', 'num'], + [.001, 0, 0, .001, 0, 0]], + [13, 'UniqueID', 'num', null], + [5, 'FontBBox', ['num', 'num', 'num', 'num'], [0, 0, 0, 0]], + [[12, 8], 'StrokeWidth', 'num', 0], + [14, 'XUID', 'array', null], + [15, 'charset', 'offset', 0], + [16, 'Encoding', 'offset', 0], + [17, 'CharStrings', 'offset', 0], + [18, 'Private', ['offset', 'offset'], null], + [[12, 21], 'PostScript', 'sid', null], + [[12, 22], 'BaseFontName', 'sid', null], + [[12, 23], 'BaseFontBlend', 'delta', null], + [[12, 31], 'CIDFontVersion', 'num', 0], + [[12, 32], 'CIDFontRevision', 'num', 0], + [[12, 33], 'CIDFontType', 'num', 0], + [[12, 34], 'CIDCount', 'num', 8720], + [[12, 35], 'UIDBase', 'num', null], + [[12, 36], 'FDArray', 'offset', null], + [[12, 37], 'FDSelect', 'offset', null], + [[12, 38], 'FontName', 'sid', null]]; + var tables = null; + function CFFTopDict(strings) { + if (tables === null) + tables = CFFDict.createTables(layout); + CFFDict.call(this, tables, strings); + this.privateDict = null; + } + CFFTopDict.prototype = Object.create(CFFDict.prototype); + return CFFTopDict; +})(); + +var CFFPrivateDict = (function CFFPrivateDictClosure() { + var layout = [ + [6, 'BlueValues', 'delta', null], + [7, 'OtherBlues', 'delta', null], + [8, 'FamilyBlues', 'delta', null], + [9, 'FamilyOtherBlues', 'delta', null], + [[12, 9], 'BlueScale', 'num', 0.039625], + [[12, 10], 'BlueShift', 'num', 7], + [[12, 11], 'BlueFuzz', 'num', 1], + [10, 'StdHW', 'num', null], + [11, 'StdVW', 'num', null], + [[12, 12], 'StemSnapH', 'delta', null], + [[12, 13], 'StemSnapV', 'delta', null], + [[12, 14], 'ForceBold', 'num', 0], + [[12, 17], 'LanguageGroup', 'num', 0], + [[12, 18], 'ExpansionFactor', 'num', 0.06], + [[12, 19], 'initialRandomSeed', 'num', 0], + [19, 'Subrs', 'offset', null], + [20, 'defaultWidthX', 'num', 0], + [21, 'nominalWidthX', 'num', 0] + ]; + var tables = null; + function CFFPrivateDict(strings) { + if (tables === null) + tables = CFFDict.createTables(layout); + CFFDict.call(this, tables, strings); + this.subrsIndex = null; + } + CFFPrivateDict.prototype = Object.create(CFFDict.prototype); + return CFFPrivateDict; +})(); + +var CFFCharsetPredefinedTypes = { + ISO_ADOBE: 0, + EXPERT: 1, + EXPERT_SUBSET: 2 +}; +var CFFCharsetEmbeddedTypes = { + FORMAT0: 0, + FORMAT1: 1, + FORMAT2: 2 +}; +var CFFCharset = (function CFFCharsetClosure() { + function CFFCharset(predefined, format, charset, raw) { + this.predefined = predefined; + this.format = format; + this.charset = charset; + this.raw = raw; + } + return CFFCharset; +})(); + +var CFFEncodingPredefinedTypes = { + STANDARD: 0, + EXPERT: 1 +}; +var CFFCharsetEmbeddedTypes = { + FORMAT0: 0, + FORMAT1: 1 +}; +var CFFEncoding = (function CFFEncodingClosure() { + function CFFEncoding(predefined, format, encoding, raw) { + this.predefined = predefined; + this.format = format; + this.encoding = encoding; + this.raw = raw; + } + return CFFEncoding; +})(); + +var CFFFDSelect = (function CFFFDSelectClosure() { + function CFFFDSelect(fdSelect, raw) { + this.fdSelect = fdSelect; + this.raw = raw; + } + return CFFFDSelect; +})(); + +// Helper class to keep track of where an offset is within the data and helps +// filling in that offset once it's known. +var CFFOffsetTracker = (function CFFOffsetTracker() { + function CFFOffsetTracker() { + this.offsets = {}; + } + CFFOffsetTracker.prototype = { + isTracking: function isTracking(key) { + return key in this.offsets; + }, + track: function track(key, location) { + if (key in this.offsets) + error('Already tracking location of ' + key); + this.offsets[key] = location; + }, + offset: function offset(value) { + for (var key in this.offsets) { + this.offsets[key] += value; + } + }, + setEntryLocation: function setEntryLocation(key, values, output) { + if (!(key in this.offsets)) + error('Not tracking location of ' + key); + var data = output.data; + var dataOffset = this.offsets[key]; + var size = 5; + for (var i = 0, ii = values.length; i < ii; ++i) { + var offset0 = i * size + dataOffset; + var offset1 = offset0 + 1; + var offset2 = offset0 + 2; + var offset3 = offset0 + 3; + var offset4 = offset0 + 4; + // It's easy to screw up offsets so perform this sanity check. + if (data[offset0] !== 0x1d || data[offset1] !== 0 || + data[offset2] !== 0 || data[offset3] !== 0 || data[offset4] !== 0) + error('writing to an offset that is not empty'); + var value = values[i]; + data[offset0] = 0x1d; + data[offset1] = (value >> 24) & 0xFF; + data[offset2] = (value >> 16) & 0xFF; + data[offset3] = (value >> 8) & 0xFF; + data[offset4] = value & 0xFF; + } + } + }; + return CFFOffsetTracker; +})(); + +// Takes a CFF and converts it to the binary representation. +var CFFCompiler = (function CFFCompilerClosure() { + function stringToArray(str) { + var array = []; + for (var i = 0, ii = str.length; i < ii; ++i) + array[i] = str.charCodeAt(i); + + return array; + }; + function CFFCompiler(cff) { + this.cff = cff; + } + CFFCompiler.prototype = { + compile: function compile() { + var cff = this.cff; + var output = { + data: [], + length: 0, + add: function add(data) { + this.data = this.data.concat(data); + this.length = this.data.length; + } + }; + + // Compile the five entries that must be in order. + var header = this.compileHeader(cff.header); + output.add(header); + + var nameIndex = this.compileNameIndex(cff.names); + output.add(nameIndex); + + var compiled = this.compileTopDicts([cff.topDict], output.length); + output.add(compiled.output); + var topDictTracker = compiled.trackers[0]; + + var stringIndex = this.compileStringIndex(cff.strings.strings); + output.add(stringIndex); + + var globalSubrIndex = this.compileIndex(cff.globalSubrIndex); + output.add(globalSubrIndex); + + // Now start on the other entries that have no specfic order. + if (cff.encoding && cff.topDict.hasName('Encoding')) { + if (cff.encoding.predefined) { + topDictTracker.setEntryLocation('Encoding', [cff.encoding.format], + output); + } else { + var encoding = this.compileEncoding(cff.encoding); + topDictTracker.setEntryLocation('Encoding', [output.length], output); + output.add(encoding); + } + } + + if (cff.charset && cff.topDict.hasName('charset')) { + if (cff.charset.predefined) { + topDictTracker.setEntryLocation('charset', [cff.charset.format], + output); + } else { + var charset = this.compileCharset(cff.charset); + topDictTracker.setEntryLocation('charset', [output.length], output); + output.add(charset); + } + } + + var charStrings = this.compileCharStrings(cff.charStrings); + topDictTracker.setEntryLocation('CharStrings', [output.length], output); + output.add(charStrings); + + if (cff.isCIDFont) { + var compiled = this.compileTopDicts(cff.fdArray, output.length); + topDictTracker.setEntryLocation('FDArray', [output.length], output); + output.add(compiled.output); + var fontDictTrackers = compiled.trackers; + + this.compilePrivateDicts(cff.fdArray, fontDictTrackers, output); + + topDictTracker.setEntryLocation('FDSelect', [output.length], output); + var fdSelect = this.compileFDSelect(cff.fdSelect.raw); + output.add(fdSelect); + } + + this.compilePrivateDicts([cff.topDict], [topDictTracker], output); + + return output.data; + }, + encodeNumber: function encodeNumber(value) { + if (parseFloat(value) == parseInt(value) && !isNaN(value)) // isInt + return this.encodeInteger(value); + else + return this.encodeFloat(value); + }, + encodeFloat: function encodeFloat(value) { + value = value.toString(); + // Strip off the any leading zeros. + if (value.substr(0, 2) === '0.') + value = value.substr(1); + else if (value.substr(0, 3) === '-0.') + value = '-' + value.substr(2); + var nibbles = []; + for (var i = 0, ii = value.length; i < ii; ++i) { + var a = value.charAt(i), b = value.charAt(i + 1); + var nibble; + if (a === 'e' && b === '-') { + nibble = 0xc; + ++i; + } else if (a === '.') { + nibble = 0xa; + } else if (a === 'E') { + nibble = 0xb; + } else if (a === '-') { + nibble = 0xe; + } else { + nibble = a; + } + nibbles.push(nibble); + } + nibbles.push(0xf); + if (nibbles.length % 2) + nibbles.push(0xf); + var out = [30]; + for (var i = 0, ii = nibbles.length; i < ii; i += 2) + out.push(nibbles[i] << 4 | nibbles[i + 1]); + return out; + }, + encodeInteger: function encodeInteger(value) { + var code; + if (value >= -107 && value <= 107) { + code = [value + 139]; + } else if (value >= 108 && value <= 1131) { + value = [value - 108]; + code = [(value >> 8) + 247, value & 0xFF]; + } else if (value >= -1131 && value <= -108) { + value = -value - 108; + code = [(value >> 8) + 251, value & 0xFF]; + } else if (value >= -32768 && value <= 32767) { + code = [0x1c, (value >> 8) & 0xFF, value & 0xFF]; + } else { + code = [0x1d, + (value >> 24) & 0xFF, + (value >> 16) & 0xFF, + (value >> 8) & 0xFF, + value & 0xFF]; + } + return code; + }, + compileHeader: function compileHeader(header) { + return [ + header.major, + header.minor, + header.hdrSize, + header.offSize + ]; + }, + compileNameIndex: function compileNameIndex(names) { + var nameIndex = new CFFIndex(); + for (var i = 0, ii = names.length; i < ii; ++i) + nameIndex.add(stringToArray(names[i])); + return this.compileIndex(nameIndex); + }, + compileTopDicts: function compileTopDicts(dicts, length) { + var fontDictTrackers = []; + var fdArrayIndex = new CFFIndex(); + for (var i = 0, ii = dicts.length; i < ii; ++i) { + var fontDict = dicts[i]; + var fontDictTracker = new CFFOffsetTracker(); + var fontDictData = this.compileDict(fontDict, fontDictTracker); + fontDictTrackers.push(fontDictTracker); + fdArrayIndex.add(fontDictData); + fontDictTracker.offset(length); + } + fdArrayIndex = this.compileIndex(fdArrayIndex, fontDictTrackers); + return { + trackers: fontDictTrackers, + output: fdArrayIndex + }; + }, + compilePrivateDicts: function compilePrivateDicts(dicts, trackers, output) { + for (var i = 0, ii = dicts.length; i < ii; ++i) { + var fontDict = dicts[i]; + if (!fontDict.privateDict || !fontDict.hasName('Private')) + continue; + var privateDict = fontDict.privateDict; + var privateDictTracker = new CFFOffsetTracker(); + var privateDictData = this.compileDict(privateDict, privateDictTracker); + + privateDictTracker.offset(output.length); + trackers[i].setEntryLocation('Private', + [privateDictData.length, output.length], + output); + output.add(privateDictData); + + if (privateDict.subrsIndex && privateDict.hasName('Subrs')) { + var subrs = this.compileIndex(privateDict.subrsIndex); + privateDictTracker.setEntryLocation('Subrs', [privateDictData.length], + output); + output.add(subrs); + } + } + }, + compileDict: function compileDict(dict, offsetTracker) { + var out = []; + // The dictionary keys must be in a certain order. + var order = dict.order; + for (var i = 0; i < order.length; ++i) { + var key = order[i]; + if (!(key in dict.values)) + continue; + //console.log('dict order: ' + dict.keyToNameMap[key]); + var values = dict.values[key]; + var types = dict.types[key]; + if (!isArray(types)) types = [types]; + if (!isArray(values)) values = [values]; + + // Remove any empty dict values. + if (values.length === 0) + continue; + + for (var j = 0, jj = types.length; j < jj; ++j) { + var type = types[j]; + var value = values[j]; + switch (type) { + case 'num': + case 'sid': + out = out.concat(this.encodeNumber(value)); + break; + case 'offset': + // For offsets we just insert a 32bit integer so we don't have to + // deal with figuring out the length of the offset when it gets + // replaced later on by the compiler. + var name = dict.keyToNameMap[key]; + // Some offsets have the offset and the length, so just record the + // position of the first one. + if (!offsetTracker.isTracking(name)) + offsetTracker.track(name, out.length); + out = out.concat([0x1d, 0, 0, 0, 0]); + break; + case 'array': + case 'delta': + out = out.concat(this.encodeNumber(value)); + for (var k = 1, kk = values.length; k < kk; ++k) + out = out.concat(this.encodeNumber(values[k])); + break; + default: + error('Unknown data type of ' + type); + break; + } + } + out = out.concat(dict.opcodes[key]); + } + return out; + }, + compileStringIndex: function compileStringIndex(strings) { + var stringIndex = new CFFIndex(); + for (var i = 0, ii = strings.length; i < ii; ++i) + stringIndex.add(stringToArray(strings[i])); + return this.compileIndex(stringIndex); + }, + compileGlobalSubrIndex: function compileGlobalSubrIndex() { + var globalSubrIndex = this.cff.globalSubrIndex; + this.out.writeByteArray(this.compileIndex(globalSubrIndex)); + }, + compileCharStrings: function compileCharStrings(charStrings) { + return this.compileIndex(charStrings); + }, + compileCharset: function compileCharset(charset) { + return this.compileTypedArray(charset.raw); + }, + compileEncoding: function compileEncoding(encoding) { + return this.compileTypedArray(encoding.raw); + }, + compileFDSelect: function compileFDSelect(fdSelect) { + return this.compileTypedArray(fdSelect); + }, + compileTypedArray: function compileTypedArray(data) { + var out = new Array(data.length); + for (var i = 0, ii = data.length; i < ii; ++i) + out[i] = data[i]; + return out; + }, + compileIndex: function compileIndex(index, trackers) { + trackers = trackers || []; + var objects = index.objects; + // First 2 bytes contains the number of objects contained into this index + var count = objects.length; + + // If there is no object, just create an index. This technically + // should just be [0, 0] but OTS has an issue with that. + if (count == 0) + return [0, 0, 0]; + + var data = [(count >> 8) & 0xFF, count & 0xff]; + + var lastOffset = 1; + for (var i = 0; i < count; ++i) + lastOffset += objects[i].length; + + var offsetSize; + if (lastOffset < 0x100) + offsetSize = 1; + else if (lastOffset < 0x10000) + offsetSize = 2; + else if (lastOffset < 0x1000000) + offsetSize = 3; + else + offsetSize = 4; + + // Next byte contains the offset size use to reference object in the file + data.push(offsetSize); + + // Add another offset after this one because we need a new offset + var relativeOffset = 1; + for (var i = 0; i < count + 1; i++) { + if (offsetSize === 1) { + data.push(relativeOffset & 0xFF); + } else if (offsetSize === 2) { + data.push((relativeOffset >> 8) & 0xFF, + relativeOffset & 0xFF); + } else if (offsetSize === 3) { + data.push((relativeOffset >> 16) & 0xFF, + (relativeOffset >> 8) & 0xFF, + relativeOffset & 0xFF); + } else { + data.push((relativeOffset >>> 24) & 0xFF, + (relativeOffset >> 16) & 0xFF, + (relativeOffset >> 8) & 0xFF, + relativeOffset & 0xFF); + } + + if (objects[i]) + relativeOffset += objects[i].length; + } + var offset = data.length; + + for (var i = 0; i < count; i++) { + // Notify the tracker where the object will be offset in the data. + if (trackers[i]) + trackers[i].offset(data.length); + for (var j = 0, jj = objects[i].length; j < jj; j++) + data.push(objects[i][j]); + } + return data; + } + }; + return CFFCompiler; })(); diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 9460cfbec..85565f670 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -24,4 +24,5 @@ !type4psfunc.pdf !S2.pdf !zerowidthline.pdf +!issue1002.pdf !issue925.pdf diff --git a/test/pdfs/issue1002.pdf b/test/pdfs/issue1002.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b764c88414c3d4af30adfe1447a4556bc4a1c898 GIT binary patch literal 19172 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 KRdw}u;{pJPoy0u= literal 0 HcmV?d00001 diff --git a/test/test_manifest.json b/test/test_manifest.json index 23a0f8ed9..ce5bfb4aa 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -488,6 +488,13 @@ "link": true, "type": "eq" }, + { "id": "issue1002", + "file": "pdfs/issue1002.pdf", + "md5": "af62d6cd95079322d4af18edd960d15c", + "rounds": 1, + "link": false, + "type": "eq" + }, { "id": "issue1243", "file": "pdfs/issue1243.pdf", "md5": "130c849b83513d5ac5e03c6421fc7489", diff --git a/test/unit/font_spec.js b/test/unit/font_spec.js new file mode 100644 index 000000000..a0e609fe8 --- /dev/null +++ b/test/unit/font_spec.js @@ -0,0 +1,223 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +'use strict'; + +describe('font', function() { + function hexDump(bytes) { + var line = ''; + for (var i = 0, ii = bytes.length; i < ii; ++i) { + var b = bytes[i].toString(16); + if (b.length < 2) + b = '0' + b; + line += b.toString(16); + } + return line; + } + // This example font comes from the CFF spec: + // http://www.adobe.com/content/dam/Adobe/en/devnet/font/pdfs/5176.CFF.pdf + var exampleFont = '0100040100010101134142434445462b' + + '54696d65732d526f6d616e000101011f' + + 'f81b00f81c02f81d03f819041c6f000d' + + 'fb3cfb6efa7cfa1605e911b8f1120003' + + '01010813183030312e30303754696d65' + + '7320526f6d616e54696d657300000002' + + '010102030e0e7d99f92a99fb7695f773' + + '8b06f79a93fc7c8c077d99f85695f75e' + + '9908fb6e8cf87393f7108b09a70adf0b' + + 'f78e14'; + var fontData = []; + for (var i = 0; i < exampleFont.length; i += 2) { + var hex = exampleFont.substr(i, 2); + fontData.push(parseInt(hex, 16)); + } + var bytes = new Uint8Array(fontData); + fontData = {getBytes: function() { return bytes}}; + + function bytesToString(bytesArray) { + var str = ''; + for (var i = 0, ii = bytesArray.length; i < ii; i++) + str += String.fromCharCode(bytesArray[i]); + return str; + } + + describe('CFFParser', function() { + var parser = new CFFParser(fontData); + var cff = parser.parse(); + + it('parses header', function() { + var header = cff.header; + expect(header.major).toEqual(1); + expect(header.minor).toEqual(0); + expect(header.hdrSize).toEqual(4); + expect(header.offSize).toEqual(1); + }); + + it('parses name index', function() { + var names = cff.names; + expect(names.length).toEqual(1); + expect(names[0]).toEqual('ABCDEF+Times-Roman'); + }); + + it('sanitizes name index', function() { + var index = new CFFIndex(); + index.add(['['.charCodeAt(0), 'a'.charCodeAt(0)]); + + var names = parser.parseNameIndex(index); + expect(names).toEqual(['_a']); + + index = new CFFIndex(); + var longName = []; + for (var i = 0; i < 129; i++) + longName.push(0); + index.add(longName); + names = parser.parseNameIndex(index); + expect(names[0].length).toEqual(127); + }); + + it('parses string index', function() { + var strings = cff.strings; + expect(strings.count).toEqual(3); + expect(strings.get(0)).toEqual('.notdef'); + expect(strings.get(391)).toEqual('001.007'); + }); + + it('parses top dict', function() { + var topDict = cff.topDict; + // 391 version 392 FullName 393 FamilyName 389 Weight 28416 UniqueID + // -168 -218 1000 898 FontBBox 94 CharStrings 45 102 Private + expect(topDict.getByName('version')).toEqual(391); + expect(topDict.getByName('FullName')).toEqual(392); + expect(topDict.getByName('FamilyName')).toEqual(393); + expect(topDict.getByName('Weight')).toEqual(389); + expect(topDict.getByName('UniqueID')).toEqual(28416); + expect(topDict.getByName('FontBBox')).toEqual([-168, -218, 1000, 898]); + expect(topDict.getByName('CharStrings')).toEqual(94); + expect(topDict.getByName('Private')).toEqual([45, 102]); + }); + + it('parses predefined charsets', function() { + var charset = parser.parseCharsets(0, 0, null, true); + expect(charset.predefined).toEqual(true); + }); + + it('parses charset format 0', function() { + // The first three bytes make the offset large enough to skip predefined. + var bytes = new Uint8Array([0x00, 0x00, 0x00, + 0x00, // format + 0x00, 0x02 // sid/cid + ]); + parser.bytes = bytes; + var charset = parser.parseCharsets(3, 2, new CFFStrings(), false); + expect(charset.charset[1]).toEqual('exclam'); + + // CID font + var charset = parser.parseCharsets(3, 2, new CFFStrings(), true); + expect(charset.charset[1]).toEqual(2); + }); + + it('parses charset format 1', function() { + // The first three bytes make the offset large enough to skip predefined. + var bytes = new Uint8Array([0x00, 0x00, 0x00, + 0x01, // format + 0x00, 0x08, // sid/cid start + 0x01 // sid/cid left + ]); + parser.bytes = bytes; + var charset = parser.parseCharsets(3, 2, new CFFStrings(), false); + expect(charset.charset).toEqual(['.notdef', 'quoteright', 'parenleft']); + + // CID font + var charset = parser.parseCharsets(3, 2, new CFFStrings(), true); + expect(charset.charset).toEqual(['.notdef', 8, 9]); + }); + + it('parses charset format 2', function() { + // format 2 is the same as format 1 but the left is card16 + // The first three bytes make the offset large enough to skip predefined. + var bytes = new Uint8Array([0x00, 0x00, 0x00, + 0x02, // format + 0x00, 0x08, // sid/cid start + 0x00, 0x01 // sid/cid left + ]); + parser.bytes = bytes; + var charset = parser.parseCharsets(3, 2, new CFFStrings(), false); + expect(charset.charset).toEqual(['.notdef', 'quoteright', 'parenleft']); + + // CID font + var charset = parser.parseCharsets(3, 2, new CFFStrings(), true); + expect(charset.charset).toEqual(['.notdef', 8, 9]); + }); + + it('parses encoding format 0', function() { + // The first two bytes make the offset large enough to skip predefined. + var bytes = new Uint8Array([0x00, 0x00, + 0x00, // format + 0x01, // count + 0x08 // start + ]); + parser.bytes = bytes; + var encoding = parser.parseEncoding(2, {}, new CFFStrings(), null); + expect(encoding.encoding).toEqual({0x8: 1}); + }); + + it('parses encoding format 1', function() { + // The first two bytes make the offset large enough to skip predefined. + var bytes = new Uint8Array([0x00, 0x00, + 0x01, // format + 0x01, // num ranges + 0x07, // range1 start + 0x01 // range2 left + ]); + parser.bytes = bytes; + var encoding = parser.parseEncoding(2, {}, new CFFStrings(), null); + expect(encoding.encoding).toEqual({0x7: 0x01, 0x08: 0x02}); + }); + + it('parses fdselect format 0', function() { + var bytes = new Uint8Array([0x00, // format + 0x00, // gid: 0 fd: 0 + 0x01 // gid: 1 fd: 1 + ]); + parser.bytes = bytes; + var fdSelect = parser.parseFDSelect(0, 2); + expect(fdSelect.fdSelect).toEqual([0, 1]); + }); + + it('parses fdselect format 3', function() { + var bytes = new Uint8Array([0x03, // format + 0x00, 0x02, // range count + 0x00, 0x00, // first gid + 0x09, // font dict 1 id + 0x00, 0x02, // nex gid + 0x0a, // font dict 2 gid + 0x00, 0x04, // sentinel (last gid) + ]); + parser.bytes = bytes; + var fdSelect = parser.parseFDSelect(0, 2); + expect(fdSelect.fdSelect).toEqual([9, 9, 0xa, 0xa]); + }); + // TODO fdArray + }); + describe('CFFCompiler', function() { + it('encodes integers', function() { + var c = new CFFCompiler(); + // all the examples from the spec + expect(c.encodeInteger(0)).toEqual([0x8b]); + expect(c.encodeInteger(100)).toEqual([0xef]); + expect(c.encodeInteger(-100)).toEqual([0x27]); + expect(c.encodeInteger(1000)).toEqual([0xfa, 0x7c]); + expect(c.encodeInteger(-1000)).toEqual([0xfe, 0x7c]); + expect(c.encodeInteger(10000)).toEqual([0x1c, 0x27, 0x10]); + expect(c.encodeInteger(-10000)).toEqual([0x1c, 0xd8, 0xf0]); + expect(c.encodeInteger(100000)).toEqual([0x1d, 0x00, 0x01, 0x86, 0xa0]); + expect(c.encodeInteger(-100000)).toEqual([0x1d, 0xff, 0xfe, 0x79, 0x60]); + }); + it('encodes floats', function() { + var c = new CFFCompiler(); + expect(c.encodeFloat(-2.25)).toEqual([0x1e, 0xe2, 0xa2, 0x5f]); + expect(c.encodeFloat(5e-11)).toEqual([0x1e, 0x5c, 0x11, 0xff]); + }); + // TODO a lot more compiler tests + }); +});