@nextgensoftwares/folio-fonts
HarfBuzz text measurer with CSS-style face matching and per-grapheme fallback. Guide: Fonts & measurement.
class FontEngine implements TextMeasurer
ts
interface FontEngineOptions {
fallback?: string[]; // families tried after the requested stack, e.g. ['Noto Sans Arabic']
cacheSize?: number; // max cached widths before the cache resets (default 100,000)
}
interface FontFaceInput {
family: string;
data: ArrayBuffer | Uint8Array;
style?: 'normal' | 'italic';
weight?: number | [number, number]; // default: the wght axis range, else OS/2 usWeightClass
}| Member | Signature | Description |
|---|---|---|
FontEngine.create | (options?: FontEngineOptions) => Promise<FontEngine> | loads HarfBuzz (once) and returns an engine |
addFont | (input: FontFaceInput) => LoadedFace | registers a face; clears width and stack caches |
width | (text, font: FontSpec) => number | advance width in px (cached), shaped with font.features, plus font.letterSpacing per grapheme cluster |
positions | (text, font) => number[] | px offset at every UTF-16 boundary, one shaping pass per run (features and letter spacing included) |
hasFeature | (font, tag: string) => boolean | whether the stack's primary face has an OpenType GSUB feature ('smcp', 'c2sc') |
metrics | (font) => { ascent; descent; lineGap } | primary face's ascent/descent/hhea line gap as fractions of the size (0.8 / 0.2 / 0 if none loaded) |
addAlias | (family, target) => void | family resolves to target's faces while it has none of its own (a stand-in: Verdana → DejaVu Sans) |
hasFamily | (family) => boolean | faces registered under family itself (aliases don't count) |
singleLine | (family, weight?, style?) => number | undefined | single line height (ascent + descent + line gap, × size) of the face family resolves to, own or aliased: pass it to importers as lineMetrics |
runs | (text, font) => FaceRun[] | splits text into single-face runs (per-grapheme fallback); throws if no font is loaded for the stack |
shape | (run: FaceRun, weight: number, features?: string) => glyph infos and positions | raw HarfBuzz output in font units (for PDF writers) |
ts
interface FaceDescriptor { family: string; style: 'normal' | 'italic'; weightMin: number; weightMax: number }
interface LoadedFace extends FaceDescriptor {
id: number; face: HbFace; upem: number; variable: boolean; cmap: Set<number>;
ascent: number; descent: number; instances: Map<number, HbFont>;
}
interface FaceRun { face: LoadedFace; text: string; start: number } // start = UTF-16 offset in the measured stringOffice font stand-ins and lazy loading
| Export | Signature | Description |
|---|---|---|
OFFICE_FALLBACKS | Record<string, readonly string[]> | lower-case Office family → open stand-ins, best first: Times New Roman/Arial/Courier New → Liberation Serif/Sans/Mono, Calibri → Carlito, Cambria → Caladea, Georgia → Gelasio, Segoe UI → Selawik (metric-compatible); Verdana → DejaVu Sans, Tahoma → DejaVu Sans Condensed; Symbol/Wingdings → DejaVu Sans |
fontFallbacks | (family, table?) => readonly string[] | stand-ins for a family (table replaces the built-in one) |
createFontLoader | (opts: FontLoaderOptions) => FontLoader | loads only the families a document asks for, through the host's loadFamily hook; a family the host can't load gets the first stand-in it can (registered as an alias) |
FontLoaderOptions | { engine; loadFamily: LoadFamily; fallbacks?: (family) => readonly string[]; onFace?: (face, name) => void | Promise<void> } | onFace runs for every face under every name it draws (its own family, then each family it stands in for): register a browser FontFace there |
LoadFamily | (family) => Promise<readonly FontFaceInput[] | null> | the host's font source (bundle, CDN); called once per family |
FontLoader | { ensure(families): Promise<FontReport>; addFaces(faces): Promise<void> } | ensure takes names or CSS stacks (generic names skipped); addFaces registers a document's embedded faces first |
FontReport | { loaded: string[]; substituted: { family; with }[]; missing: string[] } | what ensure did |
FontRegistry | { addFont(input: FontFaceInput); addAlias(family, target); hasFamily(family): boolean } | the part of FontEngine the loader drives (FontLoaderOptions.engine); any object with these three methods works |
Face matching
| Export | Signature | Description |
|---|---|---|
parseFamilyList | (value: string) => string[] | split a CSS font-family list, unquote, lowercase |
rankByWeight | <T extends FaceDescriptor>(faces: readonly T[], w: number) => T[] | CSS Fonts §5.2 weight fallback order |
matchFace | (faces, style, weight) => T | undefined | style first (italic ↔ normal fallback), then weight |
loadHarfBuzz | () => Promise<HarfBuzz> | lazy, single import('harfbuzzjs') (top-level-await WASM) |
Font files and static instances
| Export | Signature | Description |
|---|---|---|
fontFiles | (faces: readonly FaceFile[]) => (font: FontSpec) => Promise<Uint8Array | null> | a fonts callback (PDF and DOCX exporters) over font URLs: nearest weight/style, each file fetched once (fetch) |
FaceFile | { family; src; weight?: number | [number, number]; style? } | one font file |
loadFontInstancer | (wasm?: ArrayBuffer | Uint8Array | string | URL) => Promise<FontInstancer> | HarfBuzz's subsetter (harfbuzz-subset.wasm, shipped next to the build as @nextgensoftwares/folio-fonts/harfbuzz-subset.wasm; in Node it is found automatically) |
FontInstancer | { instance(font: Uint8Array, axes: Record<string, number>): Uint8Array | null } | a static instance of a variable font: axes pinned, every glyph and layout table kept |
renameFont | (font: Uint8Array, names: FontNames) => Uint8Array | new style-linked names (name IDs 1/2/4/6), OS/2 weight and fsSelection, head.macStyle; fresh checksums |
FontNames | { family: string; subfamily: 'Regular' | 'Bold' | 'Italic' | 'Bold Italic'; weight: number } |
The DOCX exporter uses these to embed exactly the outlines and advances layout measured: Word draws embedded fonts through GDI, which can't apply variations.