@flighthq/font-formats 0.3.1-next.1041.35b950c
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 +27 -0
- package/dist/openTypeTestHelper.d.ts.map +1 -0
- package/dist/openTypeTestHelper.js +520 -0
- package/dist/openTypeTestHelper.js.map +1 -0
- package/dist/sfntAssembly.d.ts +20 -0
- package/dist/sfntAssembly.d.ts.map +1 -0
- package/dist/sfntAssembly.js +237 -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 +23 -0
- package/dist/woff2GlyfTransform.d.ts.map +1 -0
- package/dist/woff2GlyfTransform.js +437 -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 +122 -0
- package/dist/woffFont.js.map +1 -0
- package/package.json +51 -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/openTypeGlyfWinding.test.ts +80 -0
- package/src/openTypeGlyphOutlineSource.test.ts +298 -0
- package/src/openTypeMetrics.test.ts +103 -0
- package/src/sfntAssembly.test.ts +228 -0
- package/src/sfntTableDirectory.test.ts +64 -0
- package/src/woff2Font.test.ts +129 -0
- package/src/woff2GlyfTransform.test.ts +315 -0
- package/src/woffFont.test.ts +207 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createWoff2TransformReverser,
|
|
5
|
+
decodeWoff2Triplet,
|
|
6
|
+
getWoff2BboxBitmapByteLength,
|
|
7
|
+
hasWoff2GlyphBbox,
|
|
8
|
+
isWoff2PointOnCurve,
|
|
9
|
+
measureWoff2CompositeGlyph,
|
|
10
|
+
readWoff2GlyfStreams,
|
|
11
|
+
readWoff2Short,
|
|
12
|
+
reverseWoff2GlyfTransform,
|
|
13
|
+
} from './woff2GlyfTransform';
|
|
14
|
+
|
|
15
|
+
// Builds a transformed `glyf` header over seven streams of the given sizes, filled with a distinct byte
|
|
16
|
+
// each so a mis-ordered carve shows up as the wrong contents rather than only the wrong length.
|
|
17
|
+
function transformedGlyf(sizes: readonly number[], glyphCount = 3, indexFormat = 0): Uint8Array {
|
|
18
|
+
const total = 36 + sizes.reduce((sum, size) => sum + size, 0);
|
|
19
|
+
const out = new Uint8Array(total);
|
|
20
|
+
const view = new DataView(out.buffer);
|
|
21
|
+
view.setUint16(4, glyphCount);
|
|
22
|
+
view.setUint16(6, indexFormat);
|
|
23
|
+
sizes.forEach((size, index) => view.setUint32(8 + index * 4, size));
|
|
24
|
+
let at = 36;
|
|
25
|
+
sizes.forEach((size, index) => {
|
|
26
|
+
out.fill(index + 1, at, at + size);
|
|
27
|
+
at += size;
|
|
28
|
+
});
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('createWoff2TransformReverser', () => {
|
|
33
|
+
it('refuses the hmtx transform DELIBERATELY, which is a decision and not an unfinished job', () => {
|
|
34
|
+
// Passing the transformed bytes through under the table's own tag would assemble a font whose
|
|
35
|
+
// directory says `hmtx` over something that is not one — readable, wrong and silent.
|
|
36
|
+
//
|
|
37
|
+
// ★ THIS ASSERTION IS THE DECISION, NOT JUST A CHECK OF IT. The container defines an `hmtx`
|
|
38
|
+
// transform that producers do not emit, so implementing one adds a branch no font reaches and no
|
|
39
|
+
// font can test. That reasoning lives in a comment beside the refusal, and a comment is a fragile
|
|
40
|
+
// home for it: deleting the rationale breaks nothing, and leaves an apparent gap that a later
|
|
41
|
+
// reader will helpfully fill in with the very untested branch it argued against. A test cannot be
|
|
42
|
+
// dropped as quietly — anyone implementing the transform has to come here and delete a case whose
|
|
43
|
+
// name says the refusal was chosen. `censusWoff2Transforms` in the reversal oracle is the check to
|
|
44
|
+
// re-run before deciding producers have started emitting it.
|
|
45
|
+
const reverse = createWoff2TransformReverser();
|
|
46
|
+
expect(reverse('hmtx', new Uint8Array(4), new Map())).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('answers loca even when it is asked for before glyf', () => {
|
|
50
|
+
// Reversal order is the container's choice, not ours, so neither tag may depend on arriving first.
|
|
51
|
+
const transformed = transformedGlyf([6, 0, 0, 0, 0, 0, 0], 3);
|
|
52
|
+
new DataView(transformed.buffer).setUint16(6, 1);
|
|
53
|
+
// The helper fills each stream with a marker byte; zeroed contour counts make these blank glyphs,
|
|
54
|
+
// which is the smallest font that exercises the ordering rather than the decoding.
|
|
55
|
+
transformed.fill(0, 36, 42);
|
|
56
|
+
const tables = new Map([['glyf', transformed]]);
|
|
57
|
+
expect(createWoff2TransformReverser()('loca', new Uint8Array(0), tables)).not.toBeNull();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('returns the sentinel for loca when no glyf accompanies it', () => {
|
|
61
|
+
expect(createWoff2TransformReverser()('loca', new Uint8Array(0), new Map())).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('decodeWoff2Triplet', () => {
|
|
66
|
+
const bytes = Uint8Array.from([0x12, 0x34, 0x56, 0x78]);
|
|
67
|
+
|
|
68
|
+
it('moves on one axis only for the two single-axis blocks', () => {
|
|
69
|
+
// Codes 0-9 move in y alone, 10-19 in x alone; the other axis is not merely small, it is zero.
|
|
70
|
+
expect(decodeWoff2Triplet(0, bytes, 0)).toEqual({ dx: 0, dy: -0x12, used: 1 });
|
|
71
|
+
expect(decodeWoff2Triplet(10, bytes, 0)).toEqual({ dx: -0x12, dy: 0, used: 1 });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('steps the single-axis base by 256 every two codes', () => {
|
|
75
|
+
expect(decodeWoff2Triplet(2, bytes, 0)!.dy).toBe(-(256 + 0x12));
|
|
76
|
+
expect(decodeWoff2Triplet(9, bytes, 0)!.dy).toBe(1024 + 0x12);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('reads the X sign from bit 0 of the group and Y from bit 1, not the reverse', () => {
|
|
80
|
+
// The group order is (-,-) (+,-) (-,+) (+,+). Swapping the two bits mirrors every diagonal delta
|
|
81
|
+
// and still draws a glyph, so this is the assertion that catches it.
|
|
82
|
+
expect(decodeWoff2Triplet(20, bytes, 0)).toEqual({ dx: -(1 + 0x1), dy: -(1 + 0x2), used: 1 });
|
|
83
|
+
expect(decodeWoff2Triplet(21, bytes, 0)).toEqual({ dx: 1 + 0x1, dy: -(1 + 0x2), used: 1 });
|
|
84
|
+
expect(decodeWoff2Triplet(22, bytes, 0)).toEqual({ dx: -(1 + 0x1), dy: 1 + 0x2, used: 1 });
|
|
85
|
+
expect(decodeWoff2Triplet(23, bytes, 0)).toEqual({ dx: 1 + 0x1, dy: 1 + 0x2, used: 1 });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('bases the four-bit block at 1, 17, 33 and 49 across its sixteen-code spans', () => {
|
|
89
|
+
expect(decodeWoff2Triplet(23, bytes, 0)!.dx).toBe(1 + 0x1);
|
|
90
|
+
expect(decodeWoff2Triplet(39, bytes, 0)!.dx).toBe(17 + 0x1);
|
|
91
|
+
expect(decodeWoff2Triplet(55, bytes, 0)!.dx).toBe(33 + 0x1);
|
|
92
|
+
expect(decodeWoff2Triplet(71, bytes, 0)!.dx).toBe(49 + 0x1);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('takes two bytes for the eight-bit block, based at 1, 257 and 513', () => {
|
|
96
|
+
expect(decodeWoff2Triplet(87, bytes, 0)).toEqual({ dx: 1 + 0x12, dy: 1 + 0x34, used: 2 });
|
|
97
|
+
expect(decodeWoff2Triplet(99, bytes, 0)!.dx).toBe(257 + 0x12);
|
|
98
|
+
expect(decodeWoff2Triplet(111, bytes, 0)!.dx).toBe(513 + 0x12);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('adds NO base to the twelve- and sixteen-bit blocks, unlike every narrower one', () => {
|
|
102
|
+
// An off-by-one here shifts only the largest deltas, so most glyphs would still look right.
|
|
103
|
+
expect(decodeWoff2Triplet(123, bytes, 0)).toEqual({ dx: 0x123, dy: 0x456, used: 3 });
|
|
104
|
+
expect(decodeWoff2Triplet(127, bytes, 0)).toEqual({ dx: 0x1234, dy: 0x5678, used: 4 });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('returns the sentinel rather than a short read when the stream ends mid-point', () => {
|
|
108
|
+
expect(decodeWoff2Triplet(127, bytes, 1)).toBeNull();
|
|
109
|
+
expect(decodeWoff2Triplet(0, bytes, 4)).toBeNull();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('getWoff2BboxBitmapByteLength', () => {
|
|
114
|
+
it('pads to a 32-bit boundary, not to a byte', () => {
|
|
115
|
+
// The distinction is invisible for a count that lands on the same 4-byte cell either way, so a
|
|
116
|
+
// byte-rounded reading is correct for some fonts and wrong for others. 1754 rounds to 220 under
|
|
117
|
+
// both rules and can never tell them apart; 1741 rounds to 218 by byte and 220 by word.
|
|
118
|
+
expect(getWoff2BboxBitmapByteLength(1754)).toBe(220);
|
|
119
|
+
expect(getWoff2BboxBitmapByteLength(1741)).toBe(220);
|
|
120
|
+
expect(getWoff2BboxBitmapByteLength(1)).toBe(4);
|
|
121
|
+
expect(getWoff2BboxBitmapByteLength(32)).toBe(4);
|
|
122
|
+
expect(getWoff2BboxBitmapByteLength(33)).toBe(8);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('is zero only for a font with no glyphs', () => {
|
|
126
|
+
expect(getWoff2BboxBitmapByteLength(0)).toBe(0);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('hasWoff2GlyphBbox', () => {
|
|
131
|
+
it('reads glyph zero as the HIGH bit of the first byte', () => {
|
|
132
|
+
// Least-significant-first recovers a scattered subset of the boxes and mis-assigns the rest, which
|
|
133
|
+
// is worse than being plainly wrong: it is the reading that looks partly right.
|
|
134
|
+
expect(hasWoff2GlyphBbox(Uint8Array.from([0x80]), 0)).toBe(true);
|
|
135
|
+
expect(hasWoff2GlyphBbox(Uint8Array.from([0x01]), 0)).toBe(false);
|
|
136
|
+
expect(hasWoff2GlyphBbox(Uint8Array.from([0x01]), 7)).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('walks into later bytes by glyph index', () => {
|
|
140
|
+
const bitmap = Uint8Array.from([0x00, 0x40, 0x00, 0x02]);
|
|
141
|
+
expect(hasWoff2GlyphBbox(bitmap, 9)).toBe(true);
|
|
142
|
+
expect(hasWoff2GlyphBbox(bitmap, 30)).toBe(true);
|
|
143
|
+
expect(hasWoff2GlyphBbox(bitmap, 8)).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('reports absent rather than throwing past the end of the bitmap', () => {
|
|
147
|
+
expect(hasWoff2GlyphBbox(Uint8Array.from([0xff]), 64)).toBe(false);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('isWoff2PointOnCurve', () => {
|
|
152
|
+
it('reads the high bit as OFF-curve, the opposite of the glyf convention', () => {
|
|
153
|
+
// Carrying the glyf convention across inverts every point in the font, and an inverted outline is
|
|
154
|
+
// still a closed outline that draws as a glyph — so this assertion is the only thing standing
|
|
155
|
+
// between the two readings.
|
|
156
|
+
expect(isWoff2PointOnCurve(0x00)).toBe(true);
|
|
157
|
+
expect(isWoff2PointOnCurve(0x80)).toBe(false);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('ignores the triplet code in the low seven bits', () => {
|
|
161
|
+
// The same code must read the same either way, or the sense would depend on the coordinate size.
|
|
162
|
+
expect(isWoff2PointOnCurve(0x7f)).toBe(true);
|
|
163
|
+
expect(isWoff2PointOnCurve(0xff)).toBe(false);
|
|
164
|
+
expect(isWoff2PointOnCurve(0x2a)).toBe(true);
|
|
165
|
+
expect(isWoff2PointOnCurve(0xaa)).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Component records as the `glyf` format writes them: uint16 flags, uint16 glyph index, then the
|
|
170
|
+
// argument and transform bytes the flags call for. The bodies are left zero — only the sizes are read.
|
|
171
|
+
function composite(components: readonly { flags: number }[]): Uint8Array {
|
|
172
|
+
const parts: number[] = [];
|
|
173
|
+
for (const { flags } of components) {
|
|
174
|
+
parts.push(flags >> 8, flags & 0xff, 0, 0);
|
|
175
|
+
for (let index = 0; index < ((flags & 0x0001) !== 0 ? 4 : 2); index += 1) parts.push(0);
|
|
176
|
+
const transform = (flags & 0x0008) !== 0 ? 2 : (flags & 0x0040) !== 0 ? 4 : (flags & 0x0080) !== 0 ? 8 : 0;
|
|
177
|
+
for (let index = 0; index < transform; index += 1) parts.push(0);
|
|
178
|
+
}
|
|
179
|
+
return new Uint8Array(parts);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
describe('measureWoff2CompositeGlyph', () => {
|
|
183
|
+
it('sizes a single component from its argument width', () => {
|
|
184
|
+
// Flags and glyph index are four bytes, then the placement arguments: two bytes as signed bytes, or
|
|
185
|
+
// four when ARG_1_AND_2_ARE_WORDS is set.
|
|
186
|
+
expect(measureWoff2CompositeGlyph(composite([{ flags: 0 }]), 0)).toEqual({
|
|
187
|
+
byteLength: 6,
|
|
188
|
+
hasInstructions: false,
|
|
189
|
+
});
|
|
190
|
+
expect(measureWoff2CompositeGlyph(composite([{ flags: 0x0001 }]), 0)!.byteLength).toBe(8);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('sizes each of the three transform forms', () => {
|
|
194
|
+
expect(measureWoff2CompositeGlyph(composite([{ flags: 0x0008 }]), 0)!.byteLength).toBe(8);
|
|
195
|
+
expect(measureWoff2CompositeGlyph(composite([{ flags: 0x0040 }]), 0)!.byteLength).toBe(10);
|
|
196
|
+
expect(measureWoff2CompositeGlyph(composite([{ flags: 0x0080 }]), 0)!.byteLength).toBe(14);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('treats the transform forms as exclusive rather than cumulative', () => {
|
|
200
|
+
// A record cannot carry two transforms. Adding the widths instead of choosing the widest set bit
|
|
201
|
+
// would read 2 + 4 + 8 here and overshoot every transformed component in the font.
|
|
202
|
+
expect(measureWoff2CompositeGlyph(composite([{ flags: 0x0008 | 0x0040 | 0x0080 }]), 0)!.byteLength).toBe(8);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('walks every component while MORE_COMPONENTS is set', () => {
|
|
206
|
+
const stream = composite([{ flags: 0x0020 }, { flags: 0 }]);
|
|
207
|
+
expect(measureWoff2CompositeGlyph(stream, 0)!.byteLength).toBe(12);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('reports instructions requested by a component other than the first', () => {
|
|
211
|
+
// The bit is per-component, and a caller that inspected only the first record would miss this one —
|
|
212
|
+
// which is the shape that desynchronises the glyph stream for every simple glyph that follows.
|
|
213
|
+
const stream = composite([{ flags: 0x0020 }, { flags: 0x0100 }]);
|
|
214
|
+
expect(measureWoff2CompositeGlyph(stream, 0)).toEqual({ byteLength: 12, hasInstructions: true });
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('measures from the offset it is given rather than the start of the stream', () => {
|
|
218
|
+
const stream = composite([{ flags: 0x0080 }, { flags: 0 }]);
|
|
219
|
+
expect(measureWoff2CompositeGlyph(stream, 14)).toEqual({ byteLength: 6, hasInstructions: false });
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('returns the sentinel when the records run past the end of the stream', () => {
|
|
223
|
+
// MORE_COMPONENTS set with nothing following it: a clamped read here would report a plausible size
|
|
224
|
+
// for a record that is not there.
|
|
225
|
+
expect(measureWoff2CompositeGlyph(composite([{ flags: 0x0020 }]), 0)).toBeNull();
|
|
226
|
+
expect(measureWoff2CompositeGlyph(composite([{ flags: 0 }]).subarray(0, 5), 0)).toBeNull();
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('readWoff2GlyfStreams', () => {
|
|
231
|
+
it('carves the seven streams in their declared order', () => {
|
|
232
|
+
// Distinct sizes AND distinct fills: equal sizes would let a wrongly-ordered carve pass.
|
|
233
|
+
const streams = readWoff2GlyfStreams(transformedGlyf([6, 1, 2, 3, 4, 5, 7]))!;
|
|
234
|
+
expect(streams.nContourStream.byteLength).toBe(6);
|
|
235
|
+
expect(streams.nPointsStream.byteLength).toBe(1);
|
|
236
|
+
expect(streams.flagStream.byteLength).toBe(2);
|
|
237
|
+
expect(streams.glyphStream.byteLength).toBe(3);
|
|
238
|
+
expect(streams.compositeStream.byteLength).toBe(4);
|
|
239
|
+
expect(streams.bboxStream.byteLength).toBe(5);
|
|
240
|
+
expect(streams.instructionStream.byteLength).toBe(7);
|
|
241
|
+
// Each stream was filled with its own index, so this pins WHICH bytes each name got.
|
|
242
|
+
expect([...streams.nContourStream]).toEqual([1, 1, 1, 1, 1, 1]);
|
|
243
|
+
expect([...streams.instructionStream]).toEqual([7, 7, 7, 7, 7, 7, 7]);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('carries the glyph count and loca width from the header', () => {
|
|
247
|
+
const streams = readWoff2GlyfStreams(transformedGlyf([2, 1, 1, 1, 1, 1, 1], 41, 1))!;
|
|
248
|
+
expect(streams.glyphCount).toBe(41);
|
|
249
|
+
expect(streams.indexFormat).toBe(1);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('returns the sentinel when the sizes do not account for the table exactly', () => {
|
|
253
|
+
// A truncated stream still decodes into real-looking contour counts, so a partial carve would be
|
|
254
|
+
// silently wrong rather than visibly broken.
|
|
255
|
+
const short = transformedGlyf([4, 1, 1, 1, 1, 1, 1]).subarray(0, 40);
|
|
256
|
+
expect(readWoff2GlyfStreams(short)).toBeNull();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('returns the sentinel when the table carries bytes no stream claims', () => {
|
|
260
|
+
const table = transformedGlyf([4, 1, 1, 1, 1, 1, 1]);
|
|
261
|
+
const padded = new Uint8Array(table.byteLength + 3);
|
|
262
|
+
padded.set(table);
|
|
263
|
+
expect(readWoff2GlyfStreams(padded)).toBeNull();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('returns the sentinel for a table shorter than its own header', () => {
|
|
267
|
+
expect(readWoff2GlyfStreams(new Uint8Array(20))).toBeNull();
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe('readWoff2Short', () => {
|
|
272
|
+
it('reads a plain byte below the first escape code', () => {
|
|
273
|
+
const cursor = { at: 0 };
|
|
274
|
+
expect(readWoff2Short(Uint8Array.from([200]), cursor, 1)).toBe(200);
|
|
275
|
+
expect(cursor.at).toBe(1);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('reads the two-byte word form', () => {
|
|
279
|
+
const cursor = { at: 0 };
|
|
280
|
+
expect(readWoff2Short(Uint8Array.from([253, 0x12, 0x34]), cursor, 3)).toBe(0x1234);
|
|
281
|
+
expect(cursor.at).toBe(3);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('offsets the two one-more-byte forms by different amounts', () => {
|
|
285
|
+
// 254 and 255 differ only in their offset, so a reader that swapped them would still return a
|
|
286
|
+
// plausible small number. Pinning both against the same payload byte is what separates them.
|
|
287
|
+
expect(readWoff2Short(Uint8Array.from([255, 10]), { at: 0 }, 2)).toBe(263);
|
|
288
|
+
expect(readWoff2Short(Uint8Array.from([254, 10]), { at: 0 }, 2)).toBe(516);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('returns the sentinel rather than a short value when the stream ends mid-escape', () => {
|
|
292
|
+
expect(readWoff2Short(Uint8Array.from([253, 0x12]), { at: 0 }, 2)).toBe(-1);
|
|
293
|
+
expect(readWoff2Short(Uint8Array.from([255]), { at: 0 }, 1)).toBe(-1);
|
|
294
|
+
expect(readWoff2Short(Uint8Array.from([]), { at: 0 }, 0)).toBe(-1);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe('reverseWoff2GlyfTransform', () => {
|
|
299
|
+
it('returns the sentinel rather than a partial table when a stream runs short', () => {
|
|
300
|
+
// A truncated walk produces real-looking glyphs for every index before the break, so a partial
|
|
301
|
+
// result is the silent failure and a refusal is the visible one.
|
|
302
|
+
const streams = readWoff2GlyfStreams(transformedGlyf([2, 1, 1, 1, 0, 0, 0], 3))!;
|
|
303
|
+
expect(reverseWoff2GlyfTransform(streams)).toBeNull();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('emits a zero-length record for a glyph with no contours, and a loca that says so', () => {
|
|
307
|
+
// Three blank glyphs: loca must hold four equal offsets, which is how a blank glyph is expressed.
|
|
308
|
+
const streams = readWoff2GlyfStreams(transformedGlyf([6, 0, 0, 0, 0, 0, 0], 3))!;
|
|
309
|
+
streams.nContourStream.fill(0);
|
|
310
|
+
const out = reverseWoff2GlyfTransform(streams)!;
|
|
311
|
+
expect(out.glyf.byteLength).toBe(0);
|
|
312
|
+
const view = new DataView(out.loca.buffer, out.loca.byteOffset, out.loca.byteLength);
|
|
313
|
+
expect([0, 1, 2, 3].map((index) => view.getUint16(index * 2))).toEqual([0, 0, 0, 0]);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import {
|
|
2
|
+
registerDecompressor,
|
|
3
|
+
registerDeflateDecompressor,
|
|
4
|
+
unregisterDecompressor,
|
|
5
|
+
} from '@flighthq/compression/contract';
|
|
6
|
+
import { collectImportDiagnostics } from '@flighthq/importdiagnostics/contract';
|
|
7
|
+
import { Compression, CompressionFraming, ImportDiagnosticSeverity } from '@flighthq/types/contract';
|
|
8
|
+
import type { Path } from '@flighthq/types/contract';
|
|
9
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
10
|
+
|
|
11
|
+
import { createGlyphOutlineSourceFromOpenTypeFont } from './openTypeGlyphOutlineSource';
|
|
12
|
+
import {
|
|
13
|
+
createSyntheticFont,
|
|
14
|
+
encodeSyntheticWoff,
|
|
15
|
+
squareSyntheticGlyph,
|
|
16
|
+
emptySyntheticGlyph,
|
|
17
|
+
} from './openTypeTestHelper';
|
|
18
|
+
import { readSfntTableDirectory } from './sfntTableDirectory';
|
|
19
|
+
import { readWoffChecksumMismatches, readWoffFont } from './woffFont';
|
|
20
|
+
|
|
21
|
+
afterEach(() => unregisterDecompressor(Compression.Deflate));
|
|
22
|
+
|
|
23
|
+
describe('readWoffChecksumMismatches', () => {
|
|
24
|
+
it('reports nothing when the container states its tables truthfully', () => {
|
|
25
|
+
expect(readWoffChecksumMismatches(encodeSyntheticWoff(createSyntheticFont()), null)).toEqual([]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('names the table, what the file claimed, and what the bytes actually sum to', () => {
|
|
29
|
+
const woff = encodeSyntheticWoff(createSyntheticFont());
|
|
30
|
+
// Corrupt the FIRST entry's stored checksum only, so the report must single it out rather than
|
|
31
|
+
// flagging everything — a check that reported all tables would pass a weaker assertion.
|
|
32
|
+
const view = new DataView(woff.buffer);
|
|
33
|
+
const tag = view.getUint32(44);
|
|
34
|
+
view.setUint32(44 + 16, 0xdeadbeef);
|
|
35
|
+
const found = readWoffChecksumMismatches(woff, null);
|
|
36
|
+
expect(found.length).toBe(1);
|
|
37
|
+
expect(found[0]!.stored).toBe(0xdeadbeef);
|
|
38
|
+
expect(found[0]!.computed).not.toBe(0xdeadbeef);
|
|
39
|
+
expect(found[0]!.tag).toBe(
|
|
40
|
+
String.fromCharCode((tag >>> 24) & 0xff, (tag >>> 16) & 0xff, (tag >>> 8) & 0xff, tag & 0xff),
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('still loads the font it reported a mismatch for, rather than refusing it', () => {
|
|
45
|
+
// The whole point of the seam: a mismatch is information, not a verdict.
|
|
46
|
+
const woff = encodeSyntheticWoff(createSyntheticFont());
|
|
47
|
+
new DataView(woff.buffer).setUint32(44 + 16, 0xdeadbeef);
|
|
48
|
+
expect(readWoffChecksumMismatches(woff, null).length).toBe(1);
|
|
49
|
+
expect(readWoffFont(woff, null)).not.toBeNull();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('returns an empty report for a container it cannot read at all', () => {
|
|
53
|
+
expect(readWoffChecksumMismatches(new Uint8Array(8), null)).toEqual([]);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('readWoffFont', () => {
|
|
58
|
+
it('produces the same glyph outlines as the uncompressed font it wraps', () => {
|
|
59
|
+
// The end-to-end falsifier: byte-equality of table CONTENTS is a stronger claim per table, but it
|
|
60
|
+
// never runs the outline readers. This drives the whole producer — directory, offsets, `loca`,
|
|
61
|
+
// `glyf` — through both the plain font and its wrapper and compares what a caller actually gets.
|
|
62
|
+
const original = createSyntheticFont({ glyphs: [emptySyntheticGlyph(), squareSyntheticGlyph(100)] });
|
|
63
|
+
const rebuilt = readWoffFont(encodeSyntheticWoff(original), null)!;
|
|
64
|
+
const before = createGlyphOutlineSourceFromOpenTypeFont(original)!;
|
|
65
|
+
const after = createGlyphOutlineSourceFromOpenTypeFont(rebuilt)!;
|
|
66
|
+
const a: Path = { commands: [], data: [], winding: 'nonZero' };
|
|
67
|
+
const b: Path = { commands: [], data: [], winding: 'nonZero' };
|
|
68
|
+
expect(before.getGlyphOutline(a, 1)).toBe(true);
|
|
69
|
+
expect(after.getGlyphOutline(b, 1)).toBe(true);
|
|
70
|
+
expect(b.commands).toEqual(a.commands);
|
|
71
|
+
expect(b.data).toEqual(a.data);
|
|
72
|
+
expect(a.commands.length).toBeGreaterThan(0);
|
|
73
|
+
expect(after.getGlyphOutlineAdvance(1)).toBe(before.getGlyphOutlineAdvance(1));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('rebuilds an sfnt whose tables match the original', () => {
|
|
77
|
+
const original = createSyntheticFont();
|
|
78
|
+
const rebuilt = readWoffFont(encodeSyntheticWoff(original), null)!;
|
|
79
|
+
const before = readSfntTableDirectory(original)!;
|
|
80
|
+
const after = readSfntTableDirectory(rebuilt)!;
|
|
81
|
+
expect([...after.tables.keys()].sort()).toEqual([...before.tables.keys()].sort());
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('preserves each table byte for byte through the unwrap', () => {
|
|
85
|
+
const original = createSyntheticFont();
|
|
86
|
+
const rebuilt = readWoffFont(encodeSyntheticWoff(original), null)!;
|
|
87
|
+
const before = readSfntTableDirectory(original)!;
|
|
88
|
+
const after = readSfntTableDirectory(rebuilt)!;
|
|
89
|
+
for (const [tag, range] of before.tables) {
|
|
90
|
+
const other = after.tables.get(tag)!;
|
|
91
|
+
expect(rebuilt.subarray(other.offset, other.offset + other.length)).toEqual(
|
|
92
|
+
original.subarray(range.offset, range.offset + range.length),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// The wrapper carries the sfnt version the tables belonged to, so a wrapped CFF font must come out
|
|
98
|
+
// declaring itself a CFF font rather than one inferred from which tables happen to be present.
|
|
99
|
+
it('carries the original flavor through, rather than inferring one', () => {
|
|
100
|
+
const cff = createSyntheticFont({ flavor: 'opentype' });
|
|
101
|
+
const rebuilt = readWoffFont(encodeSyntheticWoff(cff), null)!;
|
|
102
|
+
expect(readSfntTableDirectory(rebuilt)?.sfntVersion).toBe(0x4f54544f);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// The sfnt directory is defined as tag-ordered, so a reader entitled to binary-search it would find the
|
|
106
|
+
// wrong table if the rebuild preserved WOFF's order instead.
|
|
107
|
+
it('writes the rebuilt directory in tag order even when the container is not', () => {
|
|
108
|
+
// The container is built in DESCENDING tag order on purpose. A sorted fixture would let this pass
|
|
109
|
+
// whether or not the rebuild sorted anything, which is how the first version of this test was
|
|
110
|
+
// useless — mutation testing found it, not review.
|
|
111
|
+
const rebuilt = readWoffFont(encodeSyntheticWoff(createSyntheticFont(), true), null)!;
|
|
112
|
+
const view = new DataView(rebuilt.buffer, rebuilt.byteOffset, rebuilt.byteLength);
|
|
113
|
+
const count = view.getUint16(4);
|
|
114
|
+
const tags = Array.from({ length: count }, (_, index) => view.getUint32(12 + index * 16));
|
|
115
|
+
expect(tags).toEqual([...tags].sort((a, b) => a - b));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('starts every rebuilt table on a four-byte boundary', () => {
|
|
119
|
+
// The rebuilt sfnt is a public output, so it follows the alignment every real font is written
|
|
120
|
+
// with. Justified as a PRODUCER convention, not as a consumer requirement: a misaligned font was
|
|
121
|
+
// measured against fontconfig/FreeType and accepted, so the claim that a consumer rejects one is
|
|
122
|
+
// not supported and is deliberately not the reason recorded here.
|
|
123
|
+
//
|
|
124
|
+
// This reader indexes tables by explicit offset and length, so nothing else in the suite can
|
|
125
|
+
// notice if the padding stops — which is why the assertion is on the byte layout rather than on
|
|
126
|
+
// a round trip.
|
|
127
|
+
const rebuilt = readWoffFont(encodeSyntheticWoff(createSyntheticFont()), null)!;
|
|
128
|
+
const view = new DataView(rebuilt.buffer, rebuilt.byteOffset, rebuilt.byteLength);
|
|
129
|
+
const count = view.getUint16(4);
|
|
130
|
+
const lengths = Array.from({ length: count }, (_, index) => view.getUint32(12 + index * 16 + 12));
|
|
131
|
+
// A fixture whose tables all happened to be multiples of four would satisfy the alignment claim
|
|
132
|
+
// without any padding ever running, which is how the tag-order test above was once vacuous.
|
|
133
|
+
expect(lengths.some((length) => length % 4 !== 0)).toBe(true);
|
|
134
|
+
const offsets = Array.from({ length: count }, (_, index) => view.getUint32(12 + index * 16 + 8));
|
|
135
|
+
expect(offsets.filter((offset) => offset % 4 !== 0)).toEqual([]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('reads a deflated table when a decompressor is registered', () => {
|
|
139
|
+
registerDeflateDecompressor();
|
|
140
|
+
// The synthetic wrapper stores tables uncompressed, so this proves the registered path is reached
|
|
141
|
+
// and returns the same bytes rather than proving inflate itself, which compression owns.
|
|
142
|
+
const rebuilt = readWoffFont(encodeSyntheticWoff(createSyntheticFont()), null);
|
|
143
|
+
expect(rebuilt).not.toBeNull();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('returns the sentinel for a truncated container rather than a partial font', () => {
|
|
147
|
+
const woff = encodeSyntheticWoff(createSyntheticFont());
|
|
148
|
+
expect(readWoffFont(woff.subarray(0, 20), null)).toBeNull();
|
|
149
|
+
expect(readWoffFont(woff.subarray(0, woff.length - 8), null)).toBeNull();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('returns the sentinel for a container declaring no tables', () => {
|
|
153
|
+
const woff = encodeSyntheticWoff(createSyntheticFont());
|
|
154
|
+
new DataView(woff.buffer).setUint16(12, 0);
|
|
155
|
+
expect(readWoffFont(woff, null)).toBeNull();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// A decompressor returning the wrong length has produced something that is not this table, and
|
|
159
|
+
// reassembling it would yield a directory whose lengths lie about its own contents.
|
|
160
|
+
it('returns the sentinel when a decompressor yields the wrong length', () => {
|
|
161
|
+
const woff = encodeSyntheticWoff(createSyntheticFont());
|
|
162
|
+
// Mark the first table as compressed so the decompressor path is taken.
|
|
163
|
+
new DataView(woff.buffer).setUint32(44 + 8, 4);
|
|
164
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
165
|
+
expect(
|
|
166
|
+
readWoffFont(
|
|
167
|
+
woff,
|
|
168
|
+
(_compressed, _uncompressedLength, framing) => {
|
|
169
|
+
expect(framing).toBe(CompressionFraming.Rfc1950);
|
|
170
|
+
return new Uint8Array(3);
|
|
171
|
+
},
|
|
172
|
+
sink,
|
|
173
|
+
),
|
|
174
|
+
).toBeNull();
|
|
175
|
+
});
|
|
176
|
+
expect(diagnostics.map((entry) => entry.kind)).toEqual(['woff.decompression-failed']);
|
|
177
|
+
expect(diagnostics[0]!.severity).toBe(ImportDiagnosticSeverity.Reject);
|
|
178
|
+
expect(diagnostics[0]!.origin).toBe('readWoffFont');
|
|
179
|
+
expect(diagnostics[0]!.detail?.outputLength).toBe(3);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('returns the sentinel for a compressed table when no decompressor is available', () => {
|
|
183
|
+
const woff = encodeSyntheticWoff(createSyntheticFont());
|
|
184
|
+
new DataView(woff.buffer).setUint32(44 + 8, 4);
|
|
185
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
186
|
+
expect(readWoffFont(woff, null, sink)).toBeNull();
|
|
187
|
+
});
|
|
188
|
+
expect(diagnostics.map((entry) => entry.kind)).toEqual(['woff.no-decompressor-registered']);
|
|
189
|
+
expect(diagnostics[0]!.severity).toBe(ImportDiagnosticSeverity.Reject);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('emits a Reject diagnostic from the public WOFF importer when decompression fails', () => {
|
|
193
|
+
const woff = encodeSyntheticWoff(createSyntheticFont());
|
|
194
|
+
new DataView(woff.buffer).setUint32(44 + 8, 4);
|
|
195
|
+
registerDecompressor(Compression.Deflate, (_compressed, _uncompressedLength, framing) => {
|
|
196
|
+
expect(framing).toBe(CompressionFraming.Rfc1950);
|
|
197
|
+
return null;
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
201
|
+
expect(createGlyphOutlineSourceFromOpenTypeFont(woff, sink)).toBeNull();
|
|
202
|
+
});
|
|
203
|
+
expect(diagnostics.map((entry) => entry.kind)).toEqual(['woff.decompression-failed']);
|
|
204
|
+
expect(diagnostics[0]!.severity).toBe(ImportDiagnosticSeverity.Reject);
|
|
205
|
+
expect(diagnostics[0]!.origin).toBe('readWoffFont');
|
|
206
|
+
});
|
|
207
|
+
});
|