Eli Heuer’s Blog

Nastaliq Distilled:
Converting TTFs to NTFs

index

A previous post introduced the NeuralType concept: a font can be a small neural network instead of a table of outlines. The first .ntf font used square Kufic, a style that lives on a binary grid and resembles low-resolution pixel art. On its own, that is easy to write off as a neat trick, not a serious challenge to the OpenType format. This post is the next step, and it tries to prove that the technology is a viable alternative. We convert an existing OpenType font into a .ntf neural font, in the style that OpenType serves worst: nastaliq. The target is Gulzar, an OFL nastaliq by Borna Izadpanah, Fiona Ross, Alice Savoie, and Simon Cozens.

Nastaliq is the correct target for a demo of this technology because OpenType limits this style the most. Nastaliq is a cursive, flowing style. Letters join or break by the rules of the Arabic script, so a word is a chain of connected runs. Within a run, each letter hangs below the letter before it, and a long run curves far down before the word lands on the line. A baseline grid barely exists in this style. Words land on the line, but the letterforms, dots, and swashes sit in free space around it. OpenType approximates this behavior with more than one thousand glyphs and cursive-attachment rules. Even so, no OpenType nastaliq font has come anywhere near the style as it developed in manuscript and calligraphy culture. If a neural font has value anywhere, it has value here.

Nastaliq calligraphy by Mishkín-Qalam.

Calligraphy by Mishkín-Qalam (1826–1912). Public domain, via Wikimedia Commons.

The demo above runs the full pipeline in your browser. A 1.36M-parameter model is the font, stored as a 5.4 MB .ntf file. Rust code, compiled to WASM, runs the model, places each letter in the cascade, and traces the contours. The result is real text: click the canvas, place the cursor, select, copy, and paste. One caveat: the training corpus stops at three-letter words for now, so letters inside the longer words of the basmala sit in contexts the model has not seen. Those words render rough until the corpus grows.

The goal of the NeuralType format is not to reproduce existing OpenType nastaliq fonts. The goal is fonts that do justice to the manuscript tradition that nastaliq comes from, and no OpenType font does. I think the best type designers and font engineers working on nastaliq today will agree. If you think I am wrong, send me an email or a pull request.

Distillation of an existing OFL nastaliq font is a technical exercise. It shows that a font that contains a small neural network is a viable alternative to OpenType. Once that is demonstrated, the next step is new nastaliq fonts, designed to show what a NeuralType font can do that an OpenType font cannot. Distillation also answers the file-size question early: how many parameters does nastaliq need?

A calligraphic composition by Mishkín-Qalam.

Calligraphy by Mishkín-Qalam (1826–1912). Public domain, via Wikimedia Commons.

The Distillation Process: Measuring Gulzar

The first step toward a nastaliq .ntf font is to measure the problem: how much contextual behavior does Gulzar contain? To answer this, we shaped every ordered pair of Arabic letters through Gulzar with harfrust. Then we counted the distinct glyphs that each letter takes for each neighbor:

  • ن takes 16 different first-position glyphs. The letter that follows selects the glyph. ي takes 15. م takes 14.
  • Across 31 letters, the pairs produce 344 distinct first-position variants. Amiri, a naskh with rich contextual behavior, produces 150 on the same test.

The figure below shows word-initial ن in green, with the following letter in gray. The follower selects the shape.

Fourteen ordered pairs. The green word-initial ن takes a different shape for each gray follower.

We also measured the cascade directly. The shaped word نستعليق places glyphs at vertical offsets up to 1.38 em above the baseline. One word spans more than a full em of vertical travel. That number breaks the metal-type box emulation concept and shows why a new format is needed.

The figure below shows the shaped word نستعليق. The letters alternate green and gray, the red chain joins the letter origins, and the gray rule is the baseline.

The word nastaliq shaped by Gulzar: letters step down from high above the baseline, and the final swash lands below it.

A .ntf font takes a different path. OpenType stores every variant as a separate glyph and selects between them with rules, like sorts in a case of metal type. A .ntf font stores no variants at all. The neighboring letters are inputs to the model, and the model draws the correct shape for that context directly. The cascade works the same way. It is not an attachment rule between stored pieces. It is geometry that the model generates, one displacement at a time.

Extraction

The teacher is Gulzar plus its shaping engine. For any text, HarfBuzz consults the rule tables of the font and answers: these glyphs, at these positions. The answers are deterministic, and the questions are free. That is what makes distillation cheap.

