[ Content & formats ]
@sdxc/font
TrueType font reader: glyph lookup, metrics, kerning, outlines and line layout
- Installs with
- @sdxc/result
- Used by
- blog
- Source
- packages/font
TrueType font reader: glyph lookup, metrics, kerning, outlines and line layout, in plain JavaScript with no WebAssembly.
Installation
npm add @sdxc/font @sdxc/result
parseFont answers a Result from @sdxc/result.
Usage
import { parseFont } from "@sdxc/font";
import { isFailure } from "@sdxc/result";
let result = parseFont(await (await fetch(fontUrl)).arrayBuffer());
if (isFailure(result)) throw result.error;
let font = result.data;
font.measure("Hello, world", 32); // width in pixels
Lay out a line and turn each glyph into a pixel-space path for a rasterizer to fill:
let line = font.layout("Sign and verify JWTs", 76, { tracking: -3 });
for (let { glyph, x } of line.glyphs) {
let commands = font.path(glyph, 76, left + x, baseline);
// commands: { type: "move" | "line", x, y } | { type: "quad", cx, cy, x, y } | { type: "close" }
}
API
parseFont(bytes: Uint8Array | ArrayBuffer): Result<Font, FontError>
Reads a .ttf (OpenType with glyf outlines). Opening a font reads only the table headers; glyphs are decoded on first use. Fails with FontError whose code is "unsupported" (CFF OTTO fonts, ttcf collections, no Unicode cmap), "missing-table" or "malformed", and whose table names the table at fault.
Font
unitsPerEm,ascender,descender(negative),lineGap— font units, fromOS/2when the font setsUSE_TYPO_METRICS,hheaotherwise.familyandsubfamilycome fromname;glyphCountis the number of glyphs.glyphIndex(codePoint)— the glyph for a character,0(.notdef) when the font has none.advance(glyph)andkerning(left, right)— font units. Kerning reads thekerntable, or GPOS pair positioning when the font has nokerntable.outline(glyph)—Font.Command[]in font units, y up, composite glyphs resolved.path(glyph, size, x, y)— the outline atsizepixels per em, y down, with(x, y)on the baseline.layout(text, size, { tracking?, kerning? })—{ glyphs: { glyph, codePoint, x, advance }[], width }in pixels.trackingadds pixels between glyphs;kerningdefaults totrue.measure(text, size, options?)—layout(...).widthwithout the glyph list.
Layout maps one code point to one glyph: no ligatures, no GPOS mark positioning, no bidirectional text.
Pattern: Wrap a paragraph to a width
import type { Font } from "@sdxc/font";
function wrap(font: Font, text: string, size: number, maxWidth: number): string[] {
let lines: string[] = [];
let line = "";
for (let word of text.split(/\s+/)) {
let candidate = line === "" ? word : `${line} ${word}`;
if (line !== "" && font.measure(candidate, size) > maxWidth) {
lines.push(line);
line = word;
} else line = candidate;
}
if (line !== "") lines.push(line);
return lines;
}
let lineHeight = ((font.ascender - font.descender + font.lineGap) / font.unitsPerEm) * 48;