A style is one calligraphic hand (Naskh, Ruqaa, Thuluth…) packaged for
Midad. Each lives in its own folder under styles/:
styles/naskh-amiri/
├── style.toml # manifest — read by the compiler AND the engine
├── OFL.txt # license of the font (SIL Open Font License 1.1)
├── source/
│ └── Amiri-Regular.ttf # input of the compiler (never loaded at runtime)
└── dist/
├── MidadNaskh-Regular.ttf # compiled font, loaded by the engine
└── build-info.json # hashes + stats written by the compiler
dist/ is committed so that working on the editor never requires Python.
CI fails if it is stale (python -m midad_style check styles/*).
pip install -e tools
python -m midad_style inspect styles/naskh-amiri/source/Amiri-Regular.ttf # what does the font offer?
# edit styles/naskh-amiri/style.toml
python -m midad_style compile styles/naskh-amiri
npm run dev # sync-styles.mjs copies the package into the web app
Check a style from the command line, without the UI:
cargo run -p midad-cli -- info -s styles/naskh-amiri
cargo run -p midad-cli -- alternates -s styles/naskh-amiri -t "بسم الله" --at 2
cargo run -p midad-cli -- render -s styles/naskh-amiri -t "بسم الله" -o out.svg
style.toml reference[style]| Key | Type | Meaning |
|---|---|---|
id |
string | Stable identifier, used in .midad documents. Never change it after release. |
version |
semver string | Bump when glyphs or rules change. |
name |
{ ar, en, … } |
Display name. |
description |
{ ar, en, … } |
One sentence. |
license |
SPDX id | OFL-1.1 for fonts. |
credits |
string | Designers, sources. Shown in the editor. |
upstream |
URL (optional) | Where the original font lives. |
[font]| Key | Meaning |
|---|---|
source |
Font the compiler reads (relative path). |
compiled |
Font the engine loads (relative path). |
family_name |
Family name written into the compiled font. Required by the OFL spirit: a modified font must not pose as the original. |
[metrics]| Key | Meaning |
|---|---|
nuqta |
Size of one rhombic pen dot in font units. The editor shows offsets and kashida lengths in dots and draws the dot ladder. Measure the alef height and divide by alef_dots. |
alef_dots |
Height of the alef in dots (5 for Naskh, 7 for Thuluth…). |
[shaping]| Key | Default | Meaning |
|---|---|---|
script |
arab |
OpenType or ISO 15924 script tag. |
language |
none | BCP 47 language (ar, ur, fa) if the font has language-specific forms. |
features |
[] |
Extra features for the whole text, HarfBuzz syntax: "ss05", "-liga", "cv01=2". |
[alternates]| Key | Used by | Meaning |
|---|---|---|
feature |
compiler + engine | GSUB feature holding the alternates (type-3 lookups). Default salt. If the source font already has that feature, choose a private tag such as MDAL. |
edge_features |
compiler | Features whose substitutions mean “variant of the same letter”. Usually rlig, calt, liga plus the stylistic sets that swap letter shapes. |
max_per_glyph |
compiler | Cap on alternates per glyph (keeps the picker usable). |
exclude |
compiler | Glyph ids never offered as alternates (shapes that look wrong out of context). |
remove |
compiler | { "gid" = [gid, …] } — alternates taken away from one glyph only (a shape that breaks its joins in that position). midad review writes this table. |
extra |
compiler | { "gid" = [gid, …] } — alternates added by hand. |
Amiri ships without glyph names, so corrections use glyph ids. Run
midad alternates … --at <char>to see the ids of a letter’s shapes.
[[options]]User-toggleable features (usually stylistic sets). The editor shows each one
as a tile that draws sample with the option on or off; label becomes the
tile’s name on hover. Without a sample, the label is shown as a switch.
[[options]]
tag = "ss07"
label = { ar = "كشيدة مستقيمة (بدون تقويس)", en = "Straight kashida (no curves)" }
default = false
sample = "بـــه" # optional: a short word this option visibly changes
midad_style inspect prints each stylistic set with the name the font
gives it — copy the meaning from there, never guess it.
[kashida]| Key | Default | Meaning |
|---|---|---|
enabled |
true |
Allow elongation at all (also requires a tatweel glyph in the font). |
max_length |
1500 |
Longest elongation, font units. |
max_per_word |
2 |
Elongations per word (0 = unlimited). |
exclude_words |
[] |
Words (compared without diacritics) never elongated, e.g. الله. |
never_before |
[] |
Letters a kashida never precedes (e.g. alef). |
never_after |
[] |
Letters a kashida never follows. |
curve |
"flat" |
Shape of the madd stroke: flat (straight), rise, fall, wave. |
curve_height |
120 |
How far a curved madd deviates from the baseline, font units. |
flat_option |
none | Tag of the style option that asks for straight elongations (Amiri’s ss07): while the user has it on, the madd is flat whatever curve says. |
[pen]The reed pen the engine writes with itself: madd strokes, the curved kashida, and pen ornaments.
| Key | Default | Meaning |
|---|---|---|
angle |
62 |
Angle the nib is held at, degrees from the baseline (0–180). |
halo |
0.3 |
Width of the interlacing gap, in nuqat (0–2). |
fillers |
[] |
What auto-fill may put in the space: pen ornaments by name (dot, two_dots, two_dots_stacked, three_dots, shin_arc, crescent, shard, long_madda, long_fatha) and single characters (a haraka, a small ihmal letter such as "ع"). Empty: the engine’s own choice. |
[joints]| Key | Default | Meaning |
|---|---|---|
overlap |
0 |
Extra overlap (font units) applied when a gap is closed. |
max_gap |
0 |
Close gaps up to this size; larger gaps are only reported. 0 = diagnostics only. |
[axis]| Key | Default | Meaning |
|---|---|---|
angle |
0 |
Rotation of each letter around its join point, degrees (negative = Nastaliq slant). |
step |
0 |
Vertical drop per letter, font units: letter N sits step × N lower (the cascade). |
angle and step are the Nastaliq multi-axis. 0/0 = off (a flat
baseline, correct for Naskh). Marks share their base letter’s drop. A piece
may override both (layout.slant, layout.cascade) from the editor.
[[stacking]]Letter groups written above each other (تركيب), as Thuluth and Diwani write
«لم» or «لمح». Applied after shaping, before blocks; manual offsets stay
possible on top, and a piece can switch the style’s rules off
(layout.style_rules).
| Key | Meaning |
|---|---|
sequence |
The letters, in reading order, joined one to the next (harakat between them do not count). |
steps |
For each letter after the first, [x, y] in nuqat (x to the right, y up): how far it moves, with the rest of its piece, from where the letter before it left it. One step per letter after the first. |
[[stacking]]
sequence = "لم"
steps = [[0.5, -1.0]] # the meem half a nuqta right and one nuqta down
Moving a letter only helps where the font draws a join that meets it there; a font without stacked forms shows the gap. No shipped style uses a rule yet.
[[sizes]]Letters drawn larger or smaller than the font has them.
| Key | Default | Meaning |
|---|---|---|
letters |
— | The letters the rule is for, e.g. "ني". |
position |
"any" |
isolated, initial, medial, final or any, from how the letter joins its neighbours. |
scale |
— | 1 = as the font draws it (0.3–3). |
A final letter grows around its join with the letter before it, an initial one around its join with the next, so the joins stay where the shaper put them. Rules apply before the letter’s own scale from the editor.
Joint gaps are always diagnosed and reported in the scene (scene.joints),
regardless of these keys. Non-joining letters (alef, dal, reh…) are never
treated as broken joints — their gap is natural.
The shaper already prevents impossible positions (after a non-joining letter, inside a ligature). These rules express calligraphic convention on top of that.
styles/<id>/ with source/<font>.ttf, OFL.txt and a
style.toml (copy naskh-amiri and edit).python -m midad_style inspect the font; fill [[options]] from the
stylistic-set names it prints; choose edge_features.[metrics].exclude.crates/midad-core/tests/ if the style
has special rules.CONTRIBUTING.md).A candidate second style is Aref Ruqaa (OFL) — it exercises a hand with very different joins and few ligatures, which is a good test of the compiler’s assumptions.
Every font in styles/ must be under the SIL Open Font License 1.1. A
compiled font is a Modified Version under the OFL: it keeps the original
copyright and license, and gets its own family name (family_name). Do not
use a Reserved Font Name of the original. Drawings contributed by
calligraphers are accepted only under the OFL.