The extraction tool (crates/neuraltype-distill) asks every short question. The corpus is every single letter, every ordered pair, and every ordered triple: 30,783 small words. Triples matter because a triple is the shortest text where a letter has a neighbor on each side. Those neighbors produce the medial forms.

For every letter in every word, the tool records two facts. The first fact is the composed shape: the base letterform and its dots, as one unit. The second fact is the displacement of the letter from the previous letter. The chain of displacements is the cascade. A list of small vectors captures all the nastaliq-specific geometry.

The dataset holds 91,263 letter-in-context records, 411 unique glyphs, and 1,145 unique composed shapes. More than 100 of those shapes belong to خ alone. Between adjacent letters, the cascade drops as much as 1.66 em.

The figure below shows every composed shape of خ in the dataset, 108 in total.

All 108 composed shapes of خ in the dataset, in a grid.

One measurement decided the design of the model. With one letter of context on each side, 358 contexts were ambiguous: the same three letters produced different shapes. The rules of Gulzar look further than one letter. With two letters of context on each side, the ambiguity is zero. Five letters of context fully determine the choice that Gulzar makes. So the model does not approximate a fuzzy relationship. It compresses an exact function. The square Kufic font was the same situation, three orders of magnitude smaller.

Fields

We render each of the 1,145 shapes to a signed-distance field. A signed-distance field is a grayscale grid. Each cell holds the distance to the edge of the shape, positive inside and negative outside. This field is the continuous version of the binary grid in NeuralType. The project settled on fields for a reason: a field is figure and ground, black and white, with no stroke or skeleton assumptions.

The image below shows the isolated خ from the training set. On the left is the shape. On the right is its signed-distance field, with the zero contour in red. The model learns to paint the picture on the right.

The isolated خ from Gulzar, and the same shape as a signed-distance field with the zero contour marked in red.

We rendered test shapes at two resolutions and looked. At 64 pixels per em, the tapers and hairlines of Gulzar survive the threshold. At 96 pixels per em, the result looks the same at reading size and costs 2.3 times the model outputs. So the resolution is 64. The shared canvas is 155×219 pixels. The canvas is large because nastaliq is large: swash tails and dots extend far from the origin of each letter.

Below is the isolated خ at both resolutions, shown at the same size: 64 pixels per em on the left, 96 on the right. The threshold keeps the tapers at both resolutions.

The isolated خ thresholded at 64 and 96 pixels per em, at the same size. The two are nearly identical.

Training

The model is small. Five context embeddings feed a multilayer perceptron. The perceptron feeds a stack of five transposed convolutions, and the convolutions paint the field. A separate two-value head predicts the displacement. The total is 1.36 million parameters: 5.4 MB in f32, against the 963 KB TTF of Gulzar.

Training runs in candle, so the same code trains on CPU, Metal, or CUDA. Inference in the engine is hand-rolled Rust without a framework, because a .ntf file is only weights.

The first night gave one forensic note. The same training run learns about twenty times faster per epoch on CPU than on the Metal backend of candle. Same code, same data. At equal epochs, the CPU run reached a contour overlap (IoU) of 0.21 against 0.01 for Metal. We moved the overnight run to CPU, and words became legible by epoch 12.

Build-log status, afternoon of 2026-07-31: epoch 79 of 100, contour IoU 0.90. The IoU still climbs. Thin vertical strokes form last, and the loss explains this: a hairline is worth tens of pixels, and a bowl is worth thousands. A fine-tune pass with a decayed learning rate follows the main run. The same schedule took the Kufic font from almost correct to exact.

The sheet below compares the teacher with the model checkpoint from epoch 79. Gulzar is gray on top. The model is green below.

Six corpus words rendered by Gulzar in gray above, and by the epoch-79 model in green below. The rows match closely.

Status

  1. Extract: complete. Corpus, cluster records, displacements.
  2. Fields: complete. SDFs at 64 px/em on a shared canvas.
  3. Train: in progress. The model outputs a field plus a displacement. The engine chains the displacements, and that chain is the cascade.
  4. Trace: complete, simple version. Sub-pixel marching squares now. img2bez is the planned quality pass for font-grade bézier outlines.
  5. Measure: held-out IoU per epoch exists. Worst-case proof sheets and tests on real Urdu and Persian text remain.
  6. Demo: live above, in the same editor island as the NeuralType post.

The work happens in the post-opentype repo. The plan lives in docs/DISTILL.md. The license requires a new name for a derivative work, so the distilled font will ship under the OFL with a new name.