Eli Heuer’s Blog

NeuralType:
A Post-OpenType
Font Format

index

The NeuralType font format (.ntf) is an experiment in what comes after OpenType. An .ntf font file is a small neural network that draws letterforms in context instead of storing fixed shapes in the rectangles inherited from metal type. The spec, research notes, and Rust engine are on GitHub at github.com/eliheuer/post-opentype, dual-licensed under the Apache License 2.0 or MIT License.

The demo below loads one small .ntf file: a header describing the font and model, followed by the network’s weights. When you type, the engine determines each letter’s joining form. The network uses the letter, its joining form, and the elongation setting to generate a small bitmap. The engine assembles these letterforms into a line and traces the result into vector outlines, which the browser draws on the canvas. A hidden native text input handles cursor movement, selection, copying, and pasting. Click the canvas, then type in Arabic or Latin capitals.

Square Kufic may not convince you that a neural font can replace OpenType. The next demo uses Nasta’liq, a more difficult Perso-Arabic style that exposes the limits of OpenType Layout. Its letters and marks can shift and stack in ways that are difficult and expensive to encode as predefined glyphs and rules. John Hudson reports that Eliyezer Kohen, one of the original inventors of OpenType Layout, did not expect it to handle cascading Arabic and considered that work outside the technology’s scope.

This font was distilled from Gulzar, an OFL-licensed Nasta’liq font, into a model of 1.36 million parameters. With 8-bit weights, the .ntf file is 1.38 MB. A separate post covers how it works: Nasta’liq Distilled.

Section Index

01. A Font That Is a Neural Network

OpenType stores predefined glyph outlines and tables that describe how a shaping engine should select and position them. This works well for many kinds of type, but the approach has well-known limitations. Every form and behavior must be represented in the font ahead of time.

Any font system can be described as a function. Text and context go in. Shapes and positions come out.

shapes, positions = f(text, context, parameters)

The difference between the two formats is how they produce that result. In an OpenType font, the cmap table maps Unicode characters to nominal glyphs. A shaping engine such as HarfBuzz then applies GSUB substitutions and GPOS positioning to produce the shaped glyph sequence. Variable fonts add interpolation between drawn masters, but the principle holds: the font can only produce stored shapes or blends of them.

If a font needs a form its tables do not contain, a designer must add it and define when it appears. More contextual behavior requires more glyphs, rules, and testing. AI tools can help draw the glyphs, but the result remains a finite system of predefined forms and procedures.

A .ntf font replaces predefined glyph selection with a learned computation. The file stores the weights of a small neural network. Rendering runs the model to generate shapes and positions, followed by outline extraction. The square Kufic demo makes one forward pass per letter whenever the text changes. Here is the glyph function used by that model:

impl GlyphSource for NeuralFont {
/// Generate the glyph for one character in context.
/// A single forward pass through the network.
fn glyph(&self, ch: char, form: Form, elongation: f64) -> Option<GlyphImage> {
let ch = canonical_char(ch);
let letter = self.alphabet.iter().position(|&a| a == ch)?;
let input = encode_input(letter, form, elongation);
let output = self.mlp.forward(&input);
Some(decode_output(&output))
}
}

In this first model, the character, its joining form, and the elongation value go in. The letterform comes out. The font’s header lists the supported characters, and the code finds the typed character’s position in that list. This identifies the letter. The network then draws the letterform from that identity, its joining form, and its elongation value.

Because the model computes forms instead of choosing from stored results, the font does not have to enumerate every possible output. In practice this means:

  • Continuous parameters. Justification by elongation is a number, not a substituted glyph. Weight, slope, formality, or another learned axis could work the same way without storing separate masters.
  • Context as input. The square Kufic model sees the letter, its joining form, and its elongation. The distilled Nasta’liq model also uses nearby text and predicts placement. Future models could consider more of the text and the available space.
  • A small, uniform file. The file contains a header and network weights instead of glyph tables and shaping rules. The font in the first demo is 215 KB with 32-bit weights. The Nasta’liq model takes 5.4 MB with 32-bit weights and 1.38 MB with the 8-bit weights used by the demo.

“Generative” here does not mean random. The current demos are deterministic: the same text and settings produce the same output in the current engine. This is a choice made by these models, not a requirement of the format. A future font could accept a seed that produces controlled, reproducible variation.

02. How the First Model Works

The first demo uses a small square Kufic model. Its engine is written in Rust, uses Linebender libraries, and is compiled to WASM. The pipeline has four stages:

  1. Shape. Assign a joining form to each Unicode character.
  2. Generate. Run one model pass for each form.
  3. Layout. Assemble the generated forms into a line.
  4. Trace. Convert the composed line into vector outlines.

The square Kufic model learns its letterforms from handwritten Rust code that draws each form as an ASCII art grid. During training, this program supplies the examples that the network learns to copy, so it serves as the teacher. Each grid contains only the letter’s skeleton (the rasm). A separate step adds the dots, which is why the examples below have none. ب, ت, and ث all share this skeleton. Here are two forms of Beh:

// Beh body (ب ت ث teeth family): open bowl; tooth when joined.
(Beh, Isolated) => art!(8, [
"#...#",
"#...#",
"#####",
]),
(Beh, Medial) => art!(8, [
".#.",
".#.",
"###",
]),

The trained model generates the complete form, including its dots. The training set contains all 437 supported combinations of letter, form, and elongation. A small multilayer perceptron (MLP), a simple neural network made of sequential layers, learns the set in a few minutes on a CPU and reproduces every sample exactly. The teacher program is used only for training and is not part of the .ntf file. The file contains only the trained weights. Those weights are the font.

03. The .ntf File

The original square Kufic file contains four parts:

magic bytes "NTF0"
header length
JSON header (format, script, style, alphabet, layer sizes)
f32 weights, layer by layer

The original v0 byte-level spec is in SPEC.md in the repo.

The Nasta’liq model uses the same general structure, but stores the weights as 8-bit values with the information needed to recover their scale. The header identifies the model and describes its inputs, outputs, and weight tensors. An engine reads the header and loads the weights in the appropriate format.

The .ntf format is intended to support different scripts and network designs. A font does not have to use either the square Kufic model or the distilled Nasta’liq model.