Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add read / write support for CFF private DICT data #759

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 49 additions & 18 deletions src/tables/cff.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,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 @@ -390,31 +401,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 @@ -490,7 +502,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 @@ -1281,13 +1293,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 @@ -1415,6 +1427,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 @@ -1560,10 +1586,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 @@ -1607,7 +1637,7 @@ function makeCFFTable(glyphs, options) {
attrs.strokeWidth = topDictOptions.strokeWidth || 0;
}

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

const glyphNames = [];
let glyph;
Expand All @@ -1627,7 +1657,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 @@ -1643,6 +1673,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 @@ -215,4 +215,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);
});
});