Log inGet started

font

Updated October 20, 2026

Font primitive — parse a font file ONCE into a baked, vectorized glyph format (ZFNT), then drive every text surface from it. A font is a general CPU resource addressed by name (the CPU counterpart of a renderer GPU resource), so a single registration is usable from UI text, 2D text, and true 3D glyph geometry.Authored fonts are .font assets whose onRegister hook calls font.register.

require("@builtin::modules.api.engine.font") -- font (also available as global 'font')

The font namespace: 36 functions.

globals/font/coverage

font.coverage(bytes: buffer | string) -> string?

The codepoints a font file maps to a glyph, as the coverage text font.declare reads: comma-separated inclusive hexadecimal ranges.

Parameters

  • bytes buffer | string — Raw font-file bytes (TTF / OTF).

Returns string? — Coverage text, or nil if the bytes don't parse as a font.

local cov = font.coverage(assert(vfs.read("/zero/source/fonts/MyCJK.otf")))

globals/font/declare

font.declare(name: string, coverage: string, opts: table) -> number

Declare an on-demand face: a face known by the codepoints it covers, whose bytes are read only when a text surface first meets one of those codepoints with no installed face that draws it. Until then the face costs its coverage and nothing else, so a session that never shows its script never reads its file. When a label, a text.* object, or font.prefetch reaches a covered character, the engine requests the face: its file is read and registered with font.register(name, bytes, opts), and every text surface draws the character from it from then on. A .font asset whose face.json lists onDemand chunks declares each of them through this call.

Parameters

  • name string — The name the face is declared and registered under.
  • coverage string — The codepoints it covers: comma-separated inclusive hexadecimal ranges ("3040-30ff,4e00"), the text font.coverage(bytes) writes.
  • opts table — { file: string, family: string?, role: string?, aliases: {string}? } naming the VFS path of the font file (TTF / OTF, or a baked ZFNT) and the font.register options it installs with.

Returns number — How many codepoints the coverage holds.

font.declare("MyCJK", assert(font.coverage(assert(vfs.read("/zero/source/fonts/MyCJK.otf")))), { file = "/zero/source/fonts/MyCJK.otf" })

globals/font/glyph

font.glyph(name: string, codepoint: number) -> any

Read one glyph's vectorized outline from a registered font, in font units (resolution-independent — scale by fontSize / unitsPerEm).

Parameters

  • name string — Registered family name.
  • codepoint number — Unicode codepoint (e.g. string.byte("A")).

Returns any — { advance, unitsPerEm, bbox = {xMin,yMin,xMax,yMax}, contours } where each contour is { start = {x,y}, segments = { {kind="line|quad|cubic", ...} } }, or nil if the font isn't registered.

local g = font.glyph("Inter", string.byte("A"))

globals/font/list

font.list() -> { string }

List every registered font family name.

Returns { string } — Array of family-name strings.

for _, fam in font.list() do print(fam) end

globals/font/observe

font.observe() -> { any }

What the text system is holding for fonts: one row per family the shaper can resolve, with its face count, the numeric weights those faces carry, whether any of them is slanted, and whether the family arrived through a registration rather than from the platform. weights is what a style's weight can name for that family. The same rows are fonts in text.observe().

Returns { any } — Array of { family, faces, weights, italic, loaded }.

