Skip to content

Commit a9c8ad5

Browse files
opengraphicaConnum
authored andcommitted
Add vhea and vmtx parsing
1 parent f4d2b5d commit a9c8ad5

File tree

9 files changed

+232
-4
lines changed

9 files changed

+232
-4
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,8 +442,10 @@ A Glyph is an individual mark that often corresponds to a character. Some glyphs
442442
* `unicode`: The primary unicode value of this glyph (can be `undefined`).
443443
* `unicodes`: The list of unicode values for this glyph (most of the time this will be `1`, can also be empty).
444444
* `index`: The index number of the glyph.
445-
* `advanceWidth`: The width to advance the pen when drawing this glyph.
445+
* `advanceWidth`: The width to advance the pen when drawing this glyph horizontally.
446446
* `leftSideBearing`: The horizontal distance from the previous character to the origin (`0, 0`); a negative value indicates an overhang
447+
* `advanceHeight`: The height to advance the pen when drawing this glyph vertically.
448+
* `topSideBearing`: The vertical distance from the previous character to the origin (`0, 0`); a negative value indicates an overhang
447449
* `xMin`, `yMin`, `xMax`, `yMax`: The bounding box of the glyph.
448450
* `path`: The raw, unscaled path of the glyph.
449451

src/glyph.mjs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ function getPathDefinition(glyph, path) {
3737
* @property {number} [yMax]
3838
* @property {number} [advanceWidth]
3939
* @property {number} [leftSideBearing]
40+
* @property {number} [advanceHeight]
41+
* @property {number} [topSideBearing]
4042
*/
4143

4244
// A Glyph is an individual mark that often corresponds to a character.
@@ -103,6 +105,14 @@ Glyph.prototype.bindConstructorValues = function(options) {
103105
this.leftSideBearing = options.leftSideBearing;
104106
}
105107

108+
if ('advanceHeight' in options) {
109+
this.advanceHeight = options.advanceHeight;
110+
}
111+
112+
if ('topSideBearing' in options) {
113+
this.topSideBearing = options.topSideBearing;
114+
}
115+
106116
if ('points' in options) {
107117
this.points = options.points;
108118
}
@@ -324,7 +334,8 @@ Glyph.prototype.getMetrics = function() {
324334
yMin: Math.min.apply(null, yCoords),
325335
xMax: Math.max.apply(null, xCoords),
326336
yMax: Math.max.apply(null, yCoords),
327-
leftSideBearing: this.leftSideBearing
337+
leftSideBearing: this.leftSideBearing,
338+
topSideBearing: this.topSideBearing
328339
};
329340

330341
if (!isFinite(metrics.xMin)) {
@@ -344,6 +355,9 @@ Glyph.prototype.getMetrics = function() {
344355
}
345356

346357
metrics.rightSideBearing = this.advanceWidth - metrics.leftSideBearing - (metrics.xMax - metrics.xMin);
358+
metrics.bottomSideBearing = metrics.topSideBearing != null
359+
? this.advanceHeight - metrics.topSideBearing - (metrics.yMax - metrics.yMin)
360+
: undefined;
347361
return metrics;
348362
};
349363

src/glyphset.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ GlyphSet.prototype.get = function(index) {
8888

8989
this.glyphs[index].advanceWidth = this.font._hmtxTableData[index].advanceWidth;
9090
this.glyphs[index].leftSideBearing = this.font._hmtxTableData[index].leftSideBearing;
91+
if (this.font._vmtxTableData) {
92+
this.glyphs[index].advanceHeight = this.font._vmtxTableData[index].advanceHeight;
93+
this.glyphs[index].topSideBearing = this.font._vmtxTableData[index].topSideBearing;
94+
}
9195
} else {
9296
if (typeof this.glyphs[index] === 'function') {
9397
this.glyphs[index] = this.glyphs[index]();

src/opentype.mjs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ import gpos from './tables/gpos.mjs';
2626
import gsub from './tables/gsub.mjs';
2727
import head from './tables/head.mjs';
2828
import hhea from './tables/hhea.mjs';
29+
import vhea from './tables/vhea.mjs';
2930
import hmtx from './tables/hmtx.mjs';
31+
import vmtx from './tables/vmtx.mjs';
3032
import kern from './tables/kern.mjs';
3133
import ltag from './tables/ltag.mjs';
3234
import loca from './tables/loca.mjs';
@@ -189,6 +191,7 @@ function parseBuffer(buffer, opt={}) {
189191
let gsubTableEntry;
190192
let hmtxTableEntry;
191193
let hvarTableEntry;
194+
let vmtxTableEntry;
192195
let kernTableEntry;
193196
let locaTableEntry;
194197
let nameTableEntry;
@@ -248,6 +251,16 @@ function parseBuffer(buffer, opt={}) {
248251
case 'hmtx':
249252
hmtxTableEntry = tableEntry;
250253
break;
254+
case 'vhea':
255+
table = uncompressTable(data, tableEntry);
256+
font.tables.vhea = vhea.parse(table.data, table.offset);
257+
font.vertTypoAscender = font.tables.vhea.vertTypoAscender;
258+
font.vertTypoDescender = font.tables.vhea.vertTypoDescender;
259+
font.numOfLongVerMetrics = font.tables.vhea.numOfLongVerMetrics;
260+
break;
261+
case 'vmtx':
262+
vmtxTableEntry = tableEntry;
263+
break;
251264
case 'ltag':
252265
table = uncompressTable(data, tableEntry);
253266
ltagTable = ltag.parse(table.data, table.offset);
@@ -343,8 +356,14 @@ function parseBuffer(buffer, opt={}) {
343356
throw new Error('Font doesn\'t contain TrueType, CFF or CFF2 outlines.');
344357
}
345358

346-
const hmtxTable = uncompressTable(data, hmtxTableEntry);
347-
hmtx.parse(font, hmtxTable.data, hmtxTable.offset, font.numberOfHMetrics, font.numGlyphs, font.glyphs, opt);
359+
if (hmtxTableEntry) {
360+
const hmtxTable = uncompressTable(data, hmtxTableEntry);
361+
hmtx.parse(font, hmtxTable.data, hmtxTable.offset, font.numberOfHMetrics, font.numGlyphs, font.glyphs, opt);
362+
}
363+
if (vmtxTableEntry) {
364+
const vmtxTable = uncompressTable(data, vmtxTableEntry);
365+
vmtx.parse(font, vmtxTable.data, vmtxTable.offset, font.numOfLongVerMetrics, font.numGlyphs, font.glyphs, opt);
366+
}
348367
addGlyphNames(font, opt);
349368

350369
if (kernTableEntry) {

src/tables/vhea.mjs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// The `vhea` table contains information for vertical layout.
2+
// https://learn.microsoft.com/en-us/typography/opentype/spec/vhea
3+
4+
import parse from '../parse.mjs';
5+
import table from '../table.mjs';
6+
7+
// Parse the vertical header `vhea` table
8+
function parseVheaTable(data, start) {
9+
const vhea = {};
10+
const p = new parse.Parser(data, start);
11+
vhea.version = p.parseVersion();
12+
vhea.ascent = p.parseShort(); // v1.0
13+
vhea.vertTypoAscender = vhea.ascent; // v1.1
14+
vhea.descent = p.parseShort(); // v1.0
15+
vhea.vertTypoDescender = vhea.descent; // v1.1
16+
vhea.lineGap = p.parseShort(); // v1.0
17+
vhea.vertTypoLineGap = vhea.lineGap; // v1.1
18+
vhea.advanceHeightMax = p.parseUShort();
19+
vhea.minTopSideBearing = p.parseShort();
20+
vhea.minBottomSideBearing = p.parseShort();
21+
vhea.yMaxExtent = p.parseShort();
22+
vhea.caretSlopeRise = p.parseShort();
23+
vhea.caretSlopeRun = p.parseShort();
24+
vhea.caretOffset = p.parseShort();
25+
p.relativeOffset += 8;
26+
vhea.metricDataFormat = p.parseShort();
27+
vhea.numOfLongVerMetrics = p.parseUShort();
28+
return vhea;
29+
}
30+
31+
function makeVheaTable(options) {
32+
return new table.Table('vhea', [
33+
{name: 'version', type: 'FIXED', value: 0x00010000},
34+
{name: 'ascent', type: 'FWORD', value: 0},
35+
{name: 'descent', type: 'FWORD', value: 0},
36+
{name: 'lineGap', type: 'FWORD', value: 0},
37+
{name: 'advanceHeightMax', type: 'UFWORD', value: 0},
38+
{name: 'minTopSideBearing', type: 'FWORD', value: 0},
39+
{name: 'minBottomSideBearing', type: 'FWORD', value: 0},
40+
{name: 'yMaxExtent', type: 'FWORD', value: 0},
41+
{name: 'caretSlopeRise', type: 'SHORT', value: 1},
42+
{name: 'caretSlopeRun', type: 'SHORT', value: 0},
43+
{name: 'caretOffset', type: 'SHORT', value: 0},
44+
{name: 'reserved1', type: 'SHORT', value: 0},
45+
{name: 'reserved2', type: 'SHORT', value: 0},
46+
{name: 'reserved3', type: 'SHORT', value: 0},
47+
{name: 'reserved4', type: 'SHORT', value: 0},
48+
{name: 'metricDataFormat', type: 'SHORT', value: 0},
49+
{name: 'numOfLongVerMetrics', type: 'USHORT', value: 0}
50+
], options);
51+
}
52+
53+
export default { parse: parseVheaTable, make: makeVheaTable };

src/tables/vmtx.mjs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// The `vmtx` table contains the vertical metrics for all glyphs.
2+
// https://learn.microsoft.com/en-us/typography/opentype/spec/vmtx
3+
4+
import parse from '../parse.mjs';
5+
import table from '../table.mjs';
6+
7+
function parseVmtxTableAll(data, start, numMetrics, numGlyphs, glyphs) {
8+
let advanceHeight;
9+
let topSideBearing;
10+
const p = new parse.Parser(data, start);
11+
for (let i = 0; i < numGlyphs; i += 1) {
12+
// If the font is monospaced, only one entry is needed. This last entry applies to all subsequent glyphs.
13+
if (i < numMetrics) {
14+
advanceHeight = p.parseUShort();
15+
topSideBearing = p.parseShort();
16+
}
17+
18+
const glyph = glyphs.get(i);
19+
glyph.advanceHeight = advanceHeight;
20+
glyph.topSideBearing = topSideBearing;
21+
}
22+
}
23+
24+
function parseVmtxTableOnLowMemory(font, data, start, numMetrics, numGlyphs) {
25+
font._vmtxTableData = {};
26+
27+
let advanceHeight;
28+
let topSideBearing;
29+
const p = new parse.Parser(data, start);
30+
for (let i = 0; i < numGlyphs; i += 1) {
31+
// If the font is monospaced, only one entry is needed. This last entry applies to all subsequent glyphs.
32+
if (i < numMetrics) {
33+
advanceHeight = p.parseUShort();
34+
topSideBearing = p.parseShort();
35+
}
36+
37+
font._vmtxTableData[i] = {
38+
advanceHeight: advanceHeight,
39+
topSideBearing: topSideBearing
40+
};
41+
}
42+
}
43+
44+
// Parse the `vmtx` table, which contains the horizontal metrics for all glyphs.
45+
// This function augments the glyph array, adding the advanceHeight and topSideBearing to each glyph.
46+
function parseVmtxTable(font, data, start, numMetrics, numGlyphs, glyphs, opt) {
47+
if (opt.lowMemory)
48+
parseVmtxTableOnLowMemory(font, data, start, numMetrics, numGlyphs);
49+
else
50+
parseVmtxTableAll(data, start, numMetrics, numGlyphs, glyphs);
51+
}
52+
53+
function makeVmtxTable(glyphs) {
54+
const t = new table.Table('vmtx', []);
55+
for (let i = 0; i < glyphs.length; i += 1) {
56+
const glyph = glyphs.get(i);
57+
const advanceHeight = glyph.advanceHeight || 0;
58+
const topSideBearing = glyph.topSideBearing || 0;
59+
t.fields.push({name: 'advanceHeight_' + i, type: 'USHORT', value: advanceHeight});
60+
t.fields.push({name: 'topSideBearing_' + i, type: 'SHORT', value: topSideBearing});
61+
}
62+
63+
return t;
64+
}
65+
66+
export default { parse: parseVmtxTable, make: makeVmtxTable };

test/fonts/NotoSansJP-Medium.ttf

5.46 MB
Binary file not shown.

test/tables/vhea.spec.mjs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import assert from 'assert';
2+
import { parse } from '../../src/opentype.mjs';
3+
import { readFileSync } from 'fs';
4+
const loadSync = (url, opt) => parse(readFileSync(url), opt);
5+
6+
describe('tables/vhea.mjs', function() {
7+
const fonts = {
8+
notoSansJp: loadSync('./test/fonts/NotoSansJP-Medium.ttf'),
9+
};
10+
it('correctly parses the vertical header table', function() {
11+
// tests for all fonts
12+
const { notoSansJp } = fonts;
13+
assert.equal(notoSansJp.tables.vhea.version, 1.1);
14+
assert.equal(notoSansJp.tables.vhea.ascent, 500);
15+
assert.equal(notoSansJp.tables.vhea.vertTypoAscender, 500);
16+
assert.equal(notoSansJp.tables.vhea.descent, -500);
17+
assert.equal(notoSansJp.tables.vhea.vertTypoDescender, -500);
18+
assert.equal(notoSansJp.tables.vhea.lineGap, 0);
19+
assert.equal(notoSansJp.tables.vhea.vertTypoLineGap, 0);
20+
assert.equal(notoSansJp.tables.vhea.advanceHeightMax, 3000);
21+
assert.equal(notoSansJp.tables.vhea.minTopSideBearing, -224);
22+
assert.equal(notoSansJp.tables.vhea.minBottomSideBearing, -689);
23+
assert.equal(notoSansJp.tables.vhea.yMaxExtent, 2927);
24+
assert.equal(notoSansJp.tables.vhea.caretSlopeRise, 0);
25+
assert.equal(notoSansJp.tables.vhea.caretSlopeRun, 1);
26+
assert.equal(notoSansJp.tables.vhea.caretOffset, 0);
27+
assert.equal(notoSansJp.tables.vhea.metricDataFormat, 0);
28+
assert.equal(notoSansJp.tables.vhea.numOfLongVerMetrics, 17481);
29+
30+
// Directly exposed equivalents to ascender, descender, numberOfHMetrics
31+
assert.equal(notoSansJp.vertTypoAscender, 500);
32+
assert.equal(notoSansJp.vertTypoDescender, -500);
33+
assert.equal(notoSansJp.numOfLongVerMetrics, 17481);
34+
});
35+
});

test/tables/vmtx.spec.mjs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import assert from 'assert';
2+
import { parse } from '../../src/opentype.mjs';
3+
import { readFileSync } from 'fs';
4+
const loadSync = (url, opt) => parse(readFileSync(url), opt);
5+
6+
describe('tables/vmtx.mjs', function() {
7+
const fonts = {
8+
notoSansJp: loadSync('./test/fonts/NotoSansJP-Medium.ttf'),
9+
notoSansJpLowMemory: loadSync('./test/fonts/NotoSansJP-Medium.ttf', { lowMemory: true }),
10+
};
11+
12+
it('correctly parses the vertical metrics table - high memory', function() {
13+
// tests for all fonts
14+
const { notoSansJp } = fonts;
15+
16+
const a = notoSansJp.charToGlyph('あ');
17+
assert.equal(a.topSideBearing, 80);
18+
assert.equal(a.advanceHeight, 1000);
19+
const aMetrics = a.getMetrics();
20+
assert.equal(aMetrics.topSideBearing, 80);
21+
assert.equal(aMetrics.bottomSideBearing, 64);
22+
});
23+
24+
it('correctly parses the vertical metrics table - low memory', function() {
25+
// tests for all fonts
26+
const { notoSansJpLowMemory } = fonts;
27+
28+
const a = notoSansJpLowMemory.charToGlyph('あ');
29+
assert.equal(a.topSideBearing, 80);
30+
assert.equal(a.advanceHeight, 1000);
31+
const aMetrics = a.getMetrics();
32+
assert.equal(aMetrics.topSideBearing, 80);
33+
assert.equal(aMetrics.bottomSideBearing, 64);
34+
});
35+
});

0 commit comments

Comments
 (0)