@create-font/target
v0.0.2
Published
A rigorously validated logical SFNT compilation target for OpenType variable TrueType fonts
Downloads
1,014
Readme
@create-font/target
@create-font/target is a rigorously validated, low-level TypeScript
representation of an OpenType variable font. It includes both the logical SFNT
boundary after design-space compilation and the deterministic target-v1 binary
serializer.
This library is the low-level compilation target of the public create-font npm
toolchain. The unscoped package now provides the preliminary
package-manager-resolved CLI and Elysia/Eden server boundary while depending on
the workspace's implementation layers. The complete build pipeline feeds this
validated target as an independently usable API. Project source and bundled
editor assets remain the responsibility of higher layers; see
the repository
architecture and
roadmap.
Version 1 models the meaning needed to produce a TrueType-flavored variable
font, not the byte layout of its tables. Glyph IDs, default outlines, metrics,
axes, normalized variation regions, complete deltas, names, and character
mappings are already resolved. The binary serializer only packs the selected
records, assigns offsets and name IDs, and calculates checksums.
createLoweringPlan already derives the shared head, hhea, maxp, glyph,
table-directory, canonical encoding, and exact table-length facts needed by
serializeVariableFont consumes those facts and emits the final Uint8Array.
Version 1 profile
The v1 schema is deliberately closed. Unknown properties are ingestion errors, and the profile supports only:
- one variable-font resource with at least one axis;
- unhinted, simple quadratic TrueType glyphs, including empty glyphs;
- a fixed glyph order with glyph 0 named
.notdef; - default-instance
glyfoutlines andhmtxmetrics; gvartuple regions with complete deltas for every contour point, plus four explicit phantom-point deltas;- optional per-axis
avarmaps and named instances; - a non-empty Unicode character map.
Complete point coverage is intentional: sparse gvar data and IUP inference
are serialization optimizations, not part of the v1 logical contract. Composite
glyphs, instructions, and component-relative variation are also outside this
version. A future serializer may omit point deltas only after proving that IUP
inference reproduces the complete logical deltas exactly.
Glyph names are unique logical identifiers for compilation and diagnostics.
They deliberately disappear when v1 lowers to a format-3.0 post table, which
has no glyph-name array; they are not constrained as PostScript glyph names.
Source values and validated fonts
Authoring code uses VariableFontSource and the other *Source interfaces.
Their fields are ordinary JavaScript strings, numbers, booleans, arrays, and
optional bigint timestamps, so they are convenient to construct from decoded
data. Ingestion accepts inert own-property data objects and dense ordinary
arrays; inherited fields, accessors, array subclasses, and extra properties are
rejected rather than observed as schema data.
ingestVariableFont(value: unknown) is the only supported route to a
VariableFont. It validates and clones the source, converts coordinate records
to axis-ordered tuples, supplies optional defaults, sorts the character map,
returns branded scalar-domain types, and deeply freezes the entire result. The
validated font type carries a private nominal compile-time proof, and lowering
and evaluation APIs verify the originating object in a runtime proof set. An
object spread or type assertion therefore cannot turn edited data back into a
validated font.
Ingestion does not throw for validation failures. It returns:
{ ok: true, value, warnings }with a branded, deeply frozenVariableFont; or{ ok: false, errors, warnings }, whereerrorsis statically non-empty.
Every diagnostic has a stable code, JSONPath-like path, human-readable
message, severity, and associated OpenType table. Diagnostics are sorted
deterministically. Warnings do not prevent a successful result.
Geometric O example
This minimal source uses the razor design as the default wght instance and a
single normalized tuple for the Black extreme. Both visible glyphs are the same
geometric O; the Black delta closes most of the counter without changing its
topology.
import {
CREATE_FONT_FORMAT,
CREATE_FONT_IR_VERSION,
createLoweringPlan,
ingestVariableFont,
type SimpleGlyphSource,
type VariableFontSource,
} from "@create-font/target"
const contours = [
[
{ x: 100, y: 0, onCurve: true },
{ x: 100, y: 700, onCurve: true },
{ x: 700, y: 700, onCurve: true },
{ x: 700, y: 0, onCurve: true },
],
[
{ x: 120, y: 20, onCurve: true },
{ x: 680, y: 20, onCurve: true },
{ x: 680, y: 680, onCurve: true },
{ x: 120, y: 680, onCurve: true },
],
] as const
const blackVariation = {
region: { peak: { wght: 1 } },
deltas: {
points: [
{ x: 0, y: 0 },
{ x: 0, y: 0 },
{ x: 0, y: 0 },
{ x: 0, y: 0 },
{ x: 180, y: 280 },
{ x: -180, y: 280 },
{ x: -180, y: -280 },
{ x: 180, y: -280 },
],
phantom: { left: 0, right: 0, top: 0, bottom: 0 },
},
} as const
const geometricO = (name: string): SimpleGlyphSource => ({
kind: "simple",
name,
advanceWidth: 800,
leftSideBearing: 100,
contours,
variations: [blackVariation],
})
const source = {
format: CREATE_FONT_FORMAT,
irVersion: CREATE_FONT_IR_VERSION,
metadata: {
unitsPerEm: 1024,
fontRevision: 1,
vendorId: "CRFT",
lowestPpem: 9,
},
names: {
family: "Create Font O",
subfamily: "Razor",
uniqueId: "Create Font O Razor 1.000",
fullName: "Create Font O Razor",
version: "Version 1.000",
postScriptName: "CreateFontO-Razor",
typographicFamily: "Create Font O",
typographicSubfamily: "Razor",
},
metrics: {
ascender: 800,
descender: -224,
lineGap: 0,
winAscent: 800,
winDescent: 224,
xHeight: 512,
capHeight: 700,
underlinePosition: -100,
underlineThickness: 50,
},
style: {
weightClass: 100,
widthClass: 5,
italic: false,
bold: false,
oblique: false,
italicAngle: 0,
},
axes: [
{
tag: "wght",
name: "Weight",
min: 100,
default: 100,
max: 900,
},
],
instances: [
{
name: "Black",
coordinates: { wght: 900 },
postScriptName: "CreateFontO-Black",
},
],
glyphs: [geometricO(".notdef"), geometricO("O")],
cmap: [{ codePoint: 0x4f, glyph: 1 }],
} satisfies VariableFontSource
const result = ingestVariableFont(source)
if (!result.ok) {
throw new Error(
result.errors
.map((diagnostic) => `${diagnostic.path}: ${diagnostic.message}`)
.join("\n"),
)
}
const font = result.value // VariableFont, branded and deeply frozen
const plan = createLoweringPlan(font) // deterministic derived SFNT factsOmit an axis map for identity normalization. When supplied, it is expressed
in exact normalized F2Dot14 values and must include -1 -> -1, 0 -> 0, and
1 -> 1.
Enforced invariants
Ingestion currently enforces the following major constraints:
- Shape: every parsed object has an exact property set; scalar values have the required runtime type, integer range, or exact Fixed16.16/F2Dot14 representation.
- Axes: tags are valid and unique; registered axes stay within registered
ranges;
min < max; defaults and instance coordinates are in range; every coordinate record has exactly the declared axes; named locations are unique. avar: source coordinates are strictly increasing, target coordinates are nondecreasing, and the three required anchors are present.- Regions: normalized tuples contain every axis, peaks are nonzero, and
intermediate supports satisfy
start <= peak <= endwithout crossing zero around a nonzero peak. - Glyphs: names are unique,
.notdefis first, contours are non-empty, points remain within the TrueType grid from -16,384 through 16,383 FUnits, relativeglyfcoordinate deltas fit signed 16-bit fields, and glyph/table counts fit their OpenType fields. - Metrics: advances fit unsigned 16-bit fields; an empty glyph has zero LSB;
otherwise LSB equals
xMin, as required for variable TrueType fonts. Derivedhheaextrema andOS/2.xAvgCharWidthmust be representable, and Windows ascent/descent must cover the supplied vertical variation. One-axis bounds are evaluated exactly at every piecewise support boundary; multi-axis bounds use a conservative closed-profile proof and can reject a font whose regions are safe only because some multi-axis supports are mutually exclusive. gvar: a glyph has at most 4,095 tuples; each tuple has exactly one delta per flattened contour point and all four phantom deltas; packed-header limits remain encodable.- Style and names: required names are non-empty and fit name-table storage;
version and PostScript names have valid syntax;
wght,wdth,slnt, anditaldefaults agree with the correspondingOS/2/postvalues and flags. cmap: at least one unique Unicode scalar maps to an in-range glyph ID; surrogate values are rejected and successful maps are sorted by code point. BMP-only maps must fit the selected Windows format-4 packing; maps containing supplementary characters use format 12. A BMP-only U+FFFF mapping is rejected because that code point is the required format-4 terminal segment.
Recommendations are warnings: a power-of-two unitsPerEm, a visible .notdef
outline, default-location instance names that reuse the font's default names,
and avoiding simultaneous ital and slnt axes unless both are truly needed.
Binary serialization
OpenType requires eight tables in any functional outline font and adds TrueType- and variation-specific requirements. A v1 font lowers to these 13 required tables:
cmap, head, hhea, hmtx, maxp, name, OS/2, post, glyf, loca,
fvar, gvar, and STAT.
A lowerer additionally emits an avar table when an axis has a non-null
map. HVAR is not required for TrueType outlines because gvar carries metric
variation through phantom points, though the OpenType specification recommends
adding it for performance.
The logical model intentionally excludes binary-only and redundant fields.
createLoweringPlan(font) currently derives immutable glyph bounds/counts,
head bounds, hhea extrema and compression, maxp values, and the sorted
table set. Its encoding plan fixes long loca and gvar offsets, uncompressed
glyf coordinates, complete private point lists, no shared gvar tuples,
Windows format 4 or 12 cmap, OS/2 version 4, post format 3.0, STAT axis
value format, name-record counts, every table length, and the padded SFNT size.
The binary serializer consumes those decisions and performs only byte-level work:
- the SFNT directory, table order, offsets, four-byte padding, table checksums,
and
head.checksumAdjustment; headflags and the already-planned bounds andindexToLocFormat;- packed
glyfflags and relative coordinates, planned longlocaoffsets,hmtxrecords, and the already-derivedmaxp/hheavalues; - the planned
cmap,name,OS/2, and format-3.0postrecords, including custom name-ID assignment and Unicode-range coverage fields; fvar/STATname references and headers, plus the planned privategvartuples, delta runs, long offsets, and flags.
Those choices must not repair or reinterpret the design. If lowering requires new design decisions, the input belongs in a higher-level compiler rather than this IR.
Build bytes only after successful ingestion:
const result = ingestVariableFont(source)
if (!result.ok) throw new Error(`invalid font`)
const bytes = serializeVariableFont(result.value)Serialization verifies every table length against the encoding plan, writes
directory records in bytewise tag order, pads tables to four bytes, and sets
head.checkSumAdjustment so the complete SFNT checksum is 0xB1B0AFBA.
Non-goals and next steps
create-font does not ingest existing font binaries. This package is not a
design-space/master compiler: deriveSimpleGlyphDeltas is a narrow
topology-checking subtraction helper, and instantiateGlyph is a conformance
evaluator for already-compiled tuple data. Version 1 excludes composite glyphs,
TrueType instructions, sparse/IUP gvar, HVAR, MVAR, vertical metrics,
CFF/CFF2, OpenType Layout, color, and bitmap tables.
Higher-level packages target VariableFontSource, use ingestion as the
technical validity boundary, and hand only a validated VariableFont to the
serializer. Future target profiles can add composites, layout, hinting, and
other tables without weakening that proof boundary.
OpenType references
The contract follows the authoritative OpenType 1.9.1 specification:
- The OpenType Font File
defines required SFNT tables, alignment, and checksums; the
Font Variations overview
requires
fvarandSTAT, andgvarfor TrueType variable outlines. fvar,avar,gvar, variation common formats, andSTATdefine the variable-font model and point-number semantics.glyf,loca,head,hhea,hmtx, andmaxpdefine TrueType outlines and their cross-table metrics.cmap,name,OS/2,post, and the font recommendations cover installation-facing metadata and interoperability.
