diff --git a/packages/fallbacks/fallbacks.test.ts b/packages/fallbacks/fallbacks.test.ts index bd9b433..19af57c 100644 --- a/packages/fallbacks/fallbacks.test.ts +++ b/packages/fallbacks/fallbacks.test.ts @@ -54,15 +54,15 @@ describe("getFallbackDecision", () => { }); test("a measured 'no open font' is distinct from an unknown font", () => { - // The whole point of the decision API: docfonts MEASURED Aptos and Verdana; it never heard of "Foo". + // The whole point of the decision API: docfonts measured Aptos and Candara; it never heard of "Foo". expect(getFallbackDecision("Aptos")).toEqual({ kind: "customer_supplied", evidenceId: "aptos", generic: "sans-serif", }); - expect(getFallbackDecision("Verdana")).toEqual({ + expect(getFallbackDecision("Candara")).toEqual({ kind: "no_recommended_fallback", - evidenceId: "verdana", + evidenceId: "candara", generic: "sans-serif", }); expect(getFallbackDecision("Cambria Math")).toEqual({ @@ -106,13 +106,19 @@ describe("getRenderableFallback", () => { } }); - test("a monospace cell_width_only substitute is lineBreakSafe (advances match)", () => { - // Consolas -> Inconsolata SemiExpanded: glyph shapes differ, but cell width (every advance) matches. - const fb = getRenderableFallback("Consolas", { + test("a monospace cell_width_only face is lineBreakSafe (advances match)", () => { + // Consolas -> Inconsolata SemiExpanded: glyph shapes differ, but real R/B cell widths match. + const family = getRenderableFallback("Consolas", { canRenderFamily: () => true, }); - expect(fb?.verdict).toBe("cell_width_only"); - expect(fb?.lineBreakSafe).toBe(true); + expect(family?.verdict).toBe("visual_only"); + expect(family?.lineBreakSafe).toBe(false); + + const regular = getRenderableFallbackForFace("Consolas", "regular", { + canRenderFamily: () => true, + }); + expect(regular?.verdict).toBe("cell_width_only"); + expect(regular?.lineBreakSafe).toBe(true); }); }); @@ -328,9 +334,11 @@ describe("selected visual fallback rows", () => { ["Comic Sans MS", "Comic Relief", "category_fallback"], ["Garamond", "Cardo", "category_fallback"], ["Gill Sans MT Condensed", "PT Sans Narrow", "category_fallback"], + ["ITC Bookman", "TeX Gyre Bonum", "substitute"], ["Lucida Console", "Noto Sans Mono", "category_fallback"], ["Tahoma", "Noto Sans", "category_fallback"], ["Trebuchet MS", "PT Sans", "category_fallback"], + ["Verdana", "Noto Sans", "category_fallback"], ] as const; for (const [logical, physical, policyAction] of expected) { @@ -419,6 +427,79 @@ describe("selected visual fallback rows", () => { }); } }); + + test("Consolas keeps cell-width evidence for real Inconsolata SemiExpanded faces", () => { + for (const face of ["regular", "bold"] as const) { + expect( + getRenderableFallbackForFace("Consolas", face, renderAll), + `Consolas ${face}`, + ).toMatchObject({ + substituteFamily: "Inconsolata SemiExpanded", + verdict: "cell_width_only", + lineBreakSafe: true, + }); + expect( + getRenderableFallbackForFace("Consolas", face, renderAll)?.faceSource, + ).toBeUndefined(); + } + + expect( + getRenderableFallbackForFace("Consolas", "italic", renderAll), + ).toMatchObject({ + substituteFamily: "Inconsolata SemiExpanded", + verdict: "visual_only", + lineBreakSafe: false, + faceSource: { kind: "synthetic", from: "regular" }, + }); + expect( + getRenderableFallbackForFace("Consolas", "boldItalic", renderAll), + ).toMatchObject({ + substituteFamily: "Inconsolata SemiExpanded", + verdict: "visual_only", + lineBreakSafe: false, + faceSource: { kind: "synthetic", from: "bold" }, + }); + }); + + test("Century rows expose measured safe faces without advertising the whole family as safe", () => { + expect(getRenderableFallback("Century", renderAll)).toMatchObject({ + substituteFamily: "C059", + verdict: "visual_only", + lineBreakSafe: false, + }); + expect( + getRenderableFallbackForFace("Century", "regular", renderAll), + ).toMatchObject({ + substituteFamily: "C059", + verdict: "metric_safe", + lineBreakSafe: true, + glyphExceptions: [{ slot: "regular", codepoint: 0x00af }], + }); + + expect( + getRenderableFallbackForFace("Century Schoolbook", "regular", renderAll), + ).toMatchObject({ + substituteFamily: "C059", + verdict: "metric_safe", + lineBreakSafe: true, + glyphExceptions: [{ slot: "regular", codepoint: 0x00af }], + }); + expect( + getRenderableFallbackForFace("Century Schoolbook", "bold", renderAll), + ).toMatchObject({ + substituteFamily: "C059", + verdict: "metric_safe", + lineBreakSafe: true, + glyphExceptions: [{ slot: "bold", codepoint: 0x00af }], + }); + expect( + getRenderableFallbackForFace("Century Schoolbook", "italic", renderAll), + ).toMatchObject({ + substituteFamily: "C059", + verdict: "visual_only", + lineBreakSafe: false, + }); + }); }); describe("candidate license metadata", () => { diff --git a/packages/fallbacks/records.json b/packages/fallbacks/records.json index 00c673a..77708a0 100644 --- a/packages/fallbacks/records.json +++ b/packages/fallbacks/records.json @@ -374,6 +374,31 @@ "exportRule": "preserve_original_name", "candidateLicense": "LicenseRef-GUST-Font-License-1.0" }, + { + "evidenceId": "itc-bookman", + "generic": "serif", + "logicalFamily": "ITC Bookman", + "physicalFamily": "TeX Gyre Bonum", + "verdict": "visual_only", + "faces": { + "regular": true, + "bold": true, + "italic": true, + "boldItalic": true + }, + "gates": { + "static": "pass", + "metric": "fail", + "layout": "not_run", + "ship": "fail" + }, + "policyAction": "substitute", + "measurementRefs": [ + "itc-bookman__tex-gyre-bonum#alias_of_bookman-old-style#2026-06-10" + ], + "exportRule": "preserve_original_name", + "candidateLicense": "LicenseRef-GUST-Font-License-1.0" + }, { "evidenceId": "century", "generic": "serif", @@ -388,14 +413,33 @@ }, "gates": { "static": "pass", - "metric": "fail", + "metric": "pass", "layout": "not_run", "ship": "fail" }, "policyAction": "substitute", - "measurementRefs": ["century__c059#visual_review#2026-06-09"], + "measurementRefs": [ + "century_regular__c059#regular#w400#analytic_advance#2026-06-10", + "century__c059#visual_review#2026-06-09" + ], "exportRule": "preserve_original_name", - "candidateLicense": "AGPL-3.0-only WITH PS-or-PDF-font-exception-20170817" + "candidateLicense": "AGPL-3.0-only WITH PS-or-PDF-font-exception-20170817", + "advance": { + "basis": "latin_full", + "meanDelta": 0.00293647, + "maxDelta": 0.167 + }, + "faceVerdicts": { + "regular": "metric_safe" + }, + "glyphExceptions": [ + { + "slot": "regular", + "codepoint": 175, + "advanceDelta": 0.167, + "note": "Century Regular vs C059 Roman: macron (U+00AF) advance differs ~16.7%; yen (U+00A5), plus-minus (U+00B1), division sign (U+00F7), and middle dot (U+00B7) also exceed the direct threshold. Body-text Latin sample is metric_safe." + } + ] }, { "evidenceId": "century-schoolbook", @@ -416,9 +460,40 @@ "ship": "fail" }, "policyAction": "substitute", - "measurementRefs": ["century-schoolbook__c059#visual_review#2026-06-09"], + "measurementRefs": [ + "century-schoolbook_regular__c059#regular#w400#analytic_advance#2026-06-10", + "century-schoolbook_bold__c059#bold#w700#analytic_advance#2026-06-10", + "century-schoolbook_italic__c059#italic#w400#analytic_advance#2026-06-10", + "century-schoolbook_boldItalic__c059#boldItalic#w700#analytic_advance#2026-06-10", + "century-schoolbook__c059#visual_review#2026-06-09" + ], "exportRule": "preserve_original_name", - "candidateLicense": "AGPL-3.0-only WITH PS-or-PDF-font-exception-20170817" + "candidateLicense": "AGPL-3.0-only WITH PS-or-PDF-font-exception-20170817", + "advance": { + "basis": "latin_full", + "meanDelta": 0.00337127, + "maxDelta": 0.167 + }, + "faceVerdicts": { + "regular": "metric_safe", + "bold": "metric_safe", + "italic": "visual_only", + "boldItalic": "visual_only" + }, + "glyphExceptions": [ + { + "slot": "regular", + "codepoint": 175, + "advanceDelta": 0.167, + "note": "Century Schoolbook Regular vs C059 Roman: macron (U+00AF) advance differs ~16.7%; yen (U+00A5), plus-minus (U+00B1), division sign (U+00F7), and middle dot (U+00B7) also exceed the direct threshold. Body-text Latin sample is metric_safe." + }, + { + "slot": "bold", + "codepoint": 175, + "advanceDelta": 0.167, + "note": "Century Schoolbook Bold vs C059 Bold: macron (U+00AF) advance differs ~16.7%; yen (U+00A5), micro sign (U+00B5), plus-minus (U+00B1), and division sign (U+00F7) also exceed the direct threshold. Body-text Latin sample is metric_safe." + } + ] }, { "evidenceId": "garamond", @@ -457,53 +532,69 @@ "generic": "monospace", "logicalFamily": "Consolas", "physicalFamily": "Inconsolata SemiExpanded", - "verdict": "cell_width_only", + "verdict": "visual_only", "faces": { - "regular": false, - "bold": false, + "regular": true, + "bold": true, "italic": false, "boldItalic": false }, "gates": { - "static": "not_run", - "metric": "not_run", + "static": "pass", + "metric": "pass", "layout": "not_run", - "ship": "not_run" + "ship": "fail" }, "policyAction": "category_fallback", "measurementRefs": [ - "consolas__inconsolata-semiexpanded#analytic_advance#2026-06-03" + "consolas__inconsolata-semiexpanded#monospace_cell#analytic_advance#2026-06-10" ], "exportRule": "preserve_original_name", "advance": { "basis": "monospace_cell", - "meanDelta": 0.00035999999999999997, - "maxDelta": 0.00035999999999999997 + "meanDelta": 0.00019531, + "maxDelta": 0.00019531 }, - "candidateLicense": "OFL-1.1" + "candidateLicense": "OFL-1.1", + "faceSources": { + "italic": { + "kind": "synthetic", + "from": "regular" + }, + "boldItalic": { + "kind": "synthetic", + "from": "bold" + } + }, + "faceVerdicts": { + "regular": "cell_width_only", + "bold": "cell_width_only", + "italic": "visual_only", + "boldItalic": "visual_only" + } }, { "evidenceId": "verdana", "generic": "sans-serif", "logicalFamily": "Verdana", - "physicalFamily": null, + "physicalFamily": "Noto Sans", "verdict": "visual_only", "faces": { - "regular": false, - "bold": false, - "italic": false, - "boldItalic": false + "regular": true, + "bold": true, + "italic": true, + "boldItalic": true }, "gates": { - "static": "not_run", + "static": "pass", "metric": "fail", "layout": "not_run", - "ship": "not_run" + "ship": "fail" }, "policyAction": "category_fallback", - "measurementRefs": ["verdana#top_candidates#2026-06-03"], + "measurementRefs": ["verdana__noto-sans#visual_review#2026-06-10"], "exportRule": "preserve_original_name", - "candidateLicense": null + "candidateLicense": "OFL-1.1" }, { "evidenceId": "tahoma", diff --git a/packages/fallbacks/src/data.ts b/packages/fallbacks/src/data.ts index 866522e..c6a88af 100644 --- a/packages/fallbacks/src/data.ts +++ b/packages/fallbacks/src/data.ts @@ -383,6 +383,31 @@ export const SUBSTITUTION_EVIDENCE: readonly SubstitutionEvidence[] = [ "exportRule": "preserve_original_name", "candidateLicense": "LicenseRef-GUST-Font-License-1.0" }, + { + "evidenceId": "itc-bookman", + "generic": "serif", + "logicalFamily": "ITC Bookman", + "physicalFamily": "TeX Gyre Bonum", + "verdict": "visual_only", + "faces": { + "regular": true, + "bold": true, + "italic": true, + "boldItalic": true + }, + "gates": { + "static": "pass", + "metric": "fail", + "layout": "not_run", + "ship": "fail" + }, + "policyAction": "substitute", + "measurementRefs": [ + "itc-bookman__tex-gyre-bonum#alias_of_bookman-old-style#2026-06-10" + ], + "exportRule": "preserve_original_name", + "candidateLicense": "LicenseRef-GUST-Font-License-1.0" + }, { "evidenceId": "century", "generic": "serif", @@ -397,16 +422,33 @@ export const SUBSTITUTION_EVIDENCE: readonly SubstitutionEvidence[] = [ }, "gates": { "static": "pass", - "metric": "fail", + "metric": "pass", "layout": "not_run", "ship": "fail" }, "policyAction": "substitute", "measurementRefs": [ + "century_regular__c059#regular#w400#analytic_advance#2026-06-10", "century__c059#visual_review#2026-06-09" ], "exportRule": "preserve_original_name", - "candidateLicense": "AGPL-3.0-only WITH PS-or-PDF-font-exception-20170817" + "candidateLicense": "AGPL-3.0-only WITH PS-or-PDF-font-exception-20170817", + "advance": { + "basis": "latin_full", + "meanDelta": 0.00293647, + "maxDelta": 0.167 + }, + "faceVerdicts": { + "regular": "metric_safe" + }, + "glyphExceptions": [ + { + "slot": "regular", + "codepoint": 175, + "advanceDelta": 0.167, + "note": "Century Regular vs C059 Roman: macron (U+00AF) advance differs ~16.7%; yen (U+00A5), plus-minus (U+00B1), division sign (U+00F7), and middle dot (U+00B7) also exceed the direct threshold. Body-text Latin sample is metric_safe." + } + ] }, { "evidenceId": "century-schoolbook", @@ -428,10 +470,39 @@ export const SUBSTITUTION_EVIDENCE: readonly SubstitutionEvidence[] = [ }, "policyAction": "substitute", "measurementRefs": [ + "century-schoolbook_regular__c059#regular#w400#analytic_advance#2026-06-10", + "century-schoolbook_bold__c059#bold#w700#analytic_advance#2026-06-10", + "century-schoolbook_italic__c059#italic#w400#analytic_advance#2026-06-10", + "century-schoolbook_boldItalic__c059#boldItalic#w700#analytic_advance#2026-06-10", "century-schoolbook__c059#visual_review#2026-06-09" ], "exportRule": "preserve_original_name", - "candidateLicense": "AGPL-3.0-only WITH PS-or-PDF-font-exception-20170817" + "candidateLicense": "AGPL-3.0-only WITH PS-or-PDF-font-exception-20170817", + "advance": { + "basis": "latin_full", + "meanDelta": 0.00337127, + "maxDelta": 0.167 + }, + "faceVerdicts": { + "regular": "metric_safe", + "bold": "metric_safe", + "italic": "visual_only", + "boldItalic": "visual_only" + }, + "glyphExceptions": [ + { + "slot": "regular", + "codepoint": 175, + "advanceDelta": 0.167, + "note": "Century Schoolbook Regular vs C059 Roman: macron (U+00AF) advance differs ~16.7%; yen (U+00A5), plus-minus (U+00B1), division sign (U+00F7), and middle dot (U+00B7) also exceed the direct threshold. Body-text Latin sample is metric_safe." + }, + { + "slot": "bold", + "codepoint": 175, + "advanceDelta": 0.167, + "note": "Century Schoolbook Bold vs C059 Bold: macron (U+00AF) advance differs ~16.7%; yen (U+00A5), micro sign (U+00B5), plus-minus (U+00B1), and division sign (U+00F7) also exceed the direct threshold. Body-text Latin sample is metric_safe." + } + ] }, { "evidenceId": "garamond", @@ -472,55 +543,71 @@ export const SUBSTITUTION_EVIDENCE: readonly SubstitutionEvidence[] = [ "generic": "monospace", "logicalFamily": "Consolas", "physicalFamily": "Inconsolata SemiExpanded", - "verdict": "cell_width_only", + "verdict": "visual_only", "faces": { - "regular": false, - "bold": false, + "regular": true, + "bold": true, "italic": false, "boldItalic": false }, "gates": { - "static": "not_run", - "metric": "not_run", + "static": "pass", + "metric": "pass", "layout": "not_run", - "ship": "not_run" + "ship": "fail" }, "policyAction": "category_fallback", "measurementRefs": [ - "consolas__inconsolata-semiexpanded#analytic_advance#2026-06-03" + "consolas__inconsolata-semiexpanded#monospace_cell#analytic_advance#2026-06-10" ], "exportRule": "preserve_original_name", "advance": { "basis": "monospace_cell", - "meanDelta": 0.00035999999999999997, - "maxDelta": 0.00035999999999999997 + "meanDelta": 0.00019531, + "maxDelta": 0.00019531 }, - "candidateLicense": "OFL-1.1" + "candidateLicense": "OFL-1.1", + "faceSources": { + "italic": { + "kind": "synthetic", + "from": "regular" + }, + "boldItalic": { + "kind": "synthetic", + "from": "bold" + } + }, + "faceVerdicts": { + "regular": "cell_width_only", + "bold": "cell_width_only", + "italic": "visual_only", + "boldItalic": "visual_only" + } }, { "evidenceId": "verdana", "generic": "sans-serif", "logicalFamily": "Verdana", - "physicalFamily": null, + "physicalFamily": "Noto Sans", "verdict": "visual_only", "faces": { - "regular": false, - "bold": false, - "italic": false, - "boldItalic": false + "regular": true, + "bold": true, + "italic": true, + "boldItalic": true }, "gates": { - "static": "not_run", + "static": "pass", "metric": "fail", "layout": "not_run", - "ship": "not_run" + "ship": "fail" }, "policyAction": "category_fallback", "measurementRefs": [ - "verdana#top_candidates#2026-06-03" + "verdana__noto-sans#visual_review#2026-06-10" ], "exportRule": "preserve_original_name", - "candidateLicense": null + "candidateLicense": "OFL-1.1" }, { "evidenceId": "tahoma",