Skip to content

Commit 86b289f

Browse files
authored
fix fvar axis/instance name parsing and writing (fix #687) (#694)
* fix fvar axis/instance name parsing and writing (fix #687) * fix unsupported tables disrupting the writing process
1 parent be0d441 commit 86b289f

File tree

5 files changed

+147
-70
lines changed

5 files changed

+147
-70
lines changed

src/tables/fvar.js

Lines changed: 50 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,16 @@
44
import check from '../check.js';
55
import parse from '../parse.js';
66
import table from '../table.js';
7-
import { objectsEqual } from '../util.js';
8-
9-
function addName(name, names) {
10-
let nameID = 256;
11-
for (let platform in names) {
12-
for (let nameKey in names[platform]) {
13-
let n = parseInt(nameKey);
14-
if (!n || n < 256) {
15-
continue;
16-
}
17-
18-
if (objectsEqual(names[platform][nameKey], name)) {
19-
return n;
20-
}
21-
22-
if (nameID <= n) {
23-
nameID = n + 1;
24-
}
25-
}
26-
names[platform][nameID] = name;
27-
}
28-
29-
return nameID;
30-
}
7+
import { getNameByID } from './name.js';
318

32-
function makeFvarAxis(n, axis, names) {
33-
const nameID = addName(axis.name, names);
9+
function makeFvarAxis(n, axis) {
3410
return [
3511
{name: 'tag_' + n, type: 'TAG', value: axis.tag},
3612
{name: 'minValue_' + n, type: 'FIXED', value: axis.minValue << 16},
3713
{name: 'defaultValue_' + n, type: 'FIXED', value: axis.defaultValue << 16},
3814
{name: 'maxValue_' + n, type: 'FIXED', value: axis.maxValue << 16},
3915
{name: 'flags_' + n, type: 'USHORT', value: 0},
40-
{name: 'nameID_' + n, type: 'USHORT', value: nameID}
16+
{name: 'nameID_' + n, type: 'USHORT', value: axis.axisNameID}
4117
];
4218
}
4319

@@ -49,14 +25,15 @@ function parseFvarAxis(data, start, names) {
4925
axis.defaultValue = p.parseFixed();
5026
axis.maxValue = p.parseFixed();
5127
p.skip('uShort', 1); // reserved for flags; no values defined
52-
axis.name = (names.macintosh || names.windows || names.unicode)[p.parseUShort()] || {};
28+
const axisNameID = p.parseUShort();
29+
axis.axisNameID = axisNameID;
30+
axis.name = getNameByID(names, axisNameID);
5331
return axis;
5432
}
5533

56-
function makeFvarInstance(n, inst, axes, names) {
57-
const nameID = addName(inst.name, names);
34+
function makeFvarInstance(n, inst, axes, optionalFields = {}) {
5835
const fields = [
59-
{name: 'nameID_' + n, type: 'USHORT', value: nameID},
36+
{name: 'nameID_' + n, type: 'USHORT', value: inst.subfamilyNameID},
6037
{name: 'flags_' + n, type: 'USHORT', value: 0}
6138
];
6239

@@ -69,24 +46,45 @@ function makeFvarInstance(n, inst, axes, names) {
6946
});
7047
}
7148

49+
if (optionalFields && optionalFields.postScriptNameID) {
50+
fields.push({
51+
name: 'postScriptNameID_',
52+
type: 'USHORT',
53+
value: inst.postScriptNameID !== undefined? inst.postScriptNameID : 0xFFFF
54+
});
55+
}
56+
7257
return fields;
7358
}
7459

75-
function parseFvarInstance(data, start, axes, names) {
60+
function parseFvarInstance(data, start, axes, names, instanceSize) {
7661
const inst = {};
7762
const p = new parse.Parser(data, start);
78-
inst.name = (names.macintosh || names.windows || names.unicode)[p.parseUShort()] || {};
63+
const subfamilyNameID = p.parseUShort();
64+
inst.subfamilyNameID = subfamilyNameID;
65+
inst.name = getNameByID(names, subfamilyNameID, [2, 17]);
7966
p.skip('uShort', 1); // reserved for flags; no values defined
8067

8168
inst.coordinates = {};
8269
for (let i = 0; i < axes.length; ++i) {
8370
inst.coordinates[axes[i].tag] = p.parseFixed();
8471
}
8572

73+
if (p.relativeOffset === instanceSize) {
74+
inst.postScriptNameID = undefined;
75+
inst.postScriptName = undefined;
76+
return inst;
77+
}
78+
79+
const postScriptNameID = p.parseUShort();
80+
inst.postScriptNameID = postScriptNameID == 0xFFFF ? undefined : postScriptNameID;
81+
inst.postScriptName = inst.postScriptNameID !== undefined ? getNameByID(names, postScriptNameID, [6]) : '';
82+
8683
return inst;
8784
}
8885

8986
function makeFvarTable(fvar, names) {
87+
9088
const result = new table.Table('fvar', [
9189
{name: 'version', type: 'ULONG', value: 0x10000},
9290
{name: 'offsetToData', type: 'USHORT', value: 0},
@@ -102,8 +100,25 @@ function makeFvarTable(fvar, names) {
102100
result.fields = result.fields.concat(makeFvarAxis(i, fvar.axes[i], names));
103101
}
104102

103+
const optionalFields = {};
104+
105+
// first loop over instances: find out if at least one has postScriptNameID defined
106+
for (let j = 0; j < fvar.instances.length; j++) {
107+
if(fvar.instances[j].postScriptNameID !== undefined) {
108+
result.instanceSize += 2;
109+
optionalFields.postScriptNameID = true;
110+
break;
111+
}
112+
}
113+
114+
// second loop over instances: find out if at least one has postScriptNameID defined
105115
for (let j = 0; j < fvar.instances.length; j++) {
106-
result.fields = result.fields.concat(makeFvarInstance(j, fvar.instances[j], fvar.axes, names));
116+
result.fields = result.fields.concat(makeFvarInstance(
117+
j,
118+
fvar.instances[j],
119+
fvar.axes,
120+
optionalFields
121+
));
107122
}
108123

109124
return result;
@@ -129,7 +144,7 @@ function parseFvarTable(data, start, names) {
129144
const instances = [];
130145
const instanceStart = start + offsetToData + axisCount * axisSize;
131146
for (let j = 0; j < instanceCount; j++) {
132-
instances.push(parseFvarInstance(data, instanceStart + j * instanceSize, axes, names));
147+
instances.push(parseFvarInstance(data, instanceStart + j * instanceSize, axes, names, instanceSize));
133148
}
134149

135150
return {axes: axes, instances: instances};

src/tables/name.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import parse from '../parse.js';
66
import table from '../table.js';
77

88
// NameIDs for the name table.
9-
const nameTableNames = [
9+
export const nameTableNames = [
1010
'copyright', // 0
1111
'fontFamily', // 1
1212
'fontSubfamily', // 2
@@ -854,4 +854,23 @@ function makeNameTable(names, ltag) {
854854
return t;
855855
}
856856

857-
export default { parse: parseNameTable, make: makeNameTable };
857+
export function getNameByID(names, nameID, allowedStandardIDs = []) {
858+
if (nameID < 256 && nameID in nameTableNames) {
859+
if (allowedStandardIDs.length && !allowedStandardIDs.includes(parseInt(nameID))) {
860+
return undefined;
861+
}
862+
nameID = nameTableNames[nameID];
863+
}
864+
865+
for (let platform in names) {
866+
for (let nameKey in names[platform]) {
867+
if(nameKey === nameID || parseInt(nameKey) === nameID) {
868+
return names[platform][nameKey];
869+
}
870+
}
871+
}
872+
873+
return undefined;
874+
}
875+
876+
export default { parse: parseNameTable, make: makeNameTable, getNameByID };

src/tables/sfnt.js

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import cpal from './cpal.js';
2424
import fvar from './fvar.js';
2525
import stat from './stat.js';
2626
import avar from './avar.js';
27+
import cvar from './cvar.js';
2728
import gvar from './gvar.js';
2829
import gasp from './gasp.js';
2930

@@ -328,10 +329,6 @@ function fontToSfntTable(font) {
328329
names.windows.preferredSubfamily = fontNamesWindows.fontSubfamily || fontNamesUnicode.fontSubfamily || fontNamesMacintosh.fontSubfamily;
329330
}
330331

331-
// we have to handle fvar before name, because it may modify name IDs
332-
const fvarTable = font.tables.fvar ? fvar.make(font.tables.fvar, font.names) : undefined;
333-
const gaspTable = font.tables.gasp ? gasp.make(font.tables.gasp) : undefined;
334-
335332
const languageTags = [];
336333
const nameTable = _name.make(names, languageTags);
337334
const ltagTable = (languageTags.length > 0 ? ltag.make(languageTags) : undefined);
@@ -363,33 +360,31 @@ function fontToSfntTable(font) {
363360
colr,
364361
stat,
365362
avar,
363+
cvar,
364+
fvar,
366365
gvar,
366+
gasp,
367367
};
368368

369369
const optionalTableArgs = {
370-
avar: [font.tables.fvar]
370+
avar: [font.tables.fvar],
371+
fvar: [font.names],
371372
};
372373

373-
// fvar table is already handled above
374-
if (fvarTable) {
375-
tables.push(fvarTable);
376-
}
377-
378374
for (let tableName in optionalTables) {
379375
const table = font.tables[tableName];
380376
if (table) {
381-
tables.push(optionalTables[tableName].make.call(font, table, ...(optionalTableArgs[tableName] || [])));
377+
const tableData = optionalTables[tableName].make.call(font, table, ...(optionalTableArgs[tableName] || []));
378+
if (tableData) {
379+
tables.push(tableData);
380+
}
382381
}
383382
}
384383

385384
if (metaTable) {
386385
tables.push(metaTable);
387386
}
388387

389-
if (gaspTable) {
390-
tables.push(gaspTable);
391-
}
392-
393388
const sfntTable = makeSfntTable(tables);
394389

395390
// Compute the font's checkSum and store it in head.checkSumAdjustment.

test/fonts/VARTest.ttf

2.43 KB
Binary file not shown.

test/tables/fvar.js

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import assert from 'assert';
22
import { hex, unhex } from '../testutil.js';
33
import fvar from '../../src/tables/fvar.js';
4+
import { Font, loadSync, parse } from '../../src/opentype.js';
45

56
describe('tables/fvar.js', function() {
7+
const testFont = loadSync('./test/fonts/VARTest.ttf');
8+
69
const data =
710
'00 01 00 00 00 10 00 02 00 02 00 14 00 02 00 0C ' +
811
'77 67 68 74 00 64 00 00 01 90 00 00 03 84 00 00 00 00 01 01 ' +
@@ -17,40 +20,75 @@ describe('tables/fvar.js', function() {
1720
minValue: 100,
1821
defaultValue: 400,
1922
maxValue: 900,
23+
axisNameID: 257,
2024
name: {en: 'Weight', ja: 'ウエイト'}
2125
},
2226
{
2327
tag: 'wdth',
2428
minValue: 50,
2529
defaultValue: 100,
2630
maxValue: 200,
31+
axisNameID: 258,
2732
name: {en: 'Width', ja: '幅'}
2833
}
2934
],
3035
instances: [
3136
{
3237
name: {en: 'Regular', ja: 'レギュラー'},
38+
subfamilyNameID: 259,
39+
postScriptName: undefined,
40+
postScriptNameID: undefined,
3341
coordinates: {wght: 300, wdth: 100}
3442
},
3543
{
3644
name: {en: 'Condensed', ja: 'コンデンス'},
45+
subfamilyNameID: 260,
46+
postScriptName: undefined,
47+
postScriptNameID: undefined,
3748
coordinates: {wght: 300, wdth: 75}
3849
}
3950
]
4051
};
4152

53+
const names = {
54+
macintosh: {
55+
257: {en: 'Weight', ja: 'ウエイト'},
56+
258: {en: 'Width', ja: '幅'},
57+
259: {en: 'Regular', ja: 'レギュラー'},
58+
260: {en: 'Condensed', ja: 'コンデンス'}
59+
}
60+
};
61+
4262
it('can parse a font variations table', function() {
43-
const names = {
44-
macintosh: {
45-
257: {en: 'Weight', ja: 'ウエイト'},
46-
258: {en: 'Width', ja: '幅'},
47-
259: {en: 'Regular', ja: 'レギュラー'},
48-
260: {en: 'Condensed', ja: 'コンデンス'}
49-
}
50-
};
5163
assert.deepEqual(table, fvar.parse(unhex(data), 0, names));
5264
});
5365

66+
it('parses nameIDs 2 and 17 and postScriptNameID 6 correctly', function() {
67+
assert.equal(testFont.tables.fvar.instances[0].name.en, 'Regular');
68+
assert.equal(testFont.tables.fvar.instances[0].subfamilyNameID, 2);
69+
assert.equal(testFont.tables.fvar.instances[0].postScriptName.en, 'VARTestVF-Regular');
70+
assert.equal(testFont.tables.fvar.instances[0].postScriptNameID, 6);
71+
assert.equal(testFont.tables.fvar.instances[0].postScriptName.en, 'VARTestVF-Regular');
72+
73+
const font = new Font({
74+
familyName: 'TestFont',
75+
styleName: 'Medium',
76+
unitsPerEm: 1000,
77+
ascender: 800,
78+
descender: -200,
79+
glyphs: []
80+
});
81+
font.tables.fvar = JSON.parse(JSON.stringify(table));
82+
font.names.unicode.fontSubfamily = {en: 'Font Subfamily name'};
83+
font.names.unicode.preferredSubfamily = {en: 'Typographic Subfamily name'};
84+
font.tables.fvar.instances[0].subfamilyNameID = 2;
85+
font.tables.fvar.instances[1].subfamilyNameID = 17;
86+
87+
let parsedFont = parse(font.toArrayBuffer());
88+
assert.deepEqual(parsedFont.tables.fvar.instances[0].name, font.names.unicode.fontSubfamily);
89+
assert.deepEqual(parsedFont.tables.fvar.instances[1].name, font.names.unicode.preferredSubfamily);
90+
});
91+
5492
it('can make a font variations table', function() {
5593
const names = {
5694
macintosh: {
@@ -67,15 +105,25 @@ describe('tables/fvar.js', function() {
67105
}
68106
};
69107
assert.deepEqual(data, hex(fvar.make(table, names).encode()));
70-
assert.deepEqual(names, {
71-
macintosh: {
72-
111: {en: 'Name #111'},
73-
256: {en: 'Ligatures', ja: 'リガチャ'},
74-
257: {en: 'Weight', ja: 'ウエイト'},
75-
258: {en: 'Width', ja: '幅'},
76-
259: {en: 'Regular', ja: 'レギュラー'},
77-
260: {en: 'Condensed', ja: 'コンデンス'}
78-
}
79-
});
108+
});
109+
110+
it('writes postScriptNameID optionally', function() {
111+
let parsedFont = parse(testFont.toArrayBuffer());
112+
let makeTable = fvar.make(parsedFont.tables.fvar, parsedFont.names);
113+
assert.equal(parsedFont.tables.fvar.instances[0].postScriptNameID, 6);
114+
assert.equal(parsedFont.tables.fvar.instances[0].postScriptName.en, 'VARTestVF-Regular');
115+
116+
assert.equal(makeTable.instanceSize, 10);
117+
118+
parsedFont.tables.fvar.instances =
119+
parsedFont.tables.fvar.instances.map(i => { i.postScriptNameID = undefined; return i; });
120+
121+
parsedFont = parse(parsedFont.toArrayBuffer());
122+
makeTable = fvar.make(parsedFont.tables.fvar, parsedFont.names);
123+
124+
assert.equal(makeTable.instanceSize, 8);
125+
126+
assert.equal(parsedFont.tables.fvar.instances[0].postScriptNameID, undefined);
127+
assert.equal(parsedFont.tables.fvar.instances[1].postScriptNameID, undefined);
80128
});
81129
});

0 commit comments

Comments
 (0)