@flighthq/font-formats 0.3.1-next.209.43ef1bc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cffCharstring.d.ts +13 -0
- package/dist/cffCharstring.d.ts.map +1 -0
- package/dist/cffCharstring.js +336 -0
- package/dist/cffCharstring.js.map +1 -0
- package/dist/cffDict.d.ts +8 -0
- package/dist/cffDict.d.ts.map +1 -0
- package/dist/cffDict.js +118 -0
- package/dist/cffDict.js.map +1 -0
- package/dist/cffFdSelect.d.ts +2 -0
- package/dist/cffFdSelect.d.ts.map +1 -0
- package/dist/cffFdSelect.js +60 -0
- package/dist/cffFdSelect.js.map +1 -0
- package/dist/cffIndex.d.ts +3 -0
- package/dist/cffIndex.d.ts.map +1 -0
- package/dist/cffIndex.js +52 -0
- package/dist/cffIndex.js.map +1 -0
- package/dist/cffTable.d.ts +3 -0
- package/dist/cffTable.d.ts.map +1 -0
- package/dist/cffTable.js +134 -0
- package/dist/cffTable.js.map +1 -0
- package/dist/contract.d.ts +15 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +15 -0
- package/dist/contract.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/openTypeCmap.d.ts +5 -0
- package/dist/openTypeCmap.d.ts.map +1 -0
- package/dist/openTypeCmap.js +161 -0
- package/dist/openTypeCmap.js.map +1 -0
- package/dist/openTypeGlyf.d.ts +4 -0
- package/dist/openTypeGlyf.d.ts.map +1 -0
- package/dist/openTypeGlyf.js +229 -0
- package/dist/openTypeGlyf.js.map +1 -0
- package/dist/openTypeGlyphOutlineSource.d.ts +4 -0
- package/dist/openTypeGlyphOutlineSource.d.ts.map +1 -0
- package/dist/openTypeGlyphOutlineSource.js +168 -0
- package/dist/openTypeGlyphOutlineSource.js.map +1 -0
- package/dist/openTypeMetrics.d.ts +6 -0
- package/dist/openTypeMetrics.d.ts.map +1 -0
- package/dist/openTypeMetrics.js +74 -0
- package/dist/openTypeMetrics.js.map +1 -0
- package/dist/openTypeTestHelper.d.ts +26 -0
- package/dist/openTypeTestHelper.d.ts.map +1 -0
- package/dist/openTypeTestHelper.js +494 -0
- package/dist/openTypeTestHelper.js.map +1 -0
- package/dist/sfntAssembly.d.ts +7 -0
- package/dist/sfntAssembly.d.ts.map +1 -0
- package/dist/sfntAssembly.js +77 -0
- package/dist/sfntAssembly.js.map +1 -0
- package/dist/sfntTableDirectory.d.ts +4 -0
- package/dist/sfntTableDirectory.d.ts.map +1 -0
- package/dist/sfntTableDirectory.js +39 -0
- package/dist/sfntTableDirectory.js.map +1 -0
- package/dist/woff2Font.d.ts +6 -0
- package/dist/woff2Font.d.ts.map +1 -0
- package/dist/woff2Font.js +210 -0
- package/dist/woff2Font.js.map +1 -0
- package/dist/woff2GlyfTransform.d.ts +6 -0
- package/dist/woff2GlyfTransform.d.ts.map +1 -0
- package/dist/woff2GlyfTransform.js +104 -0
- package/dist/woff2GlyfTransform.js.map +1 -0
- package/dist/woffFont.d.ts +6 -0
- package/dist/woffFont.d.ts.map +1 -0
- package/dist/woffFont.js +112 -0
- package/dist/woffFont.js.map +1 -0
- package/package.json +50 -0
- package/src/cffCharstring.test.ts +177 -0
- package/src/cffDict.test.ts +67 -0
- package/src/cffFdSelect.test.ts +84 -0
- package/src/cffIndex.test.ts +97 -0
- package/src/cffTable.test.ts +84 -0
- package/src/openTypeCmap.test.ts +124 -0
- package/src/openTypeGlyf.test.ts +168 -0
- package/src/openTypeGlyphOutlineSource.test.ts +298 -0
- package/src/openTypeMetrics.test.ts +103 -0
- package/src/sfntAssembly.test.ts +92 -0
- package/src/sfntTableDirectory.test.ts +64 -0
- package/src/woff2Font.test.ts +125 -0
- package/src/woff2GlyfTransform.test.ts +88 -0
- package/src/woffFont.test.ts +167 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { PathCommand } from '@flighthq/types/contract';
|
|
2
|
+
import type { Path } from '@flighthq/types/contract';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { readOpenTypeGlyphOutline, readOpenTypeGlyphRanges } from './openTypeGlyf';
|
|
6
|
+
import { readOpenTypeGlyphCount, readOpenTypeLocaFormat } from './openTypeMetrics';
|
|
7
|
+
import { createSyntheticFont, emptySyntheticGlyph, squareSyntheticGlyph } from './openTypeTestHelper';
|
|
8
|
+
import type { SyntheticGlyph } from './openTypeTestHelper';
|
|
9
|
+
import { readSfntTableDirectory } from './sfntTableDirectory';
|
|
10
|
+
|
|
11
|
+
function createPath(): Path {
|
|
12
|
+
return { commands: [], data: [], winding: 'evenOdd' };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Reads a glyph out of a synthetic font through the same steps the source does, so these tests exercise
|
|
16
|
+
// the real `loca`/`glyf` pair rather than a hand-built range array.
|
|
17
|
+
function readGlyph(glyphs: readonly SyntheticGlyph[], glyphIndex: number): { ok: boolean; path: Path } {
|
|
18
|
+
const font = createSyntheticFont({ glyphs });
|
|
19
|
+
const directory = readSfntTableDirectory(font)!;
|
|
20
|
+
const ranges = readOpenTypeGlyphRanges(
|
|
21
|
+
font,
|
|
22
|
+
directory,
|
|
23
|
+
readOpenTypeGlyphCount(font, directory),
|
|
24
|
+
readOpenTypeLocaFormat(font, directory),
|
|
25
|
+
)!;
|
|
26
|
+
const path = createPath();
|
|
27
|
+
return { ok: readOpenTypeGlyphOutline(path, font, directory, ranges, glyphIndex), path };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('readOpenTypeGlyphOutline', () => {
|
|
31
|
+
it('writes the winding rather than leaving whatever the caller had', () => {
|
|
32
|
+
// Seeded with the WRONG rule on purpose. A fresh `createPath` already defaults to nonZero, so a
|
|
33
|
+
// test that built one would pass whether or not this reader writes the field — and a caller
|
|
34
|
+
// reusing a scratch path across glyphs is exactly the case that keeps a stale value and renders
|
|
35
|
+
// a counter as a solid blob.
|
|
36
|
+
const path: Path = { commands: [], data: [], winding: 'evenOdd' };
|
|
37
|
+
const font = createSyntheticFont({ glyphs: [emptySyntheticGlyph(), squareSyntheticGlyph(100)] });
|
|
38
|
+
const directory = readSfntTableDirectory(font)!;
|
|
39
|
+
const ranges = readOpenTypeGlyphRanges(
|
|
40
|
+
font,
|
|
41
|
+
directory,
|
|
42
|
+
readOpenTypeGlyphCount(font, directory),
|
|
43
|
+
readOpenTypeLocaFormat(font, directory),
|
|
44
|
+
)!;
|
|
45
|
+
readOpenTypeGlyphOutline(path, font, directory, ranges, 1);
|
|
46
|
+
expect(path.winding).toBe('nonZero');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('emits one line per edge and closes, without a redundant segment back to the start', () => {
|
|
50
|
+
const { ok, path } = readGlyph([emptySyntheticGlyph(), squareSyntheticGlyph(100)], 1);
|
|
51
|
+
expect(ok).toBe(true);
|
|
52
|
+
// Four corners: a move, three lines, and a close that implies the fourth edge.
|
|
53
|
+
expect(path.commands).toEqual([
|
|
54
|
+
PathCommand.MOVE_TO,
|
|
55
|
+
PathCommand.LINE_TO,
|
|
56
|
+
PathCommand.LINE_TO,
|
|
57
|
+
PathCommand.LINE_TO,
|
|
58
|
+
PathCommand.CLOSE,
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// The rule that makes this format different from every other path format: two consecutive off-curve
|
|
63
|
+
// points imply an on-curve point exactly halfway between them, which the file omits because it is
|
|
64
|
+
// recoverable. A reader that takes the point list literally visibly cuts corners.
|
|
65
|
+
it('reconstructs the on-curve point implied between two consecutive off-curve points', () => {
|
|
66
|
+
const glyph: SyntheticGlyph = {
|
|
67
|
+
endPoints: [2],
|
|
68
|
+
points: [
|
|
69
|
+
[0, 0, true],
|
|
70
|
+
[100, 0, false],
|
|
71
|
+
[100, 100, false],
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
const { path } = readGlyph([emptySyntheticGlyph(), glyph], 1);
|
|
75
|
+
expect(path.commands).toEqual([PathCommand.MOVE_TO, PathCommand.CURVE_TO, PathCommand.CURVE_TO, PathCommand.CLOSE]);
|
|
76
|
+
// First curve ends at the midpoint of the two controls — (100,0) and (100,100) → (100,50), negated.
|
|
77
|
+
expect(path.data.slice(2, 6)).toEqual([100, -0, 100, -50]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('starts at the midpoint when a contour opens and closes off-curve', () => {
|
|
81
|
+
const glyph: SyntheticGlyph = {
|
|
82
|
+
endPoints: [2],
|
|
83
|
+
points: [
|
|
84
|
+
[0, 100, false],
|
|
85
|
+
[50, 0, true],
|
|
86
|
+
[100, 100, false],
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
const { path } = readGlyph([emptySyntheticGlyph(), glyph], 1);
|
|
90
|
+
// Neither the first nor the last point is on-curve, so the contour begins halfway between them:
|
|
91
|
+
// (0,100) and (100,100) → (50,100), negated to y-down.
|
|
92
|
+
expect(path.data.slice(0, 2)).toEqual([50, -100]);
|
|
93
|
+
expect(path.commands[0]).toBe(PathCommand.MOVE_TO);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('emits a quadratic through a single off-curve control', () => {
|
|
97
|
+
const glyph: SyntheticGlyph = {
|
|
98
|
+
endPoints: [2],
|
|
99
|
+
points: [
|
|
100
|
+
[0, 0, true],
|
|
101
|
+
[50, 100, false],
|
|
102
|
+
[100, 0, true],
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
const { path } = readGlyph([emptySyntheticGlyph(), glyph], 1);
|
|
106
|
+
expect(path.commands).toEqual([PathCommand.MOVE_TO, PathCommand.CURVE_TO, PathCommand.CLOSE]);
|
|
107
|
+
expect(path.data).toEqual([0, -0, 50, -100, 100, -0]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('reads a second contour into the same path', () => {
|
|
111
|
+
const glyph: SyntheticGlyph = {
|
|
112
|
+
endPoints: [3, 7],
|
|
113
|
+
points: [
|
|
114
|
+
[0, 0, true],
|
|
115
|
+
[10, 0, true],
|
|
116
|
+
[10, 10, true],
|
|
117
|
+
[0, 10, true],
|
|
118
|
+
[20, 20, true],
|
|
119
|
+
[30, 20, true],
|
|
120
|
+
[30, 30, true],
|
|
121
|
+
[20, 30, true],
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
const { path } = readGlyph([emptySyntheticGlyph(), glyph], 1);
|
|
125
|
+
expect(path.commands.filter((command) => command === PathCommand.MOVE_TO)).toHaveLength(2);
|
|
126
|
+
expect(path.commands.filter((command) => command === PathCommand.CLOSE)).toHaveLength(2);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('returns true with an empty path for a glyph with no contours', () => {
|
|
130
|
+
const { ok, path } = readGlyph([emptySyntheticGlyph()], 0);
|
|
131
|
+
expect(ok).toBe(true);
|
|
132
|
+
expect(path.commands).toEqual([]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('replaces the previous contents of out rather than appending to them', () => {
|
|
136
|
+
const font = createSyntheticFont({ glyphs: [emptySyntheticGlyph(), squareSyntheticGlyph(100)] });
|
|
137
|
+
const directory = readSfntTableDirectory(font)!;
|
|
138
|
+
const ranges = readOpenTypeGlyphRanges(font, directory, readOpenTypeGlyphCount(font, directory), 1)!;
|
|
139
|
+
const path = createPath();
|
|
140
|
+
readOpenTypeGlyphOutline(path, font, directory, ranges, 1);
|
|
141
|
+
const first = path.commands.length;
|
|
142
|
+
readOpenTypeGlyphOutline(path, font, directory, ranges, 1);
|
|
143
|
+
expect(path.commands).toHaveLength(first);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('returns false for an index outside the range table', () => {
|
|
147
|
+
expect(readGlyph([emptySyntheticGlyph()], 5).ok).toBe(false);
|
|
148
|
+
expect(readGlyph([emptySyntheticGlyph()], -1).ok).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('readOpenTypeGlyphRanges', () => {
|
|
153
|
+
it('yields one more entry than there are glyphs, so every glyph has an end', () => {
|
|
154
|
+
const font = createSyntheticFont({ glyphs: [emptySyntheticGlyph(), squareSyntheticGlyph(10)] });
|
|
155
|
+
const directory = readSfntTableDirectory(font)!;
|
|
156
|
+
expect(readOpenTypeGlyphRanges(font, directory, 2, 1)).toHaveLength(3);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('returns the sentinel when loca is absent', () => {
|
|
160
|
+
const font = createSyntheticFont({ omitTable: 'loca' });
|
|
161
|
+
expect(readOpenTypeGlyphRanges(font, readSfntTableDirectory(font)!, 1, 1)).toBeNull();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('returns the sentinel when loca is too short for the glyph count it must index', () => {
|
|
165
|
+
const font = createSyntheticFont({ glyphs: [emptySyntheticGlyph()] });
|
|
166
|
+
expect(readOpenTypeGlyphRanges(font, readSfntTableDirectory(font)!, 999, 1)).toBeNull();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { PathCommand } from '@flighthq/types/contract';
|
|
2
|
+
import type { Path } from '@flighthq/types/contract';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { createGlyphOutlineSourceFromOpenTypeFont, explainOpenTypeFont } from './openTypeGlyphOutlineSource';
|
|
6
|
+
import {
|
|
7
|
+
createSyntheticFont,
|
|
8
|
+
emptySyntheticGlyph,
|
|
9
|
+
encodeSyntheticWoff,
|
|
10
|
+
squareSyntheticGlyph,
|
|
11
|
+
} from './openTypeTestHelper';
|
|
12
|
+
import { readSfntTableDirectory } from './sfntTableDirectory';
|
|
13
|
+
|
|
14
|
+
// Every font here is assembled byte by byte by the helper. Nothing third-party is read, fetched, or
|
|
15
|
+
// committed, and each test states the table contents its assertion depends on.
|
|
16
|
+
// A WOFF is a real font behind a compression wrapper — a different remedy from unreadable bytes, which is
|
|
17
|
+
// why it gets its own reason.
|
|
18
|
+
// A font whose only outline table is CFF2 — a different charstring dialect with variation support, which
|
|
19
|
+
// this package deliberately does not read. It keeps the stated-absence treatment `CFF ` had until the
|
|
20
|
+
// charstring interpreter landed.
|
|
21
|
+
function cff2OnlyFont(): Uint8Array {
|
|
22
|
+
const font = createSyntheticFont({ flavor: 'opentype' });
|
|
23
|
+
const directory = readSfntTableDirectory(font)!;
|
|
24
|
+
const record = [...directory.tables.keys()].sort().indexOf('CFF ');
|
|
25
|
+
// Rename the table tag in place: same bytes, different tag, so the container is well-formed and only
|
|
26
|
+
// the dialect differs.
|
|
27
|
+
for (const [index, character] of [...'CFF2'].entries()) font[12 + record * 16 + index] = character.charCodeAt(0);
|
|
28
|
+
return font;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function woff2Bytes(): Uint8Array {
|
|
32
|
+
const bytes = new Uint8Array(20);
|
|
33
|
+
bytes.set([0x77, 0x4f, 0x46, 0x32]);
|
|
34
|
+
return bytes;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Every table present, but `head` states a zero unitsPerEm — the one value that cannot be defaulted,
|
|
38
|
+
// since it is the scale denominator every coordinate divides by.
|
|
39
|
+
function malformedUnitsPerEmFont(): Uint8Array {
|
|
40
|
+
const font = createSyntheticFont();
|
|
41
|
+
const head = readSfntTableDirectory(font)!.tables.get('head')!;
|
|
42
|
+
new DataView(font.buffer, font.byteOffset, font.byteLength).setUint16(head.offset + 18, 0);
|
|
43
|
+
return font;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function createPath(): Path {
|
|
47
|
+
return { commands: [], data: [], winding: 'evenOdd' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('createGlyphOutlineSourceFromOpenTypeFont', () => {
|
|
51
|
+
it('produces a source from a well-formed TrueType font', () => {
|
|
52
|
+
expect(createGlyphOutlineSourceFromOpenTypeFont(createSyntheticFont())).not.toBeNull();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('reports the font-wide metrics in design units, with descent as a positive distance', () => {
|
|
56
|
+
const source = createGlyphOutlineSourceFromOpenTypeFont(createSyntheticFont({ unitsPerEm: 2048 }));
|
|
57
|
+
// The fixture stores descender as -200, as the format requires; the seam flips it.
|
|
58
|
+
expect(source?.getGlyphOutlineMetrics()).toEqual({ ascent: 800, descent: 200, lineGap: 100, unitsPerEm: 2048 });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('maps a codepoint to its glyph index and returns -1 for one the font does not cover', () => {
|
|
62
|
+
const source = createGlyphOutlineSourceFromOpenTypeFont(
|
|
63
|
+
createSyntheticFont({
|
|
64
|
+
codepoints: new Map([[65, 1]]),
|
|
65
|
+
glyphs: [emptySyntheticGlyph(), squareSyntheticGlyph(100)],
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
expect(source?.getGlyphOutlineIndexForCodePoint(65)).toBe(1);
|
|
69
|
+
expect(source?.getGlyphOutlineIndexForCodePoint(66)).toBe(-1);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('reads a square contour into the path, with y negated into the downward convention', () => {
|
|
73
|
+
const source = createGlyphOutlineSourceFromOpenTypeFont(
|
|
74
|
+
createSyntheticFont({ glyphs: [emptySyntheticGlyph(), squareSyntheticGlyph(100)] }),
|
|
75
|
+
);
|
|
76
|
+
const path = createPath();
|
|
77
|
+
expect(source?.getGlyphOutline(path, 1)).toBe(true);
|
|
78
|
+
expect(path.commands).toEqual([
|
|
79
|
+
PathCommand.MOVE_TO,
|
|
80
|
+
PathCommand.LINE_TO,
|
|
81
|
+
PathCommand.LINE_TO,
|
|
82
|
+
PathCommand.LINE_TO,
|
|
83
|
+
PathCommand.CLOSE,
|
|
84
|
+
]);
|
|
85
|
+
// The font states y-up (0,0) (100,0) (100,100) (0,100); ink above the baseline is negative y here.
|
|
86
|
+
expect(path.data).toEqual([0, -0, 100, -0, 100, -100, 0, -100]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('returns true with an empty path for a glyph that legitimately has no outline', () => {
|
|
90
|
+
const source = createGlyphOutlineSourceFromOpenTypeFont(createSyntheticFont({ glyphs: [emptySyntheticGlyph()] }));
|
|
91
|
+
const path = createPath();
|
|
92
|
+
// True, not false: a space still has to advance, so its absence of ink must stay observable.
|
|
93
|
+
expect(source?.getGlyphOutline(path, 0)).toBe(true);
|
|
94
|
+
expect(path.commands).toEqual([]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('returns false for a glyph index the font does not have', () => {
|
|
98
|
+
const source = createGlyphOutlineSourceFromOpenTypeFont(createSyntheticFont());
|
|
99
|
+
expect(source?.getGlyphOutline(createPath(), 99)).toBe(false);
|
|
100
|
+
expect(source?.getGlyphOutline(createPath(), -1)).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('reports each glyph its own advance', () => {
|
|
104
|
+
const source = createGlyphOutlineSourceFromOpenTypeFont(
|
|
105
|
+
createSyntheticFont({ advances: [300, 750], glyphs: [emptySyntheticGlyph(), squareSyntheticGlyph(50)] }),
|
|
106
|
+
);
|
|
107
|
+
expect(source?.getGlyphOutlineAdvance(0)).toBe(300);
|
|
108
|
+
expect(source?.getGlyphOutlineAdvance(1)).toBe(750);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// The run-length rule in `hmtx`: glyphs past `numberOfHMetrics` repeat the last stated advance. A
|
|
112
|
+
// reader treating the table as flat gives every one of them a wrong width, which is how a monospaced
|
|
113
|
+
// or CJK font silently mis-measures.
|
|
114
|
+
it('repeats the last stated advance for glyphs past the long-metric count', () => {
|
|
115
|
+
const source = createGlyphOutlineSourceFromOpenTypeFont(
|
|
116
|
+
createSyntheticFont({
|
|
117
|
+
advances: [600],
|
|
118
|
+
glyphs: [emptySyntheticGlyph(), squareSyntheticGlyph(10), squareSyntheticGlyph(20)],
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
expect(source?.getGlyphOutlineAdvance(1)).toBe(600);
|
|
122
|
+
expect(source?.getGlyphOutlineAdvance(2)).toBe(600);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('returns the sentinel rather than throwing for bytes that are not a font', () => {
|
|
126
|
+
expect(
|
|
127
|
+
createGlyphOutlineSourceFromOpenTypeFont(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])),
|
|
128
|
+
).toBeNull();
|
|
129
|
+
expect(createGlyphOutlineSourceFromOpenTypeFont(new Uint8Array(0))).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// CFF outlines are read now. This assertion is the inverse of the one it replaces, and that inversion
|
|
133
|
+
// IS the feature: an .otf used to reject with `unsupported-outlines` and now produces a source.
|
|
134
|
+
it('produces a source from a CFF-outline font, which used to be a stated rejection', () => {
|
|
135
|
+
expect(createGlyphOutlineSourceFromOpenTypeFont(createSyntheticFont({ flavor: 'opentype' }))).not.toBeNull();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('reads a CFF glyph outline as cubic curves, end to end from the container', () => {
|
|
139
|
+
const charstring = new Uint8Array([139, 139, 21, 149, 139, 149, 149, 139, 149, 8, 14]);
|
|
140
|
+
const font = createSyntheticFont({ flavor: 'opentype', charstrings: [new Uint8Array([14]), charstring] });
|
|
141
|
+
const source = createGlyphOutlineSourceFromOpenTypeFont(font)!;
|
|
142
|
+
const path = createPath();
|
|
143
|
+
expect(source.getGlyphOutline(path, 1)).toBe(true);
|
|
144
|
+
expect(path.commands).toEqual([PathCommand.MOVE_TO, PathCommand.CUBIC_CURVE_TO, PathCommand.CLOSE]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// THE CID CASE, END TO END. Both glyphs call subroutine index -107, and each FD's pool holds a
|
|
148
|
+
// DIFFERENT subroutine — one drawing a horizontal line, one vertical. If both glyphs resolved against
|
|
149
|
+
// one pool they would draw the same shape, which is exactly the plausible-garbage failure the
|
|
150
|
+
// per-glyph binding prevents.
|
|
151
|
+
it('resolves the same subroutine index differently per glyph in a CID font', () => {
|
|
152
|
+
const call = new Uint8Array([139, 139, 21, 32, 10, 14]);
|
|
153
|
+
const font = createSyntheticFont({
|
|
154
|
+
charstrings: [call, call],
|
|
155
|
+
cid: {
|
|
156
|
+
fdSelect: [0, 1],
|
|
157
|
+
pools: [[new Uint8Array([149, 139, 5, 11])], [new Uint8Array([139, 149, 5, 11])]],
|
|
158
|
+
},
|
|
159
|
+
flavor: 'opentype',
|
|
160
|
+
});
|
|
161
|
+
const source = createGlyphOutlineSourceFromOpenTypeFont(font)!;
|
|
162
|
+
const first = createPath();
|
|
163
|
+
const second = createPath();
|
|
164
|
+
expect(source.getGlyphOutline(first, 0)).toBe(true);
|
|
165
|
+
expect(source.getGlyphOutline(second, 1)).toBe(true);
|
|
166
|
+
expect(first.data).not.toEqual(second.data);
|
|
167
|
+
// FD 0 draws +10 on x; FD 1 draws +10 on y, negated into the y-down convention.
|
|
168
|
+
expect(first.data).toEqual([0, -0, 10, -0]);
|
|
169
|
+
expect(second.data).toEqual([0, -0, 0, -10]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// WOFF END TO END. The whole design claim is that it needs no new outline code: unwrap once, and the
|
|
173
|
+
// directory, both flavors and CID all read the rebuilt sfnt without knowing it arrived wrapped.
|
|
174
|
+
it('produces a source from a WOFF, which used to be an unsupported container', () => {
|
|
175
|
+
const woff = encodeSyntheticWoff(
|
|
176
|
+
createSyntheticFont({ glyphs: [emptySyntheticGlyph(), squareSyntheticGlyph(100)] }),
|
|
177
|
+
);
|
|
178
|
+
const source = createGlyphOutlineSourceFromOpenTypeFont(woff)!;
|
|
179
|
+
expect(source).not.toBeNull();
|
|
180
|
+
const path = createPath();
|
|
181
|
+
expect(source.getGlyphOutline(path, 1)).toBe(true);
|
|
182
|
+
expect(path.data).toEqual([0, -0, 100, -0, 100, -100, 0, -100]);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('reads a CFF font through the WOFF wrapper, since the wrapper is flavor-agnostic', () => {
|
|
186
|
+
const woff = encodeSyntheticWoff(createSyntheticFont({ flavor: 'opentype' }));
|
|
187
|
+
expect(createGlyphOutlineSourceFromOpenTypeFont(woff)).not.toBeNull();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('still rejects CFF2, which is a different charstring dialect and remains a stated absence', () => {
|
|
191
|
+
const font = createSyntheticFont({ flavor: 'opentype', omitTable: 'CFF ' });
|
|
192
|
+
// Re-add only CFF2, so the font carries charstrings this package deliberately does not read.
|
|
193
|
+
expect(explainOpenTypeFont(font).reason).toBe('missing-required-table');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('returns the sentinel when a required table is missing', () => {
|
|
197
|
+
for (const table of ['cmap', 'head', 'hhea', 'hmtx', 'maxp', 'loca', 'glyf']) {
|
|
198
|
+
expect(createGlyphOutlineSourceFromOpenTypeFont(createSyntheticFont({ omitTable: table }))).toBeNull();
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('returns the sentinel for a truncated font rather than reading past the buffer', () => {
|
|
203
|
+
const font = createSyntheticFont({ glyphs: [emptySyntheticGlyph(), squareSyntheticGlyph(100)] });
|
|
204
|
+
expect(createGlyphOutlineSourceFromOpenTypeFont(font.subarray(0, font.length - 8))).toBeNull();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('explainOpenTypeFont', () => {
|
|
209
|
+
it('accepts a font the producer accepts, and the two never disagree', () => {
|
|
210
|
+
const font = createSyntheticFont();
|
|
211
|
+
const explanation = explainOpenTypeFont(font);
|
|
212
|
+
expect(explanation.accepted).toBe(true);
|
|
213
|
+
expect(explanation.reason).toBe('ok');
|
|
214
|
+
expect(createGlyphOutlineSourceFromOpenTypeFont(font)).not.toBeNull();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// ALL SEVEN REJECTION REASONS, EACH ASSERTING **BOTH** THE EXPLANATION AND THE PRODUCER ON THE SAME
|
|
218
|
+
// BYTES. Previously only the accept path was cross-checked, so an explanation could have named a reason
|
|
219
|
+
// the producer never failed for — or named one while the producer happily returned a source — and the
|
|
220
|
+
// suite would have stayed green. The two share one parse so they cannot disagree BY CONSTRUCTION, and
|
|
221
|
+
// probably-holds-by-construction is exactly the claim this pins instead of assuming.
|
|
222
|
+
it.each([
|
|
223
|
+
['too-short', new Uint8Array([0, 1, 0, 0])],
|
|
224
|
+
['unrecognized', new Uint8Array(20)],
|
|
225
|
+
['unsupported-container', woff2Bytes()],
|
|
226
|
+
['missing-required-table', createSyntheticFont({ omitTable: 'cmap' })],
|
|
227
|
+
['unsupported-outlines', cff2OnlyFont()],
|
|
228
|
+
['malformed-table', malformedUnitsPerEmFont()],
|
|
229
|
+
])('rejects with reason %s, and the producer returns the sentinel on the same bytes', (reason, bytes) => {
|
|
230
|
+
const explanation = explainOpenTypeFont(bytes);
|
|
231
|
+
expect(explanation.reason).toBe(reason);
|
|
232
|
+
expect(explanation.accepted).toBe(false);
|
|
233
|
+
expect(createGlyphOutlineSourceFromOpenTypeFont(bytes)).toBeNull();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// The seventh reason, `ok`, is the inverse pairing: the explanation accepts AND the producer succeeds.
|
|
237
|
+
it('accepts with reason ok, and the producer returns a source on the same bytes', () => {
|
|
238
|
+
const font = createSyntheticFont();
|
|
239
|
+
expect(explainOpenTypeFont(font).reason).toBe('ok');
|
|
240
|
+
expect(explainOpenTypeFont(font).accepted).toBe(true);
|
|
241
|
+
expect(createGlyphOutlineSourceFromOpenTypeFont(font)).not.toBeNull();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('reports the container it recognized even on success', () => {
|
|
245
|
+
expect(explainOpenTypeFont(createSyntheticFont()).format).toBe('truetype');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('accepts a CFF font now that its charstrings are read', () => {
|
|
249
|
+
const explanation = explainOpenTypeFont(createSyntheticFont({ flavor: 'opentype' }));
|
|
250
|
+
expect(explanation.reason).toBe('ok');
|
|
251
|
+
expect(explanation.accepted).toBe(true);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('names the specific missing table', () => {
|
|
255
|
+
expect(explainOpenTypeFont(createSyntheticFont({ omitTable: 'cmap' })).table).toBe('cmap');
|
|
256
|
+
expect(explainOpenTypeFont(createSyntheticFont({ omitTable: 'hmtx' })).table).toBe('hmtx');
|
|
257
|
+
expect(explainOpenTypeFont(createSyntheticFont({ omitTable: 'loca' })).table).toBe('loca');
|
|
258
|
+
expect(explainOpenTypeFont(createSyntheticFont({ omitTable: 'cmap' })).reason).toBe('missing-required-table');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('reports a missing glyf as a missing table when no charstring table stands in its place', () => {
|
|
262
|
+
const explanation = explainOpenTypeFont(createSyntheticFont({ omitTable: 'glyf' }));
|
|
263
|
+
expect(explanation.reason).toBe('missing-required-table');
|
|
264
|
+
expect(explanation.table).toBe('glyf');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('distinguishes bytes that are no font at all from a font it will not open', () => {
|
|
268
|
+
expect(explainOpenTypeFont(new Uint8Array(20)).reason).toBe('unrecognized');
|
|
269
|
+
// WOFF2 needs Brotli and a table-transform reversal, so it stays a container this package does not
|
|
270
|
+
// open — unlike WOFF, which is read now.
|
|
271
|
+
const woff2 = new Uint8Array(20);
|
|
272
|
+
woff2.set([0x77, 0x4f, 0x46, 0x32]);
|
|
273
|
+
expect(explainOpenTypeFont(woff2).reason).toBe('unsupported-container');
|
|
274
|
+
expect(explainOpenTypeFont(woff2).format).toBe('woff2');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// The decompressor is deliberately not bundled, so an unregistered one is a real outcome with a
|
|
278
|
+
// one-line remedy — distinct from a container needing a different producer entirely.
|
|
279
|
+
it('reports a missing decompressor as its own reason rather than an unsupported container', () => {
|
|
280
|
+
const woff = encodeSyntheticWoff(createSyntheticFont());
|
|
281
|
+
new DataView(woff.buffer).setUint32(44 + 8, 4);
|
|
282
|
+
expect(explainOpenTypeFont(woff).reason).toBe('missing-decompressor');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('reports too-short for bytes below the sfnt header', () => {
|
|
286
|
+
expect(explainOpenTypeFont(new Uint8Array([0, 1, 0, 0])).reason).toBe('too-short');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('counts declared tables against readable ones, localizing a truncation', () => {
|
|
290
|
+
const font = createSyntheticFont();
|
|
291
|
+
const whole = explainOpenTypeFont(font);
|
|
292
|
+
expect(whole.tableCount).toBe(whole.readableTableCount);
|
|
293
|
+
|
|
294
|
+
const truncated = explainOpenTypeFont(font.subarray(0, font.length - 8));
|
|
295
|
+
// The directory still declares every table; fewer of them now fit inside the file.
|
|
296
|
+
expect(truncated.readableTableCount).toBeLessThan(truncated.tableCount);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
readOpenTypeAdvances,
|
|
5
|
+
readOpenTypeGlyphCount,
|
|
6
|
+
readOpenTypeLocaFormat,
|
|
7
|
+
readOpenTypeMetrics,
|
|
8
|
+
} from './openTypeMetrics';
|
|
9
|
+
import { createSyntheticFont, emptySyntheticGlyph, squareSyntheticGlyph } from './openTypeTestHelper';
|
|
10
|
+
import { readSfntTableDirectory } from './sfntTableDirectory';
|
|
11
|
+
|
|
12
|
+
describe('readOpenTypeAdvances', () => {
|
|
13
|
+
it('reads one advance per glyph', () => {
|
|
14
|
+
const font = createSyntheticFont({
|
|
15
|
+
advances: [300, 750],
|
|
16
|
+
glyphs: [emptySyntheticGlyph(), squareSyntheticGlyph(10)],
|
|
17
|
+
});
|
|
18
|
+
expect([...readOpenTypeAdvances(font, readSfntTableDirectory(font)!, 2)!]).toEqual([300, 750]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// `hmtx` is run-length shaped: glyphs past `numberOfHMetrics` repeat the last stated advance. Reading
|
|
22
|
+
// it flat gives every one of them a wrong width, which is exactly how monospaced and CJK fonts stay
|
|
23
|
+
// small — so the bug would be invisible on a Latin test font and wrong on the fonts that use it.
|
|
24
|
+
it('repeats the last stated advance past the long-metric count rather than reading zero', () => {
|
|
25
|
+
const font = createSyntheticFont({
|
|
26
|
+
advances: [600],
|
|
27
|
+
glyphs: [emptySyntheticGlyph(), squareSyntheticGlyph(10), squareSyntheticGlyph(20)],
|
|
28
|
+
});
|
|
29
|
+
expect([...readOpenTypeAdvances(font, readSfntTableDirectory(font)!, 3)!]).toEqual([600, 600, 600]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('returns the sentinel when hmtx or hhea is absent', () => {
|
|
33
|
+
for (const table of ['hmtx', 'hhea']) {
|
|
34
|
+
const font = createSyntheticFont({ omitTable: table });
|
|
35
|
+
expect(readOpenTypeAdvances(font, readSfntTableDirectory(font)!, 1)).toBeNull();
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('readOpenTypeGlyphCount', () => {
|
|
41
|
+
it('reads the count maxp declares', () => {
|
|
42
|
+
const font = createSyntheticFont({ glyphs: [emptySyntheticGlyph(), squareSyntheticGlyph(10)] });
|
|
43
|
+
expect(readOpenTypeGlyphCount(font, readSfntTableDirectory(font)!)).toBe(2);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('returns -1 rather than a count when maxp is absent, so every index is out of range', () => {
|
|
47
|
+
const font = createSyntheticFont({ omitTable: 'maxp' });
|
|
48
|
+
expect(readOpenTypeGlyphCount(font, readSfntTableDirectory(font)!)).toBe(-1);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('readOpenTypeLocaFormat', () => {
|
|
53
|
+
it('reads the index-to-loc format head declares', () => {
|
|
54
|
+
const font = createSyntheticFont();
|
|
55
|
+
expect(readOpenTypeLocaFormat(font, readSfntTableDirectory(font)!)).toBe(1);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('returns -1 for a value that is neither of the two the format defines', () => {
|
|
59
|
+
const font = createSyntheticFont();
|
|
60
|
+
const directory = readSfntTableDirectory(font)!;
|
|
61
|
+
// Guessing here would produce plausible garbage outlines instead of a clean rejection.
|
|
62
|
+
new DataView(font.buffer).setInt16(directory.tables.get('head')!.offset + 50, 7);
|
|
63
|
+
expect(readOpenTypeLocaFormat(font, directory)).toBe(-1);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('returns -1 when head is absent', () => {
|
|
67
|
+
const font = createSyntheticFont({ omitTable: 'head' });
|
|
68
|
+
expect(readOpenTypeLocaFormat(font, readSfntTableDirectory(font)!)).toBe(-1);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('readOpenTypeMetrics', () => {
|
|
73
|
+
it('reads the design-unit scale from head', () => {
|
|
74
|
+
const font = createSyntheticFont({ unitsPerEm: 2048 });
|
|
75
|
+
expect(readOpenTypeMetrics(font, readSfntTableDirectory(font)!)?.unitsPerEm).toBe(2048);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// `hhea` states descender as a negative distance; `GlyphOutlineMetrics` documents both ascent and
|
|
79
|
+
// descent as POSITIVE distances from the baseline. The flip happens once, here, at the seam.
|
|
80
|
+
it('reports descent as a positive distance although the table stores it negative', () => {
|
|
81
|
+
const font = createSyntheticFont();
|
|
82
|
+
expect(readOpenTypeMetrics(font, readSfntTableDirectory(font)!)).toEqual({
|
|
83
|
+
ascent: 800,
|
|
84
|
+
descent: 200,
|
|
85
|
+
lineGap: 100,
|
|
86
|
+
unitsPerEm: 1000,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('returns the sentinel for a zero unitsPerEm rather than a scale that divides by zero', () => {
|
|
91
|
+
const font = createSyntheticFont();
|
|
92
|
+
const directory = readSfntTableDirectory(font)!;
|
|
93
|
+
new DataView(font.buffer).setUint16(directory.tables.get('head')!.offset + 18, 0);
|
|
94
|
+
expect(readOpenTypeMetrics(font, directory)).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('returns the sentinel when head or hhea is absent', () => {
|
|
98
|
+
for (const table of ['head', 'hhea']) {
|
|
99
|
+
const font = createSyntheticFont({ omitTable: table });
|
|
100
|
+
expect(readOpenTypeMetrics(font, readSfntTableDirectory(font)!)).toBeNull();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { assembleSfntFont, computeSfntTableChecksum, packSfntTag } from './sfntAssembly';
|
|
4
|
+
|
|
5
|
+
describe('assembleSfntFont', () => {
|
|
6
|
+
it('writes the directory in tag order whatever order it was given', () => {
|
|
7
|
+
// Reversed on purpose. A pre-sorted input would pass whether or not anything sorted, which is how
|
|
8
|
+
// the WOFF tag-order test was once vacuous.
|
|
9
|
+
const font = assembleSfntFont(0x00010000, [
|
|
10
|
+
{ data: Uint8Array.from([9]), tag: packSfntTag('name') },
|
|
11
|
+
{ data: Uint8Array.from([7]), tag: packSfntTag('head') },
|
|
12
|
+
{ data: Uint8Array.from([8]), tag: packSfntTag('cmap') },
|
|
13
|
+
]);
|
|
14
|
+
const view = new DataView(font.buffer);
|
|
15
|
+
const tags = Array.from({ length: view.getUint16(4) }, (_, index) => view.getUint32(12 + index * 16));
|
|
16
|
+
expect(tags).toEqual([packSfntTag('cmap'), packSfntTag('head'), packSfntTag('name')]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('starts every table on a four-byte boundary', () => {
|
|
20
|
+
// Lengths chosen so that unpadded placement would land tables at odd offsets — without this the
|
|
21
|
+
// assertion would hold whether or not any padding ran.
|
|
22
|
+
const font = assembleSfntFont(0x00010000, [
|
|
23
|
+
{ data: Uint8Array.from([1]), tag: packSfntTag('aaaa') },
|
|
24
|
+
{ data: Uint8Array.from([2, 3, 4]), tag: packSfntTag('bbbb') },
|
|
25
|
+
{ data: Uint8Array.from([5, 6]), tag: packSfntTag('cccc') },
|
|
26
|
+
]);
|
|
27
|
+
const view = new DataView(font.buffer);
|
|
28
|
+
const count = view.getUint16(4);
|
|
29
|
+
const offsets = Array.from({ length: count }, (_, index) => view.getUint32(12 + index * 16 + 8));
|
|
30
|
+
expect(offsets.filter((offset) => offset % 4 !== 0)).toEqual([]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('preserves each table byte for byte at its declared offset and length', () => {
|
|
34
|
+
const payload = Uint8Array.from([11, 22, 33, 44, 55]);
|
|
35
|
+
const font = assembleSfntFont(0x4f54544f, [{ data: payload, tag: packSfntTag('CFF ') }]);
|
|
36
|
+
const view = new DataView(font.buffer);
|
|
37
|
+
const offset = view.getUint32(12 + 8);
|
|
38
|
+
const length = view.getUint32(12 + 12);
|
|
39
|
+
expect(length).toBe(payload.byteLength);
|
|
40
|
+
expect([...font.subarray(offset, offset + length)]).toEqual([...payload]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('writes each table a real checksum rather than a zero', () => {
|
|
44
|
+
const payload = Uint8Array.from([0, 0, 0, 7]);
|
|
45
|
+
const font = assembleSfntFont(0x00010000, [{ data: payload, tag: packSfntTag('cmap') }]);
|
|
46
|
+
const view = new DataView(font.buffer);
|
|
47
|
+
expect(view.getUint32(12 + 4)).toBe(7);
|
|
48
|
+
expect(view.getUint32(12 + 4)).toBe(computeSfntTableChecksum(payload));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('carries the flavor it was given rather than inferring one', () => {
|
|
52
|
+
const font = assembleSfntFont(0x4f54544f, [{ data: Uint8Array.from([0]), tag: packSfntTag('CFF ') }]);
|
|
53
|
+
expect(new DataView(font.buffer).getUint32(0)).toBe(0x4f54544f);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('computeSfntTableChecksum', () => {
|
|
58
|
+
it('sums big-endian words', () => {
|
|
59
|
+
expect(computeSfntTableChecksum(Uint8Array.from([0, 0, 0, 1, 0, 0, 0, 2]))).toBe(3);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('pads a partial final word with zeros rather than dropping it', () => {
|
|
63
|
+
// [0,0,1] is one short of a word. Dropping the tail would give 0; padding gives 0x100.
|
|
64
|
+
expect(computeSfntTableChecksum(Uint8Array.from([0, 0, 1]))).toBe(0x100);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('treats head checkSumAdjustment as zero, which is what stops every real font mismatching', () => {
|
|
68
|
+
// Byte 8 begins checkSumAdjustment. The same bytes must sum differently for head than for any
|
|
69
|
+
// other table, so the two calls below are what separates the exception from a no-op.
|
|
70
|
+
const head = new Uint8Array(16);
|
|
71
|
+
head[8] = 0xff;
|
|
72
|
+
head[9] = 0xff;
|
|
73
|
+
expect(computeSfntTableChecksum(head, false)).toBe(0xffff0000);
|
|
74
|
+
expect(computeSfntTableChecksum(head, true)).toBe(0);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('wraps modulo 2^32 rather than growing past a uint32', () => {
|
|
78
|
+
const big = Uint8Array.from([0xff, 0xff, 0xff, 0xff, 0, 0, 0, 2]);
|
|
79
|
+
expect(computeSfntTableChecksum(big)).toBe(1);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('packSfntTag', () => {
|
|
84
|
+
it('packs four characters big-endian', () => {
|
|
85
|
+
expect(packSfntTag('glyf')).toBe(0x676c7966);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('pads a short tag with spaces, which is how the format spells its own', () => {
|
|
89
|
+
expect(packSfntTag('cvt')).toBe(packSfntTag('cvt '));
|
|
90
|
+
expect(packSfntTag('cvt')).toBe(0x63767420);
|
|
91
|
+
});
|
|
92
|
+
});
|