Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/fallbacks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ Some fallbacks are face-scoped. Use `getRenderableFallbackForFace`, or respect t
- `verdict` - measured fidelity, such as `metric_safe`, `near_metric`, `cell_width_only`, or `visual_only`.
- `lineBreakSafe` - true when advances preserve line breaks.
- `glyphExceptions` - named glyphs that can reflow.
- `advance.basis` - sample/model used for mean and max deltas, such as `latin_full`, `latin_text`, or `monospace_cell`.
- `generic` - CSS generic family for last-resort fallback.
- `evidenceId` - stable id for the reviewed evidence row.

Expand Down
24 changes: 24 additions & 0 deletions packages/fallbacks/fallbacks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,30 @@ describe("generic CSS family metadata", () => {
});
});

describe("advance measurement basis", () => {
test("every measured row states which sample/model produced its deltas", () => {
const BASES = new Set(["latin_full", "latin_text", "monospace_cell"]);
for (const row of SUBSTITUTION_EVIDENCE) {
if (!row.advance) continue;
expect(
BASES.has(row.advance.basis),
`${row.evidenceId} (${row.advance.basis})`,
).toBe(true);
}
});

test("monospace cell-width rows are not labeled as proportional Latin measurements", () => {
expect(
SUBSTITUTION_EVIDENCE.find((row) => row.evidenceId === "consolas")
?.advance?.basis,
).toBe("monospace_cell");
expect(
SUBSTITUTION_EVIDENCE.find((row) => row.evidenceId === "calibri")?.advance
?.basis,
).toBe("latin_full");
});
});

