Skip to content
Open
67 changes: 49 additions & 18 deletions src/tables/cff.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,17 @@ function interpretDict(dict, meta, strings) {
if (m.type === 'SID') {
value = getCFFString(strings, value);
}
if (m.type === 'delta' && value !== null) {
if (!Array.isArray(value)) {
throw new Error('Read delta data invalid');
}
// Convert delta array to human readable version
let current = 0;
for(let i = 0; i < value.length; i++) {
value[i] = value[i] + current;
current = value[i];
}
}
newDict[m.name] = value;
}
}
Expand Down Expand Up @@ -394,31 +405,32 @@ const TOP_DICT_META_CFF2 = [
];

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}
];

// https://learn.microsoft.com/en-us/typography/opentype/spec/cff2#table-16-private-dict-operators
const PRIVATE_DICT_META_CFF2 = [
{name: 'blueValues', op: 6, type: 'delta'},
{name: 'otherBlues', op: 7, type: 'delta'},
{name: 'familyBlues', op: 7, type: 'delta'},
{name: 'familyBlues', op: 8, type: 'delta'},
{name: 'familyOtherBlues', op: 9, type: 'delta'},
{name: 'blueScale', op: 1209, type: 'number', value: 0.039625},
{name: 'blueShift', op: 1210, type: 'number', value: 7},
{name: 'blueFuzz', op: 1211, type: 'number', value: 1},
{name: 'stdHW', op: 10, type: 'number'},
{name: 'stdVW', op: 11, type: 'number'},
{name: 'stemSnapH', op: 1212, type: 'number'},
{name: 'stemSnapV', op: 1213, type: 'number'},
{name: 'stemSnapH', op: 1212, type: 'delta'},
{name: 'stemSnapV', op: 1213, type: 'delta'},
{name: 'languageGroup', op: 1217, type: 'number', value: 0},
{name: 'expansionFactor', op: 1218, type: 'number', value: 0.06},
{name: 'vsindex', op: 22, type: 'number', value: 0},
{name: 'subrs', op: 19, type: 'offset'},
{name: 'defaultWidthX', op: 20, type: 'number', value: 0},
{name: 'nominalWidthX', op: 21, type: 'number', value: 0},
{name: 'subrs', op: 19, type: 'offset', value: 0},
{name: 'defaultWidthX', op: 20, type: 'number', value: 0},
{name: 'nominalWidthX', op: 21, type: 'number', value: 0}
];

// https://learn.microsoft.com/en-us/typography/opentype/spec/cff2#table-16-private-dict-operators
const PRIVATE_DICT_META_CFF2 = PRIVATE_DICT_META.concat([
{name: 'vsindex', op: 22, type: 'number', value: 0},
]);

