← Jayasree ⌨ Code AI-assisted October 18, 2026 ···

Jayasree, Part 3: Teaching a Font to Draw

projects jayasree malayalam tooling data
Building JayasreePart 3

A compiled font file, the kind you install and type with, is a collection of filled shapes. It holds only the finished letter, like the answer to a sum with none of the working, and nothing about how it was drawn. The Manjari file Jayasree draws over has no record of where the pen starts on ജ, which way it loops, or where it lifts.

That does not mean the information never existed. Type designers often draw with pen paths first and keep them in the font's source files. SMC's repositories for Manjari and Chilanka do exactly that, as SVG sheets, and mlmash builds on them (see Part 1). Those sheets are not part of the font you install, and in our first experiments with them we found a few things to work through: one file can hold several alternative drawings of a letter, and the direction and order of the strokes did not always match how a person writes the letter. This is early work, and I may be missing something, so I will come back to it in a later part of the series with proper results. For now, Jayasree takes its pen information from a person, which also lets us choose the standard form of each letter.

This part is about how a person gives it to the computer, and how the result is stored and protected, because that hand-made data is the actual content of the project. Everything else in the repository can be regenerated; this can only be redrawn.

The pipeline at a glance

1 Ghost outlinesbuild_glyph_data.py + HarfBuzz 2 Record strokesstroke-recorder.html (a person draws) 3 Processcenter → smooth → straighten → expand 4 Animatejs/src/index.js (composes the rest) stroke-data.raw.jsonthe only hand-authored file

The four stages. Everything except the highlighted file is generated.

  1. Ghost outlines. HarfBuzz shapes every cluster (Part 2) and writes outlines and advance widths to glyph-data.json.
  2. Record strokes. A person draws over each ghost in a browser tool. This produces stroke-data.raw.json.
  3. Process. Four toggleable passes clean the raw strokes (Part 4) and compose derived ones.
  4. Animate. The JavaScript runtime loads the data, composes anything not already stored, and draws it.

The recorder

The recorder is a single browser page, tools/stroke-recorder.html. It shows a pale ghost of the letter, taken straight from Manjari, and a person traces over it with a mouse, stylus or finger. Each pen-down-to-pen-up gesture is one stroke. The order of strokes and the direction of each one are the information the installed font does not carry.

The ghost is the font's own outline of the letter, drawn faintly, like tracing paper under your pen. It is not the letter being taught. It is a guide that says "the letter is roughly here, this big, this shape", so a person can follow its shape while deciding where the pen should start, which way it should go and in what order. Here it is for ജ: the ghost alone, the same ghost with a hand-traced stroke over it (wobbles and all, this is the raw recording), and the stroke on its own, which is what Jayasree keeps.

1 the ghost2 traced over3 the stroke

Left to right: the ghost (Manjari's outline), the raw stroke traced over it, and the stroke on its own. The dot marks where the pen went down.

The ghost does a second job later: the clean-up stage measures how far each stroke wanders from the ghost's middle and nudges it back (Part 4).

A few design decisions that mattered:

All of the Malayalam strokes were drawn by me, on a Samsung Galaxy Tab S10 FE tablet.

What the data looks like

The raw file is a dictionary keyed by cluster. Here is the beginning of ജ's entry, exactly as recorded:

{
  "ജ": {
    "strokes": [
      { "d": "M 274 -820 C 272 -821 269 -820 269 -824 C 270 -827 270 -825 276 -828 C 282 -831 …" }
    ]
  }
}

Each stroke is an SVG path in the same font-unit space as the ghost. The recorder smooths input as it captures it, which is why the raw file is already made of cubic Bézier segments, but it is dense: ജ's single stroke has 733 of them. In Part 4 that falls to 39.

The numbers, as of this writing:

FileContentsSize
glyph-data.json2,052 clusters, 23 mark recipes~5.5 MB
stroke-data.raw.json297 hand-drawn atoms~6.8 MB
stroke-data.json1,721 processed and composed clusters~6.1 MB

Hand-drawn data grows faster than anyone expects: a few hundred letters, and the files already run to several megabytes. The remaining clusters up to the full 2,052 are composed in the browser at run time (Part 5).

Protecting hand-made data

Code can be rewritten. Hand-drawn strokes cannot be regenerated, though they can be redrawn, and redrawing hundreds of letters by hand is days of work. A single careless script that rewrites them in bulk would be a costly mistake that diff review might not catch. The repository protects this data in layers:

  1. Structural validation. tools/validate_data.py checks every committed data file on every commit. It deliberately uses only the Python standard library, because the pre-commit hook runs with a bare python3 where the HarfBuzz bindings may not be installed. The cost is one duplicated constant, which is an acceptable price.
  2. A content snapshot. python/tests/test_data_snapshot.py hashes the stroke content of every previously approved cluster. If any hash changes or goes missing, the test fails. New clusters pass. Re-approving a changed one takes an explicit make update-snapshot, committed in the same pull request. That friction is the point.
  3. Source versus generated. The raw file is written only by the recorder. glyph-data.json and stroke-data.json are generated by make build-glyph-data and make process-strokes. The rule, written into the contributor guide, is: regenerate, never patch.
  4. Review and git history. The data lives in the repository itself, as plain JSON files in git, not zipped, not in Git LFS (large-file storage) and not somewhere else. So every change to it is a reviewable commit, with a normal history and diff. That works today. The files already run to several megabytes, and as more languages arrive, moving the data to LFS or a separate store is something to consider. For now, keeping it in plain git is a deliberate choice.
  5. A read-only runtime. The JavaScript only reads data; nothing at run time writes back.

Part 4's last section shows this machinery earning its keep: a smoothing fix that would silently change 854 of 1,721 already-approved Malayalam clusters, and the decision not to do that casually.

Up next

We now have raw strokes: correct in shape and order, but shaky, off-centre and slightly crooked, because a human hand drew them on a screen. [Part 4: Making shaky strokes clean.]

Prefer GitHub?

You can also discuss this post on GitHub Discussions. A GitHub account is needed there, and nothing loads from GitHub until you click.

← All Jayasree posts sachinnandakumar.me