describe("Cooper Black -> Caprasimo (Regular-only, metric_safe)", () => {
const renderAll = { canRenderFamily: () => true };
const onlyCaprasimo = { canRenderFamily: (f: string) => f === "Caprasimo" };
Expand Down
14 changes: 14 additions & 0 deletions packages/fallbacks/records.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
],
"exportRule": "preserve_original_name",
"advance": {
"basis": "latin_full",
"meanDelta": 0,
"maxDelta": 0
},
Expand Down Expand Up @@ -56,6 +57,7 @@
],
"exportRule": "preserve_original_name",
"advance": {
"basis": "latin_full",
"meanDelta": 0.0002378,
"maxDelta": 0.2310758
},
Expand Down Expand Up @@ -97,6 +99,7 @@
"measurementRefs": ["arial__liberation-sans#analytic_advance#2026-06-03"],
"exportRule": "preserve_original_name",
"advance": {
"basis": "latin_full",
"meanDelta": 0,
"maxDelta": 0
},
Expand Down Expand Up @@ -126,6 +129,7 @@
],
"exportRule": "preserve_original_name",
"advance": {
"basis": "latin_full",
"meanDelta": 0,
"maxDelta": 0
},
Expand Down Expand Up @@ -155,6 +159,7 @@
],
"exportRule": "preserve_original_name",
"advance": {
"basis": "latin_full",
"meanDelta": 0,
"maxDelta": 0
},
Expand Down Expand Up @@ -191,6 +196,7 @@
],
"exportRule": "preserve_original_name",
"advance": {
"basis": "latin_full",
"meanDelta": 0.0000197,
"maxDelta": 0.0183727
},
Expand Down Expand Up @@ -243,6 +249,7 @@
],
"exportRule": "preserve_original_name",
"advance": {
"basis": "latin_full",
"meanDelta": 0,
"maxDelta": 0.5
},
Expand Down Expand Up @@ -309,6 +316,7 @@
],
"exportRule": "preserve_original_name",
"advance": {
"basis": "monospace_cell",
"meanDelta": 0.00035999999999999997,
"maxDelta": 0.00035999999999999997
},
Expand Down Expand Up @@ -407,6 +415,7 @@
],
"exportRule": "preserve_original_name",
"advance": {
"basis": "latin_full",
"meanDelta": 0.1005,
"maxDelta": 0.1419
},
Expand Down Expand Up @@ -503,6 +512,7 @@
"measurementRefs": ["lucida-console__cousine#analytic_advance#2026-06-03"],
"exportRule": "preserve_original_name",
"advance": {
"basis": "monospace_cell",
"meanDelta": 0.004050000000000001,
"maxDelta": 0.004050000000000001
},
Expand Down Expand Up @@ -576,6 +586,7 @@
],
"exportRule": "preserve_original_name",
"advance": {
"basis": "latin_full",
"meanDelta": 0,
"maxDelta": 0
},
Expand Down Expand Up @@ -603,6 +614,7 @@
"measurementRefs": ["calibri-light__carlito#analytic_advance#2026-06-05"],
"exportRule": "preserve_original_name",
"advance": {
"basis": "latin_full",
"meanDelta": 0.0148,
"maxDelta": 0.066
},
Expand Down Expand Up @@ -632,6 +644,7 @@
],
"exportRule": "preserve_original_name",
"advance": {
"basis": "latin_full",
"meanDelta": 0,
"maxDelta": 0.4915590863952334
},
Expand Down Expand Up @@ -672,6 +685,7 @@
],
"exportRule": "preserve_original_name",
"advance": {
"basis": "latin_full",
"meanDelta": 0,
"maxDelta": 0
},
Expand Down
14 changes: 14 additions & 0 deletions packages/fallbacks/src/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const SUBSTITUTION_EVIDENCE: readonly SubstitutionEvidence[] = [
],
"exportRule": "preserve_original_name",
"advance": {
"basis": "latin_full",
"meanDelta": 0,
"maxDelta": 0
},
Expand Down Expand Up @@ -59,6 +60,7 @@ export const SUBSTITUTION_EVIDENCE: readonly SubstitutionEvidence[] = [
],
"exportRule": "preserve_original_name",
"advance": {
"basis": "latin_full",
"meanDelta": 0.0002378,
"maxDelta": 0.2310758
},
Expand Down Expand Up @@ -102,6 +104,7 @@ export const SUBSTITUTION_EVIDENCE: readonly SubstitutionEvidence[] = [
],
"exportRule": "preserve_original_name",
"advance": {
"basis": "latin_full",
"meanDelta": 0,
"maxDelta": 0
},
Expand Down Expand Up @@ -131,6 +134,7 @@ export const SUBSTITUTION_EVIDENCE: readonly SubstitutionEvidence[] = [
],
"exportRule": "preserve_original_name",
"advance": {
"basis": "latin_full",
"meanDelta": 0,
"maxDelta": 0
},
Expand Down Expand Up @@ -160,6 +164,7 @@ export const SUBSTITUTION_EVIDENCE: readonly SubstitutionEvidence[] = [
],
"exportRule": "preserve_original_name",
"advance": {
"basis": "latin_full",
"meanDelta": 0,
"maxDelta": 0
},
Expand Down Expand Up @@ -196,6 +201,7 @@ export const SUBSTITUTION_EVIDENCE: readonly SubstitutionEvidence[] = [
],
"exportRule": "preserve_original_name",
"advance": {
"basis": "latin_full",
"meanDelta": 0.0000197,
"maxDelta": 0.0183727
},
Expand Down Expand Up @@ -248,6 +254,7 @@ export const SUBSTITUTION_EVIDENCE: readonly SubstitutionEvidence[] = [
],
"exportRule": "preserve_original_name",
"advance": {
"basis": "latin_full",
"meanDelta": 0,
"maxDelta": 0.5
},
Expand Down Expand Up @@ -316,6 +323,7 @@ export const SUBSTITUTION_EVIDENCE: readonly SubstitutionEvidence[] = [
],
"exportRule": "preserve_original_name",
"advance": {
"basis": "monospace_cell",
"meanDelta": 0.00035999999999999997,
"maxDelta": 0.00035999999999999997
},
Expand Down Expand Up @@ -420,6 +428,7 @@ export const SUBSTITUTION_EVIDENCE: readonly SubstitutionEvidence[] = [
],
"exportRule": "preserve_original_name",
"advance": {
"basis": "latin_full",
"meanDelta": 0.1005,
"maxDelta": 0.1419
},
Expand Down Expand Up @@ -524,6 +533,7 @@ export const SUBSTITUTION_EVIDENCE: readonly SubstitutionEvidence[] = [
],
"exportRule": "preserve_original_name",
"advance": {
"basis": "monospace_cell",
"meanDelta": 0.004050000000000001,
"maxDelta": 0.004050000000000001
},
Expand Down Expand Up @@ -597,6 +607,7 @@ export const SUBSTITUTION_EVIDENCE: readonly SubstitutionEvidence[] = [
],
"exportRule": "preserve_original_name",
"advance": {
"basis": "latin_full",
"meanDelta": 0,
"maxDelta": 0
},
Expand Down Expand Up @@ -626,6 +637,7 @@ export const SUBSTITUTION_EVIDENCE: readonly SubstitutionEvidence[] = [
],
"exportRule": "preserve_original_name",
"advance": {
"basis": "latin_full",
"meanDelta": 0.0148,
"maxDelta": 0.066
},
Expand Down Expand Up @@ -655,6 +667,7 @@ export const SUBSTITUTION_EVIDENCE: readonly SubstitutionEvidence[] = [
],
"exportRule": "preserve_original_name",
"advance": {
"basis": "latin_full",
"meanDelta": 0,
"maxDelta": 0.4915590863952334
},
Expand Down Expand Up @@ -695,6 +708,7 @@ export const SUBSTITUTION_EVIDENCE: readonly SubstitutionEvidence[] = [
],
"exportRule": "preserve_original_name",
"advance": {
"basis": "latin_full",
"meanDelta": 0,
"maxDelta": 0
},
Expand Down
1 change: 1 addition & 0 deletions packages/fallbacks/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export {
type RenderableFallbackOptions,
} from "./fallbacks.js";
export type {
AdvanceBasis,
AdvanceDelta,
CssGeneric,
FaceCoverage,
Expand Down
11 changes: 10 additions & 1 deletion packages/fallbacks/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,19 @@ export type FaceSlot = "regular" | "bold" | "italic" | "boldItalic";
*/
export type CssGeneric = "serif" | "sans-serif" | "monospace";

/** Which advance sample/model produced the row's mean and max deltas. */
export type AdvanceBasis = "latin_full" | "latin_text" | "monospace_cell";

/** Advance-width divergence vs the licensed reference font, as fractions (0 = identical advances). */
export interface AdvanceDelta {
/**
* Measurement basis for the deltas. `latin_full` uses the reviewed Latin sample including
* punctuation and symbols, `latin_text` uses text-carrying Latin codepoints, and `monospace_cell` is a
* cell-width measurement for monospace rows.
*/
basis: AdvanceBasis;
meanDelta: number;
/** the worst-case delta, not the mean, is what gates line-break fidelity. */
/** the worst-case delta for this basis, not the mean, is what gates line-break fidelity. */
maxDelta: number;
}

Expand Down
59 changes: 54 additions & 5 deletions tools/corpus/compare.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {

const UNITS_PER_EM = 1000;
const ADVANCES = [500, 600, 300, 750]; // by glyph id
const SYNTHETIC_SAMPLE = [0x20, 0x41, 0x42] as const;

function u16(value: number): number[] {
return [(value >> 8) & 0xff, value & 0xff];
Expand Down Expand Up @@ -265,10 +266,26 @@ describe("scoreAdvances", () => {
expect(score.maxDelta).toBe(0);
expect(score.over1Percent).toBe(0);
expect(score.over2_5Percent).toBe(0);
expect(score.tier).toBe("metric_safe");
expect(score.tier).toBe("visual_only");
expect(score.worstGlyphs).toEqual([]);
});

test("does not give a metric tier to low-coverage candidates", () => {
const reference = new Map(
LATIN_TEXT_SAMPLE.map((codepoint) => [codepoint, 0.5] as const),
);
const candidate = new Map([[0x20, 0.5]]);
const score = scoreAdvances(reference, candidate, {
reportSample: LATIN_SAMPLE,
tierSample: LATIN_TEXT_SAMPLE,
});
expect(score.compared).toBe(1);
expect(score.total).toBe(LATIN_TEXT_SAMPLE.length);
expect(score.meanDelta).toBe(0);
expect(score.maxDelta).toBe(0);
expect(score.tier).toBe("visual_only");
});

test("computes mean and max deltas and worst glyphs", () => {
const reference = new Map([
[0x41, 0.5],
Expand Down Expand Up @@ -480,6 +497,33 @@ describe("renderReport", () => {
expect(lines[3]).toContain("visual_only");
});

test("ranks fuller coverage before lower-mean partial matches within a tier", () => {
const sample = [0x41, 0x42, 0x43];
const reference = sampleMetrics(mockFont(0.5), sample);
const partial = scoreAdvances(
reference,
sampleMetrics(partialFont(0.5, [0x41]), sample),
sample,
);
const full = scoreAdvances(
reference,
sampleMetrics(mockFont(0.7), sample),
sample,
);
expect(partial.tier).toBe("visual_only");
expect(full.tier).toBe("visual_only");

const report = renderReport([
{ sourceId: "partial-src", file: "partial.otf", score: partial },
{ sourceId: "full-src", file: "full.otf", score: full },
]);
const lines = report.split("\n");
expect(lines[1]).toContain("full-src");
expect(lines[1]).toContain("3/3");
expect(lines[2]).toContain("partial-src");
expect(lines[2]).toContain("1/3");
});

test("can limit the rendered table to the top rows", () => {
const reference = sampleMetrics(mockFont(0.5), [0x41]);
const close = scoreAdvances(
Expand Down Expand Up @@ -570,11 +614,15 @@ describe("collectCandidates (GitHub tree sources)", () => {
const candidates = collectCandidates(source, cacheDir);
expect(candidates.map((c) => c.file)).toEqual(names);

const reference = sampleMetrics(parseFont(syntheticFont()));
const reference = sampleMetrics(
parseFont(syntheticFont()),
SYNTHETIC_SAMPLE,
);
for (const candidate of candidates) {
const score = scoreAdvances(
reference,
sampleMetrics(parseFont(candidate.bytes)),
sampleMetrics(parseFont(candidate.bytes), SYNTHETIC_SAMPLE),
SYNTHETIC_SAMPLE,
);
expect(score.tier).toBe("metric_safe");
expect(score.meanDelta).toBe(0);
Expand Down Expand Up @@ -649,8 +697,9 @@ describe("collectCandidates (archive sources)", () => {
"Example-Regular.ttf",
]);
const score = scoreAdvances(
sampleMetrics(parseFont(syntheticFont())),
sampleMetrics(parseFont(candidates[0].bytes)),
sampleMetrics(parseFont(syntheticFont()), SYNTHETIC_SAMPLE),
sampleMetrics(parseFont(candidates[0].bytes), SYNTHETIC_SAMPLE),
SYNTHETIC_SAMPLE,
);
expect(score.tier).toBe("metric_safe");
} finally {
Expand Down
Loading
Loading