// https://learn.microsoft.com/en-us/typography/opentype/spec/cff2#table-10-font-dict-operator-entries
const FONT_DICT_META = [
{name: 'private', op: 18, type: ['number', 'offset'], value: [0, 0]}
Expand Down Expand Up @@ -494,7 +506,7 @@ function gatherCFFTopDicts(data, start, cffIndex, strings, version) {
const privateDict = parseCFFPrivateDict(data, privateOffset + start, privateSize, strings, version);
topDict._defaultWidthX = privateDict.defaultWidthX;
topDict._nominalWidthX = privateDict.nominalWidthX;
if (privateDict.subrs !== 0) {
if (privateDict.subrs !== null && privateDict.subrs !== 0) {
const subrOffset = privateOffset + privateDict.subrs;
const subrIndex = parseCFFIndex(data, subrOffset + start, undefined, version);
topDict._subrs = subrIndex.objects;
Expand Down Expand Up @@ -1298,13 +1310,13 @@ function parseCFFTable(data, start, font, opt) {
topDict._fdSelect = parseCFFFDSelect(data, fdSelectOffset, font.numGlyphs, fdArray.length, header.formatMajor);
}

if (header.formatMajor < 2) {
if (header.formatMajor < 2 && topDict.private[0] !== 0) {
const privateDictOffset = start + topDict.private[1];
const privateDict = parseCFFPrivateDict(data, privateDictOffset, topDict.private[0], stringIndex.objects, header.formatMajor);
font.defaultWidthX = privateDict.defaultWidthX;
font.nominalWidthX = privateDict.nominalWidthX;

if (privateDict.subrs !== 0) {
if (privateDict.subrs !== null && privateDict.subrs !== 0) {
const subrOffset = privateDictOffset + privateDict.subrs;
const subrIndex = parseCFFIndex(data, subrOffset);
font.subrs = subrIndex.objects;
Expand Down Expand Up @@ -1432,6 +1444,20 @@ function makeDict(meta, attrs, strings) {
if (entry.type === 'SID') {
value = encodeString(value, strings);
}
if (entry.type === 'delta' && value !== null) {
if (!Array.isArray(value)) {
throw new Error('Provided delta data invalid');
}
// Convert human readable delta array to DICT version
// See https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf
// Private DICT data > Table 6 Operand Types > delta
let current = 0;
for(let i = 0; i < value.length; i++) {
let nextcurrent = value[i];
value[i] = value[i] - current;
current = nextcurrent;
}
}

m[entry.op] = {name: entry.name, type: entry.type, value: value};
}
Expand Down Expand Up @@ -1577,10 +1603,14 @@ function makeCharStringsIndex(glyphs, version) {
}

function makePrivateDict(attrs, strings, version) {
// we do not handle (include) subrs, so we must not create the operator
if ('subrs' in attrs) {
attrs = new Object(attrs);
delete attrs['subrs'];
}
const t = new table.Record('Private DICT', [
{name: 'dict', type: 'DICT', value: {}}
{name: 'dict', type: 'DICT', value: makeDict(version > 1 ? PRIVATE_DICT_META_CFF2 : PRIVATE_DICT_META, attrs, strings)}
]);
t.dict = makeDict(version > 1 ? PRIVATE_DICT_META_CFF2 : PRIVATE_DICT_META, attrs, strings);
return t;
}

Expand Down Expand Up @@ -1624,7 +1654,7 @@ function makeCFFTable(glyphs, options) {
attrs.strokeWidth = topDictOptions.strokeWidth || 0;
}

const privateAttrs = {};
const privateAttrs = topDictOptions._privateDict || {};

const glyphNames = [];
let glyph;
Expand All @@ -1644,7 +1674,7 @@ function makeCFFTable(glyphs, options) {
t.globalSubrIndex = makeGlobalSubrIndex();
t.charsets = makeCharsets(glyphNames, strings);
t.charStringsIndex = makeCharStringsIndex(glyphs, cffVersion);
t.privateDict = makePrivateDict(privateAttrs, strings);
t.privateDict = makePrivateDict(privateAttrs, strings, cffVersion);

// Needs to come at the end, to encode all custom strings used in the font.
t.stringIndex = makeStringIndex(strings);
Expand All @@ -1660,6 +1690,7 @@ function makeCFFTable(glyphs, options) {
attrs.encoding = 0;
attrs.charStrings = attrs.charset + t.charsets.sizeOf();
attrs.private[1] = attrs.charStrings + t.charStringsIndex.sizeOf();
attrs.private[0] = t.privateDict.sizeOf();

// Recreate the Top DICT INDEX with the correct offsets.
topDict = makeTopDict(attrs, strings);
Expand Down
10 changes: 10 additions & 0 deletions src/types.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,9 @@ encode.DICT = function(m) {
// Object.keys() return string keys, but our keys are always numeric.
const k = parseInt(keys[i], 0);
const v = m[k];
if (v.value === null) {
continue;
}
// Value comes before the key.
const enc1 = encode.OPERAND(v.value, v.type);
const enc2 = encode.OPERATOR(k);
Expand Down Expand Up @@ -855,6 +858,13 @@ encode.OPERAND = function(v, type) {
for (let j = 0; j < enc1.length; j++) {
d.push(enc1[j]);
}
} else if (type === 'delta') {
for (let i = 0; i < v.length; i++) {
const enc1 = encode.NUMBER(v[i]);
for (let j = 0; j < enc1.length; j++) {
d.push(enc1[j]);
}
}
} else {
throw new Error('Unknown operand type ' + type);
// FIXME Add support for booleans
Expand Down
40 changes: 34 additions & 6 deletions test/tables/cff.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -93,17 +93,17 @@ describe('tables/cff.mjs', function () {
assert.equal(topDict.vstore, 16);
assert.equal(topDict.fdSelect, null);

assert.deepEqual(privateDict1.blueValues, [-20, 20, 472, 18, 35, 15, 105, 15, 10, 20, 40, 20]);
assert.deepEqual(privateDict1.otherBlues, [-250, 10]);
assert.deepEqual(privateDict1.familyBlues, [-20, 20, 473, 18, 34, 15, 104, 15, 10, 20, 40, 20]);
assert.deepEqual(privateDict1.familyOtherBlues, [ -249, 10 ]);
assert.deepEqual(privateDict1.blueValues, [-20, 0, 472, 490, 525, 540, 645, 660, 670, 690, 730, 750]);
assert.deepEqual(privateDict1.otherBlues, [-250, -240]);
assert.deepEqual(privateDict1.familyBlues, [-20, 0, 473, 491, 525, 540, 644, 659, 669, 689, 729, 749]);
assert.deepEqual(privateDict1.familyOtherBlues, [ -249, -239 ]);
assert.equal(privateDict1.blueScale, 0.0375);
assert.equal(privateDict1.blueShift, 7);
assert.equal(privateDict1.blueFuzz, 0);
assert.equal(privateDict1.stdHW, 55);
assert.equal(privateDict1.stdVW, 80);
assert.deepEqual(privateDict1.stemSnapH, [40, 15]);
assert.deepEqual(privateDict1.stemSnapV, [80, 10]);
assert.deepEqual(privateDict1.stemSnapH, [40, 55]);
assert.deepEqual(privateDict1.stemSnapV, [80, 90]);
assert.equal(privateDict1.languageGroup, 0);
assert.equal(privateDict1.expansionFactor, 0.06);
assert.deepEqual(privateDict1.vsindex, 0);
Expand Down Expand Up @@ -226,4 +226,32 @@ describe('tables/cff.mjs', function () {
transformedPoints
);
});

it('does round trip CFF private DICT', function() {
const font = loadSync('./test/fonts/AbrilFatface-Regular.otf');
const checkerFunktion = function(inputFont) {
// from ttx:
// <Private>
// <BlueValues value="-10 0 476 486 700 711"/>
// <OtherBlues value="-250 -238"/>
// <BlueScale value="0.039625"/>
// <BlueShift value="7"/>
// <BlueFuzz value="1"/>
// <StdHW value="18"/>
// <StdVW value="186"/>
// <StemSnapH value="16 18 21"/>
// <StemSnapV value="120 186 205"/>
// <ForceBold value="0"/>
const privateDict = inputFont.tables.cff.topDict._privateDict;
assert.deepEqual(privateDict.blueValues, [-10, 0, 476, 486, 700, 711]);
assert.deepEqual(privateDict.otherBlues, [-250, -238]);
assert.equal(privateDict.stdHW, 18);
assert.deepEqual(privateDict.stemSnapH, [16, 18, 21]);
assert.equal(privateDict.nominalWidthX, 590);
};
checkerFunktion(font);
let buffer = font.toArrayBuffer()
let font2 = parse(buffer);
checkerFunktion(font2);
});
});