UNPKG

opentype.js

Version:
1,321 lines (1,178 loc) 47.8 kB
// The `CFF` table contains the glyph outlines in PostScript format. // https://www.microsoft.com/typography/OTSPEC/cff.htm // http://download.microsoft.com/download/8/0/1/801a191c-029d-4af3-9642-555f6fe514ee/cff.pdf // http://download.microsoft.com/download/8/0/1/801a191c-029d-4af3-9642-555f6fe514ee/type2.pdf import { CffEncoding, cffStandardEncoding, cffExpertEncoding, cffStandardStrings } from '../encoding'; import glyphset from '../glyphset'; import parse from '../parse'; import Path from '../path'; import table from '../table'; // Custom equals function that can also check lists. function equals(a, b) { if (a === b) { return true; } else if (Array.isArray(a) && Array.isArray(b)) { if (a.length !== b.length) { return false; } for (let i = 0; i < a.length; i += 1) { if (!equals(a[i], b[i])) { return false; } } return true; } else { return false; } } // Subroutines are encoded using the negative half of the number space. // See type 2 chapter 4.7 "Subroutine operators". function calcCFFSubroutineBias(subrs) { let bias; if (subrs.length < 1240) { bias = 107; } else if (subrs.length < 33900) { bias = 1131; } else { bias = 32768; } return bias; } // Parse a `CFF` INDEX array. // An index array consists of a list of offsets, then a list of objects at those offsets. function parseCFFIndex(data, start, conversionFn) { const offsets = []; const objects = []; const count = parse.getCard16(data, start); let objectOffset; let endOffset; if (count !== 0) { const offsetSize = parse.getByte(data, start + 2); objectOffset = start + ((count + 1) * offsetSize) + 2; let pos = start + 3; for (let i = 0; i < count + 1; i += 1) { offsets.push(parse.getOffset(data, pos, offsetSize)); pos += offsetSize; } // The total size of the index array is 4 header bytes + the value of the last offset. endOffset = objectOffset + offsets[count]; } else { endOffset = start + 2; } for (let i = 0; i < offsets.length - 1; i += 1) { let value = parse.getBytes(data, objectOffset + offsets[i], objectOffset + offsets[i + 1]); if (conversionFn) { value = conversionFn(value); } objects.push(value); } return {objects: objects, startOffset: start, endOffset: endOffset}; } function parseCFFIndexLowMemory(data, start) { const offsets = []; const count = parse.getCard16(data, start); let objectOffset; let endOffset; if (count !== 0) { const offsetSize = parse.getByte(data, start + 2); objectOffset = start + ((count + 1) * offsetSize) + 2; let pos = start + 3; for (let i = 0; i < count + 1; i += 1) { offsets.push(parse.getOffset(data, pos, offsetSize)); pos += offsetSize; } // The total size of the index array is 4 header bytes + the value of the last offset. endOffset = objectOffset + offsets[count]; } else { endOffset = start + 2; } return {offsets: offsets, startOffset: start, endOffset: endOffset}; } function getCffIndexObject(i, offsets, data, start, conversionFn) { const count = parse.getCard16(data, start); let objectOffset = 0; if (count !== 0) { const offsetSize = parse.getByte(data, start + 2); objectOffset = start + ((count + 1) * offsetSize) + 2; } let value = parse.getBytes(data, objectOffset + offsets[i], objectOffset + offsets[i + 1]); if (conversionFn) { value = conversionFn(value); } return value; } // Parse a `CFF` DICT real value. function parseFloatOperand(parser) { let s = ''; const eof = 15; const lookup = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', 'E', 'E-', null, '-']; while (true) { const b = parser.parseByte(); const n1 = b >> 4; const n2 = b & 15; if (n1 === eof) { break; } s += lookup[n1]; if (n2 === eof) { break; } s += lookup[n2]; } return parseFloat(s); } // Parse a `CFF` DICT operand. function parseOperand(parser, b0) { let b1; let b2; let b3; let b4; if (b0 === 28) { b1 = parser.parseByte(); b2 = parser.parseByte(); return b1 << 8 | b2; } if (b0 === 29) { b1 = parser.parseByte(); b2 = parser.parseByte(); b3 = parser.parseByte(); b4 = parser.parseByte(); return b1 << 24 | b2 << 16 | b3 << 8 | b4; } if (b0 === 30) { return parseFloatOperand(parser); } if (b0 >= 32 && b0 <= 246) { return b0 - 139; } if (b0 >= 247 && b0 <= 250) { b1 = parser.parseByte(); return (b0 - 247) * 256 + b1 + 108; } if (b0 >= 251 && b0 <= 254) { b1 = parser.parseByte(); return -(b0 - 251) * 256 - b1 - 108; } throw new Error('Invalid b0 ' + b0); } // Convert the entries returned by `parseDict` to a proper dictionary. // If a value is a list of one, it is unpacked. function entriesToObject(entries) { const o = {}; for (let i = 0; i < entries.length; i += 1) { const key = entries[i][0]; const values = entries[i][1]; let value; if (values.length === 1) { value = values[0]; } else { value = values; } if (o.hasOwnProperty(key) && !isNaN(o[key])) { throw new Error('Object ' + o + ' already has key ' + key); } o[key] = value; } return o; } // Parse a `CFF` DICT object. // A dictionary contains key-value pairs in a compact tokenized format. function parseCFFDict(data, start, size) { start = start !== undefined ? start : 0; const parser = new parse.Parser(data, start); const entries = []; let operands = []; size = size !== undefined ? size : data.length; while (parser.relativeOffset < size) { let op = parser.parseByte(); // The first byte for each dict item distinguishes between operator (key) and operand (value). // Values <= 21 are operators. if (op <= 21) { // Two-byte operators have an initial escape byte of 12. if (op === 12) { op = 1200 + parser.parseByte(); } entries.push([op, operands]); operands = []; } else { // Since the operands (values) come before the operators (keys), we store all operands in a list // until we encounter an operator. operands.push(parseOperand(parser, op)); } } return entriesToObject(entries); } // Given a String Index (SID), return the value of the string. // Strings below index 392 are standard CFF strings and are not encoded in the font. function getCFFString(strings, index) { if (index <= 390) { index = cffStandardStrings[index]; } else { index = strings[index - 391]; } return index; } // Interpret a dictionary and return a new dictionary with readable keys and values for missing entries. // This function takes `meta` which is a list of objects containing `operand`, `name` and `default`. function interpretDict(dict, meta, strings) { const newDict = {}; let value; // Because we also want to include missing values, we start out from the meta list // and lookup values in the dict. for (let i = 0; i < meta.length; i += 1) { const m = meta[i]; if (Array.isArray(m.type)) { const values = []; values.length = m.type.length; for (let j = 0; j < m.type.length; j++) { value = dict[m.op] !== undefined ? dict[m.op][j] : undefined; if (value === undefined) { value = m.value !== undefined && m.value[j] !== undefined ? m.value[j] : null; } if (m.type[j] === 'SID') { value = getCFFString(strings, value); } values[j] = value; } newDict[m.name] = values; } else { value = dict[m.op]; if (value === undefined) { value = m.value !== undefined ? m.value : null; } if (m.type === 'SID') { value = getCFFString(strings, value); } newDict[m.name] = value; } } return newDict; } // Parse the CFF header. function parseCFFHeader(data, start) { const header = {}; header.formatMajor = parse.getCard8(data, start); header.formatMinor = parse.getCard8(data, start + 1); header.size = parse.getCard8(data, start + 2); header.offsetSize = parse.getCard8(data, start + 3); header.startOffset = start; header.endOffset = start + 4; return header; } const TOP_DICT_META = [ {name: 'version', op: 0, type: 'SID'}, {name: 'notice', op: 1, type: 'SID'}, {name: 'copyright', op: 1200, type: 'SID'}, {name: 'fullName', op: 2, type: 'SID'}, {name: 'familyName', op: 3, type: 'SID'}, {name: 'weight', op: 4, type: 'SID'}, {name: 'isFixedPitch', op: 1201, type: 'number', value: 0}, {name: 'italicAngle', op: 1202, type: 'number', value: 0}, {name: 'underlinePosition', op: 1203, type: 'number', value: -100}, {name: 'underlineThickness', op: 1204, type: 'number', value: 50}, {name: 'paintType', op: 1205, type: 'number', value: 0}, {name: 'charstringType', op: 1206, type: 'number', value: 2}, { name: 'fontMatrix', op: 1207, type: ['real', 'real', 'real', 'real', 'real', 'real'], value: [0.001, 0, 0, 0.001, 0, 0] }, {name: 'uniqueId', op: 13, type: 'number'}, {name: 'fontBBox', op: 5, type: ['number', 'number', 'number', 'number'], value: [0, 0, 0, 0]}, {name: 'strokeWidth', op: 1208, type: 'number', value: 0}, {name: 'xuid', op: 14, type: [], value: null}, {name: 'charset', op: 15, type: 'offset', value: 0}, {name: 'encoding', op: 16, type: 'offset', value: 0}, {name: 'charStrings', op: 17, type: 'offset', value: 0}, {name: 'private', op: 18, type: ['number', 'offset'], value: [0, 0]}, {name: 'ros', op: 1230, type: ['SID', 'SID', 'number']}, {name: 'cidFontVersion', op: 1231, type: 'number', value: 0}, {name: 'cidFontRevision', op: 1232, type: 'number', value: 0}, {name: 'cidFontType', op: 1233, type: 'number', value: 0}, {name: 'cidCount', op: 1234, type: 'number', value: 8720}, {name: 'uidBase', op: 1235, type: 'number'}, {name: 'fdArray', op: 1236, type: 'offset'}, {name: 'fdSelect', op: 1237, type: 'offset'}, {name: 'fontName', op: 1238, type: 'SID'} ]; const PRIVATE_DICT_META = [ {name: 'subrs', op: 19, type: 'offset', value: 0}, {name: 'defaultWidthX', op: 20, type: 'number', value: 0}, {name: 'nominalWidthX', op: 21, type: 'number', value: 0} ]; // Parse the CFF top dictionary. A CFF table can contain multiple fonts, each with their own top dictionary. // The top dictionary contains the essential metadata for the font, together with the private dictionary. function parseCFFTopDict(data, strings) { const dict = parseCFFDict(data, 0, data.byteLength); return interpretDict(dict, TOP_DICT_META, strings); } // Parse the CFF private dictionary. We don't fully parse out all the values, only the ones we need. function parseCFFPrivateDict(data, start, size, strings) { const dict = parseCFFDict(data, start, size); return interpretDict(dict, PRIVATE_DICT_META, strings); } // Returns a list of "Top DICT"s found using an INDEX list. // Used to read both the usual high-level Top DICTs and also the FDArray // discovered inside CID-keyed fonts. When a Top DICT has a reference to // a Private DICT that is read and saved into the Top DICT. // // In addition to the expected/optional values as outlined in TOP_DICT_META // the following values might be saved into the Top DICT. // // _subrs [] array of local CFF subroutines from Private DICT // _subrsBias bias value computed from number of subroutines // (see calcCFFSubroutineBias() and parseCFFCharstring()) // _defaultWidthX default widths for CFF characters // _nominalWidthX bias added to width embedded within glyph description // // _privateDict saved copy of parsed Private DICT from Top DICT function gatherCFFTopDicts(data, start, cffIndex, strings) { const topDictArray = []; for (let iTopDict = 0; iTopDict < cffIndex.length; iTopDict += 1) { const topDictData = new DataView(new Uint8Array(cffIndex[iTopDict]).buffer); const topDict = parseCFFTopDict(topDictData, strings); topDict._subrs = []; topDict._subrsBias = 0; topDict._defaultWidthX = 0; topDict._nominalWidthX = 0; const privateSize = topDict.private[0]; const privateOffset = topDict.private[1]; if (privateSize !== 0 && privateOffset !== 0) { const privateDict = parseCFFPrivateDict(data, privateOffset + start, privateSize, strings); topDict._defaultWidthX = privateDict.defaultWidthX; topDict._nominalWidthX = privateDict.nominalWidthX; if (privateDict.subrs !== 0) { const subrOffset = privateOffset + privateDict.subrs; const subrIndex = parseCFFIndex(data, subrOffset + start); topDict._subrs = subrIndex.objects; topDict._subrsBias = calcCFFSubroutineBias(topDict._subrs); } topDict._privateDict = privateDict; } topDictArray.push(topDict); } return topDictArray; } // Parse the CFF charset table, which contains internal names for all the glyphs. // This function will return a list of glyph names. // See Adobe TN #5176 chapter 13, "Charsets". function parseCFFCharset(data, start, nGlyphs, strings) { let sid; let count; const parser = new parse.Parser(data, start); // The .notdef glyph is not included, so subtract 1. nGlyphs -= 1; const charset = ['.notdef']; const format = parser.parseCard8(); if (format === 0) { for (let i = 0; i < nGlyphs; i += 1) { sid = parser.parseSID(); charset.push(getCFFString(strings, sid)); } } else if (format === 1) { while (charset.length <= nGlyphs) { sid = parser.parseSID(); count = parser.parseCard8(); for (let i = 0; i <= count; i += 1) { charset.push(getCFFString(strings, sid)); sid += 1; } } } else if (format === 2) { while (charset.length <= nGlyphs) { sid = parser.parseSID(); count = parser.parseCard16(); for (let i = 0; i <= count; i += 1) { charset.push(getCFFString(strings, sid)); sid += 1; } } } else { throw new Error('Unknown charset format ' + format); } return charset; } // Parse the CFF encoding data. Only one encoding can be specified per font. // See Adobe TN #5176 chapter 12, "Encodings". function parseCFFEncoding(data, start, charset) { let code; const enc = {}; const parser = new parse.Parser(data, start); const format = parser.parseCard8(); if (format === 0) { const nCodes = parser.parseCard8(); for (let i = 0; i < nCodes; i += 1) { code = parser.parseCard8(); enc[code] = i; } } else if (format === 1) { const nRanges = parser.parseCard8(); code = 1; for (let i = 0; i < nRanges; i += 1) { const first = parser.parseCard8(); const nLeft = parser.parseCard8(); for (let j = first; j <= first + nLeft; j += 1) { enc[j] = code; code += 1; } } } else { throw new Error('Unknown encoding format ' + format); } return new CffEncoding(enc, charset); } // Take in charstring code and return a Glyph object. // The encoding is described in the Type 2 Charstring Format // https://www.microsoft.com/typography/OTSPEC/charstr2.htm function parseCFFCharstring(font, glyph, code) { let c1x; let c1y; let c2x; let c2y; const p = new Path(); const stack = []; let nStems = 0; let haveWidth = false; let open = false; let x = 0; let y = 0; let subrs; let subrsBias; let defaultWidthX; let nominalWidthX; if (font.isCIDFont) { const fdIndex = font.tables.cff.topDict._fdSelect[glyph.index]; const fdDict = font.tables.cff.topDict._fdArray[fdIndex]; subrs = fdDict._subrs; subrsBias = fdDict._subrsBias; defaultWidthX = fdDict._defaultWidthX; nominalWidthX = fdDict._nominalWidthX; } else { subrs = font.tables.cff.topDict._subrs; subrsBias = font.tables.cff.topDict._subrsBias; defaultWidthX = font.tables.cff.topDict._defaultWidthX; nominalWidthX = font.tables.cff.topDict._nominalWidthX; } let width = defaultWidthX; function newContour(x, y) { if (open) { p.closePath(); } p.moveTo(x, y); open = true; } function parseStems() { let hasWidthArg; // The number of stem operators on the stack is always even. // If the value is uneven, that means a width is specified. hasWidthArg = stack.length % 2 !== 0; if (hasWidthArg && !haveWidth) { width = stack.shift() + nominalWidthX; } nStems += stack.length >> 1; stack.length = 0; haveWidth = true; } function parse(code) { let b1; let b2; let b3; let b4; let codeIndex; let subrCode; let jpx; let jpy; let c3x; let c3y; let c4x; let c4y; let i = 0; while (i < code.length) { let v = code[i]; i += 1; switch (v) { case 1: // hstem parseStems(); break; case 3: // vstem parseStems(); break; case 4: // vmoveto if (stack.length > 1 && !haveWidth) { width = stack.shift() + nominalWidthX; haveWidth = true; } y += stack.pop(); newContour(x, y); break; case 5: // rlineto while (stack.length > 0) { x += stack.shift(); y += stack.shift(); p.lineTo(x, y); } break; case 6: // hlineto while (stack.length > 0) { x += stack.shift(); p.lineTo(x, y); if (stack.length === 0) { break; } y += stack.shift(); p.lineTo(x, y); } break; case 7: // vlineto while (stack.length > 0) { y += stack.shift(); p.lineTo(x, y); if (stack.length === 0) { break; } x += stack.shift(); p.lineTo(x, y); } break; case 8: // rrcurveto while (stack.length > 0) { c1x = x + stack.shift(); c1y = y + stack.shift(); c2x = c1x + stack.shift(); c2y = c1y + stack.shift(); x = c2x + stack.shift(); y = c2y + stack.shift(); p.curveTo(c1x, c1y, c2x, c2y, x, y); } break; case 10: // callsubr codeIndex = stack.pop() + subrsBias; subrCode = subrs[codeIndex]; if (subrCode) { parse(subrCode); } break; case 11: // return return; case 12: // flex operators v = code[i]; i += 1; switch (v) { case 35: // flex // |- dx1 dy1 dx2 dy2 dx3 dy3 dx4 dy4 dx5 dy5 dx6 dy6 fd flex (12 35) |- c1x = x + stack.shift(); // dx1 c1y = y + stack.shift(); // dy1 c2x = c1x + stack.shift(); // dx2 c2y = c1y + stack.shift(); // dy2 jpx = c2x + stack.shift(); // dx3 jpy = c2y + stack.shift(); // dy3 c3x = jpx + stack.shift(); // dx4 c3y = jpy + stack.shift(); // dy4 c4x = c3x + stack.shift(); // dx5 c4y = c3y + stack.shift(); // dy5 x = c4x + stack.shift(); // dx6 y = c4y + stack.shift(); // dy6 stack.shift(); // flex depth p.curveTo(c1x, c1y, c2x, c2y, jpx, jpy); p.curveTo(c3x, c3y, c4x, c4y, x, y); break; case 34: // hflex // |- dx1 dx2 dy2 dx3 dx4 dx5 dx6 hflex (12 34) |- c1x = x + stack.shift(); // dx1 c1y = y; // dy1 c2x = c1x + stack.shift(); // dx2 c2y = c1y + stack.shift(); // dy2 jpx = c2x + stack.shift(); // dx3 jpy = c2y; // dy3 c3x = jpx + stack.shift(); // dx4 c3y = c2y; // dy4 c4x = c3x + stack.shift(); // dx5 c4y = y; // dy5 x = c4x + stack.shift(); // dx6 p.curveTo(c1x, c1y, c2x, c2y, jpx, jpy); p.curveTo(c3x, c3y, c4x, c4y, x, y); break; case 36: // hflex1 // |- dx1 dy1 dx2 dy2 dx3 dx4 dx5 dy5 dx6 hflex1 (12 36) |- c1x = x + stack.shift(); // dx1 c1y = y + stack.shift(); // dy1 c2x = c1x + stack.shift(); // dx2 c2y = c1y + stack.shift(); // dy2 jpx = c2x + stack.shift(); // dx3 jpy = c2y; // dy3 c3x = jpx + stack.shift(); // dx4 c3y = c2y; // dy4 c4x = c3x + stack.shift(); // dx5 c4y = c3y + stack.shift(); // dy5 x = c4x + stack.shift(); // dx6 p.curveTo(c1x, c1y, c2x, c2y, jpx, jpy); p.curveTo(c3x, c3y, c4x, c4y, x, y); break; case 37: // flex1 // |- dx1 dy1 dx2 dy2 dx3 dy3 dx4 dy4 dx5 dy5 d6 flex1 (12 37) |- c1x = x + stack.shift(); // dx1 c1y = y + stack.shift(); // dy1 c2x = c1x + stack.shift(); // dx2 c2y = c1y + stack.shift(); // dy2 jpx = c2x + stack.shift(); // dx3 jpy = c2y + stack.shift(); // dy3 c3x = jpx + stack.shift(); // dx4 c3y = jpy + stack.shift(); // dy4 c4x = c3x + stack.shift(); // dx5 c4y = c3y + stack.shift(); // dy5 if (Math.abs(c4x - x) > Math.abs(c4y - y)) { x = c4x + stack.shift(); } else { y = c4y + stack.shift(); } p.curveTo(c1x, c1y, c2x, c2y, jpx, jpy); p.curveTo(c3x, c3y, c4x, c4y, x, y); break; default: console.log('Glyph ' + glyph.index + ': unknown operator ' + 1200 + v); stack.length = 0; } break; case 14: // endchar if (stack.length > 0 && !haveWidth) { width = stack.shift() + nominalWidthX; haveWidth = true; } if (open) { p.closePath(); open = false; } break; case 18: // hstemhm parseStems(); break; case 19: // hintmask case 20: // cntrmask parseStems(); i += (nStems + 7) >> 3; break; case 21: // rmoveto if (stack.length > 2 && !haveWidth) { width = stack.shift() + nominalWidthX; haveWidth = true; } y += stack.pop(); x += stack.pop(); newContour(x, y); break; case 22: // hmoveto if (stack.length > 1 && !haveWidth) { width = stack.shift() + nominalWidthX; haveWidth = true; } x += stack.pop(); newContour(x, y); break; case 23: // vstemhm parseStems(); break; case 24: // rcurveline while (stack.length > 2) { c1x = x + stack.shift(); c1y = y + stack.shift(); c2x = c1x + stack.shift(); c2y = c1y + stack.shift(); x = c2x + stack.shift(); y = c2y + stack.shift(); p.curveTo(c1x, c1y, c2x, c2y, x, y); } x += stack.shift(); y += stack.shift(); p.lineTo(x, y); break; case 25: // rlinecurve while (stack.length > 6) { x += stack.shift(); y += stack.shift(); p.lineTo(x, y); } c1x = x + stack.shift(); c1y = y + stack.shift(); c2x = c1x + stack.shift(); c2y = c1y + stack.shift(); x = c2x + stack.shift(); y = c2y + stack.shift(); p.curveTo(c1x, c1y, c2x, c2y, x, y); break; case 26: // vvcurveto if (stack.length % 2) { x += stack.shift(); } while (stack.length > 0) { c1x = x; c1y = y + stack.shift(); c2x = c1x + stack.shift(); c2y = c1y + stack.shift(); x = c2x; y = c2y + stack.shift(); p.curveTo(c1x, c1y, c2x, c2y, x, y); } break; case 27: // hhcurveto if (stack.length % 2) { y += stack.shift(); } while (stack.length > 0) { c1x = x + stack.shift(); c1y = y; c2x = c1x + stack.shift(); c2y = c1y + stack.shift(); x = c2x + stack.shift(); y = c2y; p.curveTo(c1x, c1y, c2x, c2y, x, y); } break; case 28: // shortint b1 = code[i]; b2 = code[i + 1]; stack.push(((b1 << 24) | (b2 << 16)) >> 16); i += 2; break; case 29: // callgsubr codeIndex = stack.pop() + font.gsubrsBias; subrCode = font.gsubrs[codeIndex]; if (subrCode) { parse(subrCode); } break; case 30: // vhcurveto while (stack.length > 0) { c1x = x; c1y = y + stack.shift(); c2x = c1x + stack.shift(); c2y = c1y + stack.shift(); x = c2x + stack.shift(); y = c2y + (stack.length === 1 ? stack.shift() : 0); p.curveTo(c1x, c1y, c2x, c2y, x, y); if (stack.length === 0) { break; } c1x = x + stack.shift(); c1y = y; c2x = c1x + stack.shift(); c2y = c1y + stack.shift(); y = c2y + stack.shift(); x = c2x + (stack.length === 1 ? stack.shift() : 0); p.curveTo(c1x, c1y, c2x, c2y, x, y); } break; case 31: // hvcurveto while (stack.length > 0) { c1x = x + stack.shift(); c1y = y; c2x = c1x + stack.shift(); c2y = c1y + stack.shift(); y = c2y + stack.shift(); x = c2x + (stack.length === 1 ? stack.shift() : 0); p.curveTo(c1x, c1y, c2x, c2y, x, y); if (stack.length === 0) { break; } c1x = x; c1y = y + stack.shift(); c2x = c1x + stack.shift(); c2y = c1y + stack.shift(); x = c2x + stack.shift(); y = c2y + (stack.length === 1 ? stack.shift() : 0); p.curveTo(c1x, c1y, c2x, c2y, x, y); } break; default: if (v < 32) { console.log('Glyph ' + glyph.index + ': unknown operator ' + v); } else if (v < 247) { stack.push(v - 139); } else if (v < 251) { b1 = code[i]; i += 1; stack.push((v - 247) * 256 + b1 + 108); } else if (v < 255) { b1 = code[i]; i += 1; stack.push(-(v - 251) * 256 - b1 - 108); } else { b1 = code[i]; b2 = code[i + 1]; b3 = code[i + 2]; b4 = code[i + 3]; i += 4; stack.push(((b1 << 24) | (b2 << 16) | (b3 << 8) | b4) / 65536); } } } } parse(code); glyph.advanceWidth = width; return p; } function parseCFFFDSelect(data, start, nGlyphs, fdArrayCount) { const fdSelect = []; let fdIndex; const parser = new parse.Parser(data, start); const format = parser.parseCard8(); if (format === 0) { // Simple list of nGlyphs elements for (let iGid = 0; iGid < nGlyphs; iGid++) { fdIndex = parser.parseCard8(); if (fdIndex >= fdArrayCount) { throw new Error('CFF table CID Font FDSelect has bad FD index value ' + fdIndex + ' (FD count ' + fdArrayCount + ')'); } fdSelect.push(fdIndex); } } else if (format === 3) { // Ranges const nRanges = parser.parseCard16(); let first = parser.parseCard16(); if (first !== 0) { throw new Error('CFF Table CID Font FDSelect format 3 range has bad initial GID ' + first); } let next; for (let iRange = 0; iRange < nRanges; iRange++) { fdIndex = parser.parseCard8(); next = parser.parseCard16(); if (fdIndex >= fdArrayCount) { throw new Error('CFF table CID Font FDSelect has bad FD index value ' + fdIndex + ' (FD count ' + fdArrayCount + ')'); } if (next > nGlyphs) { throw new Error('CFF Table CID Font FDSelect format 3 range has bad GID ' + next); } for (; first < next; first++) { fdSelect.push(fdIndex); } first = next; } if (next !== nGlyphs) { throw new Error('CFF Table CID Font FDSelect format 3 range has bad final GID ' + next); } } else { throw new Error('CFF Table CID Font FDSelect table has unsupported format ' + format); } return fdSelect; } // Parse the `CFF` table, which contains the glyph outlines in PostScript format. function parseCFFTable(data, start, font, opt) { font.tables.cff = {}; const header = parseCFFHeader(data, start); const nameIndex = parseCFFIndex(data, header.endOffset, parse.bytesToString); const topDictIndex = parseCFFIndex(data, nameIndex.endOffset); const stringIndex = parseCFFIndex(data, topDictIndex.endOffset, parse.bytesToString); const globalSubrIndex = parseCFFIndex(data, stringIndex.endOffset); font.gsubrs = globalSubrIndex.objects; font.gsubrsBias = calcCFFSubroutineBias(font.gsubrs); const topDictArray = gatherCFFTopDicts(data, start, topDictIndex.objects, stringIndex.objects); if (topDictArray.length !== 1) { throw new Error('CFF table has too many fonts in \'FontSet\' - count of fonts NameIndex.length = ' + topDictArray.length); } const topDict = topDictArray[0]; font.tables.cff.topDict = topDict; if (topDict._privateDict) { font.defaultWidthX = topDict._privateDict.defaultWidthX; font.nominalWidthX = topDict._privateDict.nominalWidthX; } if (topDict.ros[0] !== undefined && topDict.ros[1] !== undefined) { font.isCIDFont = true; } if (font.isCIDFont) { let fdArrayOffset = topDict.fdArray; let fdSelectOffset = topDict.fdSelect; if (fdArrayOffset === 0 || fdSelectOffset === 0) { throw new Error('Font is marked as a CID font, but FDArray and/or FDSelect information is missing'); } fdArrayOffset += start; const fdArrayIndex = parseCFFIndex(data, fdArrayOffset); const fdArray = gatherCFFTopDicts(data, start, fdArrayIndex.objects, stringIndex.objects); topDict._fdArray = fdArray; fdSelectOffset += start; topDict._fdSelect = parseCFFFDSelect(data, fdSelectOffset, font.numGlyphs, fdArray.length); } const privateDictOffset = start + topDict.private[1]; const privateDict = parseCFFPrivateDict(data, privateDictOffset, topDict.private[0], stringIndex.objects); font.defaultWidthX = privateDict.defaultWidthX; font.nominalWidthX = privateDict.nominalWidthX; if (privateDict.subrs !== 0) { const subrOffset = privateDictOffset + privateDict.subrs; const subrIndex = parseCFFIndex(data, subrOffset); font.subrs = subrIndex.objects; font.subrsBias = calcCFFSubroutineBias(font.subrs); } else { font.subrs = []; font.subrsBias = 0; } // Offsets in the top dict are relative to the beginning of the CFF data, so add the CFF start offset. let charStringsIndex; if (opt.lowMemory) { charStringsIndex = parseCFFIndexLowMemory(data, start + topDict.charStrings); font.nGlyphs = charStringsIndex.offsets.length; } else { charStringsIndex = parseCFFIndex(data, start + topDict.charStrings); font.nGlyphs = charStringsIndex.objects.length; } const charset = parseCFFCharset(data, start + topDict.charset, font.nGlyphs, stringIndex.objects); if (topDict.encoding === 0) { // Standard encoding font.cffEncoding = new CffEncoding(cffStandardEncoding, charset); } else if (topDict.encoding === 1) { // Expert encoding font.cffEncoding = new CffEncoding(cffExpertEncoding, charset); } else { font.cffEncoding = parseCFFEncoding(data, start + topDict.encoding, charset); } // Prefer the CMAP encoding to the CFF encoding. font.encoding = font.encoding || font.cffEncoding; font.glyphs = new glyphset.GlyphSet(font); if (opt.lowMemory) { font._push = function(i) { const charString = getCffIndexObject(i, charStringsIndex.offsets, data, start + topDict.charStrings); font.glyphs.push(i, glyphset.cffGlyphLoader(font, i, parseCFFCharstring, charString)); }; } else { for (let i = 0; i < font.nGlyphs; i += 1) { const charString = charStringsIndex.objects[i]; font.glyphs.push(i, glyphset.cffGlyphLoader(font, i, parseCFFCharstring, charString)); } } } // Convert a string to a String ID (SID). // The list of strings is modified in place. function encodeString(s, strings) { let sid; // Is the string in the CFF standard strings? let i = cffStandardStrings.indexOf(s); if (i >= 0) { sid = i; } // Is the string already in the string index? i = strings.indexOf(s); if (i >= 0) { sid = i + cffStandardStrings.length; } else { sid = cffStandardStrings.length + strings.length; strings.push(s); } return sid; } function makeHeader() { return new table.Record('Header', [ {name: 'major', type: 'Card8', value: 1}, {name: 'minor', type: 'Card8', value: 0}, {name: 'hdrSize', type: 'Card8', value: 4}, {name: 'major', type: 'Card8', value: 1} ]); } function makeNameIndex(fontNames) { const t = new table.Record('Name INDEX', [ {name: 'names', type: 'INDEX', value: []} ]); t.names = []; for (let i = 0; i < fontNames.length; i += 1) { t.names.push({name: 'name_' + i, type: 'NAME', value: fontNames[i]}); } return t; } // Given a dictionary's metadata, create a DICT structure. function makeDict(meta, attrs, strings) { const m = {}; for (let i = 0; i < meta.length; i += 1) { const entry = meta[i]; let value = attrs[entry.name]; if (value !== undefined && !equals(value, entry.value)) { if (entry.type === 'SID') { value = encodeString(value, strings); } m[entry.op] = {name: entry.name, type: entry.type, value: value}; } } return m; } // The Top DICT houses the global font attributes. function makeTopDict(attrs, strings) { const t = new table.Record('Top DICT', [ {name: 'dict', type: 'DICT', value: {}} ]); t.dict = makeDict(TOP_DICT_META, attrs, strings); return t; } function makeTopDictIndex(topDict) { const t = new table.Record('Top DICT INDEX', [ {name: 'topDicts', type: 'INDEX', value: []} ]); t.topDicts = [{name: 'topDict_0', type: 'TABLE', value: topDict}]; return t; } function makeStringIndex(strings) { const t = new table.Record('String INDEX', [ {name: 'strings', type: 'INDEX', value: []} ]); t.strings = []; for (let i = 0; i < strings.length; i += 1) { t.strings.push({name: 'string_' + i, type: 'STRING', value: strings[i]}); } return t; } function makeGlobalSubrIndex() { // Currently we don't use subroutines. return new table.Record('Global Subr INDEX', [ {name: 'subrs', type: 'INDEX', value: []} ]); } function makeCharsets(glyphNames, strings) { const t = new table.Record('Charsets', [ {name: 'format', type: 'Card8', value: 0} ]); for (let i = 0; i < glyphNames.length; i += 1) { const glyphName = glyphNames[i]; const glyphSID = encodeString(glyphName, strings); t.fields.push({name: 'glyph_' + i, type: 'SID', value: glyphSID}); } return t; } function glyphToOps(glyph) { const ops = []; const path = glyph.path; ops.push({name: 'width', type: 'NUMBER', value: glyph.advanceWidth}); let x = 0; let y = 0; for (let i = 0; i < path.commands.length; i += 1) { let dx; let dy; let cmd = path.commands[i]; if (cmd.type === 'Q') { // CFF only supports bézier curves, so convert the quad to a bézier. const _13 = 1 / 3; const _23 = 2 / 3; // We're going to create a new command so we don't change the original path. // Since all coordinates are relative, we round() them ASAP to avoid propagating errors. cmd = { type: 'C', x: cmd.x, y: cmd.y, x1: Math.round(_13 * x + _23 * cmd.x1), y1: Math.round(_13 * y + _23 * cmd.y1), x2: Math.round(_13 * cmd.x + _23 * cmd.x1), y2: Math.round(_13 * cmd.y + _23 * cmd.y1) }; } if (cmd.type === 'M') { dx = Math.round(cmd.x - x); dy = Math.round(cmd.y - y); ops.push({name: 'dx', type: 'NUMBER', value: dx}); ops.push({name: 'dy', type: 'NUMBER', value: dy}); ops.push({name: 'rmoveto', type: 'OP', value: 21}); x = Math.round(cmd.x); y = Math.round(cmd.y); } else if (cmd.type === 'L') { dx = Math.round(cmd.x - x); dy = Math.round(cmd.y - y); ops.push({name: 'dx', type: 'NUMBER', value: dx}); ops.push({name: 'dy', type: 'NUMBER', value: dy}); ops.push({name: 'rlineto', type: 'OP', value: 5}); x = Math.round(cmd.x); y = Math.round(cmd.y); } else if (cmd.type === 'C') { const dx1 = Math.round(cmd.x1 - x); const dy1 = Math.round(cmd.y1 - y); const dx2 = Math.round(cmd.x2 - cmd.x1); const dy2 = Math.round(cmd.y2 - cmd.y1); dx = Math.round(cmd.x - cmd.x2); dy = Math.round(cmd.y - cmd.y2); ops.push({name: 'dx1', type: 'NUMBER', value: dx1}); ops.push({name: 'dy1', type: 'NUMBER', value: dy1}); ops.push({name: 'dx2', type: 'NUMBER', value: dx2}); ops.push({name: 'dy2', type: 'NUMBER', value: dy2}); ops.push({name: 'dx', type: 'NUMBER', value: dx}); ops.push({name: 'dy', type: 'NUMBER', value: dy}); ops.push({name: 'rrcurveto', type: 'OP', value: 8}); x = Math.round(cmd.x); y = Math.round(cmd.y); } // Contours are closed automatically. } ops.push({name: 'endchar', type: 'OP', value: 14}); return ops; } function makeCharStringsIndex(glyphs) { const t = new table.Record('CharStrings INDEX', [ {name: 'charStrings', type: 'INDEX', value: []} ]); for (let i = 0; i < glyphs.length; i += 1) { const glyph = glyphs.get(i); const ops = glyphToOps(glyph); t.charStrings.push({name: glyph.name, type: 'CHARSTRING', value: ops}); } return t; } function makePrivateDict(attrs, strings) { const t = new table.Record('Private DICT', [ {name: 'dict', type: 'DICT', value: {}} ]); t.dict = makeDict(PRIVATE_DICT_META, attrs, strings); return t; } function makeCFFTable(glyphs, options) { const t = new table.Table('CFF ', [ {name: 'header', type: 'RECORD'}, {name: 'nameIndex', type: 'RECORD'}, {name: 'topDictIndex', type: 'RECORD'}, {name: 'stringIndex', type: 'RECORD'}, {name: 'globalSubrIndex', type: 'RECORD'}, {name: 'charsets', type: 'RECORD'}, {name: 'charStringsIndex', type: 'RECORD'}, {name: 'privateDict', type: 'RECORD'} ]); const fontScale = 1 / options.unitsPerEm; // We use non-zero values for the offsets so that the DICT encodes them. // This is important because the size of the Top DICT plays a role in offset calculation, // and the size shouldn't change after we've written correct offsets. const attrs = { version: options.version, fullName: options.fullName, familyName: options.familyName, weight: options.weightName, fontBBox: options.fontBBox || [0, 0, 0, 0], fontMatrix: [fontScale, 0, 0, fontScale, 0, 0], charset: 999, encoding: 0, charStrings: 999, private: [0, 999] }; const privateAttrs = {}; const glyphNames = []; let glyph; // Skip first glyph (.notdef) for (let i = 1; i < glyphs.length; i += 1) { glyph = glyphs.get(i); glyphNames.push(glyph.name); } const strings = []; t.header = makeHeader(); t.nameIndex = makeNameIndex([options.postScriptName]); let topDict = makeTopDict(attrs, strings); t.topDictIndex = makeTopDictIndex(topDict); t.globalSubrIndex = makeGlobalSubrIndex(); t.charsets = makeCharsets(glyphNames, strings); t.charStringsIndex = makeCharStringsIndex(glyphs); t.privateDict = makePrivateDict(privateAttrs, strings); // Needs to come at the end, to encode all custom strings used in the font. t.stringIndex = makeStringIndex(strings); const startOffset = t.header.sizeOf() + t.nameIndex.sizeOf() + t.topDictIndex.sizeOf() + t.stringIndex.sizeOf() + t.globalSubrIndex.sizeOf(); attrs.charset = startOffset; // We use the CFF standard encoding; proper encoding will be handled in cmap. attrs.encoding = 0; attrs.charStrings = attrs.charset + t.charsets.sizeOf(); attrs.private[1] = attrs.charStrings + t.charStringsIndex.sizeOf(); // Recreate the Top DICT INDEX with the correct offsets. topDict = makeTopDict(attrs, strings); t.topDictIndex = makeTopDictIndex(topDict); return t; } export default { parse: parseCFFTable, make: makeCFFTable };