@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,64 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { createSyntheticFont } from './openTypeTestHelper';
|
|
4
|
+
import { readSfntTableDirectory, readSfntTag } from './sfntTableDirectory';
|
|
5
|
+
|
|
6
|
+
describe('readSfntTableDirectory', () => {
|
|
7
|
+
it('reads every table a well-formed font declares', () => {
|
|
8
|
+
const directory = readSfntTableDirectory(createSyntheticFont())!;
|
|
9
|
+
expect(directory.tables.has('glyf')).toBe(true);
|
|
10
|
+
expect(directory.tables.has('cmap')).toBe(true);
|
|
11
|
+
expect(directory.declaredTableCount).toBe(directory.tables.size);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('reports the container flavor from the sfnt version', () => {
|
|
15
|
+
expect(readSfntTableDirectory(createSyntheticFont())?.sfntVersion).toBe(0x00010000);
|
|
16
|
+
// 'OTTO' — the same directory layout, PostScript outlines behind it.
|
|
17
|
+
expect(readSfntTableDirectory(createSyntheticFont({ flavor: 'opentype' }))?.sfntVersion).toBe(0x4f54544f);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('gives each table a byte range that lies inside the file', () => {
|
|
21
|
+
const font = createSyntheticFont();
|
|
22
|
+
for (const range of readSfntTableDirectory(font)!.tables.values()) {
|
|
23
|
+
expect(range.offset + range.length).toBeLessThanOrEqual(font.byteLength);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// The distinction the explanation is built on: a truncated file still DECLARES every table, and the
|
|
28
|
+
// gap between what it declares and what is readable is what localizes the damage.
|
|
29
|
+
it('keeps the declared count while dropping tables that fall outside a truncated file', () => {
|
|
30
|
+
const font = createSyntheticFont();
|
|
31
|
+
const whole = readSfntTableDirectory(font)!;
|
|
32
|
+
const truncated = readSfntTableDirectory(font.subarray(0, font.length - 8))!;
|
|
33
|
+
expect(truncated.declaredTableCount).toBe(whole.declaredTableCount);
|
|
34
|
+
expect(truncated.tables.size).toBeLessThan(whole.tables.size);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('returns the sentinel for bytes shorter than the header', () => {
|
|
38
|
+
expect(readSfntTableDirectory(new Uint8Array(11))).toBeNull();
|
|
39
|
+
expect(readSfntTableDirectory(new Uint8Array(0))).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('stops rather than reading past the buffer when the header overstates the table count', () => {
|
|
43
|
+
const bytes = new Uint8Array(12);
|
|
44
|
+
new DataView(bytes.buffer).setUint16(4, 500);
|
|
45
|
+
const directory = readSfntTableDirectory(bytes)!;
|
|
46
|
+
expect(directory.declaredTableCount).toBe(500);
|
|
47
|
+
expect(directory.tables.size).toBe(0);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('readSfntTag', () => {
|
|
52
|
+
it('reads four bytes as the characters every specification spells a tag with', () => {
|
|
53
|
+
expect(readSfntTag(new Uint8Array([0x67, 0x6c, 0x79, 0x66]), 0)).toBe('glyf');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('preserves a significant trailing space', () => {
|
|
57
|
+
// `CFF ` is four characters, the last one a space. Trimming it makes the tag miss its own table.
|
|
58
|
+
expect(readSfntTag(new Uint8Array([0x43, 0x46, 0x46, 0x20]), 0)).toBe('CFF ');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('reads from the given offset rather than the start', () => {
|
|
62
|
+
expect(readSfntTag(new Uint8Array([0, 0, 0x68, 0x65, 0x61, 0x64]), 2)).toBe('head');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Compression } from '@flighthq/types/contract';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { createSyntheticFont, encodeSyntheticWoff2 } from './openTypeTestHelper';
|
|
5
|
+
import { readSfntTableDirectory } from './sfntTableDirectory';
|
|
6
|
+
import { readWoff2Font, readWoff2TableDirectory, WOFF2_COMPRESSION } from './woff2Font';
|
|
7
|
+
|
|
8
|
+
// The synthetic container stores its stream uncompressed, so the whole container path is reachable with
|
|
9
|
+
// an identity decompressor and no Brotli implementation anywhere near the suite.
|
|
10
|
+
const identity = (compressed: Readonly<Uint8Array>): Uint8Array => compressed as Uint8Array;
|
|
11
|
+
|
|
12
|
+
describe('readWoff2Font', () => {
|
|
13
|
+
it('rebuilds an sfnt carrying the same tables as the original', () => {
|
|
14
|
+
const original = createSyntheticFont();
|
|
15
|
+
const rebuilt = readWoff2Font(encodeSyntheticWoff2(original), identity, null)!;
|
|
16
|
+
const before = readSfntTableDirectory(original)!;
|
|
17
|
+
const after = readSfntTableDirectory(rebuilt)!;
|
|
18
|
+
expect([...after.tables.keys()].sort()).toEqual([...before.tables.keys()].sort());
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('preserves each table byte for byte through the unwrap', () => {
|
|
22
|
+
const original = createSyntheticFont();
|
|
23
|
+
const rebuilt = readWoff2Font(encodeSyntheticWoff2(original), identity, null)!;
|
|
24
|
+
const before = readSfntTableDirectory(original)!;
|
|
25
|
+
const after = readSfntTableDirectory(rebuilt)!;
|
|
26
|
+
for (const [tag, table] of before.tables) {
|
|
27
|
+
const mirror = after.tables.get(tag)!;
|
|
28
|
+
expect([...rebuilt.subarray(mirror.offset, mirror.offset + mirror.length)]).toEqual([
|
|
29
|
+
...original.subarray(table.offset, table.offset + table.length),
|
|
30
|
+
]);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('carries the original flavor through, rather than inferring one', () => {
|
|
35
|
+
const original = createSyntheticFont({ flavor: 'opentype' });
|
|
36
|
+
const rebuilt = readWoff2Font(encodeSyntheticWoff2(original), identity, null)!;
|
|
37
|
+
expect(new DataView(rebuilt.buffer).getUint32(0)).toBe(0x4f54544f);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('returns the sentinel when no decompressor is available', () => {
|
|
41
|
+
expect(readWoff2Font(encodeSyntheticWoff2(createSyntheticFont()), null, null)).toBeNull();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('returns the sentinel for a transformed table when no transform reversal is supplied', () => {
|
|
45
|
+
// Refusing is the point: emitting the transformed bytes under the table's own tag would produce a
|
|
46
|
+
// font whose directory says `glyf` over something that is not one.
|
|
47
|
+
const wrapped = encodeSyntheticWoff2(createSyntheticFont(), ['glyf']);
|
|
48
|
+
expect(readWoff2Font(wrapped, identity, null)).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('hands a transformed table to the reversal and uses what it returns', () => {
|
|
52
|
+
const replacement = Uint8Array.from([1, 2, 3, 4]);
|
|
53
|
+
const seen: string[] = [];
|
|
54
|
+
const wrapped = encodeSyntheticWoff2(createSyntheticFont(), ['glyf']);
|
|
55
|
+
const rebuilt = readWoff2Font(wrapped, identity, (tag) => {
|
|
56
|
+
seen.push(tag);
|
|
57
|
+
return replacement;
|
|
58
|
+
})!;
|
|
59
|
+
expect(seen).toEqual(['glyf']);
|
|
60
|
+
const glyf = readSfntTableDirectory(rebuilt)!.tables.get('glyf')!;
|
|
61
|
+
expect([...rebuilt.subarray(glyf.offset, glyf.offset + glyf.length)]).toEqual([...replacement]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('returns the sentinel when the transform reversal declines', () => {
|
|
65
|
+
const wrapped = encodeSyntheticWoff2(createSyntheticFont(), ['glyf']);
|
|
66
|
+
expect(readWoff2Font(wrapped, identity, () => null)).toBeNull();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('returns the sentinel for a truncated container rather than a partial font', () => {
|
|
70
|
+
const wrapped = encodeSyntheticWoff2(createSyntheticFont());
|
|
71
|
+
expect(readWoff2Font(wrapped.subarray(0, 40), identity, null)).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('readWoff2TableDirectory', () => {
|
|
76
|
+
it('resolves tags carried as an index into the known-tag table', () => {
|
|
77
|
+
const directory = readWoff2TableDirectory(encodeSyntheticWoff2(createSyntheticFont()))!;
|
|
78
|
+
expect(directory.entries.map((entry) => entry.tag)).toContain('glyf');
|
|
79
|
+
expect(directory.entries.map((entry) => entry.tag)).toContain('cmap');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('points streamStart past the directory rather than at a guess from the end', () => {
|
|
83
|
+
const wrapped = encodeSyntheticWoff2(createSyntheticFont());
|
|
84
|
+
const directory = readWoff2TableDirectory(wrapped)!;
|
|
85
|
+
expect(directory.streamStart).toBeGreaterThan(48);
|
|
86
|
+
// The stream runs from there to the end of the file, so the two must agree exactly. Deriving the
|
|
87
|
+
// start by subtracting instead lands in the format's trailing padding.
|
|
88
|
+
expect(directory.streamStart + directory.totalUncompressedLength).toBe(wrapped.byteLength);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('reads the transform flag with the sense inverted for the glyph pair', () => {
|
|
92
|
+
// `glyf` is marked transformed here and `cmap` is not. Both are encoded with the version whose
|
|
93
|
+
// meaning is opposite between them, so a reader applying one rule to both gets both answers wrong.
|
|
94
|
+
const directory = readWoff2TableDirectory(encodeSyntheticWoff2(createSyntheticFont(), ['glyf']))!;
|
|
95
|
+
const glyf = directory.entries.find((entry) => entry.tag === 'glyf')!;
|
|
96
|
+
const cmap = directory.entries.find((entry) => entry.tag === 'cmap')!;
|
|
97
|
+
expect(glyf.transformed).toBe(true);
|
|
98
|
+
expect(glyf.transformVersion).toBe(0);
|
|
99
|
+
expect(cmap.transformed).toBe(false);
|
|
100
|
+
expect(cmap.transformVersion).toBe(0);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('treats the glyph pair as untransformed only at version three', () => {
|
|
104
|
+
const directory = readWoff2TableDirectory(encodeSyntheticWoff2(createSyntheticFont()))!;
|
|
105
|
+
const glyf = directory.entries.find((entry) => entry.tag === 'glyf')!;
|
|
106
|
+
expect(glyf.transformVersion).toBe(3);
|
|
107
|
+
expect(glyf.transformed).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('returns the sentinel for a container declaring no tables', () => {
|
|
111
|
+
const wrapped = encodeSyntheticWoff2(createSyntheticFont());
|
|
112
|
+
new DataView(wrapped.buffer).setUint16(12, 0);
|
|
113
|
+
expect(readWoff2TableDirectory(wrapped)).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('returns the sentinel when the directory runs past the buffer', () => {
|
|
117
|
+
expect(readWoff2TableDirectory(encodeSyntheticWoff2(createSyntheticFont()).subarray(0, 50))).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('WOFF2_COMPRESSION', () => {
|
|
122
|
+
it('names Brotli through the shared vocabulary so a caller registers what this asks for', () => {
|
|
123
|
+
expect(WOFF2_COMPRESSION).toBe(Compression.Brotli);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { readWoff2GlyfStreams, readWoff2Short } from './woff2GlyfTransform';
|
|
4
|
+
|
|
5
|
+
// Builds a transformed `glyf` header over seven streams of the given sizes, filled with a distinct byte
|
|
6
|
+
// each so a mis-ordered carve shows up as the wrong contents rather than only the wrong length.
|
|
7
|
+
function transformedGlyf(sizes: readonly number[], glyphCount = 3, indexFormat = 0): Uint8Array {
|
|
8
|
+
const total = 36 + sizes.reduce((sum, size) => sum + size, 0);
|
|
9
|
+
const out = new Uint8Array(total);
|
|
10
|
+
const view = new DataView(out.buffer);
|
|
11
|
+
view.setUint16(4, glyphCount);
|
|
12
|
+
view.setUint16(6, indexFormat);
|
|
13
|
+
sizes.forEach((size, index) => view.setUint32(8 + index * 4, size));
|
|
14
|
+
let at = 36;
|
|
15
|
+
sizes.forEach((size, index) => {
|
|
16
|
+
out.fill(index + 1, at, at + size);
|
|
17
|
+
at += size;
|
|
18
|
+
});
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('readWoff2GlyfStreams', () => {
|
|
23
|
+
it('carves the seven streams in their declared order', () => {
|
|
24
|
+
// Distinct sizes AND distinct fills: equal sizes would let a wrongly-ordered carve pass.
|
|
25
|
+
const streams = readWoff2GlyfStreams(transformedGlyf([6, 1, 2, 3, 4, 5, 7]))!;
|
|
26
|
+
expect(streams.nContourStream.byteLength).toBe(6);
|
|
27
|
+
expect(streams.nPointsStream.byteLength).toBe(1);
|
|
28
|
+
expect(streams.flagStream.byteLength).toBe(2);
|
|
29
|
+
expect(streams.glyphStream.byteLength).toBe(3);
|
|
30
|
+
expect(streams.compositeStream.byteLength).toBe(4);
|
|
31
|
+
expect(streams.bboxStream.byteLength).toBe(5);
|
|
32
|
+
expect(streams.instructionStream.byteLength).toBe(7);
|
|
33
|
+
// Each stream was filled with its own index, so this pins WHICH bytes each name got.
|
|
34
|
+
expect([...streams.nContourStream]).toEqual([1, 1, 1, 1, 1, 1]);
|
|
35
|
+
expect([...streams.instructionStream]).toEqual([7, 7, 7, 7, 7, 7, 7]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('carries the glyph count and loca width from the header', () => {
|
|
39
|
+
const streams = readWoff2GlyfStreams(transformedGlyf([2, 1, 1, 1, 1, 1, 1], 41, 1))!;
|
|
40
|
+
expect(streams.glyphCount).toBe(41);
|
|
41
|
+
expect(streams.indexFormat).toBe(1);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('returns the sentinel when the sizes do not account for the table exactly', () => {
|
|
45
|
+
// A truncated stream still decodes into real-looking contour counts, so a partial carve would be
|
|
46
|
+
// silently wrong rather than visibly broken.
|
|
47
|
+
const short = transformedGlyf([4, 1, 1, 1, 1, 1, 1]).subarray(0, 40);
|
|
48
|
+
expect(readWoff2GlyfStreams(short)).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('returns the sentinel when the table carries bytes no stream claims', () => {
|
|
52
|
+
const table = transformedGlyf([4, 1, 1, 1, 1, 1, 1]);
|
|
53
|
+
const padded = new Uint8Array(table.byteLength + 3);
|
|
54
|
+
padded.set(table);
|
|
55
|
+
expect(readWoff2GlyfStreams(padded)).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('returns the sentinel for a table shorter than its own header', () => {
|
|
59
|
+
expect(readWoff2GlyfStreams(new Uint8Array(20))).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('readWoff2Short', () => {
|
|
64
|
+
it('reads a plain byte below the first escape code', () => {
|
|
65
|
+
const cursor = { at: 0 };
|
|
66
|
+
expect(readWoff2Short(Uint8Array.from([200]), cursor, 1)).toBe(200);
|
|
67
|
+
expect(cursor.at).toBe(1);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('reads the two-byte word form', () => {
|
|
71
|
+
const cursor = { at: 0 };
|
|
72
|
+
expect(readWoff2Short(Uint8Array.from([253, 0x12, 0x34]), cursor, 3)).toBe(0x1234);
|
|
73
|
+
expect(cursor.at).toBe(3);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('offsets the two one-more-byte forms by different amounts', () => {
|
|
77
|
+
// 254 and 255 differ only in their offset, so a reader that swapped them would still return a
|
|
78
|
+
// plausible small number. Pinning both against the same payload byte is what separates them.
|
|
79
|
+
expect(readWoff2Short(Uint8Array.from([255, 10]), { at: 0 }, 2)).toBe(263);
|
|
80
|
+
expect(readWoff2Short(Uint8Array.from([254, 10]), { at: 0 }, 2)).toBe(516);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('returns the sentinel rather than a short value when the stream ends mid-escape', () => {
|
|
84
|
+
expect(readWoff2Short(Uint8Array.from([253, 0x12]), { at: 0 }, 2)).toBe(-1);
|
|
85
|
+
expect(readWoff2Short(Uint8Array.from([255]), { at: 0 }, 1)).toBe(-1);
|
|
86
|
+
expect(readWoff2Short(Uint8Array.from([]), { at: 0 }, 0)).toBe(-1);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { registerDeflateDecompressor, unregisterDecompressor } from '@flighthq/compression/contract';
|
|
2
|
+
import { Compression } from '@flighthq/types/contract';
|
|
3
|
+
import type { Path } from '@flighthq/types/contract';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { createGlyphOutlineSourceFromOpenTypeFont } from './openTypeGlyphOutlineSource';
|
|
7
|
+
import {
|
|
8
|
+
createSyntheticFont,
|
|
9
|
+
encodeSyntheticWoff,
|
|
10
|
+
squareSyntheticGlyph,
|
|
11
|
+
emptySyntheticGlyph,
|
|
12
|
+
} from './openTypeTestHelper';
|
|
13
|
+
import { readSfntTableDirectory } from './sfntTableDirectory';
|
|
14
|
+
import { readWoffChecksumMismatches, readWoffFont } from './woffFont';
|
|
15
|
+
|
|
16
|
+
afterEach(() => unregisterDecompressor(Compression.Deflate));
|
|
17
|
+
|
|
18
|
+
describe('readWoffChecksumMismatches', () => {
|
|
19
|
+
it('reports nothing when the container states its tables truthfully', () => {
|
|
20
|
+
expect(readWoffChecksumMismatches(encodeSyntheticWoff(createSyntheticFont()), null)).toEqual([]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('names the table, what the file claimed, and what the bytes actually sum to', () => {
|
|
24
|
+
const woff = encodeSyntheticWoff(createSyntheticFont());
|
|
25
|
+
// Corrupt the FIRST entry's stored checksum only, so the report must single it out rather than
|
|
26
|
+
// flagging everything — a check that reported all tables would pass a weaker assertion.
|
|
27
|
+
const view = new DataView(woff.buffer);
|
|
28
|
+
const tag = view.getUint32(44);
|
|
29
|
+
view.setUint32(44 + 16, 0xdeadbeef);
|
|
30
|
+
const found = readWoffChecksumMismatches(woff, null);
|
|
31
|
+
expect(found.length).toBe(1);
|
|
32
|
+
expect(found[0]!.stored).toBe(0xdeadbeef);
|
|
33
|
+
expect(found[0]!.computed).not.toBe(0xdeadbeef);
|
|
34
|
+
expect(found[0]!.tag).toBe(
|
|
35
|
+
String.fromCharCode((tag >>> 24) & 0xff, (tag >>> 16) & 0xff, (tag >>> 8) & 0xff, tag & 0xff),
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('still loads the font it reported a mismatch for, rather than refusing it', () => {
|
|
40
|
+
// The whole point of the seam: a mismatch is information, not a verdict.
|
|
41
|
+
const woff = encodeSyntheticWoff(createSyntheticFont());
|
|
42
|
+
new DataView(woff.buffer).setUint32(44 + 16, 0xdeadbeef);
|
|
43
|
+
expect(readWoffChecksumMismatches(woff, null).length).toBe(1);
|
|
44
|
+
expect(readWoffFont(woff, null)).not.toBeNull();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('returns an empty report for a container it cannot read at all', () => {
|
|
48
|
+
expect(readWoffChecksumMismatches(new Uint8Array(8), null)).toEqual([]);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('readWoffFont', () => {
|
|
53
|
+
it('produces the same glyph outlines as the uncompressed font it wraps', () => {
|
|
54
|
+
// The end-to-end falsifier: byte-equality of table CONTENTS is a stronger claim per table, but it
|
|
55
|
+
// never runs the outline readers. This drives the whole producer — directory, offsets, `loca`,
|
|
56
|
+
// `glyf` — through both the plain font and its wrapper and compares what a caller actually gets.
|
|
57
|
+
const original = createSyntheticFont({ glyphs: [emptySyntheticGlyph(), squareSyntheticGlyph(100)] });
|
|
58
|
+
const rebuilt = readWoffFont(encodeSyntheticWoff(original), null)!;
|
|
59
|
+
const before = createGlyphOutlineSourceFromOpenTypeFont(original)!;
|
|
60
|
+
const after = createGlyphOutlineSourceFromOpenTypeFont(rebuilt)!;
|
|
61
|
+
const a: Path = { commands: [], data: [], winding: 'nonZero' };
|
|
62
|
+
const b: Path = { commands: [], data: [], winding: 'nonZero' };
|
|
63
|
+
expect(before.getGlyphOutline(a, 1)).toBe(true);
|
|
64
|
+
expect(after.getGlyphOutline(b, 1)).toBe(true);
|
|
65
|
+
expect(b.commands).toEqual(a.commands);
|
|
66
|
+
expect(b.data).toEqual(a.data);
|
|
67
|
+
expect(a.commands.length).toBeGreaterThan(0);
|
|
68
|
+
expect(after.getGlyphOutlineAdvance(1)).toBe(before.getGlyphOutlineAdvance(1));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('rebuilds an sfnt whose tables match the original', () => {
|
|
72
|
+
const original = createSyntheticFont();
|
|
73
|
+
const rebuilt = readWoffFont(encodeSyntheticWoff(original), null)!;
|
|
74
|
+
const before = readSfntTableDirectory(original)!;
|
|
75
|
+
const after = readSfntTableDirectory(rebuilt)!;
|
|
76
|
+
expect([...after.tables.keys()].sort()).toEqual([...before.tables.keys()].sort());
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('preserves each table byte for byte through the unwrap', () => {
|
|
80
|
+
const original = createSyntheticFont();
|
|
81
|
+
const rebuilt = readWoffFont(encodeSyntheticWoff(original), null)!;
|
|
82
|
+
const before = readSfntTableDirectory(original)!;
|
|
83
|
+
const after = readSfntTableDirectory(rebuilt)!;
|
|
84
|
+
for (const [tag, range] of before.tables) {
|
|
85
|
+
const other = after.tables.get(tag)!;
|
|
86
|
+
expect(rebuilt.subarray(other.offset, other.offset + other.length)).toEqual(
|
|
87
|
+
original.subarray(range.offset, range.offset + range.length),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// The wrapper carries the sfnt version the tables belonged to, so a wrapped CFF font must come out
|
|
93
|
+
// declaring itself a CFF font rather than one inferred from which tables happen to be present.
|
|
94
|
+
it('carries the original flavor through, rather than inferring one', () => {
|
|
95
|
+
const cff = createSyntheticFont({ flavor: 'opentype' });
|
|
96
|
+
const rebuilt = readWoffFont(encodeSyntheticWoff(cff), null)!;
|
|
97
|
+
expect(readSfntTableDirectory(rebuilt)?.sfntVersion).toBe(0x4f54544f);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// The sfnt directory is defined as tag-ordered, so a reader entitled to binary-search it would find the
|
|
101
|
+
// wrong table if the rebuild preserved WOFF's order instead.
|
|
102
|
+
it('writes the rebuilt directory in tag order even when the container is not', () => {
|
|
103
|
+
// The container is built in DESCENDING tag order on purpose. A sorted fixture would let this pass
|
|
104
|
+
// whether or not the rebuild sorted anything, which is how the first version of this test was
|
|
105
|
+
// useless — mutation testing found it, not review.
|
|
106
|
+
const rebuilt = readWoffFont(encodeSyntheticWoff(createSyntheticFont(), true), null)!;
|
|
107
|
+
const view = new DataView(rebuilt.buffer, rebuilt.byteOffset, rebuilt.byteLength);
|
|
108
|
+
const count = view.getUint16(4);
|
|
109
|
+
const tags = Array.from({ length: count }, (_, index) => view.getUint32(12 + index * 16));
|
|
110
|
+
expect(tags).toEqual([...tags].sort((a, b) => a - b));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('starts every rebuilt table on a four-byte boundary', () => {
|
|
114
|
+
// The rebuilt sfnt is a public output, so it follows the alignment every real font is written
|
|
115
|
+
// with. Justified as a PRODUCER convention, not as a consumer requirement: a misaligned font was
|
|
116
|
+
// measured against fontconfig/FreeType and accepted, so the claim that a consumer rejects one is
|
|
117
|
+
// not supported and is deliberately not the reason recorded here.
|
|
118
|
+
//
|
|
119
|
+
// This reader indexes tables by explicit offset and length, so nothing else in the suite can
|
|
120
|
+
// notice if the padding stops — which is why the assertion is on the byte layout rather than on
|
|
121
|
+
// a round trip.
|
|
122
|
+
const rebuilt = readWoffFont(encodeSyntheticWoff(createSyntheticFont()), null)!;
|
|
123
|
+
const view = new DataView(rebuilt.buffer, rebuilt.byteOffset, rebuilt.byteLength);
|
|
124
|
+
const count = view.getUint16(4);
|
|
125
|
+
const lengths = Array.from({ length: count }, (_, index) => view.getUint32(12 + index * 16 + 12));
|
|
126
|
+
// A fixture whose tables all happened to be multiples of four would satisfy the alignment claim
|
|
127
|
+
// without any padding ever running, which is how the tag-order test above was once vacuous.
|
|
128
|
+
expect(lengths.some((length) => length % 4 !== 0)).toBe(true);
|
|
129
|
+
const offsets = Array.from({ length: count }, (_, index) => view.getUint32(12 + index * 16 + 8));
|
|
130
|
+
expect(offsets.filter((offset) => offset % 4 !== 0)).toEqual([]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('reads a deflated table when a decompressor is registered', () => {
|
|
134
|
+
registerDeflateDecompressor();
|
|
135
|
+
// The synthetic wrapper stores tables uncompressed, so this proves the registered path is reached
|
|
136
|
+
// and returns the same bytes rather than proving inflate itself, which compression owns.
|
|
137
|
+
const rebuilt = readWoffFont(encodeSyntheticWoff(createSyntheticFont()), null);
|
|
138
|
+
expect(rebuilt).not.toBeNull();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('returns the sentinel for a truncated container rather than a partial font', () => {
|
|
142
|
+
const woff = encodeSyntheticWoff(createSyntheticFont());
|
|
143
|
+
expect(readWoffFont(woff.subarray(0, 20), null)).toBeNull();
|
|
144
|
+
expect(readWoffFont(woff.subarray(0, woff.length - 8), null)).toBeNull();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('returns the sentinel for a container declaring no tables', () => {
|
|
148
|
+
const woff = encodeSyntheticWoff(createSyntheticFont());
|
|
149
|
+
new DataView(woff.buffer).setUint16(12, 0);
|
|
150
|
+
expect(readWoffFont(woff, null)).toBeNull();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// A decompressor returning the wrong length has produced something that is not this table, and
|
|
154
|
+
// reassembling it would yield a directory whose lengths lie about its own contents.
|
|
155
|
+
it('returns the sentinel when a decompressor yields the wrong length', () => {
|
|
156
|
+
const woff = encodeSyntheticWoff(createSyntheticFont());
|
|
157
|
+
// Mark the first table as compressed so the decompressor path is taken.
|
|
158
|
+
new DataView(woff.buffer).setUint32(44 + 8, 4);
|
|
159
|
+
expect(readWoffFont(woff, () => new Uint8Array(3))).toBeNull();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('returns the sentinel for a compressed table when no decompressor is available', () => {
|
|
163
|
+
const woff = encodeSyntheticWoff(createSyntheticFont());
|
|
164
|
+
new DataView(woff.buffer).setUint32(44 + 8, 4);
|
|
165
|
+
expect(readWoffFont(woff, null)).toBeNull();
|
|
166
|
+
});
|
|
167
|
+
});
|