@forgeax/engine-font 0.1.4 → 0.1.7
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/README.md +5 -7
- package/dist/.tsbuildinfo +1 -1
- package/dist/cli-font.d.ts.map +1 -1
- package/dist/cli-font.mjs +9 -135
- package/dist/cli-font.mjs.map +1 -1
- package/dist/font-importer.d.ts +1 -1
- package/dist/font-importer.d.ts.map +1 -1
- package/dist/font-importer.mjs +14 -182
- package/dist/font-importer.mjs.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/font-local-artifacts.test.ts +23 -245
- package/src/__tests__/font.unit.test.ts +3 -648
- package/src/cli-font.ts +10 -172
- package/src/font-importer.ts +12 -61
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// Paradigm: each block-scoped describe('<source-filename>.test.ts', ...) preserves
|
|
10
10
|
// source as ancestorTitles[0]. Top-level imports merged + deduped.
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
13
13
|
import { tmpdir } from 'node:os';
|
|
14
14
|
import { dirname, join, resolve } from 'node:path';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
@@ -173,653 +173,6 @@ async function findRealTtf(): Promise<string | undefined> {
|
|
|
173
173
|
expect((err as FontError).code).toBe('bake-failed');
|
|
174
174
|
expect((err as FontError).detail?.cause).toBe('wasm boom');
|
|
175
175
|
});
|
|
176
|
-
|
|
177
|
-
it('(d) output-root refusal is structured before generator work and repairs on the same path', async () => {
|
|
178
|
-
const ttfPath = join(tmpRoot, 'Valid.ttf');
|
|
179
|
-
await writeFile(ttfPath, TTF_MAGIC);
|
|
180
|
-
const blockedParent = join(tmpRoot, 'blocked-parent');
|
|
181
|
-
const outDir = join(blockedParent, 'atlas');
|
|
182
|
-
await writeFile(blockedParent, 'not a directory');
|
|
183
|
-
|
|
184
|
-
let factoryCalls = 0;
|
|
185
|
-
let generateCalls = 0;
|
|
186
|
-
let disposeCalls = 0;
|
|
187
|
-
const factory = async (): Promise<MsdfGenerator> => {
|
|
188
|
-
factoryCalls += 1;
|
|
189
|
-
return {
|
|
190
|
-
generateAtlas: async (bytes) => {
|
|
191
|
-
generateCalls += 1;
|
|
192
|
-
expect(bytes).toEqual(TTF_MAGIC);
|
|
193
|
-
return {
|
|
194
|
-
texture: { width: 1, height: 1, data: new Uint8Array([255, 255, 255, 255]) },
|
|
195
|
-
glyphs: [],
|
|
196
|
-
metrics: { lineHeight: 1, ascender: 1 },
|
|
197
|
-
textureSize: [1, 1],
|
|
198
|
-
fieldRange: 4,
|
|
199
|
-
};
|
|
200
|
-
},
|
|
201
|
-
dispose: async () => {
|
|
202
|
-
disposeCalls += 1;
|
|
203
|
-
},
|
|
204
|
-
};
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
const refusal = await bakeFont(ttfPath, outDir, factory).catch((error) => error);
|
|
208
|
-
expect(refusal).toBeInstanceOf(FontError);
|
|
209
|
-
expect(refusal).toMatchObject({ code: 'bake-failed' });
|
|
210
|
-
expect((refusal as FontError).detail).toMatchObject({ path: outDir });
|
|
211
|
-
expect(typeof (refusal as FontError).detail?.cause).toBe('string');
|
|
212
|
-
expect((refusal as FontError).detail?.cause).toContain('ENOTDIR');
|
|
213
|
-
expect((refusal as FontError).hint).toContain('output');
|
|
214
|
-
expect(factoryCalls).toBe(0);
|
|
215
|
-
expect(generateCalls).toBe(0);
|
|
216
|
-
expect(disposeCalls).toBe(0);
|
|
217
|
-
await expect(stat(join(outDir, 'Valid.atlas.png'))).rejects.toMatchObject({
|
|
218
|
-
code: 'ENOTDIR',
|
|
219
|
-
});
|
|
220
|
-
await expect(stat(join(outDir, 'Valid.meta.json'))).rejects.toMatchObject({
|
|
221
|
-
code: 'ENOTDIR',
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
await rm(blockedParent);
|
|
225
|
-
await mkdir(blockedParent);
|
|
226
|
-
const repaired = await bakeFont(ttfPath, outDir, factory);
|
|
227
|
-
const repairedAtlas = await readFile(repaired.atlasPath);
|
|
228
|
-
const repairedSidecar = await readFile(repaired.sidecarPath, 'utf8');
|
|
229
|
-
expect(repaired).toEqual({
|
|
230
|
-
atlasPath: join(outDir, 'Valid.atlas.png'),
|
|
231
|
-
sidecarPath: join(outDir, 'Valid.meta.json'),
|
|
232
|
-
});
|
|
233
|
-
expect(factoryCalls).toBe(1);
|
|
234
|
-
expect(generateCalls).toBe(1);
|
|
235
|
-
expect(disposeCalls).toBe(1);
|
|
236
|
-
|
|
237
|
-
const repeated = await bakeFont(ttfPath, outDir, factory);
|
|
238
|
-
expect(await readFile(repeated.atlasPath)).toEqual(repairedAtlas);
|
|
239
|
-
expect(await readFile(repeated.sidecarPath, 'utf8')).toBe(repairedSidecar);
|
|
240
|
-
expect(repeated).toEqual(repaired);
|
|
241
|
-
expect(factoryCalls).toBe(2);
|
|
242
|
-
expect(generateCalls).toBe(2);
|
|
243
|
-
expect(disposeCalls).toBe(2);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it('(d2) atlas-target refusal is structured and repairs on the same path', async () => {
|
|
247
|
-
const ttfPath = join(tmpRoot, 'Valid.ttf');
|
|
248
|
-
const outDir = join(tmpRoot, 'output');
|
|
249
|
-
await writeFile(ttfPath, TTF_MAGIC);
|
|
250
|
-
await mkdir(outDir);
|
|
251
|
-
const atlasPath = join(outDir, 'Valid.atlas.png');
|
|
252
|
-
const sidecarPath = join(outDir, 'Valid.meta.json');
|
|
253
|
-
await mkdir(atlasPath);
|
|
254
|
-
|
|
255
|
-
let factoryCalls = 0;
|
|
256
|
-
let generateCalls = 0;
|
|
257
|
-
let disposeCalls = 0;
|
|
258
|
-
const factory = async (): Promise<MsdfGenerator> => {
|
|
259
|
-
factoryCalls += 1;
|
|
260
|
-
return {
|
|
261
|
-
generateAtlas: async (bytes) => {
|
|
262
|
-
generateCalls += 1;
|
|
263
|
-
expect(bytes).toEqual(TTF_MAGIC);
|
|
264
|
-
return {
|
|
265
|
-
texture: { width: 1, height: 1, data: new Uint8Array([255, 0, 255, 255]) },
|
|
266
|
-
glyphs: [],
|
|
267
|
-
metrics: { lineHeight: 1, ascender: 1 },
|
|
268
|
-
textureSize: [1, 1],
|
|
269
|
-
fieldRange: 4,
|
|
270
|
-
};
|
|
271
|
-
},
|
|
272
|
-
dispose: async () => {
|
|
273
|
-
disposeCalls += 1;
|
|
274
|
-
},
|
|
275
|
-
};
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
const refusal = await bakeFont(ttfPath, outDir, factory).catch((error) => error);
|
|
279
|
-
expect(refusal).toBeInstanceOf(FontError);
|
|
280
|
-
expect(refusal).toMatchObject({
|
|
281
|
-
code: 'bake-failed',
|
|
282
|
-
hint: expect.stringContaining('atlas'),
|
|
283
|
-
detail: {
|
|
284
|
-
path: atlasPath,
|
|
285
|
-
cause: expect.stringContaining('EISDIR'),
|
|
286
|
-
},
|
|
287
|
-
});
|
|
288
|
-
expect((refusal as FontError).detail?.cause).toContain(atlasPath);
|
|
289
|
-
const refusedTarget = await stat(atlasPath);
|
|
290
|
-
expect(refusedTarget.isDirectory()).toBe(true);
|
|
291
|
-
await expect(stat(sidecarPath)).rejects.toMatchObject({ code: 'ENOENT' });
|
|
292
|
-
expect(factoryCalls).toBe(1);
|
|
293
|
-
expect(generateCalls).toBe(1);
|
|
294
|
-
expect(disposeCalls).toBe(1);
|
|
295
|
-
|
|
296
|
-
await rm(atlasPath, { recursive: true, force: true });
|
|
297
|
-
const repaired = await bakeFont(ttfPath, outDir, factory);
|
|
298
|
-
const repairedAtlas = await readFile(repaired.atlasPath);
|
|
299
|
-
const repairedSidecar = await readFile(repaired.sidecarPath, 'utf8');
|
|
300
|
-
expect(repaired).toEqual({ atlasPath, sidecarPath });
|
|
301
|
-
expect(factoryCalls).toBe(2);
|
|
302
|
-
expect(generateCalls).toBe(2);
|
|
303
|
-
expect(disposeCalls).toBe(2);
|
|
304
|
-
|
|
305
|
-
const repeated = await bakeFont(ttfPath, outDir, factory);
|
|
306
|
-
expect(await readFile(repeated.atlasPath)).toEqual(repairedAtlas);
|
|
307
|
-
expect(await readFile(repeated.sidecarPath, 'utf8')).toBe(repairedSidecar);
|
|
308
|
-
expect(repeated).toEqual(repaired);
|
|
309
|
-
expect(factoryCalls).toBe(3);
|
|
310
|
-
expect(generateCalls).toBe(3);
|
|
311
|
-
expect(disposeCalls).toBe(3);
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
it('(d3) sidecar-target refusal is structured and repairs on the same path', async () => {
|
|
315
|
-
const ttfPath = join(tmpRoot, 'Valid.ttf');
|
|
316
|
-
const outDir = join(tmpRoot, 'output');
|
|
317
|
-
await writeFile(ttfPath, TTF_MAGIC);
|
|
318
|
-
await mkdir(outDir);
|
|
319
|
-
const atlasPath = join(outDir, 'Valid.atlas.png');
|
|
320
|
-
const sidecarPath = join(outDir, 'Valid.meta.json');
|
|
321
|
-
await mkdir(sidecarPath);
|
|
322
|
-
|
|
323
|
-
let factoryCalls = 0;
|
|
324
|
-
let generateCalls = 0;
|
|
325
|
-
let disposeCalls = 0;
|
|
326
|
-
const factory = async (): Promise<MsdfGenerator> => {
|
|
327
|
-
factoryCalls += 1;
|
|
328
|
-
return {
|
|
329
|
-
generateAtlas: async (bytes) => {
|
|
330
|
-
generateCalls += 1;
|
|
331
|
-
expect(bytes).toEqual(TTF_MAGIC);
|
|
332
|
-
return {
|
|
333
|
-
texture: { width: 1, height: 1, data: new Uint8Array([255, 64, 128, 255]) },
|
|
334
|
-
glyphs: [],
|
|
335
|
-
metrics: { lineHeight: 1, ascender: 1 },
|
|
336
|
-
textureSize: [1, 1],
|
|
337
|
-
fieldRange: 4,
|
|
338
|
-
};
|
|
339
|
-
},
|
|
340
|
-
dispose: async () => {
|
|
341
|
-
disposeCalls += 1;
|
|
342
|
-
},
|
|
343
|
-
};
|
|
344
|
-
};
|
|
345
|
-
|
|
346
|
-
const refusal = await bakeFont(ttfPath, outDir, factory).catch((error) => error);
|
|
347
|
-
expect(refusal).toBeInstanceOf(FontError);
|
|
348
|
-
expect(refusal).toMatchObject({
|
|
349
|
-
code: 'bake-failed',
|
|
350
|
-
expected: expect.stringContaining('sidecar'),
|
|
351
|
-
hint: expect.stringContaining('sidecar'),
|
|
352
|
-
detail: {
|
|
353
|
-
path: sidecarPath,
|
|
354
|
-
cause: expect.stringContaining('EISDIR'),
|
|
355
|
-
},
|
|
356
|
-
});
|
|
357
|
-
expect((refusal as FontError).code).not.toBe('EISDIR');
|
|
358
|
-
expect('atlasPath' in (refusal as object)).toBe(false);
|
|
359
|
-
const cause = (refusal as FontError).detail?.cause;
|
|
360
|
-
expect(typeof cause).toBe('string');
|
|
361
|
-
expect((cause as string).length).toBeLessThan(1024);
|
|
362
|
-
expect((await stat(sidecarPath)).isDirectory()).toBe(true);
|
|
363
|
-
const refusedAtlas = await readFile(atlasPath);
|
|
364
|
-
expect(refusedAtlas.length).toBeGreaterThan(0);
|
|
365
|
-
expect(factoryCalls).toBe(1);
|
|
366
|
-
expect(generateCalls).toBe(1);
|
|
367
|
-
expect(disposeCalls).toBe(1);
|
|
368
|
-
|
|
369
|
-
await rm(sidecarPath, { recursive: true, force: true });
|
|
370
|
-
const repaired = await bakeFont(ttfPath, outDir, factory);
|
|
371
|
-
const repairedAtlas = await readFile(repaired.atlasPath);
|
|
372
|
-
const repairedSidecar = await readFile(repaired.sidecarPath, 'utf8');
|
|
373
|
-
expect(repaired).toEqual({ atlasPath, sidecarPath });
|
|
374
|
-
expect(repairedAtlas).toEqual(refusedAtlas);
|
|
375
|
-
expect(JSON.parse(repairedSidecar)).toMatchObject({ importer: 'font' });
|
|
376
|
-
expect(factoryCalls).toBe(2);
|
|
377
|
-
expect(generateCalls).toBe(2);
|
|
378
|
-
expect(disposeCalls).toBe(2);
|
|
379
|
-
|
|
380
|
-
const repeated = await bakeFont(ttfPath, outDir, factory);
|
|
381
|
-
expect(await readFile(repeated.atlasPath)).toEqual(repairedAtlas);
|
|
382
|
-
expect(await readFile(repeated.sidecarPath, 'utf8')).toBe(repairedSidecar);
|
|
383
|
-
expect(repeated).toEqual(repaired);
|
|
384
|
-
expect(factoryCalls).toBe(3);
|
|
385
|
-
expect(generateCalls).toBe(3);
|
|
386
|
-
expect(disposeCalls).toBe(3);
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
it('(d4) malformed atlas textures fail before publication and repair on the same factory', async () => {
|
|
390
|
-
const ttfPath = join(tmpRoot, 'MalformedAtlas.ttf');
|
|
391
|
-
await writeFile(ttfPath, TTF_MAGIC);
|
|
392
|
-
|
|
393
|
-
const repairedAtlas: BakeAtlas = {
|
|
394
|
-
texture: { width: 2, height: 2, data: new Uint8Array(16) },
|
|
395
|
-
glyphs: [
|
|
396
|
-
{
|
|
397
|
-
unicode: 65,
|
|
398
|
-
advance: 10,
|
|
399
|
-
xoffset: 1,
|
|
400
|
-
yoffset: 2,
|
|
401
|
-
atlasPosition: [0, 0],
|
|
402
|
-
atlasSize: [1, 1],
|
|
403
|
-
},
|
|
404
|
-
],
|
|
405
|
-
metrics: { lineHeight: 20, ascender: 16 },
|
|
406
|
-
textureSize: [2, 2],
|
|
407
|
-
fieldRange: 4,
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
const malformedCases: readonly [string, (atlas: BakeAtlas) => BakeAtlas][] = [
|
|
411
|
-
[
|
|
412
|
-
'non-positive-dimension',
|
|
413
|
-
(atlas) => ({
|
|
414
|
-
...atlas,
|
|
415
|
-
texture: { ...atlas.texture, width: 0 },
|
|
416
|
-
}),
|
|
417
|
-
],
|
|
418
|
-
[
|
|
419
|
-
'non-finite-dimension',
|
|
420
|
-
(atlas) => ({
|
|
421
|
-
...atlas,
|
|
422
|
-
texture: { ...atlas.texture, height: Number.NaN },
|
|
423
|
-
}),
|
|
424
|
-
],
|
|
425
|
-
[
|
|
426
|
-
'non-integer-dimension',
|
|
427
|
-
(atlas) => ({
|
|
428
|
-
...atlas,
|
|
429
|
-
texture: { ...atlas.texture, width: 1.5 },
|
|
430
|
-
}),
|
|
431
|
-
],
|
|
432
|
-
[
|
|
433
|
-
'rgba-length-mismatch',
|
|
434
|
-
(atlas) => ({
|
|
435
|
-
...atlas,
|
|
436
|
-
texture: { ...atlas.texture, data: new Uint8Array(15) },
|
|
437
|
-
}),
|
|
438
|
-
],
|
|
439
|
-
[
|
|
440
|
-
'texture-size-mismatch',
|
|
441
|
-
(atlas) => ({
|
|
442
|
-
...atlas,
|
|
443
|
-
textureSize: [1, 2],
|
|
444
|
-
}),
|
|
445
|
-
],
|
|
446
|
-
];
|
|
447
|
-
|
|
448
|
-
for (const [caseName, makeMalformed] of malformedCases) {
|
|
449
|
-
const outDir = join(tmpRoot, `malformed-${caseName}`);
|
|
450
|
-
const atlasPath = join(outDir, 'MalformedAtlas.atlas.png');
|
|
451
|
-
const sidecarPath = join(outDir, 'MalformedAtlas.meta.json');
|
|
452
|
-
let returnMalformed = true;
|
|
453
|
-
let factoryCalls = 0;
|
|
454
|
-
let generateCalls = 0;
|
|
455
|
-
let disposeCalls = 0;
|
|
456
|
-
const factory = async (): Promise<MsdfGenerator> => {
|
|
457
|
-
factoryCalls += 1;
|
|
458
|
-
return {
|
|
459
|
-
generateAtlas: async (bytes) => {
|
|
460
|
-
generateCalls += 1;
|
|
461
|
-
expect(bytes).toEqual(TTF_MAGIC);
|
|
462
|
-
return returnMalformed ? makeMalformed(repairedAtlas) : repairedAtlas;
|
|
463
|
-
},
|
|
464
|
-
dispose: async () => {
|
|
465
|
-
disposeCalls += 1;
|
|
466
|
-
},
|
|
467
|
-
};
|
|
468
|
-
};
|
|
469
|
-
|
|
470
|
-
const refusal = await bakeFont(ttfPath, outDir, factory).catch((error) => error);
|
|
471
|
-
expect(refusal).toBeInstanceOf(FontError);
|
|
472
|
-
expect(refusal).toMatchObject({
|
|
473
|
-
code: 'bake-failed',
|
|
474
|
-
expected: expect.stringContaining('atlas'),
|
|
475
|
-
hint: expect.stringContaining('atlas'),
|
|
476
|
-
detail: { cause: expect.stringContaining('malformed-atlas') },
|
|
477
|
-
});
|
|
478
|
-
const cause = (refusal as FontError).detail?.cause;
|
|
479
|
-
expect(typeof cause).toBe('string');
|
|
480
|
-
expect((cause as string).length).toBeLessThan(256);
|
|
481
|
-
expect('atlasPath' in (refusal as object)).toBe(false);
|
|
482
|
-
await expect(stat(atlasPath)).rejects.toMatchObject({ code: 'ENOENT' });
|
|
483
|
-
await expect(stat(sidecarPath)).rejects.toMatchObject({ code: 'ENOENT' });
|
|
484
|
-
expect(factoryCalls).toBe(1);
|
|
485
|
-
expect(generateCalls).toBe(1);
|
|
486
|
-
expect(disposeCalls).toBe(1);
|
|
487
|
-
|
|
488
|
-
returnMalformed = false;
|
|
489
|
-
const repaired = await bakeFont(ttfPath, outDir, factory);
|
|
490
|
-
const repairedAtlasBytes = await readFile(repaired.atlasPath);
|
|
491
|
-
const repairedSidecar = await readFile(repaired.sidecarPath, 'utf8');
|
|
492
|
-
expect(repaired).toEqual({ atlasPath, sidecarPath });
|
|
493
|
-
expect(JSON.parse(repairedSidecar)).toMatchObject({
|
|
494
|
-
common: { atlasWidth: 2, atlasHeight: 2 },
|
|
495
|
-
glyphs: {
|
|
496
|
-
'65': {
|
|
497
|
-
advance: 10,
|
|
498
|
-
bearingX: 1,
|
|
499
|
-
bearingY: 2,
|
|
500
|
-
size: { w: 1, h: 1 },
|
|
501
|
-
region: { x: 0, y: 0, w: 1, h: 1 },
|
|
502
|
-
},
|
|
503
|
-
},
|
|
504
|
-
});
|
|
505
|
-
expect(factoryCalls).toBe(2);
|
|
506
|
-
expect(generateCalls).toBe(2);
|
|
507
|
-
expect(disposeCalls).toBe(2);
|
|
508
|
-
|
|
509
|
-
const repeated = await bakeFont(ttfPath, outDir, factory);
|
|
510
|
-
expect(repeated).toEqual(repaired);
|
|
511
|
-
expect(await readFile(repeated.atlasPath)).toEqual(repairedAtlasBytes);
|
|
512
|
-
expect(await readFile(repeated.sidecarPath, 'utf8')).toBe(repairedSidecar);
|
|
513
|
-
expect(factoryCalls).toBe(3);
|
|
514
|
-
expect(generateCalls).toBe(3);
|
|
515
|
-
expect(disposeCalls).toBe(3);
|
|
516
|
-
}
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
it('(d5) malformed glyph metrics fail before publication and repair on the same factory', async () => {
|
|
520
|
-
const ttfPath = join(tmpRoot, 'MalformedGlyphs.ttf');
|
|
521
|
-
await writeFile(ttfPath, TTF_MAGIC);
|
|
522
|
-
|
|
523
|
-
const validAtlas: BakeAtlas = {
|
|
524
|
-
texture: { width: 2, height: 2, data: new Uint8Array(16) },
|
|
525
|
-
glyphs: [
|
|
526
|
-
{
|
|
527
|
-
unicode: 65,
|
|
528
|
-
advance: 10,
|
|
529
|
-
xoffset: 1,
|
|
530
|
-
yoffset: 2,
|
|
531
|
-
atlasPosition: [0, 0],
|
|
532
|
-
atlasSize: [1, 1],
|
|
533
|
-
},
|
|
534
|
-
],
|
|
535
|
-
metrics: { lineHeight: 20, ascender: 16 },
|
|
536
|
-
textureSize: [2, 2],
|
|
537
|
-
fieldRange: 4,
|
|
538
|
-
};
|
|
539
|
-
const baseGlyph = validAtlas.glyphs[0];
|
|
540
|
-
if (baseGlyph === undefined) {
|
|
541
|
-
throw new Error('test fixture must contain a base glyph');
|
|
542
|
-
}
|
|
543
|
-
const malformedCases: readonly [string, (atlas: BakeAtlas) => BakeAtlas][] = [
|
|
544
|
-
[
|
|
545
|
-
'non-finite-metric',
|
|
546
|
-
(atlas) => ({
|
|
547
|
-
...atlas,
|
|
548
|
-
glyphs: [{ ...baseGlyph, advance: Number.NaN }],
|
|
549
|
-
}),
|
|
550
|
-
],
|
|
551
|
-
[
|
|
552
|
-
'duplicate-unicode',
|
|
553
|
-
(atlas) => ({
|
|
554
|
-
...atlas,
|
|
555
|
-
glyphs: [...atlas.glyphs, { ...baseGlyph, atlasPosition: [1, 0] }],
|
|
556
|
-
}),
|
|
557
|
-
],
|
|
558
|
-
[
|
|
559
|
-
'out-of-atlas-region',
|
|
560
|
-
(atlas) => ({
|
|
561
|
-
...atlas,
|
|
562
|
-
glyphs: [{ ...baseGlyph, atlasPosition: [1, 1], atlasSize: [2, 1] }],
|
|
563
|
-
}),
|
|
564
|
-
],
|
|
565
|
-
];
|
|
566
|
-
|
|
567
|
-
for (const [caseName, makeMalformed] of malformedCases) {
|
|
568
|
-
const outDir = join(tmpRoot, `malformed-glyph-${caseName}`);
|
|
569
|
-
const atlasPath = join(outDir, 'MalformedGlyphs.atlas.png');
|
|
570
|
-
const sidecarPath = join(outDir, 'MalformedGlyphs.meta.json');
|
|
571
|
-
let returnMalformed = true;
|
|
572
|
-
let factoryCalls = 0;
|
|
573
|
-
let generateCalls = 0;
|
|
574
|
-
let disposeCalls = 0;
|
|
575
|
-
const factory = async (): Promise<MsdfGenerator> => {
|
|
576
|
-
factoryCalls += 1;
|
|
577
|
-
return {
|
|
578
|
-
generateAtlas: async (bytes) => {
|
|
579
|
-
generateCalls += 1;
|
|
580
|
-
expect(bytes).toEqual(TTF_MAGIC);
|
|
581
|
-
return returnMalformed ? makeMalformed(validAtlas) : validAtlas;
|
|
582
|
-
},
|
|
583
|
-
dispose: async () => {
|
|
584
|
-
disposeCalls += 1;
|
|
585
|
-
},
|
|
586
|
-
};
|
|
587
|
-
};
|
|
588
|
-
|
|
589
|
-
const refusal = await bakeFont(ttfPath, outDir, factory).catch((error) => error);
|
|
590
|
-
expect(refusal).toBeInstanceOf(FontError);
|
|
591
|
-
expect(refusal).toMatchObject({
|
|
592
|
-
code: 'bake-failed',
|
|
593
|
-
expected: expect.stringContaining('glyph'),
|
|
594
|
-
hint: expect.stringContaining('glyph'),
|
|
595
|
-
detail: { cause: expect.stringContaining('malformed-glyph') },
|
|
596
|
-
});
|
|
597
|
-
const cause = (refusal as FontError).detail?.cause;
|
|
598
|
-
expect(typeof cause).toBe('string');
|
|
599
|
-
expect((cause as string).length).toBeLessThan(256);
|
|
600
|
-
expect('atlasPath' in (refusal as object)).toBe(false);
|
|
601
|
-
await expect(stat(atlasPath)).rejects.toMatchObject({ code: 'ENOENT' });
|
|
602
|
-
await expect(stat(sidecarPath)).rejects.toMatchObject({ code: 'ENOENT' });
|
|
603
|
-
expect(factoryCalls).toBe(1);
|
|
604
|
-
expect(generateCalls).toBe(1);
|
|
605
|
-
expect(disposeCalls).toBe(1);
|
|
606
|
-
|
|
607
|
-
returnMalformed = false;
|
|
608
|
-
const repaired = await bakeFont(ttfPath, outDir, factory);
|
|
609
|
-
const repairedAtlasBytes = await readFile(repaired.atlasPath);
|
|
610
|
-
const repairedSidecar = await readFile(repaired.sidecarPath, 'utf8');
|
|
611
|
-
expect(repaired).toEqual({ atlasPath, sidecarPath });
|
|
612
|
-
expect(factoryCalls).toBe(2);
|
|
613
|
-
expect(generateCalls).toBe(2);
|
|
614
|
-
expect(disposeCalls).toBe(2);
|
|
615
|
-
|
|
616
|
-
const repeated = await bakeFont(ttfPath, outDir, factory);
|
|
617
|
-
expect(repeated).toEqual(repaired);
|
|
618
|
-
expect(await readFile(repeated.atlasPath)).toEqual(repairedAtlasBytes);
|
|
619
|
-
expect(await readFile(repeated.sidecarPath, 'utf8')).toBe(repairedSidecar);
|
|
620
|
-
expect(factoryCalls).toBe(3);
|
|
621
|
-
expect(generateCalls).toBe(3);
|
|
622
|
-
expect(disposeCalls).toBe(3);
|
|
623
|
-
}
|
|
624
|
-
});
|
|
625
|
-
|
|
626
|
-
it('(d6) generator disposal rejection is structured and preserves primary failure', async () => {
|
|
627
|
-
const ttfPath = join(tmpRoot, 'GeneratorDispose.ttf');
|
|
628
|
-
await writeFile(ttfPath, TTF_MAGIC);
|
|
629
|
-
|
|
630
|
-
const validAtlas: BakeAtlas = {
|
|
631
|
-
texture: { width: 2, height: 2, data: new Uint8Array(16) },
|
|
632
|
-
glyphs: [
|
|
633
|
-
{
|
|
634
|
-
unicode: 65,
|
|
635
|
-
advance: 10,
|
|
636
|
-
xoffset: 1,
|
|
637
|
-
yoffset: 2,
|
|
638
|
-
atlasPosition: [0, 0],
|
|
639
|
-
atlasSize: [1, 1],
|
|
640
|
-
},
|
|
641
|
-
],
|
|
642
|
-
metrics: { lineHeight: 20, ascender: 16 },
|
|
643
|
-
textureSize: [2, 2],
|
|
644
|
-
fieldRange: 4,
|
|
645
|
-
};
|
|
646
|
-
const cases: readonly [string, 'dispose' | 'primary'][] = [
|
|
647
|
-
['dispose-rejection', 'dispose'],
|
|
648
|
-
['primary-failure-preservation', 'primary'],
|
|
649
|
-
];
|
|
650
|
-
|
|
651
|
-
for (const [caseName, failure] of cases) {
|
|
652
|
-
const outDir = join(tmpRoot, `generator-dispose-${caseName}`);
|
|
653
|
-
const atlasPath = join(outDir, 'GeneratorDispose.atlas.png');
|
|
654
|
-
const sidecarPath = join(outDir, 'GeneratorDispose.meta.json');
|
|
655
|
-
let fail = true;
|
|
656
|
-
let factoryCalls = 0;
|
|
657
|
-
let generateCalls = 0;
|
|
658
|
-
let disposeCalls = 0;
|
|
659
|
-
const factory = async (): Promise<MsdfGenerator> => {
|
|
660
|
-
factoryCalls += 1;
|
|
661
|
-
return {
|
|
662
|
-
generateAtlas: async (bytes) => {
|
|
663
|
-
generateCalls += 1;
|
|
664
|
-
expect(bytes).toEqual(TTF_MAGIC);
|
|
665
|
-
if (failure === 'primary' && fail) {
|
|
666
|
-
throw new Error('primary generator failure');
|
|
667
|
-
}
|
|
668
|
-
return validAtlas;
|
|
669
|
-
},
|
|
670
|
-
dispose: async () => {
|
|
671
|
-
disposeCalls += 1;
|
|
672
|
-
if (failure === 'dispose' && fail) {
|
|
673
|
-
throw new Error('dispose sentinel must not escape');
|
|
674
|
-
}
|
|
675
|
-
if (failure === 'primary' && fail) {
|
|
676
|
-
throw new Error('cleanup sentinel must not replace primary');
|
|
677
|
-
}
|
|
678
|
-
},
|
|
679
|
-
};
|
|
680
|
-
};
|
|
681
|
-
|
|
682
|
-
const refusal = await bakeFont(ttfPath, outDir, factory).catch((error) => error);
|
|
683
|
-
expect(refusal).toBeInstanceOf(FontError);
|
|
684
|
-
expect(refusal).toMatchObject({
|
|
685
|
-
code: 'bake-failed',
|
|
686
|
-
expected: expect.stringContaining('generator'),
|
|
687
|
-
hint: expect.stringContaining('generator'),
|
|
688
|
-
detail: { cause: expect.any(String) },
|
|
689
|
-
});
|
|
690
|
-
const cause = (refusal as FontError).detail?.cause;
|
|
691
|
-
expect(typeof cause).toBe('string');
|
|
692
|
-
expect((cause as string).length).toBeLessThan(256);
|
|
693
|
-
if (failure === 'dispose') {
|
|
694
|
-
expect(cause).toContain('dispose');
|
|
695
|
-
} else {
|
|
696
|
-
expect(cause).toBe('primary generator failure');
|
|
697
|
-
expect(cause).not.toContain('cleanup sentinel');
|
|
698
|
-
}
|
|
699
|
-
expect('atlasPath' in (refusal as object)).toBe(false);
|
|
700
|
-
await expect(stat(atlasPath)).rejects.toMatchObject({ code: 'ENOENT' });
|
|
701
|
-
await expect(stat(sidecarPath)).rejects.toMatchObject({ code: 'ENOENT' });
|
|
702
|
-
expect(factoryCalls).toBe(1);
|
|
703
|
-
expect(generateCalls).toBe(1);
|
|
704
|
-
expect(disposeCalls).toBe(1);
|
|
705
|
-
|
|
706
|
-
fail = false;
|
|
707
|
-
const repaired = await bakeFont(ttfPath, outDir, factory);
|
|
708
|
-
const repairedAtlasBytes = await readFile(repaired.atlasPath);
|
|
709
|
-
const repairedSidecar = await readFile(repaired.sidecarPath, 'utf8');
|
|
710
|
-
expect(repaired).toEqual({ atlasPath, sidecarPath });
|
|
711
|
-
expect(factoryCalls).toBe(2);
|
|
712
|
-
expect(generateCalls).toBe(2);
|
|
713
|
-
expect(disposeCalls).toBe(2);
|
|
714
|
-
|
|
715
|
-
const repeated = await bakeFont(ttfPath, outDir, factory);
|
|
716
|
-
expect(repeated).toEqual(repaired);
|
|
717
|
-
expect(await readFile(repeated.atlasPath)).toEqual(repairedAtlasBytes);
|
|
718
|
-
expect(await readFile(repeated.sidecarPath, 'utf8')).toBe(repairedSidecar);
|
|
719
|
-
expect(factoryCalls).toBe(3);
|
|
720
|
-
expect(generateCalls).toBe(3);
|
|
721
|
-
expect(disposeCalls).toBe(3);
|
|
722
|
-
}
|
|
723
|
-
});
|
|
724
|
-
|
|
725
|
-
it('(e) unreadable source yields structured bake-failed before generator or output work', async () => {
|
|
726
|
-
const ttfPath = join(tmpRoot, 'missing-source.ttf');
|
|
727
|
-
const outDir = join(tmpRoot, 'untouched-output');
|
|
728
|
-
let factoryCalls = 0;
|
|
729
|
-
let generateCalls = 0;
|
|
730
|
-
let disposeCalls = 0;
|
|
731
|
-
const factory = async (): Promise<MsdfGenerator> => {
|
|
732
|
-
factoryCalls += 1;
|
|
733
|
-
return {
|
|
734
|
-
generateAtlas: async () => {
|
|
735
|
-
generateCalls += 1;
|
|
736
|
-
return {} as BakeAtlas;
|
|
737
|
-
},
|
|
738
|
-
dispose: async () => {
|
|
739
|
-
disposeCalls += 1;
|
|
740
|
-
},
|
|
741
|
-
};
|
|
742
|
-
};
|
|
743
|
-
|
|
744
|
-
const error = await bakeFont(ttfPath, outDir, factory).catch((e) => e);
|
|
745
|
-
expect(error).toBeInstanceOf(FontError);
|
|
746
|
-
expect(error).toMatchObject({
|
|
747
|
-
code: 'bake-failed',
|
|
748
|
-
expected: expect.stringContaining('readable'),
|
|
749
|
-
hint: expect.stringContaining('source'),
|
|
750
|
-
});
|
|
751
|
-
expect((error as FontError).detail).toMatchObject({
|
|
752
|
-
path: ttfPath,
|
|
753
|
-
cause: expect.stringContaining('ENOENT'),
|
|
754
|
-
});
|
|
755
|
-
expect(factoryCalls).toBe(0);
|
|
756
|
-
expect(generateCalls).toBe(0);
|
|
757
|
-
expect(disposeCalls).toBe(0);
|
|
758
|
-
await expect(stat(outDir)).rejects.toMatchObject({ code: 'ENOENT' });
|
|
759
|
-
});
|
|
760
|
-
|
|
761
|
-
it('(e) repairs the same source path and repeats deterministic output with balanced disposal', async () => {
|
|
762
|
-
const ttfPath = join(tmpRoot, 'repair-in-place.ttf');
|
|
763
|
-
const outDir = join(tmpRoot, 'repair-output');
|
|
764
|
-
let factoryCalls = 0;
|
|
765
|
-
let generateCalls = 0;
|
|
766
|
-
let disposeCalls = 0;
|
|
767
|
-
const factory = async (): Promise<MsdfGenerator> => {
|
|
768
|
-
factoryCalls += 1;
|
|
769
|
-
return {
|
|
770
|
-
generateAtlas: async (bytes) => {
|
|
771
|
-
generateCalls += 1;
|
|
772
|
-
expect(bytes.slice(0, TTF_MAGIC.length)).toEqual(TTF_MAGIC);
|
|
773
|
-
return {
|
|
774
|
-
texture: { width: 2, height: 2, data: new Uint8Array(16) },
|
|
775
|
-
glyphs: [
|
|
776
|
-
{
|
|
777
|
-
unicode: 65,
|
|
778
|
-
advance: 10,
|
|
779
|
-
xoffset: 1,
|
|
780
|
-
yoffset: 2,
|
|
781
|
-
atlasPosition: [0, 0],
|
|
782
|
-
atlasSize: [1, 1],
|
|
783
|
-
},
|
|
784
|
-
],
|
|
785
|
-
metrics: { lineHeight: 20, ascender: 16 },
|
|
786
|
-
textureSize: [2, 2],
|
|
787
|
-
fieldRange: 4,
|
|
788
|
-
};
|
|
789
|
-
},
|
|
790
|
-
dispose: async () => {
|
|
791
|
-
disposeCalls += 1;
|
|
792
|
-
},
|
|
793
|
-
};
|
|
794
|
-
};
|
|
795
|
-
|
|
796
|
-
const missing = await bakeFont(ttfPath, outDir, factory).catch((e) => e);
|
|
797
|
-
expect(missing).toBeInstanceOf(FontError);
|
|
798
|
-
expect((missing as FontError).code).toBe('bake-failed');
|
|
799
|
-
expect(factoryCalls).toBe(0);
|
|
800
|
-
expect(generateCalls).toBe(0);
|
|
801
|
-
expect(disposeCalls).toBe(0);
|
|
802
|
-
|
|
803
|
-
await writeFile(ttfPath, TTF_MAGIC);
|
|
804
|
-
const first = await bakeFont(ttfPath, outDir, factory);
|
|
805
|
-
const firstPng = await readFile(first.atlasPath);
|
|
806
|
-
const firstSidecar = await readFile(first.sidecarPath, 'utf8');
|
|
807
|
-
|
|
808
|
-
const second = await bakeFont(ttfPath, outDir, factory);
|
|
809
|
-
const secondPng = await readFile(second.atlasPath);
|
|
810
|
-
const secondSidecar = await readFile(second.sidecarPath, 'utf8');
|
|
811
|
-
|
|
812
|
-
expect(first).toEqual({
|
|
813
|
-
atlasPath: join(outDir, 'repair-in-place.atlas.png'),
|
|
814
|
-
sidecarPath: join(outDir, 'repair-in-place.meta.json'),
|
|
815
|
-
});
|
|
816
|
-
expect(second).toEqual(first);
|
|
817
|
-
expect(secondPng).toEqual(firstPng);
|
|
818
|
-
expect(secondSidecar).toBe(firstSidecar);
|
|
819
|
-
expect(factoryCalls).toBe(2);
|
|
820
|
-
expect(generateCalls).toBe(2);
|
|
821
|
-
expect(disposeCalls).toBe(2);
|
|
822
|
-
});
|
|
823
176
|
});
|
|
824
177
|
});
|
|
825
178
|
}
|
|
@@ -840,6 +193,8 @@ describe('font package root surface', () => {
|
|
|
840
193
|
expect('FONT_PACKAGE_VERSION' in mod).toBe(false);
|
|
841
194
|
expect(typeof mod.fontContribution).toBe('object');
|
|
842
195
|
expect('bakeFont' in mod).toBe(false);
|
|
196
|
+
expect('encodePng' in mod).toBe(false);
|
|
197
|
+
expect('atlasToSidecar' in mod).toBe(false);
|
|
843
198
|
expect('runCliFont' in mod).toBe(false);
|
|
844
199
|
});
|
|
845
200
|
});
|