1157 lines
39 KiB
Plaintext
1157 lines
39 KiB
Plaintext
|
// 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';
|
||
|
|
||
|
// 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)
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export default { parse: parseCFFTable };
|