font

    OpenType font parsing, shaping and text bounds for MoonBit

    font
    ttf
    woff
    woff2
    opentype
    otf
    cff
    shaping
    text-layout
    Download zip
    Author
    Version
    0.8.0
    License
    Apache-2.0
    Last updated
    yesterday
    Downloads
    38K

    #mizchi/font

    TrueType/OTF(CFF)/WOFF/WOFF2 font parser written in MoonBit.

    Parses font binary data and converts glyph outlines to @svg.PathCommand (from mizchi/svg).

    Current release: 0.8.0. See CHANGELOG.md for changes and migration notes.

    #Usage

    let data : Bytes = ... // TTF, OTF, WOFF, WOFF2, or TTC file bytes
    let font = @font.parse_font(data).unwrap()

    // Get glyph outline as SVG path commands
    let cmds = font.scaled_outline('A'.to_int(), 48.0)

    // Get glyph metrics
    let gid = font.glyph_index('A'.to_int())
    let metrics = font.glyph_metrics(gid)

    // Text layout with kerning
    let positions = font.layout_text("Hello", 48.0)
    let width = font.measure_text("Hello", 48.0)

    // Baseline-relative logical/ink rectangles and per-glyph UTF-16 ranges
    let bounds = font.layout_text_bounds("Hello", 48.0)
    let ink = bounds.ink_bounds // None for empty text or spaces

    // OpenType shaping and mixed LTR/RTL layout, with source cluster ranges
    let shaped = font.shape_text("office AV אבג", 48.0, direction=@font.Auto)
    let width = shaped.bounds.advance_width
    let runs = shaped.runs // Visual order, script, embedding level and glyph ranges

    // Variable font with axis values
    let cmds = font.char_outline_at('A'.to_int(), { "wght": 700.0 })

    // Vertical text layout
    let vpositions = font.layout_text_vertical("縦書き", 48.0)
    let vheight = font.measure_text_vertical("縦書き", 48.0)

    // Font subsetting (glyf-based TrueType only)
    let subset = @font.subset_font(data, [0x41, 0x42, 0x43]) // A, B, C

    #Supported Specifications

    This library covers glyph rendering, metrics, variable fonts, OpenType shaping and bidirectional layout. shape_text connects the pure MoonBit Swash port for GSUB/GPOS/GDEF to Unicode BiDi. It returns positioned glyphs, cluster source ranges and visual runs. layout_text, measure_text and layout_text_bounds retain their basic codepoint-to-glyph LTR behavior with legacy kern adjustment.

    #Font Formats

    FormatNotes
    TTF (TrueType)✅sfnt flavor 0x00010000 and 0x74727565
    OTF (OpenType/CFF)✅sfnt flavor OTTO
    WOFF1✅zlib decompression
    WOFF2✅Brotli decompression with glyf/loca/hmtx transforms
    TTC/OTC (Font Collections)✅Index-based or first-font access

    #OpenType Tables

    #✅ Parsed

    TableDescription
    headunits_per_em, indexToLocFormat
    maxpnumGlyphs
    hheaascent, descent, lineGap, numOfLongHorMetrics
    hmtxadvanceWidth, leftSideBearing per glyph
    cmapFormat 0, 4 (BMP), 6, and 12 (full Unicode)
    locaShort and long formats
    glyfSimple and compound glyphs
    CFF Type 2 charstrings with subroutines
    CFF2Variable font support with blend/vsindex
    fvarAxis definitions (wght, wdth, opsz, etc.)
    avarPiecewise linear axis value mapping
    gvarTrueType delta interpolation with IUP
    kernFormat 0 horizontal pairs
    nameWindows Unicode, Unicode platform, Mac Roman
    OS/2Weight/width class, PANOSE, x-height, cap-height
    postItalic angle, fixed pitch, glyph names (v2.0)
    vhea/vmtxVertical metrics — advance heights and top side bearings
    gaspGrid-fitting and scan-conversion ranges
    VORGVertical origin Y coordinates for CFF glyphs

    shape_text additionally processes GSUB (substitution), GPOS (positioning) and GDEF (glyph classes and lookup filtering) through Swash. These tables are not exposed as fields on TTFont.

    #❌ Not Parsed

    The tables below are not parsed. Most of them are only needed for advanced use cases.

    📐 Additional Layout Tables

    TableDescription
    BASEBaseline offsets for mixing scripts (e.g. Latin + CJK in one line)
    JSTFJustification alternatives for full-justified text

    🎨 Color & Emoji — Required for rendering color emoji and multi-color glyphs. Not needed for monochrome text rendering.

    TableDescription
    COLR/CPALColor layers with palettes (COLRv0/v1 color fonts)
    SVG SVG glyph documents (SVG-in-OpenType color fonts)
    CBDT/CBLCColor bitmap glyphs (Google/Android emoji)
    sbixApple bitmap glyphs (Apple emoji)
    EBDT/EBLCMonochrome/grayscale embedded bitmaps (legacy low-res screens)

    📐 Specialized Rendering — Needed only for specific rendering scenarios.

    TableDescription
    MATHMath layout constants and glyph assembly — only for TeX-like math typesetting engines
    cvt/fpgm/prepTrueType hinting programs — grid-fitting instructions for low-DPI rasterization. Irrelevant for SVG/vector output
    hdmxPre-computed device widths for specific PPEMs — legacy optimization for bitmap rendering

    🍎 Apple AAT — Apple-proprietary layout system. Most modern fonts use OpenType (GSUB/GPOS) instead. Only found in macOS system fonts and some legacy fonts.

    TableDescription
    morx/mortApple Advanced Typography — state-machine-based shaping

    🔒 Other — Rarely needed for rendering or layout.

    TableDescription
    DSIGDigital signature — font authenticity verification, does not affect rendering
    cvarCVT variations — hinting value adjustments in variable fonts, only relevant if hinting is executed

    #CFF / CFF2 Charstring Operators

    All standard operators for outline extraction are supported:

    CategoryOperators
    Move✅rmoveto, hmoveto, vmoveto
    Line✅rlineto, hlineto, vlineto
    Curve✅rrcurveto, hhcurveto, vvcurveto, hvcurveto, vhcurveto
    Mixed✅rcurveline, rlinecurve
    Flex✅flex, hflex, vflex, hflex1, flex1
    Hint✅hstem, vstem, hstemhm, vstemhm, hintmask, cntrmask
    Subroutine✅callsubr, callgsubr, return
    Arithmetic✅add, sub, mul, div, neg, abs, sqrt, eq
    Logic✅and, or, not, ifelse
    Stack✅dup, exch, drop, index, roll, put, get
    CFF2 Variation✅blend, vsindex
    Control✅endchar
    Deprecated➖seac, dotsection — removed from modern specs

    #Glyph Outlines

    FeatureNotes
    Simple glyphs (TrueType)✅
    Compound glyphs (TrueType)✅Scale, 2x2 matrix, XY offset transforms
    CFF1 charstrings✅Local/global subroutines
    CFF2 charstrings✅Blend interpolation
    TrueType variations (gvar)✅Shared tuples, IUP, delta unpacking
    CFF2 variations (ItemVariationStore)✅Region scalars, blend deltas
    Hinting / instruction execution❌Not needed for vector/SVG output

    #cmap Formats

    Formats 0, 4, 6, and 12 cover virtually all modern fonts. The unsupported formats are legacy or niche:

    FormatCoverage
    Format 0✅Mac Roman 256-char
    Format 4✅BMP (U+0000–U+FFFF)
    Format 6✅Trimmed table
    Format 12✅Full Unicode (preferred when available)
    Format 2❌CJK mixed 8/16-bit — obsolete encoding, replaced by Format 12
    Format 14❌Unicode Variation Sequences — needed for CJK glyph variants (e.g. JP vs CN forms)

    #Kerning

    Basic layout uses the legacy kern table. shape_text applies GPOS kerning through Swash:

    FeatureNotes
    kern table Format 0 (flat pairs)✅Covers most Latin fonts
    kern table Format 1 (Apple state machine)❌Apple-only, rare in cross-platform fonts
    GPOS pair adjustment (PairPos)✅Pair and class adjustments through shape_text
    GPOS contextual positioning✅Handled by the shaping backend

    #Font Subsetting

    Subsetting extracts only the glyphs you need, reducing file size for web delivery:

    FeatureNotes
    Glyf-based TrueType subsetting✅
    Compound glyph dependency resolution✅
    Glyph ID remapping✅
    cmap Format 12 rebuild✅
    Table copy (head, hhea, hmtx, maxp, name, OS/2, post)✅
    CFF/CFF2 subsetting❌CFF charstring rewriting is complex
    WOFF/WOFF2 output❌Outputs raw sfnt — wrap with external compressor
    Layout table subsetting (GSUB/GPOS)❌Requires lookup/coverage rewriting

    #Text Layout

    Basic LTR/vertical APIs and the shaped horizontal paragraph API serve different layout needs:

    FeatureNotes
    Horizontal advance widths✅
    Pair kerning (kern table)✅
    UTF-16 surrogate pair handling✅
    Text width measurement✅
    OpenType shaping (GSUB/GPOS)✅shape_text: ligatures, contextual forms, kerning and mark positioning
    Bidirectional text✅Resolved levels, isolates, mirroring and visual runs through Unicode BiDi
    Vertical layout✅Top-to-bottom CJK typesetting (vhea/vmtx/VORG)
    Line breaking❌Text wrapping — typically handled by the application layer

    #Shaped paragraphs

    let shaped = font.shape_text(
    "fi AV אבג 123",
    32.0,
    direction=@font.Auto, // Also Ltr and Rtl
    language="und", // BCP 47, e.g. "ar", "he", "tr"
    kerning=true,
    ligatures=true,
    font_weight=700.0, // Optional: select wght on a variable font
    axis_values={ "wdth": 90.0 }, // Optional: explicit axes override font_weight
    )
    let fit = shaped.bounds.check_width(80.0, 240.0)
    // fit.status: Underflow, Fits or Overflow; inclusive limits in CSS pixels

    ShapedText.bounds contains advance/logical/ink bounds and glyphs in visual order. Each glyph's text_start/text_end is a half-open UTF-16 cluster range into the original string. A ligature spans several characters; bases and combining marks can share one range. codepoint is the first original code point of that cluster, not a reverse mapping of the substituted glyph. runs adds source/glyph ranges, script, BiDi level, direction, x and advance. missing_codepoints reports unsupported input characters, excluding default-ignorable formatting controls.

    The API shapes one paragraph. It uses the default variation instance unless font_weight or axis_values is supplied; selected axes are reported in ShapedText.variations. Weight must be finite and between 1 and 1000. A static face accepts only its own OS/2 weight: load the appropriate face instead of expecting synthetic bold. Unknown or non-finite axes raise ShapingError; valid axis values are clamped to the font's range. The pinned Swash 0.1.15 adapter compensates for its avar reader, so non-linear axis mapping affects both shaped advances and outlines.

    It also raises ShapingError for an invalid size/font or paragraph separators. Font fallback, CSS spacing, wrapping, shaped vertical layout and caret positions inside ligatures are outside this API. The browser can supply fallback glyphs for missing code points, so such results are diagnostic comparisons rather than parity guarantees. Variable compound-glyph outlines still fall back to their default outlines; use browser layout for fit inspection rather than treating every MoonBit ink rectangle as a browser paint bound. Consumers constructing TTFont records manually must initialize the new sfnt_offset field to zero for standalone sfnt data; parsed TTC faces retain their actual header offset.

    check_text_width(width, min, max, tolerance=0.01) and TextBounds.check_width perform finite, nonnegative interval checks without wrapping. Results include the measured width and the amount below or above the limits. Classification uses tolerance; reported deviations retain their original fractional values.

    #JS Bindings

    The bindings in src/js build to a JavaScript ESM module with moon build --target js --release. MoonBit also generates TypeScript declarations next to the module.

    18 exported functions:

    FunctionReturnsDescription
    loadFont(data)JSON stringParse font, return metrics
    getFontInfo()JSON stringGet cached font info
    glyphToSvgPath(codepoint, fontSize)SVG path stringScaled glyph outline
    glyphOutlineCommands(codepoint, fontSize)JSON stringScaled path commands
    glyphAdvance(codepoint, fontSize)DoubleScaled advance width
    fontName(nameId)StringName table lookup
    kernAdvance(cp1, cp2, fontSize)DoubleScaled kerning value
    layoutText(text, fontSize)JSON stringGlyph positions with kerning
    measureText(text, fontSize)DoubleTotal text width
    layoutTextBounds(text, fontSize)JSON stringAdvance, logical/ink bounds and positioned glyph bounds; null if unloaded or size is invalid
    shapeText(text, fontSize, direction, language, kerning, ligatures)JSON string{ok:true,result:ShapedText} or {ok:false,error:{code,message}}
    shapeTextWithWeight(text, fontSize, direction, language, kerning, ligatures, weight)JSON stringSame shaped contract, selecting the requested weight
    shapeTextWithAxes(text, fontSize, direction, language, kerning, ligatures, weight, axesJson)JSON stringSame contract with explicit numeric variation axes; malformed input returns an error
    fontWeightClass()IntOS/2 weight class
    isFixedPitch()Int (0/1)Post table fixed-pitch flag
    codepointCoverage()JSON stringAll supported codepoints
    glyphIds()JSON stringAll glyph IDs (0 to numGlyphs-1)
    tableSizes()JSON stringTable tag to byte size

    Font info includes weight_class, italic and variable axes (tag, min, default, max), in addition to the existing metrics.

    #Checking whether text fits

    Use browser/fit.js for checks against actual browser layout. It waits for web fonts and returns both modes in one report:

    import { checkTextFit, inspectTextFit } from '/browser/fit.js'; const result = await checkTextFit('Hello world Hello world', { minWidth: 40, maxWidth: 120, maxHeight: 96, // Omit for an unconstrained height requiredFonts: ['YourLoadedFont'], // Optional: require a registered web font allowTruncation: false, // Default: require the full text style: { fontFamily: 'YourLoadedFont, sans-serif', fontSize: '20px', fontWeight: '700', lineHeight: '28px', whiteSpace: 'normal', overflowWrap: 'anywhere', }, }); console.log(result.unwrapped.status); // Natural max-content width vs [40, 120] console.log(result.layout.status); // Actual wrapping and height at 120 x 96 console.log(result.layout.overflowX, result.layout.overflowY); console.log(result.accepted); // Wrapped-layout acceptance, including fonts/clipping console.log(result.presentation); // Full / truncated / clipped / scrollable / overflow // Inspect an existing element using its actual fonts, inline spans and CSS. // This does not resize or modify the source element. const existing = await inspectTextFit(document.querySelector('.label'), { minWidth: 40, maxWidth: 120, });

    unwrapped compares natural max-content width with the requested inclusive interval, returning underflow, fits or overflow. Explicit newlines and white-space remain meaningful: this is the width without optional soft wraps, not removal of hard line breaks. intrinsic.minContentWidth and maxContentWidth separately report CSS intrinsic sizes; they are not the caller's minimum/maximum limits.

    layout checks horizontal overflow of actual Range fragments, atomic inline boxes and scroll geometry, and compares the required line-box height with the current content-box height. Range em rectangles can exceed CSS line-height even when a line fits, so they are reported separately from required height. layout.status is overflow if either axis or the parent Flex/Grid allocation overflows; otherwise it compares the occupied text span with the width interval. placement separately reports Flex/Grid container boundaries, including siblings pushed outside by an unshrinkable label. With auto height, wrapping may grow vertically without causing overflow.

    Ordinary inline labels share their parent's line boxes and cannot independently constrain height. For these roots, required height uses their native occupied span; ancestor clipping is still checked. A synthetic block's line-height is not compared with the inline element's em rectangle.

    presentation.status distinguishes full, deliberate truncated text, unintended clipped text, scrollable overflow and visible overflow. clippedBy records target/ancestor clipping boundaries. accepted checks wrapped layout and presentation together: a label can fit its own box while being cut by an ancestor. allowTruncation: true accepts ellipsis or line clamp, while keeping layout.status: 'overflow' accurate. The constrained text span must still be within the requested width interval; ancestor clipping still rejects the result. Unknown results have accepted: null. Temporary checkTextFit boxes inspect their own clipping, independently of the host page's clipping.

    requiredFonts names registered web-font families that must be requested and load successfully. Missing, unused or failed required families return font-error; selected web-font loading errors are also reported. fonts.faces records registered faces and loading states. This is not a per-glyph font audit: standard DOM APIs do not expose the face used for every glyph, so fonts.glyphCoverage remains unverified. System fallback, mixed-script fallback and synthetic weight are measured natively without claiming that every glyph came from the requested face.

    checkTextFit lays out a temporary content box at maxWidth and optional maxHeight. inspectTextFit uses the existing element's content box: supplied limits do not resize it. Results include computed typography, content/text rectangles, fragments and overflow distances. Font fallback, synthetic weight, variable axes, spacing, Japanese line breaking and inline style selection are delegated to the target browser. SVG/image icons, inline blocks, badges and quoted-string/empty pseudo-elements participate in measurement. Images reuse their decoded dimensions; measurement clones never copy their resource URLs. Measurement nodes are removed afterward.

    Text/email/url/tel inputs and textareas inspect the current value, or the placeholder when empty, including placeholder typography. Textarea trailing newlines and wrap=off are supported. control identifies the source and length without returning the value itself. Inspection preserves the value, selection and scroll position. Native scroll geometry complements the text probe; internal control rectangles are inferred, not DOM Range rectangles from browser-private shadow content.

    Positive axis-aligned scale/translate and CSS zoom on the inspected element or its ancestors are supported. coordinates.space: 'normalized-viewport' means all rectangles use viewport coordinates divided by the inspected element's total scaleX / scaleY. Multiply coordinates/dimensions by those factors to recover viewport CSS pixels. Width limits, intrinsic sizes and required height remain unscaled CSS pixels. Zoomed border widths are measured in the target engine instead of guessing its pixel-snapping rules.

    The contract is basis: 'layout': it checks text layout, not every painted pixel, italic overhang, shadows or decorations. Detached or display:none elements return unmeasurable. Rotation/skew/reflection/3D, transformed or positioned descendants, standalone non-inline flows, video/canvas/iframe/select, password/search/other decorated inputs, shadow content, counter/URL/positioned pseudo-elements, vertical writing and multicolumn layouts return unsupported with reasons. These results never silently become fits. This is a supported text-layout scope, not a guarantee for arbitrary CSS or all font formats. The completed priority list and deferred features are recorded in TODO.md.

    # Report both results; --mode selects which result --assert-fit checks just check-fit --text 'Hello world Hello world' --weight 700 \ --min-width 40 --max-width 120 --height 96 --assert-fit # Single-line check; returns exit code 1 for overflow/underflow/unknown just check-fit --text 'Hello world' --max-width 80 \ --mode single-line --assert-fit # Inspect a real page in the selected headless engine just check-fit --url http://localhost:3000 --selector '.label' \ --browser firefox --min-width 40 --max-width 120 \ --require-font 'YourLoadedFont' --allow-truncation --assert-fit

    The default fit font is the committed variable Roboto subset. --font selects another local font; --white-space, --word-break, --overflow-wrap, --letter-spacing, --line-height, --overflow, --text-overflow, --line-clamp, --stretch (e.g. 75%), --optical-sizing and --variation-settings configure its CSS. With --url, the page's existing CSS is used. --assert-fit checks unwrapped.status in single-line mode and accepted in wrapped mode. Fit reports use schema version 2 and default to _build/measurements/fit.json.

    #Headless browser measurements

    Requires Node.js 24+, pnpm and the latest stable MoonBit toolchain. Run moon upgrade to update the toolchain. This migration was verified with moon 0.1.20261104 and moonc v0.10.20+65efd5b93 (2026-11-05).

    just setup-browser just test-browser # Save six default samples as _build/measurements/font-metrics.json just measure-browser # Measure a local font, with optional DOM wrapping; --text can be repeated just measure-browser --font fixtures/NotoSansMono-Regular.ttf \ --text "Hello world" --text "Ag" --size 20 --width 70 \ --line-height 30 --output _build/measurements/wrapped.json # GSUB/GPOS and mixed-direction samples just measure-shaping just measure-browser --font fixtures/shaping-reference/arabic.ttf \ --text "العربية" --direction rtl --language ar --size 32 just measure-browser --font fixtures/shaping-reference/roboto-variable.ttf \ --text "Hello world" --weight 700 --size 24 just measure-browser --font fixtures/shaping-reference/robotoserif-variable.ttf \ --text "Hello world" --axis wdth=75 --axis opsz=8 --size 32 # Optional feature switches: --no-kerning --no-ligatures

    The metric comparisons load identical font bytes into MoonBit and headless Chromium, wait for FontFace.load(), and compare TTF, WOFF, WOFF2, CFF and legacy kerning. Both Canvas and the comparison DOM use text-rendering: geometricPrecision for unhinted design advances; Linux's default hinting can otherwise round each glyph's advance to whole pixels. The selected mode is recorded in measurement options. Shaping cases cover GSUB ligatures/contextual forms, GPOS pair/class kerning and marks, Arabic joining, Hebrew marks, mixed directions, brackets, isolates, and feature switches. Pinned Noto subsets exercise real layout tables as well as original synthetic fixtures. Advances allow 0.02 CSS pixels, or 0.05 for variable-font cases; ink comparisons allow 2 CSS pixels. Roboto is tested at weights 100/400/700/900 and sizes 13.25/24/32; static bold and italic faces are also compared. Tests cover spaces, non-BMP UTF-16 offsets, missing glyphs and independent font handles.

    The DOM fit contract is tested in Chromium, Firefox and WebKit. Fit inspection retains the target element's actual text-rendering mode and native hinting. Cases cover interval boundaries, wrapped/unwrapped width, fixed-height/ancestor clipping, ellipsis policy, line clamp, white-space, Japanese punctuation, URLs/NBSP/ZWSP/soft hyphens, Flex/Grid allocation, icons/badges/pseudo-elements, inputs/textareas, real 400/700 face selection, font failures, mixed-script fallback, variable width/optical sizing, spacing, fractional sizes, zoom and explicit unsupported results. Canvas ink bounds differ across engines, particularly for mixed BiDi text; cross-engine MoonBit/Canvas ink parity is not asserted. CI installs all three engines and uploads reports/test attachments.

    On Linux, FreeType rounds HVAR advance deltas to integer font units. Variable-font comparisons therefore bound this backend difference by half a font unit per glyph, scaled by font size, plus 0.02px serialization precision (with a 0.05px minimum). Static-font comparisons and the actual DOM fit tolerance remain unchanged. MoonBit predictions are not used as exact browser overflow decisions.

    Each exported report records the font SHA-256, toolchain/browser versions and:

    DataCoordinates and meaning
    moonbit.advance_widthShaped paragraph advance, including spaces and kerning
    moonbit.logical_boundshhea ascent/descent box, baseline origin, y down; line gap reported separately
    moonbit.ink_boundsUnion of tight outline bounds computed from curve extrema; null when no ink
    moonbit.glyphsVisual glyph positions, advances, ink bounds and half-open UTF-16 cluster ranges
    shapingFull MoonBit shaped result, including direction, runs and missing code points
    canvasUnwrapped browser-shaped advance and baseline-relative ink bounds
    domElement/text bounds, Range fragments and grapheme rectangles, relative to the element's top-left
    missingCodepointsCode points missing from the selected font; the browser may use fallback fonts
    widthDeltaMoonBit advance minus Canvas advance, in CSS pixels

    The browser helper can also be used from a page served by node scripts/browser-server.ts, after just build-browser:

    import { loadFont } from '/browser/font.js'; const font = await loadFont('/fixtures/NotoSansMono-Regular.ttf'); try { const metrics = font.measure('Hello world', 20, { maxWidth: 70, lineHeight: 30 }); console.log(metrics.canvas.advanceWidth, metrics.dom.lineRects); } finally { font.dispose(); } // Explicit variation instances load asynchronously and are cached until dispose(). const variable = await loadFont('/fixtures/shaping-reference/robotoserif-variable.ttf'); try { const metrics = await variable.measureWithAxes('Hello world', 32, { fontWeight: 400, axisValues: { wdth: 75, opsz: 8 }, }); console.log(metrics.shaping.variations); } finally { variable.dispose(); }

    Font sizes and optional DOM dimensions must be finite and positive. The raw MoonBit binding additionally supports a zero font size. Measurement options include direction (auto, ltr, rtl), language, kerning, ligatures and fontWeight; these apply to both MoonBit and the browser. measureWithAxes adds axisValues and numeric fontStretch (mapped to wdth unless explicitly overridden). Unknown/non-finite axes reject; values are clamped to the font range. The comparison helper disables font synthesis and automatic optical sizing, and pins the default opsz in Canvas when needed. Native automatic optical sizing remains available through font.family and the fit inspector. maxWidth affects DOM wrapping only: MoonBit and Canvas remain unwrapped paragraph measurements. DOM rectangles describe graphemes and text fragments, not glyph outlines; BiDi can produce multiple fragments on one line. Measurement JSON reports use schema version 4, including explicit axis options and selected variation metadata.

    This separation follows Parley's text stack, which separates font discovery, shaping, outline metrics and layout. Its line metrics also distinguish advance, baseline and line/content bounds. This project keeps parsing/bounds and shaped layout in MoonBit, browser observation in browser/font.js, the measurement contract in browser/types.ts, and collection in the CLI. It does not depend on Parley.

    #Development

    # Download test fixtures and run tests just test # Or manually bash scripts/download-fixtures.sh moon test # Type check just check # Format code just fmt # Default-backend tests and headless browser checks just ci

    NotoSans font files are not committed to the repository. They are downloaded from GitHub Releases on first run.

    #Dependencies

    #Test Fonts

    fixtures/NotoSansMono-Regular.ttf is licensed under the SIL Open Font License.

    fixtures/shaping.{ttf,woff,ttc} are original geometric test fonts under Apache-2.0; regenerate with just gen-shaping-fixtures. fixtures/shaping-reference/ contains small, renamed Noto/Roboto subsets under the included OFL files. sources.json and fit-sources.json record pinned upstream URLs and source/subset hashes. Regenerate with just gen-shaping-references and just gen-fit-references; CI consumes the committed binaries without requiring Python or another network download.

    #License

    Apache-2.0

    ShapingError

    pub suberror ShapingError {
    InvalidFontSize
    InvalidFont
    MultipleParagraphs
    InvalidFontWeight
    UnsupportedFontWeight
    InvalidVariationAxis(String)
    }

    ShapingError::message

    fn ShapingError::message(self : ShapingError) -> String

    WidthCheckError

    pub suberror WidthCheckError {
    InvalidWidthLimits
    }

    AtlasPageInfo

    pub(all) struct AtlasPageInfo {
    page_id : Int
    width : Int
    height : Int
    dirty : Bool
    }

    AtlasPageInfo::output

    fn AtlasPageInfo::output(self : AtlasPageInfo, logger : &Logger) -> Unit

    AtlasPageInfo::to_string

    fn AtlasPageInfo::to_string(self : AtlasPageInfo) -> String

    BinaryReader

    pub(all) struct BinaryReader {
    data : Bytes
    pos : Int
    }

    BinaryReader::at

    fn BinaryReader::at(data : Bytes, pos : Int) -> BinaryReader

    BinaryReader::new

    fn BinaryReader::new(data : Bytes) -> BinaryReader

    BinaryReader::peek_int16_at

    fn BinaryReader::peek_int16_at(self : BinaryReader, offset : Int) -> Int

    BinaryReader::peek_uint16_at

    fn BinaryReader::peek_uint16_at(self : BinaryReader, offset : Int) -> Int

    BinaryReader::peek_uint32_at

    fn BinaryReader::peek_uint32_at(self : BinaryReader, offset : Int) -> Int

    BinaryReader::position

    fn BinaryReader::position(self : BinaryReader) -> Int

    BinaryReader::read_f16dot16

    fn BinaryReader::read_f16dot16(self : BinaryReader) -> Double

    BinaryReader::read_f2dot14

    fn BinaryReader::read_f2dot14(self : BinaryReader) -> Double

    BinaryReader::read_int16

    fn BinaryReader::read_int16(self : BinaryReader) -> Int

    BinaryReader::read_int8

    fn BinaryReader::read_int8(self : BinaryReader) -> Int

    BinaryReader::read_tag

    fn BinaryReader::read_tag(self : BinaryReader) -> String

    BinaryReader::read_uint16

    fn BinaryReader::read_uint16(self : BinaryReader) -> Int

    BinaryReader::read_uint32

    fn BinaryReader::read_uint32(self : BinaryReader) -> Int

    BinaryReader::read_uint8

    fn BinaryReader::read_uint8(self : BinaryReader) -> Int

    BinaryReader::seek

    fn BinaryReader::seek(self : BinaryReader, pos : Int) -> Unit

    BinaryReader::skip

    fn BinaryReader::skip(self : BinaryReader, n : Int) -> Unit

    CFFData

    pub(all) struct CFFData {
    charstrings : CFFIndex
    global_subrs : CFFIndex
    local_subrs : CFFIndex
    default_width_x : Int
    nominal_width_x : Int
    is_cff2 : Bool
    ivs : ItemVariationStore?
    }

    CFFIndex

    pub(all) struct CFFIndex {
    data_offset : Int
    offsets : Array[Int]
    }

    FontFormat

    pub enum FontFormat {
    TrueType
    OpenType
    Woff1
    Woff2
    TrueTypeCollection
    Unknown
    } derive(Eq)

    impl Show for FontFormat

    FontFormat::equal

    fn FontFormat::equal(FontFormat, FontFormat) -> Bool

    FontFormat::not_equal

    fn FontFormat::not_equal(x : FontFormat, y : FontFormat) -> Bool

    FontFormat::output

    fn FontFormat::output(self : FontFormat, logger : &Logger) -> Unit

    FontFormat::to_string

    fn FontFormat::to_string(self : FontFormat) -> String

    FontKey

    pub(all) struct FontKey {
    family : String
    weight : Int
    italic : Bool
    }

    impl Show for FontKey

    FontKey::output

    fn FontKey::output(self : FontKey, logger : &Logger) -> Unit

    FontKey::to_string

    fn FontKey::to_string(self : FontKey) -> String

    GaspRange

    pub(all) struct GaspRange {
    max_ppem : Int
    behavior : Int
    }

    Glyph

    pub(all) struct Glyph {
    glyph_id : Int
    metrics : GlyphMetrics
    outline : Array[
    PathCommand
    ]
    }

    GlyphAtlas

    pub struct GlyphAtlas {
    cache : GlyphCache
    width : Int
    height : Int
    pixels : Array[Int]
    dirty : Bool
    last_used_generation : Int
    }

    GlyphAtlas::deserialize

    fn GlyphAtlas::deserialize(data : Array[Int]) -> GlyphAtlas?

    Deserialize a GlyphAtlas from a flat Int array.

    GlyphAtlas::get_cache

    fn GlyphAtlas::get_cache(self : GlyphAtlas) -> GlyphCache

    GlyphAtlas::get_pixels

    fn GlyphAtlas::get_pixels(self : GlyphAtlas) -> Array[Int]

    GlyphAtlas::new

    fn GlyphAtlas::new(width : Int, height : Int, page_id : Int) -> GlyphAtlas

    GlyphAtlas::rasterize_glyph

    fn GlyphAtlas::rasterize_glyph(self : GlyphAtlas, font : TTFont, codepoint : Int, size_px : Double) -> GlyphCacheEntry?

    Rasterize a single glyph and place it into the atlas.

    GlyphAtlas::serialize

    fn GlyphAtlas::serialize(self : GlyphAtlas) -> Array[Int]

    Serialize a GlyphAtlas to a flat Int array for persistent caching.

    GlyphBBox

    pub(all) struct GlyphBBox {
    x_min : Int
    y_min : Int
    x_max : Int
    y_max : Int
    }

    GlyphBitmap

    pub struct GlyphBitmap {
    width : Int
    height : Int
    pixels : Array[Int]
    }

    Rasterized glyph pixel data (RGBA8 flat array: [r,g,b,a, r,g,b,a, ...]).
    impl Show for GlyphBitmap

    GlyphBitmap::output

    fn GlyphBitmap::output(self : GlyphBitmap, logger : &Logger) -> Unit

    GlyphBitmap::to_string

    fn GlyphBitmap::to_string(self : GlyphBitmap) -> String

    GlyphCache

    pub struct GlyphCache {
    entries : Map[Int, GlyphCacheEntry]
    next_x : Double
    next_y : Double
    row_height : Double
    atlas_width : Double
    atlas_height : Double
    atlas_page_id : Int
    }

    GlyphCache::allocate

    fn GlyphCache::allocate(self : GlyphCache, glyph_id : Int, width : Double, height : Double) -> GlyphCacheEntry?

    Allocate space in the atlas for a glyph. Returns the cache entry, or None if the atlas is full.

    GlyphCache::clear

    fn GlyphCache::clear(self : GlyphCache) -> Unit

    GlyphCache::entry_count

    fn GlyphCache::entry_count(self : GlyphCache) -> Int

    GlyphCache::get

    fn GlyphCache::get(self : GlyphCache, glyph_id : Int) -> GlyphCacheEntry?

    GlyphCache::new

    fn GlyphCache::new(atlas_width : Double, atlas_height : Double, atlas_page_id : Int) -> GlyphCache

    GlyphCacheEntry

    pub(all) struct GlyphCacheEntry {
    glyph_id : Int
    atlas_x : Double
    atlas_y : Double
    atlas_w : Double
    atlas_h : Double
    atlas_page_id : Int
    }

    GlyphCacheEntry::output

    fn GlyphCacheEntry::output(self : GlyphCacheEntry, logger : &Logger) -> Unit

    GlyphCacheEntry::to_string

    fn GlyphCacheEntry::to_string(self : GlyphCacheEntry) -> String

    GlyphMetrics

    pub(all) struct GlyphMetrics {
    advance_width : Int
    left_side_bearing : Int
    bbox : GlyphBBox
    }

    GlyphPosition

    pub(all) struct GlyphPosition {
    codepoint : Int
    glyph_id : Int
    x_offset : Double
    y_offset : Double
    x_advance : Double
    }

    GlyphQuad

    pub(all) struct GlyphQuad {
    glyph_id : Int
    atlas_x : Double
    atlas_y : Double
    atlas_w : Double
    atlas_h : Double
    dst_x : Double
    dst_y : Double
    dst_w : Double
    dst_h : Double
    }

    impl Show for GlyphQuad

    GlyphQuad::output

    fn GlyphQuad::output(self : GlyphQuad, logger : &Logger) -> Unit

    GlyphQuad::to_string

    fn GlyphQuad::to_string(self : GlyphQuad) -> String

    GvarData

    pub(all) struct GvarData {
    axis_count : Int
    shared_tuples : Array[Array[Double]]
    gvar_offset : Int
    glyph_var_data_offset : Int
    glyph_offsets : Array[Int]
    }

    ItemVariationData

    pub(all) struct ItemVariationData {
    region_indices : Array[Int]
    delta_sets : Array[Array[Int]]
    }

    ItemVariationStore

    pub(all) struct ItemVariationStore {
    regions : Array[VarRegion]
    data : Array[ItemVariationData]
    }

    OS2Table

    pub(all) struct OS2Table {
    version : Int
    us_weight_class : Int
    us_width_class : Int
    fs_type : Int
    panose : FixedArray[Int]
    ul_unicode_range1 : Int
    ul_unicode_range2 : Int
    ul_unicode_range3 : Int
    ul_unicode_range4 : Int
    fs_selection : Int
    s_typo_ascender : Int
    s_typo_descender : Int
    s_typo_line_gap : Int
    us_win_ascent : Int
    us_win_descent : Int
    sx_height : Int
    s_cap_height : Int
    }

    PositionedGlyphBounds

    pub(all) struct PositionedGlyphBounds {
    codepoint : Int
    glyph_id : Int
    text_start : Int
    text_end : Int
    x : Double
    y : Double
    advance_width : Double
    ink_bounds : TextRect?
    }

    A positioned glyph with a half-open UTF-16 range into the source text. Whitespace has an advance but no ink rectangle.

    PositionedGlyphBounds::to_json

    PostTable

    pub(all) struct PostTable {
    italic_angle : Double
    underline_position : Int
    underline_thickness : Int
    is_fixed_pitch : Bool
    glyph_names : Array[String]
    }

    ShapedRun

    pub(all) struct ShapedRun {
    text_start : Int
    text_end : Int
    glyph_start : Int
    glyph_end : Int
    level : Int
    direction : String
    script : String
    x : Double
    advance_width : Double
    } derive(
    ToJson
    )

    A visual run of one script and resolved BiDi embedding level. Text ranges are half-open UTF-16 offsets; glyph ranges index ShapedText.bounds.glyphs.

    ShapedRun::to_json

    ShapedText

    pub(all) struct ShapedText {
    direction : String
    bounds : TextBounds
    runs : Array[ShapedRun]
    missing_codepoints : Array[Int]
    variations : Map[String, Double]
    } derive(
    ToJson
    )

    A single shaped paragraph, with runs and glyph clusters in visual order. Glyphs sharing a cluster have the same source range; ligatures can cover several characters. Coordinates use a left baseline origin with y down.

    ShapedText::to_json

    TTFont

    pub(all) struct TTFont {
    units_per_em : Int
    index_to_loc_format : Int
    num_glyphs : Int
    ascent : Int
    descent : Int
    line_gap : Int
    num_h_metrics : Int
    glyf_offset : Int
    data : Bytes
    sfnt_offset : Int
    cmap : Map[Int, Int]
    loca : Array[Int]
    advance_widths : Array[Int]
    left_side_bearings : Array[Int]
    cff : CFFData?
    gvar : GvarData?
    var_axes : Array[VarAxis]
    kern_pairs : Map[Int, Int]
    avar_segments : Array[Array[(Double, Double)]]
    name_records : Map[Int, String]
    os2 : OS2Table?
    post : PostTable?
    vertical : VerticalMetrics?
    gasp : Array[GaspRange]
    vorg : VorgData?
    }

    TTFont::cap_height

    fn TTFont::cap_height(self : TTFont) -> Int

    Get cap-height from OS/2 table

    TTFont::char_outline

    fn TTFont::char_outline(self : TTFont, codepoint : Int) -> Array[
    PathCommand
    ]

    TTFont::char_outline_at

    fn TTFont::char_outline_at(self : TTFont, codepoint : Int, axis_values : Map[String, Double]) -> Array[
    PathCommand
    ]

    TTFont::codepoint_coverage

    fn TTFont::codepoint_coverage(self : TTFont) -> Array[Int]

    Get all codepoints covered by the font's cmap

    TTFont::font_name

    fn TTFont::font_name(self : TTFont, name_id : Int) -> String?

    Get font name by nameID (1=family, 2=subfamily, 4=full name, 6=postscript name)

    TTFont::get_glyph

    fn TTFont::get_glyph(self : TTFont, codepoint : Int) -> Glyph

    TTFont::glyph_ids

    fn TTFont::glyph_ids(self : TTFont) -> Array[Int]

    Get all glyph IDs in the font (0 to num_glyphs-1)

    TTFont::glyph_index

    fn TTFont::glyph_index(self : TTFont, codepoint : Int) -> Int

    TTFont::glyph_metrics

    fn TTFont::glyph_metrics(self : TTFont, glyph_id : Int) -> GlyphMetrics

    TTFont::glyph_metrics_at

    fn TTFont::glyph_metrics_at(self : TTFont, glyph_id : Int, axis_values : Map[String, Double]) -> GlyphMetrics

    Get glyph metrics with variable font interpolation. For glyf+gvar fonts, advance width is interpolated via phantom points. For CFF2 fonts, bbox is computed from the variable outline (advance uses static value).

    TTFont::glyph_name

    fn TTFont::glyph_name(self : TTFont, glyph_id : Int) -> String

    Get glyph name from post table

    TTFont::glyph_outline

    fn TTFont::glyph_outline(self : TTFont, glyph_id : Int) -> Array[
    PathCommand
    ]

    TTFont::glyph_outline_at

    fn TTFont::glyph_outline_at(self : TTFont, glyph_id : Int, axis_values : Map[String, Double]) -> Array[
    PathCommand
    ]

    TTFont::is_fixed_pitch

    fn TTFont::is_fixed_pitch(self : TTFont) -> Bool

    Check if font is fixed-pitch from post table

    TTFont::italic_angle

    fn TTFont::italic_angle(self : TTFont) -> Double

    Get italic angle from post table

    TTFont::kern_pair

    fn TTFont::kern_pair(self : TTFont, left_gid : Int, right_gid : Int) -> Int

    Get kerning value for a pair of glyph IDs

    TTFont::layout_text

    fn TTFont::layout_text(self : TTFont, text : String, font_size : Double) -> Array[GlyphPosition]

    Layout text into positioned glyphs with kerning

    TTFont::layout_text_bounds

    fn TTFont::layout_text_bounds(self : TTFont, text : String, font_size : Double) -> TextBounds

    Measure a single run, preserving subpixel precision. font_size must be finite and nonnegative. Bounds are baseline-relative, in pixels (y down).

    TTFont::layout_text_vertical

    fn TTFont::layout_text_vertical(self : TTFont, text : String, font_size : Double) -> Array[GlyphPosition]

    Layout text vertically (top-to-bottom) into positioned glyphs

    TTFont::measure_text

    fn TTFont::measure_text(self : TTFont, text : String, font_size : Double) -> Double

    Measure total text width

    TTFont::measure_text_vertical

    fn TTFont::measure_text_vertical(self : TTFont, text : String, font_size : Double) -> Double

    Measure total text height for vertical layout

    TTFont::scaled_outline

    fn TTFont::scaled_outline(self : TTFont, codepoint : Int, font_size : Double) -> Array[
    PathCommand
    ]

    TTFont::shape_text

    fn TTFont::shape_text(self : TTFont, text : String, font_size : Double, direction? : TextDirection, language? : String, kerning? : Bool, ligatures? : Bool, font_weight? : Double, axis_values? : Map[String, Double]) -> ShapedText raise ShapingError

    Shape a single paragraph using OpenType GSUB/GPOS and Unicode BiDi. Optional weight/axis coordinates select a variable font instance. Static weights must match the loaded face. No synthesis or font fallback is performed. Kerning and standard/contextual ligatures are enabled by default.

    TTFont::vert_origin_y

    fn TTFont::vert_origin_y(self : TTFont, glyph_id : Int) -> Int

    Get vertical origin Y for a glyph (design units)

    TTFont::weight_class

    fn TTFont::weight_class(self : TTFont) -> Int

    Get weight class from OS/2 table (400=Regular, 700=Bold)

    TTFont::x_height

    fn TTFont::x_height(self : TTFont) -> Int

    Get x-height from OS/2 table

    TableInfo

    pub(all) struct TableInfo {
    tag : String
    offset : Int
    length : Int
    }

    TextBounds

    pub(all) struct TextBounds {
    advance_width : Double
    ascent : Double
    descent : Double
    line_gap : Double
    logical_bounds : TextRect
    ink_bounds : TextRect?
    glyphs : Array[PositionedGlyphBounds]
    }

    Horizontal text metrics. Logical bounds use hhea ascent and descent; ink bounds are the union of positioned glyph bounds, excluding whitespace. For shaped clusters use shape_text; layout_text_bounds is basic LTR layout.

    TextBounds::check_width

    fn TextBounds::check_width(self : TextBounds, min_width : Double, max_width : Double, tolerance? : Double) -> WidthCheck raise WidthCheckError

    TextBounds::to_json

    TextDirection

    pub(all) enum TextDirection {
    Auto
    Ltr
    Rtl
    } derive(Eq)

    Base paragraph direction. Auto uses the first strong Unicode character.

    TextDirection::equal

    TextDirection::not_equal

    fn TextDirection::not_equal(x : TextDirection, y : TextDirection) -> Bool

    TextMetrics

    pub(all) struct TextMetrics {
    width : Double
    height : Double
    baseline : Double
    }

    impl Show for TextMetrics

    TextMetrics::output

    fn TextMetrics::output(self : TextMetrics, logger : &Logger) -> Unit

    TextMetrics::to_string

    fn TextMetrics::to_string(self : TextMetrics) -> String

    TextRect

    pub(all) struct TextRect {
    x : Double
    y : Double
    width : Double
    height : Double
    } derive(
    ToJson
    )

    A subpixel rectangle. In text layout, units are pixels relative to the baseline origin, with positive y down.

    TextRect::to_json

    TextRenderer

    pub struct TextRenderer {
    pages : Array[GlyphAtlas]
    font : TTFont
    pipeline_id : Int
    atlas_size : Int
    base_page_id : Int
    max_pages : Int
    generation : Int
    }

    TextRenderer::clear

    fn TextRenderer::clear(self : TextRenderer) -> Unit

    TextRenderer::ensure_glyph

    fn TextRenderer::ensure_glyph(self : TextRenderer, codepoint : Int, size_px : Double) -> (Int, GlyphCacheEntry)?

    Ensure a glyph is rasterized and cached. Returns the page index and cache entry. Used by GPU renderers to build draw commands.

    TextRenderer::flush_dirty_pages

    fn TextRenderer::flush_dirty_pages(self : TextRenderer) -> Array[AtlasPageInfo]

    TextRenderer::get_atlas

    fn TextRenderer::get_atlas(self : TextRenderer) -> GlyphAtlas

    TextRenderer::get_page

    fn TextRenderer::get_page(self : TextRenderer, index : Int) -> GlyphAtlas

    TextRenderer::get_page_id

    fn TextRenderer::get_page_id(self : TextRenderer) -> Int

    TextRenderer::get_page_ids

    fn TextRenderer::get_page_ids(self : TextRenderer) -> Array[Int]

    TextRenderer::glyph_count

    fn TextRenderer::glyph_count(self : TextRenderer) -> Int

    TextRenderer::load_cache

    fn TextRenderer::load_cache(self : TextRenderer, pages_data : Array[Array[Int]]) -> Int

    Restore atlas pages from serialized data. Returns number of pages loaded.

    TextRenderer::measure

    fn TextRenderer::measure(self : TextRenderer, text : String, size_px : Double) -> TextMetrics

    TextRenderer::new

    fn TextRenderer::new(font : TTFont, atlas_size : Int, base_page_id : Int, pipeline_id : Int) -> TextRenderer

    TextRenderer::new_with_options

    fn TextRenderer::new_with_options(font : TTFont, atlas_size : Int, base_page_id : Int, pipeline_id : Int, max_pages : Int) -> TextRenderer

    TextRenderer::page_count

    fn TextRenderer::page_count(self : TextRenderer) -> Int

    TextRenderer::serialize_cache

    fn TextRenderer::serialize_cache(self : TextRenderer) -> Array[Array[Int]]

    Serialize all pages of a TextRenderer.

    TextRun

    pub(all) struct TextRun {
    text : String
    style : TextStyle
    }

    impl Show for TextRun

    TextRun::output

    fn TextRun::output(self : TextRun, logger : &Logger) -> Unit

    TextRun::to_string

    fn TextRun::to_string(self : TextRun) -> String

    TextStyle

    pub(all) struct TextStyle {
    font : FontKey
    size_px : Double
    line_height : Double
    letter_spacing : Double
    }

    impl Show for TextStyle

    TextStyle::output

    fn TextStyle::output(self : TextStyle, logger : &Logger) -> Unit

    TextStyle::to_string

    fn TextStyle::to_string(self : TextStyle) -> String

    VarAxis

    pub(all) struct VarAxis {
    tag : String
    min_value : Double
    default_value : Double
    max_value : Double
    }

    VarRegion

    pub(all) struct VarRegion {
    axes : Array[VarRegionAxis]
    }

    VarRegionAxis

    pub(all) struct VarRegionAxis {
    start_coord : Double
    peak_coord : Double
    end_coord : Double
    }

    VerticalMetrics

    pub(all) struct VerticalMetrics {
    v_ascent : Int
    v_descent : Int
    advance_heights : Array[Int]
    top_side_bearings : Array[Int]
    }

    VorgData

    pub(all) struct VorgData {
    default_vert_origin_y : Int
    vert_origins : Map[Int, Int]
    }

    WidthCheck

    pub(all) struct WidthCheck {
    status : WidthStatus
    width : Double
    min_width : Double
    max_width : Double
    underflow_by : Double
    overflow_by : Double
    }

    WidthStatus

    pub(all) enum WidthStatus {
    Underflow
    Fits
    Overflow
    } derive(Eq,
    Debug
    )

    WidthStatus::equal

    fn WidthStatus::equal(WidthStatus, WidthStatus) -> Bool

    WidthStatus::not_equal

    fn WidthStatus::not_equal(x : WidthStatus, y : WidthStatus) -> Bool

    analyze_tables

    fn analyze_tables(data : Bytes) -> Array[TableInfo]?

    Analyze sfnt table directory without full parsing

    blit_to_atlas

    fn blit_to_atlas(atlas_pixels : Array[Int], atlas_width : Int, src : GlyphBitmap, dst_x : Int, dst_y : Int) -> Unit

    Copy a GlyphBitmap into a larger pixel array at the given position.

    bytes_slice

    fn bytes_slice(data : Bytes, offset : Int, length : Int) -> Bytes

    Copy a slice of Bytes

    calc_table_checksum

    fn calc_table_checksum(data : Bytes) -> Int

    Calculate table checksum per OpenType spec

    check_text_width

    fn check_text_width(width : Double, min_width : Double, max_width : Double, tolerance? : Double) -> WidthCheck raise WidthCheckError

    Inclusive width interval in pixels. Tolerance applies only to classification; deviations retain their original subpixel values. No wrapping is performed.

    compute_path_bbox

    fn compute_path_bbox(cmds : Array[
    PathCommand
    ]) -> GlyphBBox

    Compute bounding box from path commands. Calculates exact bounds including cubic/quadratic bezier curve extrema.

    compute_path_bounds

    fn compute_path_bounds(cmds : Array[
    PathCommand
    ]) -> TextRect?

    Tight path bounds in the path's coordinate system, without integer rounding. Returns None for an empty path.

    default_text_style

    fn default_text_style(family : String, size_px : Double) -> TextStyle

    detect_font_format

    fn detect_font_format(data : Bytes) -> FontFormat

    log2_floor

    fn log2_floor(n : Int) -> Int

    Floor of log2(n), minimum 0

    make_glyph_cache_key

    fn make_glyph_cache_key(glyph_id : Int, size_px : Double) -> Int

    Create a composite cache key from glyph_id and size_px. Quantizes size to 0.1px precision (supports sizes 0-4095px, glyph IDs 0-524287).

    new_text_run

    fn new_text_run(text : String, style : TextStyle) -> TextRun

    outline_bbox

    fn outline_bbox(commands : Array[
    PathCommand
    ]) -> (Double, Double, Double, Double)

    Compute bounding box from outline path commands.

    pad4

    fn pad4(n : Int) -> Int

    Round up to next multiple of 4

    parse_avar

    fn parse_avar(reader : BinaryReader, offset : Int, axis_count : Int) -> Array[Array[(Double, Double)]]

    Parse avar table: returns per-axis segment maps

    parse_cmap

    fn parse_cmap(reader : BinaryReader, offset : Int) -> Map[Int, Int]

    Parse cmap table: returns Map[codepoint -> glyph_id] Prefers Format 12 (full Unicode) over Format 4 (BMP only)

    parse_font

    fn parse_font(data : Bytes) -> TTFont?

    Parse a font from raw bytes, auto-detecting format (TTF, OTF, or WOFF1)

    parse_font_at

    fn parse_font_at(data : Bytes, index : Int) -> TTFont?

    Parse a single font from a TTC/OTC collection by index

    parse_font_collection

    fn parse_font_collection(data : Bytes) -> Array[TTFont]?

    Parse a TTC/OTC font collection, returning all fonts

    parse_fvar

    fn parse_fvar(reader : BinaryReader, offset : Int) -> Array[VarAxis]

    Parse fvar table: returns array of variation axes

    parse_gasp

    fn parse_gasp(reader : BinaryReader, offset : Int) -> Array[GaspRange]

    Parse gasp table: returns array of GaspRange records

    parse_head

    fn parse_head(reader : BinaryReader, offset : Int) -> (Int, Int)

    Parse head table: returns (units_per_em, index_to_loc_format)

    parse_hhea

    fn parse_hhea(reader : BinaryReader, offset : Int) -> (Int, Int, Int, Int)

    Parse hhea table: returns (ascent, descent, line_gap, num_h_metrics)

    parse_hmtx

    fn parse_hmtx(reader : BinaryReader, offset : Int, num_glyphs : Int, num_h_metrics : Int) -> (Array[Int], Array[Int])

    Parse hmtx table: returns (advance_widths, left_side_bearings)

    parse_kern

    fn parse_kern(reader : BinaryReader, offset : Int) -> Map[Int, Int]

    Parse kern table (format 0, horizontal only): returns Map[key -> value] Key = (left_gid << 16) | right_gid

    parse_loca

    fn parse_loca(reader : BinaryReader, offset : Int, num_glyphs : Int, format : Int) -> Array[Int]

    Parse loca table: returns array of glyph offsets into glyf table

    parse_maxp

    fn parse_maxp(reader : BinaryReader, offset : Int) -> Int

    Parse maxp table: returns num_glyphs

    parse_name

    fn parse_name(reader : BinaryReader, offset : Int) -> Map[Int, String]

    Parse name table: returns Map[nameID -> string] Prefers platformID=3/encodingID=1 (Windows Unicode BMP, UTF-16BE)

    parse_os2

    fn parse_os2(reader : BinaryReader, offset : Int) -> OS2Table

    Parse OS/2 table

    parse_post

    fn parse_post(reader : BinaryReader, offset : Int, num_glyphs : Int) -> PostTable

    Parse post table

    parse_ttf

    fn parse_ttf(data : Bytes) -> TTFont?

    Parse a TrueType/OpenType font from raw bytes

    parse_vhea

    fn parse_vhea(reader : BinaryReader, offset : Int) -> (Int, Int, Int)

    Parse vhea table: returns (v_ascent, v_descent, num_v_metrics)

    parse_vmtx

    fn parse_vmtx(reader : BinaryReader, offset : Int, num_glyphs : Int, num_v_metrics : Int) -> (Array[Int], Array[Int])

    Parse vmtx table: returns (advance_heights, top_side_bearings)

    parse_vorg

    fn parse_vorg(reader : BinaryReader, offset : Int) -> VorgData

    Parse VORG table: returns VorgData with default origin and per-glyph overrides

    parse_woff1

    fn parse_woff1(data : Bytes) -> TTFont?

    Parse a WOFF1 font, decompress tables, rebuild sfnt, and delegate to parse_ttf

    parse_woff2

    fn parse_woff2(data : Bytes) -> TTFont?

    Parse a WOFF2 font, decompress tables, reconstruct transforms, and delegate to parse_ttf

    path_commands_to_svg_d

    fn path_commands_to_svg_d(commands : Array[
    PathCommand
    ]) -> String

    Convert font outline PathCommands to SVG path d attribute string.

    rasterize_glyph

    fn rasterize_glyph(font : TTFont, codepoint : Int, size_px : Double) -> GlyphBitmap?

    Rasterize a glyph outline to a pixel bitmap using SVG rendering. Returns None for glyphs with no outline (e.g., space).

    rebuild_sfnt

    fn rebuild_sfnt(flavor : Int, tables : Array[(String, Bytes)]) -> Bytes

    Rebuild an sfnt (TrueType/OpenType) binary from individual table data. Produces: Offset Table (12 bytes) + Table Records (16 bytes x n) + Table Data (4-byte aligned)

    scale_commands

    fn scale_commands(commands : Array[
    PathCommand
    ], scale : Double, flip_y : Bool) -> Array[
    PathCommand
    ]

    subset_font

    fn subset_font(data : Bytes, codepoints : Array[Int]) -> Bytes?

    Subset a TrueType font to include only the given codepoints. Returns a new sfnt binary containing only the required glyphs. Only supports glyf-based TrueType fonts (not CFF/CFF2).

    write_be16

    fn write_be16(out : Array[Byte], offset : Int, value : Int) -> Unit

    Write a big-endian uint16 into Array[Byte]

    write_be32

    fn write_be32(out : Array[Byte], offset : Int, value : Int) -> Unit

    Write a big-endian uint32 into Array[Byte]