diff --git a/src/tables/cff.mjs b/src/tables/cff.mjs index 934589a7..1492a395 100755 --- a/src/tables/cff.mjs +++ b/src/tables/cff.mjs @@ -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; } } @@ -390,16 +401,8 @@ 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}, @@ -407,14 +410,23 @@ const PRIVATE_DICT_META_CFF2 = [ {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]} @@ -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; @@ -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; @@ -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}; } @@ -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; } @@ -1607,7 +1637,7 @@ function makeCFFTable(glyphs, options) { attrs.strokeWidth = topDictOptions.strokeWidth || 0; } - const privateAttrs = {}; + const privateAttrs = topDictOptions._privateDict || {}; const glyphNames = []; let glyph; @@ -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); @@ -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); diff --git a/src/types.mjs b/src/types.mjs index 6c1b241a..7e2f51cf 100644 --- a/src/types.mjs +++ b/src/types.mjs @@ -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); @@ -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 diff --git a/test/tables/cff.spec.mjs b/test/tables/cff.spec.mjs index ab46f19e..34bada1a 100644 --- a/test/tables/cff.spec.mjs +++ b/test/tables/cff.spec.mjs @@ -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); @@ -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: + // + // + // + // + // + // + // + // + // + // + // + 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); + }); });