@docfonts/fallbacks 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SuperDoc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # @docfonts/fallbacks
2
+
3
+ Document font substitution, measured.
4
+
5
+ Measured open-font fallbacks for proprietary document fonts (Office / Word / DOCX), as a tiny runtime data package. It carries the structured substitution evidence plus one asset-aware lookup, so a renderer can map a requested proprietary font to an open one without hand-copying tables, and without routing to a font it does not bundle.
6
+
7
+ It ships no fonts and no proprietary binaries: only the measured evidence (which open family stands in, the verdict, the advance delta, the license id, and a stable evidence id).
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ npm install @docfonts/fallbacks
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ `getFallback` answers "what does docfonts recommend for this family?". Pass `hasFamily` to keep it to fonts you actually bundle:
18
+
19
+ ```ts
20
+ import { getFallback } from "@docfonts/fallbacks";
21
+
22
+ getFallback("Helvetica", { hasFamily: (f) => bundled.has(f) });
23
+ // { family: "Liberation Sans", action: "substitute", verdict: "metric_safe", faithful: true, evidenceId: "helvetica" }
24
+ ```
25
+
26
+ `deriveFallbackMap` builds the substitute map you wire into a resolver. `hasFamily` is required here: a render map never includes a substitute whose font you cannot load.
27
+
28
+ ```ts
29
+ import { deriveFallbackMap } from "@docfonts/fallbacks";
30
+
31
+ const map = deriveFallbackMap({ hasFamily: (f) => bundled.has(f) });
32
+ // { helvetica: { family: "Liberation Sans", ... }, calibri: { ... }, ... }
33
+ // Rows whose family you do not bundle are left out, not routed to a missing asset.
34
+ ```
35
+
36
+ Both resolve to `null` (or omit the row) when docfonts has no row, the recommendation is "no open font stands in", or you do not ship the physical family.
37
+
38
+ ## What the fields mean
39
+
40
+ - `family` - the open family to render in place of the requested one.
41
+ - `action` - `substitute` (a measured metric match) or `category_fallback` (right letterforms, lower fidelity).
42
+ - `verdict` - the measured fidelity, from a fixed taxonomy (`metric_safe`, `near_metric`, `cell_width_only`, `visual_only`, ...). The headline rolls up to the worst face.
43
+ - `faithful` - a coarse "good enough for line-break fidelity" flag (`metric_safe` or `near_metric`). Not a claim of an exact clone; read `verdict` for the tier.
44
+ - `evidenceId` - the stable id for the reviewed evidence row.
45
+
46
+ The full structured rows are exported as `SUBSTITUTION_EVIDENCE` (faces, per-face verdicts, glyph exceptions) for richer reporting. Face-level routing stays yours: `getFallback` answers "which family", not "which face".
47
+
48
+ ## Provenance
49
+
50
+ The data comes from reviewed docfonts evidence. Measurements are produced against licensed originals,
51
+ but this package distributes no proprietary binaries or raw proprietary metrics.
52
+
53
+ Built by the team behind SuperDoc. Standalone and neutral.
54
+
55
+ ## License
56
+
57
+ MIT
package/dist/data.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import type { SubstitutionEvidence } from "./types";
2
+ export declare const SUBSTITUTION_EVIDENCE: readonly SubstitutionEvidence[];
package/dist/data.js ADDED
@@ -0,0 +1,649 @@
1
+ export const SUBSTITUTION_EVIDENCE = [
2
+ {
3
+ "evidenceId": "calibri",
4
+ "logicalFamily": "Calibri",
5
+ "physicalFamily": "Carlito",
6
+ "verdict": "metric_safe",
7
+ "faces": {
8
+ "regular": true,
9
+ "bold": true,
10
+ "italic": true,
11
+ "boldItalic": true
12
+ },
13
+ "gates": {
14
+ "static": "pass",
15
+ "metric": "pass",
16
+ "layout": "pass",
17
+ "ship": "pass"
18
+ },
19
+ "policyAction": "substitute",
20
+ "measurementRefs": [
21
+ "calibri__carlito#analytic_advance#2026-06-03",
22
+ "calibri__carlito#face_aggregate#2026-06-03"
23
+ ],
24
+ "exportRule": "preserve_original_name",
25
+ "advance": {
26
+ "meanDelta": 0,
27
+ "maxDelta": 0
28
+ },
29
+ "candidateLicense": "OFL-1.1"
30
+ },
31
+ {
32
+ "evidenceId": "cambria",
33
+ "logicalFamily": "Cambria",
34
+ "physicalFamily": "Caladea",
35
+ "verdict": "visual_only",
36
+ "faces": {
37
+ "regular": true,
38
+ "bold": true,
39
+ "italic": true,
40
+ "boldItalic": true
41
+ },
42
+ "gates": {
43
+ "static": "pass",
44
+ "metric": "pass",
45
+ "layout": "not_run",
46
+ "ship": "pass"
47
+ },
48
+ "policyAction": "substitute",
49
+ "measurementRefs": [
50
+ "cambria_regular__caladea#regular#w400#d2f6cad3#analytic_advance#2026-06-04",
51
+ "cambria_bold__caladea#bold#w700#74eda4fc#analytic_advance#2026-06-04",
52
+ "cambria_italic__caladea#italic#w400#9c968bf6#analytic_advance#2026-06-04",
53
+ "cambria_boldItalic__caladea#boldItalic#w700#f47a35ad#analytic_advance#2026-06-04"
54
+ ],
55
+ "exportRule": "preserve_original_name",
56
+ "advance": {
57
+ "meanDelta": 0.0002378,
58
+ "maxDelta": 0.2310758
59
+ },
60
+ "candidateLicense": "Apache-2.0",
61
+ "faceVerdicts": {
62
+ "regular": "metric_safe",
63
+ "bold": "metric_safe",
64
+ "italic": "metric_safe",
65
+ "boldItalic": "visual_only"
66
+ },
67
+ "glyphExceptions": [
68
+ {
69
+ "slot": "boldItalic",
70
+ "codepoint": 96,
71
+ "advanceDelta": 0.231,
72
+ "note": "Caladea Bold Italic grave accent (U+0060) advance diverges ~23% from Cambria; lines containing it reflow. All other glyphs, and the regular/bold/italic faces, are within the direct metric threshold."
73
+ }
74
+ ]
75
+ },
76
+ {
77
+ "evidenceId": "arial",
78
+ "logicalFamily": "Arial",
79
+ "physicalFamily": "Liberation Sans",
80
+ "verdict": "metric_safe",
81
+ "faces": {
82
+ "regular": true,
83
+ "bold": true,
84
+ "italic": true,
85
+ "boldItalic": true
86
+ },
87
+ "gates": {
88
+ "static": "pass",
89
+ "metric": "pass",
90
+ "layout": "not_run",
91
+ "ship": "pass"
92
+ },
93
+ "policyAction": "substitute",
94
+ "measurementRefs": [
95
+ "arial__liberation-sans#analytic_advance#2026-06-03"
96
+ ],
97
+ "exportRule": "preserve_original_name",
98
+ "advance": {
99
+ "meanDelta": 0,
100
+ "maxDelta": 0
101
+ },
102
+ "candidateLicense": "OFL-1.1"
103
+ },
104
+ {
105
+ "evidenceId": "times-new-roman",
106
+ "logicalFamily": "Times New Roman",
107
+ "physicalFamily": "Liberation Serif",
108
+ "verdict": "metric_safe",
109
+ "faces": {
110
+ "regular": true,
111
+ "bold": true,
112
+ "italic": true,
113
+ "boldItalic": true
114
+ },
115
+ "gates": {
116
+ "static": "pass",
117
+ "metric": "pass",
118
+ "layout": "not_run",
119
+ "ship": "pass"
120
+ },
121
+ "policyAction": "substitute",
122
+ "measurementRefs": [
123
+ "times-new-roman__liberation-serif#analytic_advance#2026-06-03"
124
+ ],
125
+ "exportRule": "preserve_original_name",
126
+ "advance": {
127
+ "meanDelta": 0,
128
+ "maxDelta": 0
129
+ },
130
+ "candidateLicense": "OFL-1.1"
131
+ },
132
+ {
133
+ "evidenceId": "courier-new",
134
+ "logicalFamily": "Courier New",
135
+ "physicalFamily": "Liberation Mono",
136
+ "verdict": "metric_safe",
137
+ "faces": {
138
+ "regular": true,
139
+ "bold": true,
140
+ "italic": true,
141
+ "boldItalic": true
142
+ },
143
+ "gates": {
144
+ "static": "pass",
145
+ "metric": "pass",
146
+ "layout": "not_run",
147
+ "ship": "pass"
148
+ },
149
+ "policyAction": "substitute",
150
+ "measurementRefs": [
151
+ "courier-new__liberation-mono#analytic_advance#2026-06-03"
152
+ ],
153
+ "exportRule": "preserve_original_name",
154
+ "advance": {
155
+ "meanDelta": 0,
156
+ "maxDelta": 0
157
+ },
158
+ "candidateLicense": "OFL-1.1"
159
+ },
160
+ {
161
+ "evidenceId": "georgia",
162
+ "logicalFamily": "Georgia",
163
+ "physicalFamily": "Gelasio",
164
+ "verdict": "near_metric",
165
+ "faces": {
166
+ "regular": true,
167
+ "bold": true,
168
+ "italic": true,
169
+ "boldItalic": true
170
+ },
171
+ "gates": {
172
+ "static": "pass",
173
+ "metric": "pass",
174
+ "layout": "pass",
175
+ "ship": "fail"
176
+ },
177
+ "policyAction": "substitute",
178
+ "measurementRefs": [
179
+ "georgia_regular__gelasio#regular#w400#1543f04d#analytic_advance#2026-06-04",
180
+ "georgia_bold__gelasio#bold#w700#5a1b9bd7#analytic_advance#2026-06-04",
181
+ "georgia_italic__gelasio#italic#w400#be1243a9#analytic_advance#2026-06-04",
182
+ "georgia_boldItalic__gelasio#boldItalic#w700#6f3b3f7a#analytic_advance#2026-06-04",
183
+ "georgia_regular__gelasio#regular#w400#1543f04d#live_layout#2026-06-03",
184
+ "georgia_bold__gelasio#bold#w700#5a1b9bd7#live_layout#2026-06-03",
185
+ "georgia_italic__gelasio#italic#w400#be1243a9#live_layout#2026-06-03",
186
+ "georgia_boldItalic__gelasio#boldItalic#w700#6f3b3f7a#live_layout#2026-06-03"
187
+ ],
188
+ "exportRule": "preserve_original_name",
189
+ "advance": {
190
+ "meanDelta": 0.0000197,
191
+ "maxDelta": 0.0183727
192
+ },
193
+ "candidateLicense": "OFL-1.1",
194
+ "faceVerdicts": {
195
+ "regular": "metric_safe",
196
+ "bold": "metric_safe",
197
+ "italic": "near_metric",
198
+ "boldItalic": "near_metric"
199
+ },
200
+ "glyphExceptions": [
201
+ {
202
+ "slot": "italic",
203
+ "codepoint": 210,
204
+ "advanceDelta": 0.0184,
205
+ "note": "Georgia Italic vs Gelasio Italic: accented capital O (U+00D2-D8: O-grave/acute/circumflex/diaeresis/stroke) advance differs ~1.84%. 5 rare glyphs; all other glyphs exact, mean 0%."
206
+ },
207
+ {
208
+ "slot": "boldItalic",
209
+ "codepoint": 204,
210
+ "advanceDelta": 0.011,
211
+ "note": "Georgia Bold Italic vs Gelasio Bold Italic: accented capital I (U+00CC-CE: I-grave/acute/circumflex) advance differs ~1.10%. 3 rare glyphs; all other glyphs exact, mean ~0%."
212
+ }
213
+ ]
214
+ },
215
+ {
216
+ "evidenceId": "arial-narrow",
217
+ "logicalFamily": "Arial Narrow",
218
+ "physicalFamily": "Liberation Sans Narrow",
219
+ "verdict": "visual_only",
220
+ "faces": {
221
+ "regular": true,
222
+ "bold": true,
223
+ "italic": true,
224
+ "boldItalic": true
225
+ },
226
+ "gates": {
227
+ "static": "pass",
228
+ "metric": "pass",
229
+ "layout": "not_run",
230
+ "ship": "fail"
231
+ },
232
+ "policyAction": "substitute",
233
+ "measurementRefs": [
234
+ "arial-narrow_regular__liberation-sans-narrow#regular#w400#546e8957#analytic_advance#2026-06-04",
235
+ "arial-narrow_bold__liberation-sans-narrow#bold#w700#8e5eb509#analytic_advance#2026-06-04",
236
+ "arial-narrow_italic__liberation-sans-narrow#italic#w400#c5de4127#analytic_advance#2026-06-04",
237
+ "arial-narrow_boldItalic__liberation-sans-narrow#boldItalic#w700#57fe1513#analytic_advance#2026-06-04"
238
+ ],
239
+ "exportRule": "preserve_original_name",
240
+ "advance": {
241
+ "meanDelta": 0,
242
+ "maxDelta": 0.5
243
+ },
244
+ "candidateLicense": "GPLv2-with-font-exception",
245
+ "faceVerdicts": {
246
+ "regular": "metric_safe",
247
+ "bold": "visual_only",
248
+ "italic": "metric_safe",
249
+ "boldItalic": "metric_safe"
250
+ },
251
+ "glyphExceptions": [
252
+ {
253
+ "slot": "bold",
254
+ "codepoint": 160,
255
+ "advanceDelta": 0.5,
256
+ "note": "Arial Narrow Bold no-break space (U+00A0) is double-width (2x the regular space); Liberation Sans Narrow Bold matches the regular space, so lines containing a non-breaking space reflow. All other glyphs, and the regular/italic/boldItalic faces, match within the direct metric threshold."
257
+ }
258
+ ]
259
+ },
260
+ {
261
+ "evidenceId": "aptos",
262
+ "logicalFamily": "Aptos",
263
+ "physicalFamily": null,
264
+ "verdict": "no_substitute",
265
+ "faces": {
266
+ "regular": false,
267
+ "bold": false,
268
+ "italic": false,
269
+ "boldItalic": false
270
+ },
271
+ "gates": {
272
+ "static": "not_run",
273
+ "metric": "fail",
274
+ "layout": "not_run",
275
+ "ship": "not_run"
276
+ },
277
+ "policyAction": "customer_supplied",
278
+ "measurementRefs": [
279
+ "aptos#top_candidates#2026-06-03"
280
+ ],
281
+ "exportRule": "preserve_original_name",
282
+ "candidateLicense": null
283
+ },
284
+ {
285
+ "evidenceId": "consolas",
286
+ "logicalFamily": "Consolas",
287
+ "physicalFamily": "Inconsolata SemiExpanded",
288
+ "verdict": "cell_width_only",
289
+ "faces": {
290
+ "regular": false,
291
+ "bold": false,
292
+ "italic": false,
293
+ "boldItalic": false
294
+ },
295
+ "gates": {
296
+ "static": "not_run",
297
+ "metric": "not_run",
298
+ "layout": "not_run",
299
+ "ship": "not_run"
300
+ },
301
+ "policyAction": "category_fallback",
302
+ "measurementRefs": [
303
+ "consolas__inconsolata-semiexpanded#analytic_advance#2026-06-03"
304
+ ],
305
+ "exportRule": "preserve_original_name",
306
+ "advance": {
307
+ "meanDelta": 0.00035999999999999997,
308
+ "maxDelta": 0.00035999999999999997
309
+ },
310
+ "candidateLicense": "OFL-1.1"
311
+ },
312
+ {
313
+ "evidenceId": "verdana",
314
+ "logicalFamily": "Verdana",
315
+ "physicalFamily": null,
316
+ "verdict": "visual_only",
317
+ "faces": {
318
+ "regular": false,
319
+ "bold": false,
320
+ "italic": false,
321
+ "boldItalic": false
322
+ },
323
+ "gates": {
324
+ "static": "not_run",
325
+ "metric": "fail",
326
+ "layout": "not_run",
327
+ "ship": "not_run"
328
+ },
329
+ "policyAction": "category_fallback",
330
+ "measurementRefs": [
331
+ "verdana#top_candidates#2026-06-03"
332
+ ],
333
+ "exportRule": "preserve_original_name",
334
+ "candidateLicense": null
335
+ },
336
+ {
337
+ "evidenceId": "tahoma",
338
+ "logicalFamily": "Tahoma",
339
+ "physicalFamily": null,
340
+ "verdict": "visual_only",
341
+ "faces": {
342
+ "regular": false,
343
+ "bold": false,
344
+ "italic": false,
345
+ "boldItalic": false
346
+ },
347
+ "gates": {
348
+ "static": "not_run",
349
+ "metric": "fail",
350
+ "layout": "not_run",
351
+ "ship": "not_run"
352
+ },
353
+ "policyAction": "category_fallback",
354
+ "measurementRefs": [
355
+ "tahoma#top_candidates#2026-06-03"
356
+ ],
357
+ "exportRule": "preserve_original_name",
358
+ "candidateLicense": null
359
+ },
360
+ {
361
+ "evidenceId": "trebuchet-ms",
362
+ "logicalFamily": "Trebuchet MS",
363
+ "physicalFamily": null,
364
+ "verdict": "visual_only",
365
+ "faces": {
366
+ "regular": false,
367
+ "bold": false,
368
+ "italic": false,
369
+ "boldItalic": false
370
+ },
371
+ "gates": {
372
+ "static": "not_run",
373
+ "metric": "fail",
374
+ "layout": "not_run",
375
+ "ship": "not_run"
376
+ },
377
+ "policyAction": "category_fallback",
378
+ "measurementRefs": [
379
+ "trebuchet-ms#top_candidates#2026-06-03"
380
+ ],
381
+ "exportRule": "preserve_original_name",
382
+ "candidateLicense": null
383
+ },
384
+ {
385
+ "evidenceId": "comic-sans-ms",
386
+ "logicalFamily": "Comic Sans MS",
387
+ "physicalFamily": "Comic Neue",
388
+ "verdict": "visual_only",
389
+ "faces": {
390
+ "regular": false,
391
+ "bold": false,
392
+ "italic": false,
393
+ "boldItalic": false
394
+ },
395
+ "gates": {
396
+ "static": "not_run",
397
+ "metric": "fail",
398
+ "layout": "not_run",
399
+ "ship": "not_run"
400
+ },
401
+ "policyAction": "category_fallback",
402
+ "measurementRefs": [
403
+ "comic-sans-ms__comic-neue#analytic_advance#2026-06-03"
404
+ ],
405
+ "exportRule": "preserve_original_name",
406
+ "advance": {
407
+ "meanDelta": 0.1005,
408
+ "maxDelta": 0.1419
409
+ },
410
+ "candidateLicense": "OFL-1.1"
411
+ },
412
+ {
413
+ "evidenceId": "candara",
414
+ "logicalFamily": "Candara",
415
+ "physicalFamily": null,
416
+ "verdict": "visual_only",
417
+ "faces": {
418
+ "regular": false,
419
+ "bold": false,
420
+ "italic": false,
421
+ "boldItalic": false
422
+ },
423
+ "gates": {
424
+ "static": "not_run",
425
+ "metric": "fail",
426
+ "layout": "not_run",
427
+ "ship": "not_run"
428
+ },
429
+ "policyAction": "category_fallback",
430
+ "measurementRefs": [
431
+ "candara#top_candidates#2026-06-03"
432
+ ],
433
+ "exportRule": "preserve_original_name",
434
+ "candidateLicense": null
435
+ },
436
+ {
437
+ "evidenceId": "constantia",
438
+ "logicalFamily": "Constantia",
439
+ "physicalFamily": null,
440
+ "verdict": "visual_only",
441
+ "faces": {
442
+ "regular": false,
443
+ "bold": false,
444
+ "italic": false,
445
+ "boldItalic": false
446
+ },
447
+ "gates": {
448
+ "static": "not_run",
449
+ "metric": "fail",
450
+ "layout": "not_run",
451
+ "ship": "not_run"
452
+ },
453
+ "policyAction": "category_fallback",
454
+ "measurementRefs": [
455
+ "constantia#top_candidates#2026-06-03"
456
+ ],
457
+ "exportRule": "preserve_original_name",
458
+ "candidateLicense": null
459
+ },
460
+ {
461
+ "evidenceId": "corbel",
462
+ "logicalFamily": "Corbel",
463
+ "physicalFamily": null,
464
+ "verdict": "visual_only",
465
+ "faces": {
466
+ "regular": false,
467
+ "bold": false,
468
+ "italic": false,
469
+ "boldItalic": false
470
+ },
471
+ "gates": {
472
+ "static": "not_run",
473
+ "metric": "fail",
474
+ "layout": "not_run",
475
+ "ship": "not_run"
476
+ },
477
+ "policyAction": "category_fallback",
478
+ "measurementRefs": [
479
+ "corbel#top_candidates#2026-06-03"
480
+ ],
481
+ "exportRule": "preserve_original_name",
482
+ "candidateLicense": null
483
+ },
484
+ {
485
+ "evidenceId": "lucida-console",
486
+ "logicalFamily": "Lucida Console",
487
+ "physicalFamily": "Cousine",
488
+ "verdict": "cell_width_only",
489
+ "faces": {
490
+ "regular": false,
491
+ "bold": false,
492
+ "italic": false,
493
+ "boldItalic": false
494
+ },
495
+ "gates": {
496
+ "static": "not_run",
497
+ "metric": "not_run",
498
+ "layout": "not_run",
499
+ "ship": "not_run"
500
+ },
501
+ "policyAction": "category_fallback",
502
+ "measurementRefs": [
503
+ "lucida-console__cousine#analytic_advance#2026-06-03"
504
+ ],
505
+ "exportRule": "preserve_original_name",
506
+ "advance": {
507
+ "meanDelta": 0.004050000000000001,
508
+ "maxDelta": 0.004050000000000001
509
+ },
510
+ "candidateLicense": "OFL-1.1"
511
+ },
512
+ {
513
+ "evidenceId": "aptos-display",
514
+ "logicalFamily": "Aptos Display",
515
+ "physicalFamily": null,
516
+ "verdict": "customer_supplied",
517
+ "faces": {
518
+ "regular": false,
519
+ "bold": false,
520
+ "italic": false,
521
+ "boldItalic": false
522
+ },
523
+ "gates": {
524
+ "static": "not_run",
525
+ "metric": "not_run",
526
+ "layout": "not_run",
527
+ "ship": "fail"
528
+ },
529
+ "policyAction": "customer_supplied",
530
+ "measurementRefs": [],
531
+ "exportRule": "preserve_original_name"
532
+ },
533
+ {
534
+ "evidenceId": "cambria-math",
535
+ "logicalFamily": "Cambria Math",
536
+ "physicalFamily": null,
537
+ "verdict": "preserve_only",
538
+ "faces": {
539
+ "regular": false,
540
+ "bold": false,
541
+ "italic": false,
542
+ "boldItalic": false
543
+ },
544
+ "gates": {
545
+ "static": "not_run",
546
+ "metric": "not_run",
547
+ "layout": "not_run",
548
+ "ship": "not_run"
549
+ },
550
+ "policyAction": "preserve_only",
551
+ "measurementRefs": [],
552
+ "exportRule": "preserve_original_name"
553
+ },
554
+ {
555
+ "evidenceId": "helvetica",
556
+ "logicalFamily": "Helvetica",
557
+ "physicalFamily": "Liberation Sans",
558
+ "verdict": "metric_safe",
559
+ "faces": {
560
+ "regular": true,
561
+ "bold": true,
562
+ "italic": true,
563
+ "boldItalic": true
564
+ },
565
+ "gates": {
566
+ "static": "not_run",
567
+ "metric": "pass",
568
+ "layout": "not_run",
569
+ "ship": "pass"
570
+ },
571
+ "policyAction": "substitute",
572
+ "measurementRefs": [
573
+ "helvetica__liberation-sans#analytic_advance#2026-06-03"
574
+ ],
575
+ "exportRule": "preserve_original_name",
576
+ "advance": {
577
+ "meanDelta": 0,
578
+ "maxDelta": 0
579
+ },
580
+ "candidateLicense": "OFL-1.1"
581
+ },
582
+ {
583
+ "evidenceId": "calibri-light",
584
+ "logicalFamily": "Calibri Light",
585
+ "physicalFamily": "Carlito",
586
+ "verdict": "visual_only",
587
+ "faces": {
588
+ "regular": false,
589
+ "bold": false,
590
+ "italic": false,
591
+ "boldItalic": false
592
+ },
593
+ "gates": {
594
+ "static": "not_run",
595
+ "metric": "fail",
596
+ "layout": "not_run",
597
+ "ship": "fail"
598
+ },
599
+ "policyAction": "category_fallback",
600
+ "measurementRefs": [
601
+ "calibri-light__carlito#analytic_advance#2026-06-05"
602
+ ],
603
+ "exportRule": "preserve_original_name",
604
+ "advance": {
605
+ "meanDelta": 0.0148,
606
+ "maxDelta": 0.066
607
+ },
608
+ "candidateLicense": "OFL-1.1"
609
+ },
610
+ {
611
+ "evidenceId": "baskerville-old-face",
612
+ "logicalFamily": "Baskerville Old Face",
613
+ "physicalFamily": "Bacasime Antique",
614
+ "verdict": "visual_only",
615
+ "faces": {
616
+ "regular": true,
617
+ "bold": false,
618
+ "italic": false,
619
+ "boldItalic": false
620
+ },
621
+ "gates": {
622
+ "static": "pass",
623
+ "metric": "fail",
624
+ "layout": "not_run",
625
+ "ship": "not_run"
626
+ },
627
+ "policyAction": "substitute",
628
+ "measurementRefs": [
629
+ "baskerville-old-face_regular__bacasime-antique#regular#w400#7dac1e5f#analytic_advance#2026-06-05"
630
+ ],
631
+ "exportRule": "preserve_original_name",
632
+ "advance": {
633
+ "meanDelta": 0,
634
+ "maxDelta": 0.4915590863952334
635
+ },
636
+ "candidateLicense": "OFL-1.1",
637
+ "faceVerdicts": {
638
+ "regular": "visual_only"
639
+ },
640
+ "glyphExceptions": [
641
+ {
642
+ "slot": "regular",
643
+ "codepoint": 160,
644
+ "advanceDelta": 0.4916,
645
+ "note": "Bacasime Antique Regular's no-break space (U+00A0) advance diverges ~49% from Baskerville Old Face; lines containing NBSP reflow. Every other Latin-core glyph is advance-identical, which is why this is visual_only with a single named exception, not near_metric."
646
+ }
647
+ ]
648
+ }
649
+ ];
@@ -0,0 +1,27 @@
1
+ import type { FontFallback } from "./types";
2
+ /** Reports whether the consumer can actually render (i.e. bundles the asset for) a physical family. */
3
+ export type HasFamily = (family: string) => boolean;
4
+ /** Options for {@link getFallback}. `hasFamily` is OPTIONAL here - omit it to get the raw recommendation. */
5
+ export interface FallbackOptions {
6
+ /**
7
+ * When given, a row whose `physicalFamily` is not available resolves to null - the row stays inert
8
+ * until the consumer bundles it. When omitted, every row with a physical family is considered present.
9
+ */
10
+ hasFamily?: HasFamily;
11
+ }
12
+ /** Options for {@link deriveFallbackMap}. `hasFamily` is REQUIRED - a render map must be asset-safe. */
13
+ export interface FallbackMapOptions {
14
+ hasFamily: HasFamily;
15
+ }
16
+ /**
17
+ * The open fallback for a requested font family, or null when docfonts has no row, the row recommends
18
+ * no substitute, or the consumer does not ship the physical family. Case- and quote-insensitive.
19
+ */
20
+ export declare function getFallback(logicalFamily: string, options?: FallbackOptions): FontFallback | null;
21
+ /**
22
+ * The renderer's substitute map: every fallback the consumer can actually render, keyed by the
23
+ * normalized (lowercased) logical family. `hasFamily` is REQUIRED - rows whose physical family the
24
+ * consumer does not bundle are excluded, so the map is safe to wire straight into a resolver. The keys
25
+ * are exactly the families it should remap. For the un-gated single recommendation, use {@link getFallback}.
26
+ */
27
+ export declare function deriveFallbackMap(options: FallbackMapOptions): Record<string, FontFallback>;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Asset-aware fallback helpers. `getFallback` can inspect the raw recommendation; `deriveFallbackMap`
3
+ * requires `hasFamily` because a renderer map must not include fonts the consumer cannot load. Face
4
+ * routing stays consumer-owned; this package answers which family, not which face.
5
+ */
6
+ import { SUBSTITUTION_EVIDENCE } from "./data";
7
+ /** The two metric-grade bands. A substitution in either is line-break faithful; everything else is not. */
8
+ const FAITHFUL_VERDICTS = new Set([
9
+ "metric_safe",
10
+ "near_metric",
11
+ ]);
12
+ /** Actions that mean "render this physical family". */
13
+ const RENDERABLE_ACTIONS = new Set([
14
+ "substitute",
15
+ "category_fallback",
16
+ ]);
17
+ /** Normalize a family name to a lookup key: trim, strip surrounding quotes, lowercase (CSS-name safe). */
18
+ function normalizeFamily(name) {
19
+ return name
20
+ .trim()
21
+ .replace(/^['"]+|['"]+$/g, "")
22
+ .trim()
23
+ .toLowerCase();
24
+ }
25
+ /** Evidence rows indexed by normalized logical family, built once. */
26
+ const BY_LOGICAL = new Map(SUBSTITUTION_EVIDENCE.map((row) => [normalizeFamily(row.logicalFamily), row]));
27
+ /** Project a row to a FontFallback, or null when it offers nothing the consumer can render. */
28
+ function toFallback(row, hasFamily) {
29
+ if (row.physicalFamily === null)
30
+ return null;
31
+ if (!RENDERABLE_ACTIONS.has(row.policyAction))
32
+ return null;
33
+ if (hasFamily && !hasFamily(row.physicalFamily))
34
+ return null;
35
+ return {
36
+ family: row.physicalFamily,
37
+ action: row.policyAction,
38
+ verdict: row.verdict,
39
+ faithful: FAITHFUL_VERDICTS.has(row.verdict),
40
+ evidenceId: row.evidenceId,
41
+ };
42
+ }
43
+ /**
44
+ * The open fallback for a requested font family, or null when docfonts has no row, the row recommends
45
+ * no substitute, or the consumer does not ship the physical family. Case- and quote-insensitive.
46
+ */
47
+ export function getFallback(logicalFamily, options = {}) {
48
+ const row = BY_LOGICAL.get(normalizeFamily(logicalFamily));
49
+ return row ? toFallback(row, options.hasFamily) : null;
50
+ }
51
+ /**
52
+ * The renderer's substitute map: every fallback the consumer can actually render, keyed by the
53
+ * normalized (lowercased) logical family. `hasFamily` is REQUIRED - rows whose physical family the
54
+ * consumer does not bundle are excluded, so the map is safe to wire straight into a resolver. The keys
55
+ * are exactly the families it should remap. For the un-gated single recommendation, use {@link getFallback}.
56
+ */
57
+ export function deriveFallbackMap(options) {
58
+ const out = {};
59
+ for (const [key, row] of BY_LOGICAL) {
60
+ const fallback = toFallback(row, options.hasFamily);
61
+ if (fallback)
62
+ out[key] = fallback;
63
+ }
64
+ return out;
65
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Runtime fallback evidence and asset-aware lookup helpers. No font parser, research data, or runtime
3
+ * dependency.
4
+ */
5
+ export { SUBSTITUTION_EVIDENCE } from "./data";
6
+ export { deriveFallbackMap, type FallbackMapOptions, type FallbackOptions, getFallback, type HasFamily, } from "./fallbacks";
7
+ export type { AdvanceDelta, FaceCoverage, FaceSlot, FontFallback, GlyphException, PolicyAction, SubstituteGates, SubstitutionEvidence, Verdict, } from "./types";
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Runtime fallback evidence and asset-aware lookup helpers. No font parser, research data, or runtime
3
+ * dependency.
4
+ */
5
+ export { SUBSTITUTION_EVIDENCE } from "./data";
6
+ export { deriveFallbackMap, getFallback, } from "./fallbacks";
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Public types for `@docfonts/fallbacks`. The package is self-contained so consumers install one
3
+ * runtime dependency.
4
+ */
5
+ /** Fidelity verdict, best to worst. */
6
+ export type Verdict = "metric_safe" | "near_metric" | "cell_width_only" | "visual_only" | "customer_supplied" | "preserve_only" | "no_substitute";
7
+ /** Renderer-neutral resolution action. */
8
+ export type PolicyAction = "substitute" | "category_fallback" | "preserve_only" | "customer_supplied";
9
+ /** Derived public gate status. Diagnostic only. */
10
+ export type GateStatus = "pass" | "not_run" | "fail";
11
+ /** RIBBI face slot - the renderer's coarse face bucket. */
12
+ export type FaceSlot = "regular" | "bold" | "italic" | "boldItalic";
13
+ /** Advance-width divergence vs the proprietary oracle, as fractions (0 = identical advances). */
14
+ export interface AdvanceDelta {
15
+ meanDelta: number;
16
+ /** the worst-case delta, not the mean, is what gates line-break fidelity. */
17
+ maxDelta: number;
18
+ }
19
+ /** Which of the four RIBBI faces the physical candidate supplies. */
20
+ export interface FaceCoverage {
21
+ regular: boolean;
22
+ bold: boolean;
23
+ italic: boolean;
24
+ boldItalic: boolean;
25
+ }
26
+ /** The four derived gate statuses behind a verdict; the proof is the referenced measurements. */
27
+ export interface SubstituteGates {
28
+ static: GateStatus;
29
+ metric: GateStatus;
30
+ layout: GateStatus;
31
+ ship: GateStatus;
32
+ }
33
+ /** A named glyph-level advance divergence that qualifies one face (e.g. one codepoint reflows). */
34
+ export interface GlyphException {
35
+ slot: FaceSlot;
36
+ codepoint: number;
37
+ advanceDelta: number;
38
+ note: string;
39
+ }
40
+ /**
41
+ * One logical font's structured fallback evidence.
42
+ */
43
+ export interface SubstitutionEvidence {
44
+ /** docfonts evidence id, e.g. "cambria". */
45
+ evidenceId: string;
46
+ /** the proprietary family the document asks for, e.g. "Cambria". */
47
+ logicalFamily: string;
48
+ /** the physical substitute rendered in its place; null when no candidate is recommended. */
49
+ physicalFamily: string | null;
50
+ /** worst-face fidelity verdict (the public summary; see `faceVerdicts` when faces disagree). */
51
+ verdict: Verdict;
52
+ /** per-face verdicts, AUTHORITATIVE when present (a QUALIFIED substitute); top-level = worst face. */
53
+ faceVerdicts?: Partial<Record<FaceSlot, Verdict>>;
54
+ /** named glyph-level divergences that qualify a face. */
55
+ glyphExceptions?: GlyphException[];
56
+ faces: FaceCoverage;
57
+ advance?: AdvanceDelta;
58
+ gates: SubstituteGates;
59
+ /** renderer-neutral action; `substitute` is what makes a consumer map the family. */
60
+ policyAction: PolicyAction;
61
+ /** stable measurement ids behind the row. */
62
+ measurementRefs: string[];
63
+ /** SPDX id of the substitute's license. */
64
+ candidateLicense?: string | null;
65
+ exportRule: "preserve_original_name";
66
+ }
67
+ /**
68
+ * The ergonomic result of {@link getFallback}: the single decision a renderer needs to act on - which
69
+ * physical family to render, how it was chosen, and whether it is metric-faithful. The full structured
70
+ * row stays available via {@link SUBSTITUTION_EVIDENCE} for richer reporting.
71
+ */
72
+ export interface FontFallback {
73
+ /** the physical family to render in place of the requested font. */
74
+ family: string;
75
+ /** how it was chosen: a metric substitute vs a same-category visual fallback. */
76
+ action: PolicyAction;
77
+ /** the worst-face fidelity verdict behind the choice. */
78
+ verdict: Verdict;
79
+ /**
80
+ * Coarse "good enough for line-break fidelity" flag: true for the metric-grade bands (verdict
81
+ * metric_safe or near_metric), false otherwise. NOT a claim of a perfect/exact clone - near_metric
82
+ * drifts a few glyphs, and a row can roll up to a worse top-level verdict because of one face (see
83
+ * Cambria). Read `verdict` (and the row's `faceVerdicts`) for the precise tier.
84
+ */
85
+ faithful: boolean;
86
+ /** stable reviewed-evidence id. */
87
+ evidenceId: string;
88
+ }
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Public types for `@docfonts/fallbacks`. The package is self-contained so consumers install one
3
+ * runtime dependency.
4
+ */
5
+ export {};
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@docfonts/fallbacks",
3
+ "version": "0.1.0",
4
+ "description": "Measured open-font fallbacks for proprietary document fonts.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "sideEffects": false,
23
+ "keywords": [
24
+ "fonts",
25
+ "font-fallback",
26
+ "font-substitution",
27
+ "docx",
28
+ "office",
29
+ "open-fonts",
30
+ "document-fonts"
31
+ ],
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/superdoc-dev/docfonts.git",
35
+ "directory": "packages/fallbacks"
36
+ },
37
+ "scripts": {
38
+ "build": "tsc -p tsconfig.build.json",
39
+ "prepack": "bun run build"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ }
44
+ }