From 415200def7f928c331648c362fd41423e2423856 Mon Sep 17 00:00:00 2001 From: Connum Date: Wed, 3 Apr 2024 13:00:52 +0200 Subject: [PATCH 1/2] fix fvar axis/instance name parsing and writing (fix #687) --- src/tables/fvar.js | 85 ++++++++++++++++++++++++----------------- src/tables/name.js | 23 ++++++++++- src/tables/sfnt.js | 20 +++------- test/fonts/VARTest.ttf | Bin 0 -> 2492 bytes test/tables/fvar.js | 84 +++++++++++++++++++++++++++++++--------- 5 files changed, 142 insertions(+), 70 deletions(-) create mode 100644 test/fonts/VARTest.ttf diff --git a/src/tables/fvar.js b/src/tables/fvar.js index 058d137f..ad9ec1cf 100644 --- a/src/tables/fvar.js +++ b/src/tables/fvar.js @@ -4,40 +4,16 @@ import check from '../check.js'; import parse from '../parse.js'; import table from '../table.js'; -import { objectsEqual } from '../util.js'; - -function addName(name, names) { - let nameID = 256; - for (let platform in names) { - for (let nameKey in names[platform]) { - let n = parseInt(nameKey); - if (!n || n < 256) { - continue; - } - - if (objectsEqual(names[platform][nameKey], name)) { - return n; - } - - if (nameID <= n) { - nameID = n + 1; - } - } - names[platform][nameID] = name; - } - - return nameID; -} +import { getNameByID } from './name.js'; -function makeFvarAxis(n, axis, names) { - const nameID = addName(axis.name, names); +function makeFvarAxis(n, axis) { return [ {name: 'tag_' + n, type: 'TAG', value: axis.tag}, {name: 'minValue_' + n, type: 'FIXED', value: axis.minValue << 16}, {name: 'defaultValue_' + n, type: 'FIXED', value: axis.defaultValue << 16}, {name: 'maxValue_' + n, type: 'FIXED', value: axis.maxValue << 16}, {name: 'flags_' + n, type: 'USHORT', value: 0}, - {name: 'nameID_' + n, type: 'USHORT', value: nameID} + {name: 'nameID_' + n, type: 'USHORT', value: axis.axisNameID} ]; } @@ -49,14 +25,15 @@ function parseFvarAxis(data, start, names) { axis.defaultValue = p.parseFixed(); axis.maxValue = p.parseFixed(); p.skip('uShort', 1); // reserved for flags; no values defined - axis.name = (names.macintosh || names.windows || names.unicode)[p.parseUShort()] || {}; + const axisNameID = p.parseUShort(); + axis.axisNameID = axisNameID; + axis.name = getNameByID(names, axisNameID); return axis; } -function makeFvarInstance(n, inst, axes, names) { - const nameID = addName(inst.name, names); +function makeFvarInstance(n, inst, axes, optionalFields = {}) { const fields = [ - {name: 'nameID_' + n, type: 'USHORT', value: nameID}, + {name: 'nameID_' + n, type: 'USHORT', value: inst.subfamilyNameID}, {name: 'flags_' + n, type: 'USHORT', value: 0} ]; @@ -69,13 +46,23 @@ function makeFvarInstance(n, inst, axes, names) { }); } + if (optionalFields && optionalFields.postScriptNameID) { + fields.push({ + name: 'postScriptNameID_', + type: 'USHORT', + value: inst.postScriptNameID !== undefined? inst.postScriptNameID : 0xFFFF + }); + } + return fields; } -function parseFvarInstance(data, start, axes, names) { +function parseFvarInstance(data, start, axes, names, instanceSize) { const inst = {}; const p = new parse.Parser(data, start); - inst.name = (names.macintosh || names.windows || names.unicode)[p.parseUShort()] || {}; + const subfamilyNameID = p.parseUShort(); + inst.subfamilyNameID = subfamilyNameID; + inst.name = getNameByID(names, subfamilyNameID, [2, 17]); p.skip('uShort', 1); // reserved for flags; no values defined inst.coordinates = {}; @@ -83,10 +70,21 @@ function parseFvarInstance(data, start, axes, names) { inst.coordinates[axes[i].tag] = p.parseFixed(); } + if (p.relativeOffset === instanceSize) { + inst.postScriptNameID = undefined; + inst.postScriptName = undefined; + return inst; + } + + const postScriptNameID = p.parseUShort(); + inst.postScriptNameID = postScriptNameID == 0xFFFF ? undefined : postScriptNameID; + inst.postScriptName = inst.postScriptNameID !== undefined ? getNameByID(names, postScriptNameID, [6]) : ''; + return inst; } function makeFvarTable(fvar, names) { + const result = new table.Table('fvar', [ {name: 'version', type: 'ULONG', value: 0x10000}, {name: 'offsetToData', type: 'USHORT', value: 0}, @@ -102,8 +100,25 @@ function makeFvarTable(fvar, names) { result.fields = result.fields.concat(makeFvarAxis(i, fvar.axes[i], names)); } + const optionalFields = {}; + + // first loop over instances: find out if at least one has postScriptNameID defined + for (let j = 0; j < fvar.instances.length; j++) { + if(fvar.instances[j].postScriptNameID !== undefined) { + result.instanceSize += 2; + optionalFields.postScriptNameID = true; + break; + } + } + + // second loop over instances: find out if at least one has postScriptNameID defined for (let j = 0; j < fvar.instances.length; j++) { - result.fields = result.fields.concat(makeFvarInstance(j, fvar.instances[j], fvar.axes, names)); + result.fields = result.fields.concat(makeFvarInstance( + j, + fvar.instances[j], + fvar.axes, + optionalFields + )); } return result; @@ -129,7 +144,7 @@ function parseFvarTable(data, start, names) { const instances = []; const instanceStart = start + offsetToData + axisCount * axisSize; for (let j = 0; j < instanceCount; j++) { - instances.push(parseFvarInstance(data, instanceStart + j * instanceSize, axes, names)); + instances.push(parseFvarInstance(data, instanceStart + j * instanceSize, axes, names, instanceSize)); } return {axes: axes, instances: instances}; diff --git a/src/tables/name.js b/src/tables/name.js index b722127b..a0d5aad4 100644 --- a/src/tables/name.js +++ b/src/tables/name.js @@ -6,7 +6,7 @@ import parse from '../parse.js'; import table from '../table.js'; // NameIDs for the name table. -const nameTableNames = [ +export const nameTableNames = [ 'copyright', // 0 'fontFamily', // 1 'fontSubfamily', // 2 @@ -854,4 +854,23 @@ function makeNameTable(names, ltag) { return t; } -export default { parse: parseNameTable, make: makeNameTable }; +export function getNameByID(names, nameID, allowedStandardIDs = []) { + if (nameID < 256 && nameID in nameTableNames) { + if (allowedStandardIDs.length && !allowedStandardIDs.includes(parseInt(nameID))) { + return undefined; + } + nameID = nameTableNames[nameID]; + } + + for (let platform in names) { + for (let nameKey in names[platform]) { + if(nameKey === nameID || parseInt(nameKey) === nameID) { + return names[platform][nameKey]; + } + } + } + + return undefined; +} + +export default { parse: parseNameTable, make: makeNameTable, getNameByID }; diff --git a/src/tables/sfnt.js b/src/tables/sfnt.js index 8abdb844..972e7c50 100644 --- a/src/tables/sfnt.js +++ b/src/tables/sfnt.js @@ -327,10 +327,6 @@ function fontToSfntTable(font) { names.windows.preferredSubfamily = fontNamesWindows.fontSubfamily || fontNamesUnicode.fontSubfamily || fontNamesMacintosh.fontSubfamily; } - // we have to handle fvar before name, because it may modify name IDs - const fvarTable = font.tables.fvar ? fvar.make(font.tables.fvar, font.names) : undefined; - const gaspTable = font.tables.gasp ? gasp.make(font.tables.gasp) : undefined; - const languageTags = []; const nameTable = _name.make(names, languageTags); const ltagTable = (languageTags.length > 0 ? ltag.make(languageTags) : undefined); @@ -361,18 +357,16 @@ function fontToSfntTable(font) { cpal, colr, stat, - avar + avar, + fvar, + gasp }; const optionalTableArgs = { - avar: [font.tables.fvar] + avar: [font.tables.fvar], + fvar: [font.names], }; - // fvar table is already handled above - if (fvarTable) { - tables.push(fvarTable); - } - for (let tableName in optionalTables) { const table = font.tables[tableName]; if (table) { @@ -384,10 +378,6 @@ function fontToSfntTable(font) { tables.push(metaTable); } - if (gaspTable) { - tables.push(gaspTable); - } - const sfntTable = makeSfntTable(tables); // Compute the font's checkSum and store it in head.checkSumAdjustment. diff --git a/test/fonts/VARTest.ttf b/test/fonts/VARTest.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e09fb897e776a4c6c306236218a8a3335ed38066 GIT binary patch literal 2492 zcmd5-U2IcT9RHnrZ`U%)ww85;fo69tY_iElOWO@PYzuVYgpH4F>iAe`$2zjH4F&=+ zhO%h2N*gi*>ql5fu^5yY?*pRra{7n0k;cIhQR)o9~SQefVpbkpBF^6uoo$f39gA5bs%|F(ZH9<$$_ zdjwEkB>(C_?9h2xHLh$M79l8(f~93tsxG`%}oF{VyTcMK1=)qU0A ztZN${J`$-+1ocIFRBv)uCM%;U(tM_EdY#S(3aXjgS>-dvUkn_y2fSr=r%Pp@>vih! zJ&l*v4c1?H+dTT-#oNU*6JuiYyH{tnRZ`U*Pn+?8|m~nm3#h$D0IB|wN++5p*T{!kg`-gb=F5~3e8oKt|&f?%NP}K<;-w&P( ztZg}8oJ4h|K{?AxVev2zqlr7%jxnNt4b{rCod1vSifnAzh#z7k9_mD<0b|S#^l{cW z&3p$n(3DN$O_*R&UYEI}B|D29@Zes)DQPvIZ*S?`h|8$JX|+P`LA;4$b_a2XwG8TH zlSs>zr5qU%XAN?v5_T!Cz*5ws5zpfYPG>TPq?VFp2qPvXneQp%1(5kX^Jyk#_$UHz zxce=P2ts&@dO?E zsqu0L59ex99{6ba#DDUiAGwU6lRNe?@+33aKsrJ000tiDD_jA*3)N< zR(*&w&p~>PF{|8nojJ+4AJkSGRpG`WEarcrnt3hZ-=daSEk&JCbspxtoH_duV68M_ z1+fkCZ?Xn!nNc(UD(mqyQGbRRZA2^Du!;L`;fb{~-cHu`bF8E;Y~#&R3J1#gPnw7M zyqOnZA?CuyJHEuI;u1zH>r_SI3%$t^eut2+@$EAQZtbM(SBh9E?|v zCwWK9_Kq=v`#s9Ok_bFx58<1P>i!6**6mF7baR* xh^CvhAw4Tl?w^rF9zw{Ik$yR-#@WexP7wuCV#p00DeW>ZOxYamj`nO6e*x^#iG~0G literal 0 HcmV?d00001 diff --git a/test/tables/fvar.js b/test/tables/fvar.js index 8f87fb03..0f8b4b77 100644 --- a/test/tables/fvar.js +++ b/test/tables/fvar.js @@ -1,8 +1,11 @@ import assert from 'assert'; import { hex, unhex } from '../testutil.js'; import fvar from '../../src/tables/fvar.js'; +import { Font, loadSync, parse } from '../../src/opentype.js'; describe('tables/fvar.js', function() { + const testFont = loadSync('./test/fonts/VARTest.ttf'); + const data = '00 01 00 00 00 10 00 02 00 02 00 14 00 02 00 0C ' + '77 67 68 74 00 64 00 00 01 90 00 00 03 84 00 00 00 00 01 01 ' + @@ -17,6 +20,7 @@ describe('tables/fvar.js', function() { minValue: 100, defaultValue: 400, maxValue: 900, + axisNameID: 257, name: {en: 'Weight', ja: 'ウエイト'} }, { @@ -24,33 +28,67 @@ describe('tables/fvar.js', function() { minValue: 50, defaultValue: 100, maxValue: 200, + axisNameID: 258, name: {en: 'Width', ja: '幅'} } ], instances: [ { name: {en: 'Regular', ja: 'レギュラー'}, + subfamilyNameID: 259, + postScriptName: undefined, + postScriptNameID: undefined, coordinates: {wght: 300, wdth: 100} }, { name: {en: 'Condensed', ja: 'コンデンス'}, + subfamilyNameID: 260, + postScriptName: undefined, + postScriptNameID: undefined, coordinates: {wght: 300, wdth: 75} } ] }; + const names = { + macintosh: { + 257: {en: 'Weight', ja: 'ウエイト'}, + 258: {en: 'Width', ja: '幅'}, + 259: {en: 'Regular', ja: 'レギュラー'}, + 260: {en: 'Condensed', ja: 'コンデンス'} + } + }; + it('can parse a font variations table', function() { - const names = { - macintosh: { - 257: {en: 'Weight', ja: 'ウエイト'}, - 258: {en: 'Width', ja: '幅'}, - 259: {en: 'Regular', ja: 'レギュラー'}, - 260: {en: 'Condensed', ja: 'コンデンス'} - } - }; assert.deepEqual(table, fvar.parse(unhex(data), 0, names)); }); + it('parses nameIDs 2 and 17 and postScriptNameID 6 correctly', function() { + assert.equal(testFont.tables.fvar.instances[0].name.en, 'Regular'); + assert.equal(testFont.tables.fvar.instances[0].subfamilyNameID, 2); + assert.equal(testFont.tables.fvar.instances[0].postScriptName.en, 'VARTestVF-Regular'); + assert.equal(testFont.tables.fvar.instances[0].postScriptNameID, 6); + assert.equal(testFont.tables.fvar.instances[0].postScriptName.en, 'VARTestVF-Regular'); + + const font = new Font({ + familyName: 'TestFont', + styleName: 'Medium', + unitsPerEm: 1000, + ascender: 800, + descender: -200, + glyphs: [] + }); + font.tables.fvar = JSON.parse(JSON.stringify(table)); + font.names.unicode.fontSubfamily = {en: 'Font Subfamily name'}; + font.names.unicode.preferredSubfamily = {en: 'Typographic Subfamily name'}; + font.tables.fvar.instances[0].subfamilyNameID = 2; + font.tables.fvar.instances[1].subfamilyNameID = 17; + + let parsedFont = parse(font.toArrayBuffer()); + assert.deepEqual(parsedFont.tables.fvar.instances[0].name, font.names.unicode.fontSubfamily); + assert.deepEqual(parsedFont.tables.fvar.instances[1].name, font.names.unicode.preferredSubfamily); + }); + it('can make a font variations table', function() { const names = { macintosh: { @@ -67,15 +105,25 @@ describe('tables/fvar.js', function() { } }; assert.deepEqual(data, hex(fvar.make(table, names).encode())); - assert.deepEqual(names, { - macintosh: { - 111: {en: 'Name #111'}, - 256: {en: 'Ligatures', ja: 'リガチャ'}, - 257: {en: 'Weight', ja: 'ウエイト'}, - 258: {en: 'Width', ja: '幅'}, - 259: {en: 'Regular', ja: 'レギュラー'}, - 260: {en: 'Condensed', ja: 'コンデンス'} - } - }); + }); + + it('writes postScriptNameID optionally', function() { + let parsedFont = parse(testFont.toArrayBuffer()); + let makeTable = fvar.make(parsedFont.tables.fvar, parsedFont.names); + assert.equal(parsedFont.tables.fvar.instances[0].postScriptNameID, 6); + assert.equal(parsedFont.tables.fvar.instances[0].postScriptName.en, 'VARTestVF-Regular'); + + assert.equal(makeTable.instanceSize, 10); + + parsedFont.tables.fvar.instances = + parsedFont.tables.fvar.instances.map(i => { i.postScriptNameID = undefined; return i; }); + + parsedFont = parse(parsedFont.toArrayBuffer()); + makeTable = fvar.make(parsedFont.tables.fvar, parsedFont.names); + + assert.equal(makeTable.instanceSize, 8); + + assert.equal(parsedFont.tables.fvar.instances[0].postScriptNameID, undefined); + assert.equal(parsedFont.tables.fvar.instances[1].postScriptNameID, undefined); }); }); From f54a8c243116b4355115b7c253e616ec72b16492 Mon Sep 17 00:00:00 2001 From: Connum Date: Wed, 10 Apr 2024 10:35:20 +0200 Subject: [PATCH 2/2] fix unsupported tables disrupting the writing process --- src/tables/sfnt.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/tables/sfnt.js b/src/tables/sfnt.js index 8e19a203..6083e2ba 100644 --- a/src/tables/sfnt.js +++ b/src/tables/sfnt.js @@ -24,6 +24,7 @@ import cpal from './cpal.js'; import fvar from './fvar.js'; import stat from './stat.js'; import avar from './avar.js'; +import cvar from './cvar.js'; import gvar from './gvar.js'; import gasp from './gasp.js'; @@ -359,6 +360,7 @@ function fontToSfntTable(font) { colr, stat, avar, + cvar, fvar, gvar, gasp, @@ -372,7 +374,10 @@ function fontToSfntTable(font) { for (let tableName in optionalTables) { const table = font.tables[tableName]; if (table) { - tables.push(optionalTables[tableName].make.call(font, table, ...(optionalTableArgs[tableName] || []))); + const tableData = optionalTables[tableName].make.call(font, table, ...(optionalTableArgs[tableName] || [])); + if (tableData) { + tables.push(tableData); + } } }