for _, f in ipairs(font.observe()) do print(f.family, #f.weights) end

globals/font/onDemand

font.onDemand() -> { any }

Every on-demand face font.declare declared, in declaration order, and where each stands: declared (its file never read), requested (a character it covers reached a text surface that could not draw it, and its file is being read), installed (registered on every text surface; bytes is the size of the file it installed from) or failed (error says why). requestedBy is the character whose miss requested it, and family the family group it installs into (opts.family of its font.declare, else its own name); ui.listFonts() lists each face under that family's row.

Returns { any } — Array of { name, family, state, codepoints, requestedBy, bytes, error }.

for _, f in ipairs(font.onDemand()) do print(f.name, f.state, f.requestedBy) end

globals/font/parse

font.parse(bytes: buffer | string) -> string?

Parse a font file (TTF / OTF raw bytes) ONCE into the baked, vectorized glyph format (ZFNT): per-glyph vector outlines + metrics + character map, plus the original bytes. Heavy — run at import time (the .font assetType's onCreate / the font importer), then store the result as the asset payload. font.register loads it cheaply.

Parameters

  • bytes buffer | string — Raw font-file bytes (binary-safe) — TTF / OTF.

Returns string? — Baked ZFNT payload (binary-safe string), or nil if the bytes don't parse as a font.

local zfnt = font.parse(assert(vfs.read("/zero/source/Inter.ttf")))

globals/font/prefetch

font.prefetch(text: string) -> number

Request, ahead of drawing it, every on-demand face covering a character of text that no requested or installed face covers: a screen about to show a script starts reading its faces before its first frame.

Parameters

  • text string — Text whose characters the faces must cover.

Returns number — How many faces this call requested.

font.prefetch("日本語")

globals/font/ready

font.ready(text: string) -> boolean

Whether text draws without waiting on an on-demand face: no character of it is covered by a face still declared or requested. Call font.prefetch(text) first, then open the screen once this answers true, and its first frame draws every glyph. True as well for a character no declared face covers (ui.missingGlyphs() names it once drawn) and for a face that failed (font.onDemand() says why).

Parameters

  • text string — Text to be drawn.

Returns boolean — Whether every on-demand face the text needs is installed.

font.prefetch(title); task.spawn(function() while not font.ready(title) do task.wait() end; ui.showScreen("title") end)

globals/font/reconcile

font.reconcile() -> { any }

Every family the text shaper can resolve, held against what the shaper does with it. family is the name, faces how many faces of it the font database holds, weights the numeric weights those faces carry, loaded whether it arrived through a registration rather than from the platform, registered whether content registered the name, selectable whether some style naming the family reaches it, matched whether fontFamily = family on its own reaches it — the family name at the default weight over Latin text — weight the weight it needs when the default is not it, shapedWith the family the name selects, spelled the way the font file spells it, which for a registration name is the family declared inside the file rather than the name the asset carries, and reason why when the face that answered is not that family. A family is probed at its own weights and over content from several scripts, so a family reachable only at one weight or covering only one script is reported selectable, with matched false and weight naming what the style must carry. Every probe object is destroyed again, so the live text-object count is where it was.

Returns { any } — Array of { family, faces, weights, loaded, registered, selectable, matched, weight, shapedWith, reason }.

for _, f in ipairs(font.reconcile()) do if f.selectable and not f.matched then print(f.family, f.weight) end end

globals/font/register

font.register(name: string, zfnt: string, opts: table?) -> any

Register a baked font (ZFNT from font.parse) under name, making it usable on every text surface via fontFamily = "<name>". Loads the vectorized glyph data into the runtime store (for font.glyph / font.textMesh) and feeds the embedded face to the 2D text and egui UI systems. Raw font bytes (TTF / OTF) register too: their tables are read and the glyph outlines are baked the first time font.glyph / font.textMesh reads one. A name declared with font.declare is installed by this call. Registering a name again replaces what the earlier call installed under it: a name, group or slot the earlier call gave the face and this one does not stops selecting it, and text laid out under a name whose face changed is laid out again. opts groups several weight/style faces under one CSS family and maps web-font names onto it: opts.family is the shared group key, opts.role is "regular" | "bold" | "italic" | "bolditalic", and opts.aliases is a list of extra selectable names (web fonts + CSS generics like "Arial", "sans-serif") that resolve to this group, matched case-insensitively. With a group set, font-weight / font-style on a font-family pick the real metric-compatible face instead of a synthesized one.

Parameters

  • name string — Family name to register under.
  • zfnt string — Baked ZFNT payload from font.parse (binary-safe string).
  • opts table (optional) — { family: string?, role: string?, aliases: {string}? } — group key, weight/style role, and case-insensitive selectable aliases.

Returns any — { family, faces, glyphCount } on success, or nil on failure.

local info = font.register("Inter", assert(font.parse(assert(vfs.read("/zero/source/Inter.ttf")))))

globals/font/textMesh

font.textMesh(name: string, text: string, opts: table?) -> any

Tessellate a string into renderable mesh geometry from a registered font's glyph outlines — true 3D text, laid out left-to-right by advance (newlines drop a line). Hand the result to renderer.mesh.create() (GPU) or asset.create("mesh") (persistable).

Parameters

  • name string — Registered family name.
  • text string — String to lay out.
  • opts table (optional) — { size?=1, depth?=0 (extrude, EM units), tolerance?=0.0015, letterSpacing?=0, lineHeight?=0 }.

Returns any — { positions, indices, normals, uvs } as flat float / u32 arrays, or nil if the font isn't registered or the string is all whitespace.

local geom = font.textMesh("Inter", "Hello", { size = 1, depth = 0.1 })

modules/font/coverage

coverage(bytes: buffer | string): string?

The codepoints a font file maps to a glyph, as the coverage text font.declare reads: comma-separated inclusive hexadecimal ranges.

Parameters

  • bytes buffer | string — Raw font-file bytes (TTF / OTF).
local cov = font.coverage(assert(vfs.read("/zero/source/fonts/MyCJK.otf")))

modules/font/declare

declare(name: string, coverage: string, opts: table): number

Declare an on-demand face: a face known by the codepoints it covers, whose bytes are read only when a text surface first meets one of those codepoints with no installed face that draws it. Until then the face costs its coverage and nothing else, so a session that never shows its script never reads its file. When a label, a text.* object, or font.prefetch reaches a covered character, the engine requests the face: its file is read and registered with font.register(name, bytes, opts), and every text surface draws the character from it from then on. A .font asset whose face.json lists onDemand chunks declares each of them through this call.

Parameters

  • name string — The name the face is declared and registered under.
  • coverage string — The codepoints it covers: comma-separated inclusive hexadecimal ranges ("3040-30ff,4e00"), the text font.coverage(bytes) writes.
  • opts table — { file: string, family: string?, role: string?, aliases: {string}? } naming the VFS path of the font file (TTF / OTF, or a baked ZFNT) and the font.register options it installs with.
font.declare("MyCJK", assert(font.coverage(assert(vfs.read("/zero/source/fonts/MyCJK.otf")))), { file = "/zero/source/fonts/MyCJK.otf" })

modules/font/glyph

glyph(name: string, codepoint: number): any

Read one glyph's vectorized outline from a registered font, in font units (resolution-independent — scale by fontSize / unitsPerEm).

Parameters

  • name string — Registered family name.
  • codepoint number — Unicode codepoint (e.g. string.byte("A")).
local g = font.glyph("Inter", string.byte("A"))

modules/font/list

list(): { string }

List every registered font family name.

for _, fam in font.list() do print(fam) end

modules/font/observe

observe(): { any }

What the text system is holding for fonts: one row per family the shaper can resolve, with its face count, the numeric weights those faces carry, whether any of them is slanted, and whether the family arrived through a registration rather than from the platform. weights is what a style's weight can name for that family. The same rows are fonts in text.observe().

for _, f in ipairs(font.observe()) do print(f.family, #f.weights) end

modules/font/onDemand

onDemand(): { any }

Every on-demand face font.declare declared, in declaration order, and where each stands: declared (its file never read), requested (a character it covers reached a text surface that could not draw it, and its file is being read), installed (registered on every text surface; bytes is the size of the file it installed from) or failed (error says why). requestedBy is the character whose miss requested it, and family the family group it installs into (opts.family of its font.declare, else its own name); ui.listFonts() lists each face under that family's row.

for _, f in ipairs(font.onDemand()) do print(f.name, f.state, f.requestedBy) end

modules/font/parse

parse(bytes: buffer | string): string?

Parse a font file (TTF / OTF raw bytes) ONCE into the baked, vectorized glyph format (ZFNT): per-glyph vector outlines + metrics + character map, plus the original bytes. Heavy — run at import time (the .font assetType's onCreate / the font importer), then store the result as the asset payload. font.register loads it cheaply.

Parameters

  • bytes buffer | string — Raw font-file bytes (binary-safe) — TTF / OTF.
local zfnt = font.parse(assert(vfs.read("/zero/source/Inter.ttf")))

modules/font/prefetch

prefetch(text: string): number

Request, ahead of drawing it, every on-demand face covering a character of text that no requested or installed face covers: a screen about to show a script starts reading its faces before its first frame.

Parameters

  • text string — Text whose characters the faces must cover.
font.prefetch("日本語")

modules/font/ready

ready(text: string): boolean

Whether text draws without waiting on an on-demand face: no character of it is covered by a face still declared or requested. Call font.prefetch(text) first, then open the screen once this answers true, and its first frame draws every glyph. True as well for a character no declared face covers (ui.missingGlyphs() names it once drawn) and for a face that failed (font.onDemand() says why).

Parameters

  • text string — Text to be drawn.
font.prefetch(title); task.spawn(function() while not font.ready(title) do task.wait() end; ui.showScreen("title") end)

modules/font/reconcile

reconcile(): { any }

Every family the text shaper can resolve, held against what the shaper does with it. family is the name, faces how many faces of it the font database holds, weights the numeric weights those faces carry, loaded whether it arrived through a registration rather than from the platform, registered whether content registered the name, selectable whether some style naming the family reaches it, matched whether fontFamily = family on its own reaches it — the family name at the default weight over Latin text — weight the weight it needs when the default is not it, shapedWith the family the name selects, spelled the way the font file spells it, which for a registration name is the family declared inside the file rather than the name the asset carries, and reason why when the face that answered is not that family. A family is probed at its own weights and over content from several scripts, so a family reachable only at one weight or covering only one script is reported selectable, with matched false and weight naming what the style must carry. Every probe object is destroyed again, so the live text-object count is where it was.

for _, f in ipairs(font.reconcile()) do if f.selectable and not f.matched then print(f.family, f.weight) end end

modules/font/register

register(name: string, zfnt: string, opts: table?): any

Register a baked font (ZFNT from font.parse) under name, making it usable on every text surface via fontFamily = "<name>". Loads the vectorized glyph data into the runtime store (for font.glyph / font.textMesh) and feeds the embedded face to the 2D text and egui UI systems. Raw font bytes (TTF / OTF) register too: their tables are read and the glyph outlines are baked the first time font.glyph / font.textMesh reads one. A name declared with font.declare is installed by this call. Registering a name again replaces what the earlier call installed under it: a name, group or slot the earlier call gave the face and this one does not stops selecting it, and text laid out under a name whose face changed is laid out again. opts groups several weight/style faces under one CSS family and maps web-font names onto it: opts.family is the shared group key, opts.role is "regular" | "bold" | "italic" | "bolditalic", and opts.aliases is a list of extra selectable names (web fonts + CSS generics like "Arial", "sans-serif") that resolve to this group, matched case-insensitively. With a group set, font-weight / font-style on a font-family pick the real metric-compatible face instead of a synthesized one.

Parameters

  • name string — Family name to register under.
  • zfnt string — Baked ZFNT payload from font.parse (binary-safe string).
  • opts table? (optional) — { family: string?, role: string?, aliases: {string}? } — group key, weight/style role, and case-insensitive selectable aliases.
local info = font.register("Inter", assert(font.parse(assert(vfs.read("/zero/source/Inter.ttf")))))

modules/font/textMesh

textMesh(name: string, text: string, opts: table?): any

Tessellate a string into renderable mesh geometry from a registered font's glyph outlines — true 3D text, laid out left-to-right by advance (newlines drop a line). Hand the result to renderer.mesh.create() (GPU) or asset.create("mesh") (persistable).

Parameters

  • name string — Registered family name.
  • text string — String to lay out.
  • opts table? (optional) — { size?=1, depth?=0 (extrude, EM units), tolerance?=0.0015, letterSpacing?=0, lineHeight?=0 }.
local geom = font.textMesh("Inter", "Hello", { size = 1, depth = 0.1 })

typed/builtin//modules/api/engine/font/font/coverage

font.coverage(bytes: buffer | string) -> string?

The codepoints a font file maps to a glyph, as the coverage text font.declare reads: comma-separated inclusive hexadecimal ranges.

Parameters

  • bytes buffer | string — Raw font-file bytes (TTF / OTF).

Returns string? — Coverage text, or nil if the bytes don't parse as a font.

local cov = font.coverage(assert(vfs.read("/zero/source/fonts/MyCJK.otf")))

typed/builtin//modules/api/engine/font/font/declare

font.declare(name: string, coverage: string, opts: table) -> number

Declare an on-demand face: a face known by the codepoints it covers, whose bytes are read only when a text surface first meets one of those codepoints with no installed face that draws it. Until then the face costs its coverage and nothing else, so a session that never shows its script never reads its file. When a label, a text.* object, or font.prefetch reaches a covered character, the engine requests the face: its file is read and registered with font.register(name, bytes, opts), and every text surface draws the character from it from then on. A .font asset whose face.json lists onDemand chunks declares each of them through this call.

Parameters

  • name string — The name the face is declared and registered under.
  • coverage string — The codepoints it covers: comma-separated inclusive hexadecimal ranges ("3040-30ff,4e00"), the text font.coverage(bytes) writes.
  • opts table — { file: string, family: string?, role: string?, aliases: {string}? } naming the VFS path of the font file (TTF / OTF, or a baked ZFNT) and the font.register options it installs with.

Returns number — How many codepoints the coverage holds.

font.declare("MyCJK", assert(font.coverage(assert(vfs.read("/zero/source/fonts/MyCJK.otf")))), { file = "/zero/source/fonts/MyCJK.otf" })

typed/builtin//modules/api/engine/font/font/glyph

font.glyph(name: string, codepoint: number) -> any

Read one glyph's vectorized outline from a registered font, in font units (resolution-independent — scale by fontSize / unitsPerEm).

Parameters

  • name string — Registered family name.
  • codepoint number — Unicode codepoint (e.g. string.byte("A")).

Returns any — { advance, unitsPerEm, bbox = {xMin,yMin,xMax,yMax}, contours } where each contour is { start = {x,y}, segments = { {kind="line|quad|cubic", ...} } }, or nil if the font isn't registered.

local g = font.glyph("Inter", string.byte("A"))

typed/builtin//modules/api/engine/font/font/list

font.list() -> { string }

List every registered font family name.

Returns { string } — Array of family-name strings.

for _, fam in font.list() do print(fam) end

typed/builtin//modules/api/engine/font/font/observe

font.observe() -> { any }

What the text system is holding for fonts: one row per family the shaper can resolve, with its face count, the numeric weights those faces carry, whether any of them is slanted, and whether the family arrived through a registration rather than from the platform. weights is what a style's weight can name for that family. The same rows are fonts in text.observe().

Returns { any } — Array of { family, faces, weights, italic, loaded }.

for _, f in ipairs(font.observe()) do print(f.family, #f.weights) end

typed/builtin//modules/api/engine/font/font/onDemand

font.onDemand() -> { any }

Every on-demand face font.declare declared, in declaration order, and where each stands: declared (its file never read), requested (a character it covers reached a text surface that could not draw it, and its file is being read), installed (registered on every text surface; bytes is the size of the file it installed from) or failed (error says why). requestedBy is the character whose miss requested it, and family the family group it installs into (opts.family of its font.declare, else its own name); ui.listFonts() lists each face under that family's row.

Returns { any } — Array of { name, family, state, codepoints, requestedBy, bytes, error }.

for _, f in ipairs(font.onDemand()) do print(f.name, f.state, f.requestedBy) end

typed/builtin//modules/api/engine/font/font/parse

font.parse(bytes: buffer | string) -> string?

Parse a font file (TTF / OTF raw bytes) ONCE into the baked, vectorized glyph format (ZFNT): per-glyph vector outlines + metrics + character map, plus the original bytes. Heavy — run at import time (the .font assetType's onCreate / the font importer), then store the result as the asset payload. font.register loads it cheaply.

Parameters

  • bytes buffer | string — Raw font-file bytes (binary-safe) — TTF / OTF.

Returns string? — Baked ZFNT payload (binary-safe string), or nil if the bytes don't parse as a font.

local zfnt = font.parse(assert(vfs.read("/zero/source/Inter.ttf")))

typed/builtin//modules/api/engine/font/font/prefetch

font.prefetch(text: string) -> number

Request, ahead of drawing it, every on-demand face covering a character of text that no requested or installed face covers: a screen about to show a script starts reading its faces before its first frame.

Parameters

  • text string — Text whose characters the faces must cover.

Returns number — How many faces this call requested.

font.prefetch("日本語")

typed/builtin//modules/api/engine/font/font/ready

font.ready(text: string) -> boolean

Whether text draws without waiting on an on-demand face: no character of it is covered by a face still declared or requested. Call font.prefetch(text) first, then open the screen once this answers true, and its first frame draws every glyph. True as well for a character no declared face covers (ui.missingGlyphs() names it once drawn) and for a face that failed (font.onDemand() says why).

Parameters

  • text string — Text to be drawn.

Returns boolean — Whether every on-demand face the text needs is installed.

font.prefetch(title); task.spawn(function() while not font.ready(title) do task.wait() end; ui.showScreen("title") end)

typed/builtin//modules/api/engine/font/font/reconcile

font.reconcile() -> { any }

Every family the text shaper can resolve, held against what the shaper does with it. family is the name, faces how many faces of it the font database holds, weights the numeric weights those faces carry, loaded whether it arrived through a registration rather than from the platform, registered whether content registered the name, selectable whether some style naming the family reaches it, matched whether fontFamily = family on its own reaches it — the family name at the default weight over Latin text — weight the weight it needs when the default is not it, shapedWith the family the name selects, spelled the way the font file spells it, which for a registration name is the family declared inside the file rather than the name the asset carries, and reason why when the face that answered is not that family. A family is probed at its own weights and over content from several scripts, so a family reachable only at one weight or covering only one script is reported selectable, with matched false and weight naming what the style must carry. Every probe object is destroyed again, so the live text-object count is where it was.

Returns { any } — Array of { family, faces, weights, loaded, registered, selectable, matched, weight, shapedWith, reason }.

for _, f in ipairs(font.reconcile()) do if f.selectable and not f.matched then print(f.family, f.weight) end end

typed/builtin//modules/api/engine/font/font/register

font.register(name: string, zfnt: string, opts: table?) -> any

Register a baked font (ZFNT from font.parse) under name, making it usable on every text surface via fontFamily = "<name>". Loads the vectorized glyph data into the runtime store (for font.glyph / font.textMesh) and feeds the embedded face to the 2D text and egui UI systems. Raw font bytes (TTF / OTF) register too: their tables are read and the glyph outlines are baked the first time font.glyph / font.textMesh reads one. A name declared with font.declare is installed by this call. Registering a name again replaces what the earlier call installed under it: a name, group or slot the earlier call gave the face and this one does not stops selecting it, and text laid out under a name whose face changed is laid out again. opts groups several weight/style faces under one CSS family and maps web-font names onto it: opts.family is the shared group key, opts.role is "regular" | "bold" | "italic" | "bolditalic", and opts.aliases is a list of extra selectable names (web fonts + CSS generics like "Arial", "sans-serif") that resolve to this group, matched case-insensitively. With a group set, font-weight / font-style on a font-family pick the real metric-compatible face instead of a synthesized one.

Parameters

  • name string — Family name to register under.
  • zfnt string — Baked ZFNT payload from font.parse (binary-safe string).
  • opts table (optional) — { family: string?, role: string?, aliases: {string}? } — group key, weight/style role, and case-insensitive selectable aliases.

Returns any — { family, faces, glyphCount } on success, or nil on failure.

local info = font.register("Inter", assert(font.parse(assert(vfs.read("/zero/source/Inter.ttf")))))

typed/builtin//modules/api/engine/font/font/textMesh

font.textMesh(name: string, text: string, opts: table?) -> any

Tessellate a string into renderable mesh geometry from a registered font's glyph outlines — true 3D text, laid out left-to-right by advance (newlines drop a line). Hand the result to renderer.mesh.create() (GPU) or asset.create("mesh") (persistable).

Parameters

  • name string — Registered family name.
  • text string — String to lay out.
  • opts table (optional) — { size?=1, depth?=0 (extrude, EM units), tolerance?=0.0015, letterSpacing?=0, lineHeight?=0 }.

Returns any — { positions, indices, normals, uvs } as flat float / u32 arrays, or nil if the font isn't registered or the string is all whitespace.

local geom = font.textMesh("Inter", "Hello", { size = 1, depth = 0.1 })
  • api
  • reference