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
20 changes: 20 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,37 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check package changes
id: package_changes
run: |
latest_tag="$(git describe --tags --match 'v*' --abbrev=0 2>/dev/null || true)"
if [ -z "$latest_tag" ]; then
echo "changed=true" >> "$GITHUB_OUTPUT"
exit 0
fi

if git diff --quiet "$latest_tag"..HEAD -- packages/fallbacks; then
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "No packages/fallbacks changes since $latest_tag."
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- uses: oven-sh/setup-bun@v2
if: steps.package_changes.outputs.changed == 'true'
with:
bun-version: 1.3.12
- run: bun install --frozen-lockfile
if: steps.package_changes.outputs.changed == 'true'
- run: bun run build
if: steps.package_changes.outputs.changed == 'true'
- name: Clear Bun install tree before npm publish
if: steps.package_changes.outputs.changed == 'true'
run: rm -rf node_modules packages/*/node_modules
- env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
ANTHROPIC_API_KEY_RELEASE_NOTES: ${{ secrets.ANTHROPIC_API_KEY_RELEASE_NOTES }}
if: steps.package_changes.outputs.changed == 'true'
run: >
npx --yes
--package semantic-release@24
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ dev/
.wrangler/
.mcp.json
mockups/
.cache/
packages/fallbacks/.cache/
STATE.md
24 changes: 7 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,21 @@
> Document font substitution, measured.

docfonts publishes `@docfonts/fallbacks`, a small runtime package for document renderers.
It maps common proprietary document fonts to reviewed open-font fallback decisions.

The package ships no font binaries and no proprietary data. It contains a public evidence snapshot,
asset-aware lookup helpers, and tests that prove the npm package only includes supported runtime files.
It maps common proprietary document fonts to reviewed open-font fallback decisions. It ships no font binaries and no proprietary data.

Built by the team behind [SuperDoc](https://github.com/superdoc-dev/superdoc). Standalone and neutral.

## Package
## Structure

- `packages/fallbacks` - runtime fallback decisions and lookup helpers.
- `tools/corpus` - local source acquisition and comparison tools.

## Workflows
## Use

- Runtime: install `@docfonts/fallbacks` and call the lookup helpers.
- Acquire: run `bun run --cwd packages/fallbacks acquire` to download reviewed open-font source
archives into an ignored local cache and write local hash snapshots.
- Compare: planned local tooling. Results should stay local unless deliberately published through a
curated product surface.

## API

- `getRenderableFallback` - returns the open family to render, or `null` when none is renderable.
- `getFallbackDecision` - explains the outcome for UI, diagnostics, and reporting.
- `createFallbackMap` - builds a resolver map from only the font families you can render.
- `normalizeFamilyName` - normalizes map lookup keys.
- Acquire: run `bun run corpus:acquire` to download open-font sources into an ignored local cache.
- Compare: run `bun run corpus:compare` to rank acquired open fonts against a licensed local
reference. Results stay local unless deliberately published through a curated product surface.

## Install

Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
"typecheck": "tsc --noEmit",
"lint": "biome check .",
"format": "biome check --write .",
"test": "bun test packages/fallbacks",
"test": "bun test packages/fallbacks tools/corpus",
"corpus:acquire": "bun run tools/corpus/acquire.ts",
"corpus:compare": "bun run tools/corpus/compare.ts",
"check": "bun run typecheck && bun run test && bun run lint && bun run build",
"check:fast": "bun run typecheck && bun run lint",
"prepare": "if [ -z \"$CI\" ]; then bunx lefthook install; fi"
Expand Down
106 changes: 26 additions & 80 deletions packages/fallbacks/README.md
Original file line number Diff line number Diff line change
@@ -1,70 +1,58 @@
# @docfonts/fallbacks

Document font substitution, measured.
Measured open-font fallbacks for proprietary document fonts.

Measured open-font fallbacks for proprietary document fonts. Use it to decide whether a requested document font can render with an open family you actually ship.

It ships no fonts and no proprietary binaries. It ships decisions: the recommended open family when one exists, the fidelity verdict, and the honest cases where no open family should be used.
The package ships decisions, not fonts: which open family to render when one is reviewed, how faithful it is, and when no open fallback should be used.

## Install

```sh
npm install @docfonts/fallbacks
```

ESM-only. Use `import`, or let your bundler handle it. CommonJS `require()` is not supported.
ESM-only.

## Render A Font
## Render a font

Use `getRenderableFallback` when you need one font family to render now. Pass `canRenderFamily` so docfonts only returns families your app can load.
Use `getRenderableFallback` when you need one family to render now. Pass `canRenderFamily` so the result only includes fonts your app can load.

```ts
import { getRenderableFallback } from "@docfonts/fallbacks";

const fallback = getRenderableFallback("Helvetica", {
canRenderFamily: (family) => bundledFamilies.has(family),
});

// { substituteFamily: "Liberation Sans", policyAction: "substitute", verdict: "metric_safe", lineBreakSafe: true, evidenceId: "helvetica", generic: "sans-serif" }
```

The result is `null` when there is nothing renderable from your available assets. Use `getFallbackDecision` when you need to know why.
Returns `null` when docfonts has no renderable fallback from your available assets.

## Explain A Decision
## Explain a decision

Use `getFallbackDecision` for UI, diagnostics, and reporting. It distinguishes known fonts with no recommended fallback from fonts docfonts has never seen.
Use `getFallbackDecision` for UI, diagnostics, and reports.

```ts
import { getFallbackDecision } from "@docfonts/fallbacks";

getFallbackDecision("Aptos");
// { kind: "customer_supplied", evidenceId: "aptos", generic: "sans-serif" }

getFallbackDecision("Tahoma");
// { kind: "no_recommended_fallback", evidenceId: "tahoma", generic: "sans-serif" }

getFallbackDecision("Made Up Font");
// { kind: "unknown" }

getFallbackDecision("Georgia", {
canRenderFamily: (family) => bundledFamilies.has(family),
});
// { kind: "asset_missing", substituteFamily: "Gelasio", verdict: "near_metric", evidenceId: "georgia", generic: "serif" }
// { kind: "asset_missing", substituteFamily: "Gelasio", verdict: "near_metric", ... }
```

Decision kinds:
Important decision kinds:

- `fallback` - render the returned `substituteFamily`.
- `asset_missing` - docfonts has a fallback, but your app does not load that family.
- `face_missing` - (face-aware lookups only) the family has a substitute, but not for the requested face. Route that face through your absence handling; do not substitute it.
- `no_recommended_fallback` - docfonts knows the font but recommends no renderable open family.
- `customer_supplied` - the real font should come from the customer or environment.
- `preserve_only` - keep the original family name. Do not substitute.
- `fallback` - render `fallback.substituteFamily`.
- `asset_missing` - docfonts has a fallback, but your app does not load it.
- `face_missing` - the fallback does not provide the requested face.
- `customer_supplied`, `preserve_only`, or `no_recommended_fallback` - do not substitute.
- `unknown` - docfonts has no evidence for this family.

## Create A Resolver Map
## Build a resolver map

Use `createFallbackMap` when wiring a resolver. `canRenderFamily` is required because a resolver map must never point at fonts you cannot load.
Use `createFallbackMap` when wiring a resolver. `canRenderFamily` is required so the map never points at fonts you cannot load.

```ts
import { createFallbackMap, normalizeFamilyName } from "@docfonts/fallbacks";
Expand All @@ -73,63 +61,21 @@ const map = createFallbackMap({
canRenderFamily: (family) => bundledFamilies.has(family),
});

map[normalizeFamilyName("Times New Roman")]; // { substituteFamily: "Liberation Serif", ... }
```

Keys are normalized. Use `normalizeFamilyName` for lookups. Rows whose substitute family is not available are omitted. Each entry carries `faces`: a Regular-only entry is only safe in a **face-aware** resolver (one that checks `faces` or uses `getRenderableFallbackForFace`), since applying it to bold/italic would route a face the substitute does not provide.

## What the fields mean

- `substituteFamily` - the open family to render in place of the requested one.
- `policyAction` - what a renderer should do, not a quality claim. Use `verdict` for fidelity.
- `verdict` - the measured fidelity. Examples: `metric_safe`, `near_metric`, `cell_width_only`, `visual_only`.
- `lineBreakSafe` - true when advances preserve line breaks: `metric_safe`, `near_metric`, or monospace `cell_width_only`.
- `faces` - reviewed face coverage for this evidence row. If any face is `true`, respect it as face-scoped coverage (a row can be Regular-only). If all faces are `false`, the row is **not** face-scoped (e.g. a category fallback whose physical font does have faces) and the face-aware helpers treat it as renderable for any face.
- `evidenceId` - the stable id for the reviewed evidence row; look the full row up in `SUBSTITUTION_EVIDENCE`.
- `generic` - the logical font's broad CSS category (`serif`, `sans-serif`, or `monospace`), for a last-resort generic `font-family` keyword when no named substitute renders. Also present on the known (non-`unknown`) decision kinds.
- `glyphExceptions` - named glyph-level divergences that qualify this fallback (e.g. one codepoint reflows), or omitted when none. A family lookup carries all of the row's; a face lookup (`getRenderableFallbackForFace`) carries only that face's, so Cambria Regular shows none while Bold Italic shows its grave-accent exception.

`cell_width_only` keeps monospace advances stable, but glyph shapes can still differ. A `substitute` can still have a lower-fidelity `verdict` when one face or glyph is qualified. The verdict is the fidelity signal.

## Face-aware routing (Regular-only substitutes)

Some substitutes provide only some faces - e.g. Baskerville Old Face -> Bacasime Antique is Regular-only. The family-level helpers above answer "which family", and every result carries `faces`, so a resolver must route per-face. The face-aware helpers do it for you:

```ts
import { getRenderableFallbackForFace } from "@docfonts/fallbacks";
const opts = { canRenderFamily: (family) => bundledFamilies.has(family) };

getRenderableFallbackForFace("Baskerville Old Face", "regular", opts)?.substituteFamily; // "Bacasime Antique"
getRenderableFallbackForFace("Baskerville Old Face", "bold", opts); // null (Regular-only)
map[normalizeFamilyName("Times New Roman")];
```

`getFallbackDecisionForFace(family, face, options)` reports the reason - `face_missing` when the substitute exists but lacks that face. A covered face carries its OWN verdict, not the family's worst-face rollup (e.g. `Cambria` regular is `metric_safe` even though the family rolls up to `visual_only`).
Some fallbacks are face-scoped. Use `getRenderableFallbackForFace`, or respect the returned `faces` field before applying a fallback to bold or italic text.

The full structured rows are exported as `SUBSTITUTION_EVIDENCE` for richer reporting (faces, per-face verdicts, glyph exceptions).
## Fidelity fields

## Local tools
- `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.
- `generic` - CSS generic family for last-resort fallback.
- `evidenceId` - stable id for the reviewed evidence row.

These maintainer tools use ignored `.cache` files and are not shipped in the package.

`bun run acquire` downloads open-font candidates into `.cache/sources`. Sources come in two shapes: release archives (zip or tar.gz) and pinned source trees. Set `DOCFONTS_SOURCE_CACHE` to use another cache directory, or pass `--source google-fonts` to acquire one source.

`bun run compare` checks a private reference font against acquired OTF/TTF candidates and prints a ranked Latin advance-width table. It writes no fonts, paths, or results to the tree.

```sh
bun run --cwd packages/fallbacks compare -- \
--reference /path/to/reference.ttf \
--family "Bookman Old Style" \
--source tex-gyre-bonum
```

- `--reference` (required) - path to the font to measure against.
- `--family` - a label shown in the report header.
- `--source` - restrict to one or more acquired source ids (repeat the flag or comma-separate). Defaults to every acquired source.

The comparison is a lead finder, not an automatic verdict. It measures Latin advance widths over a fixed sample and reports the tier, coverage, outlier counts, and worst glyphs for each candidate.
`SUBSTITUTION_EVIDENCE` exposes the full reviewed rows for richer reporting.

## Provenance

The data comes from reviewed docfonts evidence. Measurements are produced against licensed originals, but this package distributes no proprietary binaries or raw proprietary metrics.

Built by the team behind SuperDoc. Standalone and neutral.
Measurements are produced against licensed originals. This package distributes no proprietary binaries, raw proprietary metrics, or font files.
2 changes: 0 additions & 2 deletions packages/fallbacks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@
},
"scripts": {
"gen:data": "bun run scripts/generate-data.ts",
"acquire": "bun run scripts/acquire.ts",
"compare": "bun run scripts/compare.ts",
"build": "tsc -p tsconfig.build.json",
"prepack": "bun run build"
},
Expand Down
Loading
Loading