midad

The .midad document format

A .midad file is UTF-8 JSON written by Document::to_json (crates/midad-core/src/document.rs). It stores the text and the calligrapher’s decisions — never glyph positions.

{
  "format": 1,
  "style": { "id": "naskh-amiri", "version": "0.1.0" },
  "text": "بسم الله الرحمن الرحيم",
  "options": ["ss07"],
  "glyphs": [
    { "cluster": 2, "index": 0, "alternate": 1854 },
    { "cluster": 7, "index": 0, "dx": 12, "dy": 140, "scale": 1.25, "rotate": -8, "z": 1, "halo": true }
  ],
  "kashidas": { "0": 600 },
  "blocks": [
    { "start": 9, "end": 15, "dx": 600, "dy": 340, "scale": 1.6, "z": 1, "halo": true },
    { "start": 12, "end": 15, "dx": -80, "rotate": 6 }
  ],
  "guide": { "shape": "teardrop", "cx": -2600, "cy": 0, "width": 4200, "height": 5100, "main": [4, 8] },
  "stamps": [
    { "id": 1, "kind": "ornament", "ornament": "crescent", "x": -2400, "y": 900, "scale": 1.4, "auto": true },
    { "id": 2, "kind": "char", "ch": "ع", "x": -900, "y": -300, "scale": 0.35 },
    { "id": 3, "kind": "ornament", "ornament": "dot", "x": 82, "y": -100, "attach": { "cluster": 0, "index": 0 } },
    { "id": 4, "kind": "ornament", "ornament": "shard", "x": -1500, "y": 200, "z": -1 }
  ],
  "flourishes": [
    {
      "id": 1,
      "points": [[-120, -40], [-600, -700], [-2400, -900], [-3600, -500], [-4300, -250], [-4700, 100], [-4800, 500]],
      "angle": 62, "width": 79, "taper": true, "taper_start": false,
      "attach": { "cluster": 9, "index": 0 }, "z": 1, "halo": true
    }
  ],
  "layout": { "align": "right", "line_spacing": 1.0, "mirror": true, "mirror_gap": 200 }
}
Field Meaning
format Format version. The engine refuses files with a higher number.
style.id, style.version Style the document was made with. Opening requires a loaded style with the same id.
text The text, \n between lines.
options Tags of the style options switched on.
glyphs[] Overrides. cluster = char index (Unicode scalar values, not bytes, not UTF-16 units) of the first character of the glyph’s cluster; index = glyph position inside the cluster in logical order (0 = base letter). Omitted fields mean “unchanged”: alternate (glyph id), dx/dy (font units, y up), scale (1 = unchanged), rotate (degrees, counter-clockwise), z (layer, see below; the letter’s harakat and kashida share it) and halo (a gap where it crosses other ink, see below).
kashidas Char index → elongation length in font units. The tatweel goes between char i and i + 1. Keys are strings because JSON object keys must be.
blocks[] Words or connected pieces placed as one unit (تركيب). start/end = char range [start, end); every glyph whose source starts in it (letters, harakat, elongations) moves with it. Omitted fields mean “unchanged”: dx/dy (font units, y up), scale, rotate (degrees, counter-clockwise) around the centre of the block’s ink, z (a higher block paints over a lower one, whatever the layers of its letters) and halo (the whole block cuts a gap where it crosses other ink; its own letters never cut each other). Applied after every glyph override. Blocks may nest (a piece inside its word; its offset is then in the word’s frame) but never cross. Omitted when empty.
guide The template the words are fitted into (drawn while editing, never exported): shape (teardrop, oval, circle, arch, diamond, golden, crown, cascade), its frame (cx, cy, width, height, scene units, y up) and optionally main, the [start, end) of the main word, which gets a row of its own. Fitting writes blocks; the guide only records the target. Omitted when there is none.
stamps[] Ornaments (حِلى), in painting order. kind: "char" draws ch with the style’s font (a haraka, or a letter’s isolated form, small); kind: "ornament" draws a pen ornament with the style’s nib: dot, two_dots, two_dots_stacked, three_dots, shin_arc, crescent, shard, long_madda, long_fatha. x, y = centre, in scene units, or with attach (a glyph key) in that glyph’s own units, so the ornament moves, scales and turns with its letter. scale and rotate as for glyphs; z its layer (on a letter, counted from the letter’s). auto: true marks the ornaments auto-fill placed (the next auto-fill replaces them). Omitted when empty.
flourishes[] Madd strokes written with the reed pen (مدّات), in painting order. points = the path: a start point, then two handles and an end point per cubic segment (font units, y up), in scene units or, with attach, in that letter’s own units (the stroke grows out of the letter and follows it). angle = pen angle in degrees from the baseline, width = nib width in font units. taper / taper_start: the pen turns onto its edge at that end, so the ink runs out into a point. z and halo as for glyphs. Files of the first form (p0, c1, c2, p1, one segment) still open; they are read as one segment tapered at both ends. Omitted when empty.
glyphs[].alternate_shape Fingerprint (16 hex digits) of the chosen alternate’s outline. In a font rebuilt with another glyph order, the alternate with this shape is used instead of the stored alternate id; when no alternate has it (the shape was redrawn), the id is used. Written with every alternate.
glyphs[].dotless true: the letter is drawn without its dots (its dotless twin, e.g. ٮ for ب); its dots are then usually a stamp attached to it.
layout.align right, center or left.
layout.line_spacing Multiplier of the font’s natural line height.
layout.slant, layout.cascade The slant axis for this piece, overriding the style’s [axis]: every letter turned by slant degrees around its join, letter N of a line dropped by cascade × N font units. Omitted: the style’s.
layout.close_joints false: every join as the font draws it; true: small gaps at the joins closed (by the style’s [joints], or by up to 0.9 nuqta when the style closes none). Omitted: the style’s.
layout.marks_follow false: harakat stay where they are when their letter moves. Omitted: they follow.
layout.style_rules false: the style’s [[stacking]] and [[sizes]] rules are not applied. Omitted: applied.
layout.mirror, layout.mirror_gap Mirrored writing (مثنّى): the whole piece again, reflected, to its left, mirror_gap font units away (negative: they overlap). Omitted when off.

Layers and halos

Letters, madd strokes and ornaments are painted in one order, bottom to top: by layer (a glyph’s z, plus 100 × its blocks’ z; a stroke’s or an ornament’s z, plus its letter’s layer when it rides on one), then letters before strokes before ornaments, then in scene order. A halo is a real gap, not paper-coloured paint: the ink painted under haloed ink is cleared within 0.3 × nuqta of its outline (an SVG mask), so a transparent export stays transparent. Haloed ink never cuts its own connected piece, nor its own block when the halo is the block’s, so a word’s joins stay whole.

Robustness rules

Versioning policy

Known limitation, planned for format 2

alternate stores a glyph id. If a style is recompiled with a different glyph order, alternates in old documents may point to other shapes (they are validated, so the worst case is a stale-alternate warning, not a crash). Format 2 should store a stable glyph name once styles are built from named sources (UFO / Glyphs).