- Python 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| proofs | ||
| scripts | ||
| sources | ||
| .gitattributes | ||
| .gitignore | ||
| AUTHORS.txt | ||
| build.py | ||
| build.py.lock | ||
| FONTLOG.txt | ||
| OFL.txt | ||
| proof.py | ||
| README.md | ||
Tilo Sans
Tilo's UI typeface: Instrument Sans by Rodrigo Fuenzalida and Jordan Egstad, extended for Tilo.
Instrument Sans is a refined, compact grotesque with a width and a weight axis, tabular figures and no Reserved Font Name, but its character set stops at Latin. Tilo Sans keeps every Latin outline and spacing of it unchanged and adds what a desktop used around the world needs:
- complete Vietnamese: every precomposed letter, stacked tones that fit the line, the horns, ₫, and decomposed input composing to the same glyphs;
- monotonic Greek: tonos, dialytika, the ano teleia and the Greek question mark;
- Cyrillic for Russian, Ukrainian, Belarusian, Bulgarian (
locl BGR), Serbian and Macedonian (locl SRB/MKD) and Kazakh; - a distinct I and l for identifiers (a serif capital I, a tailed l:
ss13,cv01,cv02), so usernames, codes and passwords cannot be misread; - a slashed zero (
zero) whose slash stays inside the counter (and, in the condensed and heavy weights, clear of it at both ends).
Everything is drawn in all eight masters (upright and italic, Condensed to Normal, Regular to Bold) from Instrument's own parts and stems. Not all of it ships yet: Latin, Vietnamese, Greek, the distinct I/l and the slashed zero are in the default build Tilo uses; Cyrillic is held back until review approves it (Coverage and what ships says why).
Licensed under the SIL Open Font License 1.1. Instrument Sans declares no Reserved Font Name; the extended family is renamed anyway so it can never be mistaken for upstream's files. The name table credits Instrument Sans and its authors in the copyright (ID 0), designer (ID 9) and description (ID 10) strings. Authors and the change history are in AUTHORS.txt and FONTLOG.txt.
Quick start
Everything is a uv script that declares its own
dependencies; run them from the repository root. The proofs render with headless
Chromium: uvx playwright install chromium-headless-shell (or set CHROME to a
Chromium or Chrome binary).
uv run build.py # sources/ -> fonts/ (both variable fonts, plus fonts/default/ and fonts/full/)
uv run scripts/shaping_tests.py # HarfBuzz checks on the binaries
uv run scripts/verify_upstream.py # every Instrument Sans glyph unchanged
uv run proof.py --set vietnamese # proof sheets -> proofs/work/vietnamese/
The comparison fonts the proofs and checks use (upstream Instrument Sans, Onest,
Geologica, Noto Sans as Tilo ships it) are fetched on first use from pinned
upstream locations and checked against recorded SHA-256 values
(scripts/reference_fonts.py --list).
Provenance
| Upstream | https://github.com/Instrument/instrument-sans |
| Pinned commit | 7fa22308a3d0c94ee2b3cd537a1196b65db34a3e (2023-06-14, "feat: updated example gif") |
| Upstream sources | sources/InstrumentSans.glyphs, sources/InstrumentSans-Italic.glyphs (Glyphs 3) |
| Converted with | glyphsLib 6.15 via scripts/import_upstream.py (one-time; see below) |
The upright variable font rebuilt here matches upstream's
fonts/variable/InstrumentSans[wdth,wght].ttf (byte-identical to the
InstrumentSans.ttf Tilo used before) glyph for glyph: same glyph set, cmap, advances,
outlines (point by point at all 12 named locations) and shaping (every character,
every letter pair, mark stacks, each GSUB feature and locl language). The italic
matches upstream's italic the same way. The one difference: bullet.ss01 and
bullet.case.ss01 (scaled period.ss01 components) are now decomposed before the
cubic-to-quadratic conversion, so their circles carry a few more points; the shape is
the same within one unit. scripts/verify_upstream.py checks all of this.
Coverage and what ships
The sources hold everything every area has drawn. build.py compiles all of
it, then scripts/release.py writes two sets of binaries:
fonts/default/ (what Tilo ships) |
fonts/full/ (review only, never shipped) |
|
|---|---|---|
| Latin (Instrument Sans' own set) | yes | yes |
| Vietnamese: every precomposed letter, stacked tones, ₫, decomposed input | yes | yes |
| Greek: monotonic, tonos, dialytika, ano teleia, ; | yes | yes |
Cyrillic: Russian, Ukrainian, Belarusian, Bulgarian and Serbian/Macedonian (locl), Kazakh |
no (Onest ss02 draws it) | yes |
Distinct I and l (ss13, cv01, cv02) and the slashed zero (zero) |
yes (since v1.002; the Cyrillic serif І Ї with Cyrillic) | yes |
| Glyphs / code points | 785 / 521 | 921 / 635 |
Why:
- Vietnamese ships, as the product requires. The kerning gaps review found
(ũ Ũ ẽ Ẽ ỹ Ỹ ữ Ữ without groups, T/Y before ư ụ ủ ứ ừ ử ữ ự at 0, Tố/Tấ
pushed apart in the bold and italic masters) are fixed: every Vietnamese
letter kerns like its base letter until a collision says otherwise, and
the circumflex of a stacked tone stays where â has it. Still open, for the
native reader: the capital stacks Ấ Ầ Ẩ … use the shorter
.casecircumflex while Â Ê Ô keep Instrument's (VÂN beside VẤN); the bold capital tilde and hook stacks reach 1026 units, so at a line height below 1.25 (CSSnormalis 1.22 with this font's metrics) they can touch the descenders of the line above; at 12–13 px Condensed Bold the tones beside a circumflex are two-pixel marks; ỵ's dot sits right of the descender. - Greek ships: review approved it against the fallback (Geologica at 0.85
× the weight). The product owner's sheet is
proofs/final/greek-review/. - Cyrillic is held back. Review found the shapes recognisably Instrument
and better than Onest beside Instrument's Latin, and the earlier collisions
(б and ф kerned as rounds under Т Г У in «Тбилиси», «Убрать»; the italic
∂-shaped д in «Где», «Удача»), the crowded dieresis of «її» and the clogged
bold в Ф Ю are fixed (
proofs/final/cyrillic/review-fixes.png). Still open:- the condensed italic Я (worst in Condensed Bold Italic): moving the
italic foot right by the slant left leg and stem nearly parallel, 3–32
units apart, so «ЯНВАРЬ» reads as «9I». The gap between leg and stem must
stay at least the upright master's (or 0.6 of R's), by moving the leg's
junction rather than only its foot, with a counter check in
construct.py; - ї Ї in Bold, Condensed Bold and their italics: the dots are about half the area of і's and ï's and sit higher (bottom at 626 against і's 578 in Bold), so «Україні» looks light-topped. They should reach about 0.85 of і's dot, aligned with it, keeping the narrow span;
- the Serbian/Macedonian italic г and ѓ (
locl SRB): the widened macron overhangs the advance, so inlang=srtext «гг» and «ѓг» overlap and «ѓгѓ» in the bold italics is one bar over three stems. It needs insetting like п and т, andpairs.pydoes not checkloclforms yet (it should run withlang=sr,mkandbg); - ы in Bold and Condensed Bold: the white between bowl and second stem is 47 units in Condensed Bold (63 in Bold), so it clogs at 12–13 px;
- э in Condensed Bold: the eye above the bar is about 55 × 90 units and
fills in before с and е do.
Native readers check
proofs/final/cyrillic-review/(a Bulgarian reader thelocl BGRforms) once those are fixed.
- the condensed italic Я (worst in Condensed Bold Italic): moving the
italic foot right by the slant left leg and stem nearly parallel, 3–32
units apart, so «ЯНВАРЬ» reads as «9I». The gap between leg and stem must
stay at least the upright master's (or 0.6 of R's), by moving the leg's
junction rather than only its foot, with a counter check in
- Distinct I and l and the slashed zero ship (v1.002), approved on
1x and 4x renders at 12–17 px in Regular, Bold and Condensed Bold,
upright and italic, light and dark (
proofs/final/il-review/,scripts/il_sheet.py --review). What review found on the way, in all eight masters (scripts/il_forms.pydraws them,scripts/il_kern.pyspaces them,scripts/pairs.pychecks them):- the tailed l read as L or t at 12–14 px: its tail is a short curved foot that turns up and ends square to the stroke, with a round outer corner (119–176 units past the stem, against L's 166–370); in the condensed bold, whose foot is more than a pixel thick at 12 px, the tail is lighter (88) and its end rises at 72° to 165 units, so the upturn fills a second pixel row and «lL», «LlL» never read «LL», «LLL»;
- ł read as t («michat», «tódź»): ł keeps its plain form under
ss13andcv02(lslash.ss13is gone); - the tail bridged into the next stem («isIUegal» in the condensed bold,
«Ilya_l1» read «Ilya_U» at 13 px in Bold): the tail keeps 126 units, a
pixel and a half at 12 px, from a following figure,
| ¦ ! ¡or letter whose left side is a stem beside it (l i j n m h b k r p u and the stem capitals), by its advance and by kerning; from every other letter 70 (a round curves away from the tail, a s v x y z leave no stem beside it), so «hello», «legal», «all» stay as tight as the plain l allows (with 126 or 90 from every letter the condensed bold opened into gaps around its l's); - the serifs of
II/IIIfused at 12 px: the serifs are short (66–46 units past the stem) and keep 126 units between them, 1.5 px at 12 px; so do a bar or arm level with a serif (7I,TI,IT,I7), which at 87 read as one bar in the condensed bold italic; I4/4Iand I/l against accents and¿came diagonally closer than 40 units: they keep 40 in any direction, and 80 beside a figure (about a pixel at 12 px), the figure before a tailed l included (4l,7lwere Instrument's 54 in the condensed bold, where they fused);lcaron.ss13is kerned like the other tailed l's (and keeps its own class for the caron). The zero is slashed (not dotted, which reads as θ beside Greek). In Regular the slash ends inside the ring, so it stays apart fromØ. In the condensed and heavy masters a slash that reaches the ring filled the narrow counter grey, and0,θ,ΘandØrendered as dark ovals in Condensed Bold: there the slash is shorter and stops clear of the counter at both ends (40 units along the slash, 24–36 beside it; at 70–80 it shrank to a tick that read as a dotted zero at large sizes). The condensed bold zero is 14 units wider on each side with a counter of 196 units (2.35 px at 12 px, against Instrument's 152), its sides 134 units against the plain zero's 142: at 236 units and 114-unit sides it was visibly fainter than the figures beside it at 12 px. Not fixable here:rnagainstmandclagainstdare Instrument's own Latin letters and spacing; in Condensed Bold at 12 pxrncomes close tom.
Final sheets of the merged family are in proofs/final/ (tracked): one folder per area
(vietnamese, greek, cyrillic, il, latin), and the review sheets
greek-review/ and cyrillic-review/ (scripts/review_sheet.py): every
letter large above the fallback's, interface strings and running text at 14,
16, 20 and 32 px in light and dark, Tilo Sans beside Tilo's Latin plus the
fallback. Geologica at 340 is still heavier than Tilo Sans at 400 (its stem is
about 103 units at Tilo's x-height against 80).
Checks on the binaries, besides build.py's own:
scripts/shaping_tests.py: Vietnamese and Greek typed precomposed or decomposed draw the same; the Vietnamese tones and Greek tonos/dialytika typed as combining marks sit on every vowel; ν· takes the ano teleia; ss13, cv01, cv02 and zero work infull/and do nothing indefault/; Bulgarian and Serbian/Macedonianlocl;default/has no Cyrillic and shapes every Latin, Vietnamese and Greek test string exactly likefull/.scripts/verify_upstream.py: every upstream outline and advance unchanged. Its shaping differences are intended: decomposed Vietnamese sequences now compose (E + U+0303 → Ẽ, ấ's stacked tones), the collision kerning on Instrument's own Vietnamese letters (Yì, Yí, fê, Ťý at the condensed masters), and usWinAscent 1026 for the tallest stacks.- fontbakery
check-googlefonts: the FAILs are upstream's own (the STAT combination "SemiCondensed SemiBold" is 32 characters; DutchJ́has no anchor on J), plus, infull/only, Tajik letters (ҷ ҳ җ) and ʼ missing for languages fontbakery counts as Cyrillic. The WARNs are upstream's (no dotted circle, no meta table, vendor ID, outline alignment, the soft-dotted cases other than ị) plus overlapping segments where two contours meet in β and ω.
To ship a held-back script or feature once it is approved: delete its entry
from HELD_BACK in scripts/release.py, run build.py, check the new
fonts/default/ with scripts/shaping_tests.py and the proofs, and cut a
release (see Releases and how Tilo consumes them).
Tilo's toolkit applies the distinct I/l and slashed zero by capability, so
shipping them needed no code change there; its tests check a face's features
on a few letters of the full build (scripts/toolkit_fixture.py).
Layout
tilo-sans/
OFL.txt, AUTHORS.txt, FONTLOG.txt license, copyright authors, change history
build.py compile sources/ -> fonts/ (variable TTFs)
build.py.lock the exact build dependencies (uv), so releases rebuild byte for byte
proof.py render proofs with headless Chromium -> proofs/
scripts/reference_fonts.py fetch the comparison fonts (pinned URLs, SHA-256) -> .reference-fonts/
scripts/measure.py metrics, stems, overshoots, sidebearings per master
scripts/vietnamese.py draws the Vietnamese marks, stacks and horns from a table of design values
scripts/kerning.py generates the Vietnamese kerning (groups), rebuilds and checks
scripts/components.py clearance between an accented glyph's parts, tone angles, stack heights
scripts/pairs.py neighbouring letters colliding after kerning
scripts/inkgeom.py glyph ink as polygons (shared by the two checks)
scripts/verify_upstream.py every upstream glyph unchanged?
scripts/il_kern.py kerning of the distinct I and l (ss13), solved from the built fonts
scripts/il_sheet.py the confusable characters (I l 1 | ! i j, O 0 o, rn m, cl d) at 12-14 px at 1x, enlarged
scripts/release.py fonts/default/ (shipped: held-back scripts removed) and fonts/full/
scripts/shaping_tests.py HarfBuzz tests: Vietnamese and Greek NFC = NFD, marks, I/l, locl, default = full
scripts/review_sheet.py Greek / Cyrillic beside their fallback, for native readers
scripts/toolkit_fixture.py artifact for tilo/tilo: the toolkit's small test face with the I/l and zero features
scripts/import_upstream.py how sources/ was made from upstream (do not rerun)
scripts/greek/ the Greek: outline generator, kerning, checks (see Greek)
sources/
TiloSans.designspace upright: 4 masters
TiloSans-Italic.designspace italic: 4 masters
TiloSans-{CondensedRegular,CondensedBold,Regular,Bold}.ufo
TiloSans-{CondensedItalic,CondensedBoldItalic,Italic,BoldItalic}.ufo
features/*.fea OpenType code, one file per feature, shared by all 8 masters
fonts/ (ignored) TiloSans[wdth,wght].ttf, TiloSans-Italic[wdth,wght].ttf (everything),
default/ (what Tilo ships), full/ (for review)
proofs/work/ (ignored) every proof run
proofs/sheets/ proofs written with --final, per area
proofs/final/ the sheets of the merged family: one per area, and the review sheets
.reference-fonts/ (ignored) comparison fonts fetched by scripts/reference_fonts.py
.upstream/ (ignored) upstream checkout for import_upstream.py
Every glyph is one .glif file per master, so parallel work on different glyphs
merges cleanly. Each master's features.fea only includes features/*.fea.
Commands
All tools are uv scripts that declare their own dependencies (PEP 723); run them from the repository root.
uv run build.py # lint + interpolation, clearance and pair checks + compile both (~25 s)
uv run build.py --upright # or --italic
uv run build.py --fast # skip the interpolation, clearance and pair checks while iterating
uv run build.py --check # lint only, no compile
uv run proof.py --set vietnamese
uv run proof.py --set greek --fallback geologica
uv run proof.py --set cyrillic --fallback onest
uv run proof.py --set cyrillic --lang bg --name cyrillic-bg # locl forms
uv run proof.py --chars "Il1|" --features ss13 --name il
uv run proof.py --set vietnamese --italic
uv run proof.py --set latin --fallback instrument # regression vs upstream
uv run scripts/measure.py [--glyphs n o A-cy]
uv run scripts/vietnamese.py # redraw the Vietnamese marks and stacks
uv run scripts/kerning.py [--reset] # regenerate the Vietnamese kerning
uv run scripts/components.py [--all] [--glyphs Abreveacute]
uv run scripts/pairs.py [--show 40] [--text "đĩa Tĩnh"]
uv run scripts/verify_upstream.py
uv run scripts/il_forms.py [--check] # draw the distinct I/l and slashed zero in all masters
uv run scripts/il_kern.py [--check] # after build; repeat until --check is clean
uv run scripts/il_sheet.py [--final] # confusables at UI sizes -> proofs/work/il-sheet/
uv run scripts/shaping_tests.py # after build
uv run scripts/review_sheet.py --script greek # or cyrillic -> proofs/final/<script>-review/
Kerning is written by four tools, each owning its pairs. After a change that
touches kerning, run them in this order, building in between:
scripts/greek/kern.py (Greek) and the Cyrillic kerning in
scripts/cyrillic/ (Cyrillic memberships and pairs), then
scripts/kerning.py (Vietnamese groups, and every collision pairs.py
finds, Greek and Cyrillic against punctuation included), then
scripts/il_kern.py (every pair with a serif I or tailed l), repeated until
--check reports nothing. kerning.py resets the pairs it generated, so
il_kern.py must follow it.
verify_upstream.py and proof.py --fallback instrument compare against
upstream's variable fonts at UPSTREAM_COMMIT (see scripts/import_upstream.py),
which scripts/reference_fonts.py fetches. Only import_upstream.py needs the
full upstream checkout in .upstream/instrument-sans, and it clones it itself.
Proofs
proof.py writes 2x PNGs to proofs/work/<name>/ (--final writes to
proofs/sheets/<name>/, which git tracks):
| page | what |
|---|---|
grid-<instance>[-n].png |
every glyph at 160 px in its advance box (tinted) with baseline, x-height, cap height, ascender, descender; glyph name and code point; 36 per page; one set per instance: Regular, Medium, SemiBold, Bold (wdth 100), Condensed Regular, Condensed Bold (wdth 75). A code point the font lacks is a red cell labelled MISSING. |
text-light.png, text-dark.png |
sample text at 13, 14, 16, 20, 26 px, body at 400 and a 600 heading, in Tilo's light and dark surface colours |
compare-<fallback>.png |
the glyphs (96 px, 400 and 700; 48 px) and the samples (13–26 px) in Tilo Sans and the fallback, the fallback scaled to Tilo Sans' x-height (510/1000) |
context-<style>.png |
each glyph between its script's control letters (HH?HOH?OHO?OO, nn?non?ono?oo; Cyrillic Н О н о; Greek Η Ο π ο) and the samples, at Regular, Bold and Condensed Regular 32 px and Regular 14 px |
Fallbacks: onest (Onest with ss02, same weight), geologica (Geologica at
0.85x the weight: 340 for Regular), noto (Noto Sans, the exact files Tilo
ships), instrument (upstream), or a path to any font. The named ones are
fetched by scripts/reference_fonts.py on first use. Default per set:
Greek → geologica, Cyrillic → onest, Vietnamese → noto, else instrument.
Characters Tilo Sans lacks are struck through in red on every page; Chromium would
otherwise draw them from a system font. The proof refuses to run on a font older
than its sources.
Look at proofs with an image viewer at full size, at both 160 px and the UI sizes, light and dark, and across weights and widths.
Design space
Axes as upstream; user and design coordinates coincide except one width point.
| axis | tag | min | default | max | map (user → design) |
|---|---|---|---|---|---|
| Width | wdth |
75 | 100 | 100 | 75→75, 88→87.5, 100→100 |
| Weight | wght |
400 | 400 | 700 | linear |
Masters (one UFO each; every glyph must exist in all of them, compatible):
| UFO | wdth | wght |
|---|---|---|
TiloSans-CondensedRegular.ufo / TiloSans-CondensedItalic.ufo |
75 | 400 |
TiloSans-CondensedBold.ufo / TiloSans-CondensedBoldItalic.ufo |
75 | 700 |
TiloSans-Regular.ufo / TiloSans-Italic.ufo (default) |
100 | 400 |
TiloSans-Bold.ufo / TiloSans-BoldItalic.ufo |
100 | 700 |
Named instances in fvar: Regular 400, Medium 500, SemiBold 600, Bold 700 at
wdth 100 (italic: Italic, Medium Italic, SemiBold Italic, Bold Italic). STAT also
labels Condensed (75) and SemiCondensed (87.5), and ital 0/1 (Roman/Italic).
There is no weight below 400 and no width above 100: there are no Thin or Expanded
masters.
Metrics a new glyph must match
UPM 1000. Identical in every master: x-height 510, cap height 720, ascender 740
(b d l tops sit at 720, d's top anchor at 740), descender −205, overshoot 10 above
and below for round shapes (o, O), italic angle 13° (UFO italicAngle −13).
Line metrics: typo/hhea ascender 970, descender −250, line gap 0, win 1026/350
(upstream's win ascent is 986; it follows the Vietnamese capital stacks),
USE_TYPO_METRICS set.
Stems (font units; italic stems measured across the stroke). From
scripts/measure.py:
| master | n stem | o side | o top/bottom | H stem | H bar | O side | O top/bottom |
|---|---|---|---|---|---|---|---|
| Condensed Regular | 68 | 70 | 60 | 70 | 62 | 72 | 64 |
| Condensed Bold | 138 | 142 | 108 | 142 | 110 | 144 | 112 |
| Regular | 80 | 82 | 68 | 82 | 72 | 84 | 74 |
| Bold | 150 | 156 | 118 | 154 | 118 | 156 | 122 |
| Condensed Italic | 66 | 69 | 62 | 68 | 62 | 73 | 67/66 |
| Condensed Bold Italic | 134 | 138 | 120 | 138 | 110 | 139 | 122/121 |
| Italic | 78 | 80 | 69/70 | 80 | 72 | 83 | 76 |
| Bold Italic | 146 | 152 | 123/124 | 150 | 118 | 152 | 126 |
Spacing references (advance, LSB, RSB; italic sidebearings measured on the outline slanted back upright about half the x-height, as Glyphs shows them; the italic spacing equals the upright's):
| master | n | o | H | O |
|---|---|---|---|---|
| Condensed Regular | 442, 64, 60 | 432, 36, 36 | 500, 70, 70 | 500, 40, 40 |
| Condensed Bold | 462, 44, 40 | 460, 21, 21 | 514, 50, 50 | 536, 30, 30 |
| Regular | 599, 80, 74 | 584, 40, 40 | 736, 86, 86 | 786, 50, 50 |
| Bold | 619, 60, 54 | 604, 20, 20 | 720, 50, 50 | 798, 30, 30 |
Glyph naming
Glyphs.app "nice names", as upstream: A, aacute, idotless, Ohorn,
Ecircumflexacute, A-cy, be-cy, Alpha, alphatonos, iotadieresistonos,
numero. Combining marks end in comb (acutecomb, dotbelowcomb,
hookabovecomb); ligatures join components with _ (f_i). Suffixes:
.ss01–.ssNN (stylistic set), .case (capital forms), .tf (tabular), .alt,
.liga, .loclBGR / .loclSRB / .loclMKD / .loclCAT (language forms); stack them
in feature order (Gcommaaccent.ss01.ss07). The italic uses the same names; there
are no italic-only glyph names.
Production (post table) names are derived at build time: A-cy ships as uni0410,
Ecircumflexacute as uni1EBE, Be-cy.loclBGR as uni0411.loclBGR. Do not add
public.postscriptNames entries by hand. File names inside the UFO follow the UFO
convention (an underscore after each capital: Aacute → A_acute.glif,
A-cy → A_-cy.glif); let the UFO library pick them.
Stylistic sets in use: ss01 Round dots, ss02 Single Story a, ss03 Alternate y, ss04 Alternate K, ss05 Alternate R, ss06 Alternate M, ss07 Alternate G, ss08 Alternate J, ss09 Alternate Q, ss10 Alternate 2, ss11 Alternate Ampersand, ss12 Alternate Currency, ss13 Distinct I and l. New sets start at ss14.
Distinct forms for identifiers (features/il.fea)
Off by default. Tilo's UI toolkit turns on ss13 and zero for text that must be
unambiguous: usernames, user-submitted identifiers, passwords, codes.
| feature | name | glyphs |
|---|---|---|
ss13 |
Distinct I and l | I → I.ss13 (short slab serifs, a little lighter than E's arms), l → l.ss13 (a short tail that turns up), with every precomposed form: Iacute.ss13 … Iogonek.ss13, IJ.ss13, Idieresis.ss01.ss13, lacute.ss13, lcaron.ss13, lcommaaccent.ss13, and the ligatures f_l.ss13, f_f_l.ss13; the same capital in Greek and Cyrillic: Iota.ss13, Iotadieresis.ss13, Iotadieresis.ss01.ss13, Iotatonos.ss13, I-cy.ss13, Yi-cy.ss13 |
cv01 |
Capital I with serifs | the I half of ss13 |
cv02 |
Lowercase l with tail | the l half of ss13 |
zero |
(slashed zero) | zero.zero, zero.tf.zero: a thin slash inside the counter, never past the ring (so it does not read as Ø; not a dot, which reads as θ beside Greek); in Regular it ends in the ring, in the condensed and heavy weights it stops short of it at both ends |
1, | and ! keep their upstream forms: the flagged 1, the long thin bar
and the dotted ! already differ from both new forms. ł keeps its plain
form too: its bar already sets it apart from I l 1 |, and a tail under the
bar is t's silhouette («michał» read «michat»). The lookups run after
liga and ss01, so fl becomes f_l.ss13 and Ï with round dots becomes
Idieresis.ss01.ss13. All four features are in aalt, so glyph palettes reach
the alternates.
Every new precomposed I or l needs its .ss13 composite, a line in il.fea
and a place in the kern groups below, in the same change that adds it; otherwise
ss13 silently leaves a plain stem in an identifier (shaping_tests.py checks
every I and l it knows). Greek Ι Ϊ Ί and Cyrillic І Ї take the serif form:
a bare stem beside a serif Latin I would be ambiguous again. Ί.ss13 keeps
the tonos where Ί has it and moves the serif I so its top-left serif keeps
the clearance the plain stem had; Ї.ss13 keeps the Cyrillic dots, centred.
The palochka Ӏ is not in the font. I.ss13 carries topleft for the Greek
tonos, placed as far from the serif as upstream's I places it from its stem.
scripts/il_forms.py draws every form, in all eight masters, from each
master's own I, l, t and 0 and a small table of design values (its
docstring has them); the italics are drawn upright and slanted back with
Instrument's own shear, so all masters stay point-compatible. Edit the table
and rerun it rather than editing the glyphs; --check reports any source
that differs from what it draws.
- The serif I: the plain stem with slab serifs at baseline and cap height,
reaching 66 units past the stem in Regular, 58 in Bold, 52 and 46 in the
condensed masters, a little thinner than
E's arms. The serifs are short so that two I's never meet: its sidebearings are 63 units, soIIandIIIkeep 126 units (a pixel and a half at 12 px) between their serifs in every master, where the earlier, longer serifs fused into one bar; andI1,Ilstay two shapes. - The tailed l: the stem turns at the foot in a round like
t's and runs on as a short tail along an arc, rising to 45–50° and cut square to the stroke, as Instrument cutstandc. The upturned end and the round outer corner are what tell it fromL's flat, square foot at UI sizes; the tail stays short (144 units past the stem in Regular, 170 Bold, 119 Condensed, 124 Condensed Bold: about a third ofn's width, underL's 166–370). In Condensed Bold, where the foot is more than a pixel thick at 12 px, the tail is a little lighter (88 units) and its end rises at 72° to 165 units, so the upturn spans two pixel rows. After the tail it keepst's right sidebearing, or more so the tail ends 126 units (a pixel and a half at 12 px) from a followingl's stem. - The slashed zero: a straight slash inside the counter, steeper than
Ø's and lighter than the ring (62 units in Regular, 88 in Bold, 54 in Condensed, 66 in Condensed Bold). In Regular it ends in the ring; in the other masters it is as long as it can be while it stops 40 units short of the counter along its length and keeps 24–36 beside it. The condensed bold zero is drawn 14 units wider on each side (zero.zero28 units wider,zero.tf.zerokeeping the tabular advance) with a counter of 196 units, so its sides stay 134 units, near the plain zero's 142 (a wider counter made it fainter than the figures beside it at 12 px). In that narrow counter the slash stands at 75° (57–68° elsewhere), so the side clearance still leaves it about half the counter's height, as in Bold.
Spacing and kerning. Groups: public.kern1.KO_I.ss13 / public.kern2.KO_I.ss13 (with
IJ.ss13 on the left side; its right side carries IJ's pairs; the Greek and
Cyrillic serif I's in both, except Iotatonos.ss13, whose left side is its
tonos and copies plain Ί's pairs);
public.kern1.KO_l.ss13 for l.ss13, its accented forms and the
tailed ligatures (their left sides are plain l's and stay in plain ĺ ľ ļ's
KO_j group); public.kern1.KO_lcaron.ss13 for lcaron.ss13 alone (it must
not share ď's values: the caron overhang sets those, and the tail would run
into the next letter). f_l.ss13 and f_f_l.ss13 stay in public.kern2.KO_f
and zero.zero in both KO_zero groups (with copies of zero's glyph-level
pairs), so the alternates keep every upstream pair.
Every pair with one of these groups (or their members) is written by
scripts/il_kern.py, never by hand. It measures the shaped fonts at all eight
masters, with ss13 on and off: level gaps (scanline against scanline) for the
white beside a stem, and ink distances in any direction for the floors, so a
serif tip coming towards the flag of 1, the arm of 4, the bowl of ¿ or an
accent counts as much as one level with it. The kerning that reaches a
distance is solved exactly, so a pass settles. The rules (the script's
docstring has the detail):
- the white beside the bare stem (between the serifs, above the tail) is the
plain
I/lgap plus a little: 14 / 12 units for most partners (t f v w y r, figures, operators), a share of the extra width next to rounds (O C Q Ø,o e,0 6 9, and the Greek and Cyrillic rounds: a third beside the serif I, whose serifs sit where a round curves away; a sixth beside the tailed l, whose tail fills under it), half of it next to top and middle shapes (T V W Y 7, quotes, dashes, guillemets,·). Tightening stops where the tail or a serif would sit closer to the partner than it sits to a straight stem (l n,I H), so neither the tail nor the serifs collide and no hole opens above the tail or beside the inset stem; - distances never shorter than: 24 for any pair (the collision check's 16 plus
room for the instances between masters), even where the plain letter
collides; the serif I and the tailed l 40 from anything, in any direction,
even where the plain letter comes closer (
I4,l4in the condensed bold); where the serif I's nearest approach is diagonal, also whatZ's bars keep from the same partner (Ζin Greek,Еin Cyrillic) or the plainI's, whichever is shorter; the tailed l also what Instrument'stor plainlkeep; next to1 | ¦ ! ¡and the stemsl i j(and their accented forms) the plain letter's distance up to 104 units (a clear pixel and a quarter at 12 px), soI1,Il,Iinever close up; beside a figure 80 (about a pixel at 12 px, soI4,4Istay two shapes in the condensed bold), a figure before a tailed l too (4l,7l); the tail 126 from a following figure,| ¦ ! ¡or letter whose left side is a stem beside it (l i j n m h b k r p u, the stem capitals), 70 from any other letter, so it never bridges into the next stem at 12–13 px (isIllegal,l1) and the condensed bold does not open up around its l's (hello), and 14 more beside a figure, so the instances between the condensed masters keep 126 too;ď ľ ť řbeforeI45. Each distance is solved 6 units long, so the exact outlines keep it after the scanline sampling and rounding; - level gaps: serif I beside serif I (
II,ÏÎ) 126 between the serifs, and the same beside a bar or arm level with a serif (7I,TI,IT,I7: a partner that comes 40 units nearer at the serif's height thanHdoes), where less reads as one bar at 12 px;IJ tI ťIatLZ;lIat the larger ofLZandZZ; lcaron.ss13is measured like the other tailed l's and kerns at least as loose as plainľ's value moved by the advance difference; its caron keeps 40 from anything and 80 from a figure (plainľ's own is negative againstT Y 7and the i accents);- Catalan
l·l(locl CAT): the middle dot stays centred between tailed l's.
The italic masters are measured on their own outlines, with the slant taken
out for the level gaps (as Glyphs shows italic sidebearings) and as drawn for
the distances. Letters of two scripts are never measured together (HarfBuzz
shapes them in separate runs, which are not kerned). pairs.py checks the
distinct forms as well, at the masters and the instances between them: no
pair with ss13 on may come closer than 16 units unless the plain pair already
does; beside the accents, ¿ ¡ and each other the serif I and tailed l
keep 40 in any direction at the masters (36 between them) and beside the
figures 80 (76 between them), a figure before a tailed l included; and the tail (the tailed l's ink below 200
units) keeps 126 from a following figure or stem (120 between them), all
measured exactly on the outlines (scripts/inkgeom.py).
scripts/il_sheet.py renders what the forms are for: the confusables
(I l 1 | ! i j, lL LlL ll, II III I1 I4, O 0 o Ø ø θ Θ, rn m, cl d)
and identifiers (user_Il1, Ilya_l1, IIl1lI, mail.l1@x.io, lI1O0o,
ID-I1l0O, michał, łódź, isIllegal, …) at 12, 13 and
14 px at 1x, enlarged without smoothing, in Regular, Bold and Condensed Bold,
upright and italic, light and dark, each line off and on; and the tight pairs
at 96 px. Its sheets are in proofs/sheets/il-sheet/. With --review it
writes the sign-off sheet to proofs/final/il-review/: the identifiers and
confusables in each style at 12, 13 and 14 px, default forms beside the
distinct ones, light and dark, at 1x and 2x as rendered and the 1x page
enlarged 3x.
A new letter that can sit next to a serif I or tailed l (Vietnamese Ĩ Ỉ Ị,
Cyrillic і, Greek ι, a new accent) needs no hand kerning against them: build,
run il_kern.py, build again, and il_kern.py --check must report no changes.
Upstream's glyph-level pairs on accented plain I's (Ï/-, -/Î, «/Î) and
f_l/_ ~ do not carry over: with ss13 on, an accented serif I kerns like the
serif I, and f_l.ss13 like l.ss13.
Anchors
Bases: top, bottom, ogonek, center (for strokes: Đ, Ł, Ħ), topleft,
topright (caron-as-apostrophe: ď ľ ť). Marks: _top + top (stackable),
_bottom + bottom, _ogonek, _topright (caroncomb.alt). Ligatures:
top_1…, bottom_1…, caret_1… . Marks attach at _top y = 510 (x-height) and
carry their own top for the next mark (acutecomb top at 740); a capital's
top is at 720, so the same acutecomb serves lower and upper case (there are no
.case marks yet).
The build propagates anchors into composites (Glyphs' algorithm): Aacute = A +
acutecomb gets A's bottom/ogonek and a top on the acute, so a further mark
stacks on it. Draw anchors on simple glyphs and marks; put anchors on a composite
only to override. Spacing accents (acute, tonos, …) get none. mark, mkmk,
kern and GDEF are generated from anchors, categories and kerning; never write
them by hand.
Vietnamese
Every precomposed Vietnamese letter (all 134 with Đ đ) and the đồng sign ₫,
upright and italic, in every master. Upstream already had the letters shared with
other languages (à á â ã è é ê ì í ò ó ô õ ù ú ý ỳ ă đ); the rest are composites
on these marks, placed by the anchors (build.py propagates them).
The marks, stacks, horns and mark positions are drawn by
scripts/vietnamese.py from Instrument's own marks and letters and a table of
design values per master (DESIGN, HORN); it never changes upstream glyphs and
can be rerun at any time. To change a stack, edit the table, run it, then
scripts/kerning.py and build.py.
- Hook above (
hookabovecomb): the top of Instrument's question mark, a flat foot, a round shoulder and a terminal on the left cut flat below the middle, so the curl stays an open crescent in every weight. Its stroke is a little lighter than the acute's, as the question mark's curve is lighter than its stem. - Dot below (
dotbelowcomb):i's square dot;dotbelowcomb.ss01is the round one ("Round dots"), its top level with the square dot's.ỵputs the dot just right of the descender at the same depth (the anchor would put it on the tail);y+ U+0323 becomesydotbelowinccmp. - Horns (
ơ ư Ơ Ư, single outlines): an arm that leaves the right of the stem (or the bowl's upper right) below its top, swings right and up and ends in a flat cut 40 units above the x-height or cap height; the stem keeps its flat top. The arm carries the stem's weight in every master. The advance follows one rule in every master (the italics measured unslanted): half way between a horn that overhangs the next letter as far as a flat-toppedn/Hallows (24 units clear) and one that clears the crossbar oftand the arm ofT; kerning opensợt,ỚTand the like by the rest. Accented horn letters take the horn letter's advance.horncombisơ's horn alone;ccmpcomposeso O u U+ U+031B into the drawn letters, so decomposed text gets the same horn and advance from shapers that do not recompose. - Stacked tones are ligature marks named as in Glyphs
(
circumflexcomb_acutecomb,brevecomb_tildecomb, …). Acute, grave and hook sit to the right of the circumflex, the tone's foot 36 units below the circumflex's top and 34 from its leg. The acute and grave are the master's ownacutecomb/gravecomb, cut shorter from the foot (106–112 units lowercase, 88–92 on capitals) so they keep its angle and stroke exactly: sắc and huyền stay as far apart as on a single vowel, in the italics too. They overhang the next letter instead of standing up; kerning opens the few pairs that would touch. The circumflex stays within 20 units of whereâ ê ôhave it (STACK_SHIFT) and the tone hangs out to the right; centring the pair over the letter pushed the circumflex left into the arm of aTorYbefore it (Tốwas kerned apart in the bold and italic masters). The grave goes right like the others: on the left it would run into the ascender of theth,kh,đ,lthat so often precedeầ ề ồ; a native reader should confirm the choice. Every tone sits on top of the breve, the tilde on top of the circumflex, 34 units clear (32 in the bold masters). Over the breve the tones are the short capital forms in both cases, soắdoes not tower over the ascenders, and the hook may move up to 40 units off centre when that lets its stem dip into the breve's cup. - Capital stacks (
Ấ Ẫ Ắ Ẳ …) use.casemarks starting 42 units above the cap height (single accents on capitals stay upstream's 73):circumflexcomb.caseis drawn like Instrument's circumflex (legs with open corners under a flat top) 124–126 units tall and 86–90 % as wide, with the lowercase leg stroke; the flat top keeps its share of the width and the apex stays at least 80 % of the leg stroke and at most half the mark's height, so the notch reads.brevecomb.caseis 114 units tall (118 bold) with the arms' width kept and the bowl's bottom thinner (54/64 units), so the counter is 46–53 % of the mark's height, not a shallow notch.acutecomb.case/gravecomb.caseare the master's tones 96–100 units tall,tildecomb.casea lower wave (86/106 units, the bold stroke at 80 %),hookabovecomb.casea smaller hook (132–134 units). The tallest stacks reach 1003 (RegularẪ), 1008 (BoldẮ), 1026 (Bold and Condensed BoldẪ) and 1023 (Condensed BoldẲ);scripts/components.pyfails any added glyph above 1030.usWinAscentfollows the tallest ink (scripts/vietnamese.pysets it), so Windows does not clip them. - Line spacing and clipping. typo/hhea stay upstream's (970/−250,
USE_TYPO_METRICS), so line spacing is Instrument's. At Tilo's default line height (TextStyle1.25, lines 1250 units apart) a capital stack stays clear of ag(−215) on the line above in every master; at CSSnormal(1220) the bold stacks come within the descender by up to about 20 units, and at 1.15 they touch. The stacks rise 30–56 units above the 970 ascender, so a box that clips exactly to the line box (a weboverflow: hiddenlabel at line-heightnormal) trims the top ofẪ's tilde. Decision for the Tilo toolkit: text is never clipped at its line box. Clip rects come from widget shapes (an input's field, a scroll viewport, a table body), which include their padding; any clip that hugs a single line of text must extend at least 0.06 em above the line box (usWinAscent − hhea ascender) and 0.1 em below it, the way browsers let ink overflow an unclipped line. Tilo's toolkit (crates/sdk/ui-corein tilo/tilo) follows this today (no per-line clips); new widgets must too. - ĩ Ĩ use narrow tildes with the stroke kept:
tildecomb.narrow(76 %, 80 % condensed) onĩ,tildecomb.narrow.case(60–70 %) onĨ, which overhangsIby 7 units in Regular and 19 in Bold (the full tilde: 40); decomposedi/I+ U+0303 get them inccmp. i's andi.ss01's stray bottom anchors (upstream's, up to 100 units off the stem in the italic) are gone; they inherit dotless i's, soi+ U+0323 drawsị(verify_upstream reports the changed mark positions oni).₫isđover a bar below the baseline, as thick asđ's crossbar and spanning bowl to stem (Noto Sans' construction); it kerns withđ's groups.features/vietnamese.fea(afterccmp.fea) composes decomposed horns, ligates decomposed circumflex/breve + tone into the stacked marks (.caseafterA E O), and adds the Vietnamese letters to ss01 (round dot below), ss02 (single-storey a) and ss03 (alternate y).hookabovecombis in@CombiningTopAccents, soi jlose their dot under it. Alookupflaginside a feature block carries over into the following lookups: the lookups after the mark-filtered one reset it withlookupflag 0.- Kerning is by group and generated by
scripts/kerning.py; the hand-made kerning stays Instrument's. Each letter joins the groups of its closest upstream relative (ấ→â); the horned letters' right-side groups (KO_ohorn,KO_uhorn,KO_Ohorn,KO_Uhorn) carry copies ofo u O U's pairs. Where a pair still collides, the accented letter's tone variants at the same mark height leave its group together:KO_agives upKO_a__top(á à ả ã ă â …) andKO_a__stack(ấ ầ ẩ ẫ ắ …), which inherit every pair ofKO_aand get, against the neighbour's group, the smallest kern that opens all their members to 24 units. So every tone of a letter spaces alike. Every added letter also joins its base letter's group on any side where it has none (ũ→KO_uboth sides,ữ→KO_uhornon the right), and copies its base letter's glyph-level pairs: Instrument kernsT uandY uas glyph pairs, soTư Tự Từ TủandYưget the same −94/−77 (raised only where a tone collides). These copies, like every glyph pair on an added Vietnamese letter, are generated:kerning.pyremoves and rewrites them on each run. A Greek or Cyrillic letter that collides with punctuation gets a glyph pair of its own (ν·,(ј), since those letters share Latin groups. Instrument's own exceptions are raised where they govern a colliding pair (T F K V W f 'ì, soTìmclears;Y+è); the originals are kept in the font lib and--resetrestores them. Pairs that cannot occur in Vietnamese (two toned letters, a lowercase letter before a capital) are not kerned.
Checks; build.py runs both (not with --fast):
scripts/components.py, on the sources: the ink distance between the parts of every accented glyph, in every master and at the instances between them (wght 500/550/600/650 at wdth 75 and 87.5, 500/550/600 at wdth 100): a tone or tilde on top of a breve or circumflex ≥ 30, a tone beside the circumflex ≥ 30, an accent above its letter ≥ 30, a dot below ≥ 24. Also: the stacked acute and grave lean like the master's own within 5°, and no added glyph rises above 1030.--allprints every gap.scripts/pairs.py, on the compiled fonts: every checked letter (the added ones and every accented Vietnamese letter) next to the neighbours it meets in text (ASCII, the untoned Vietnamese letters in both cases, quotes, dashes; a Cyrillic letter also every Cyrillic letter), shaped with HarfBuzz so kerning applies, at the masters and the same instances between them; fails on inks closer than 16 units.--text "…"checks the pairs of a string,--show Nlists the tightest.
For native review: the grave's side, the dot of ỵ, the horn's size and the
height of the capital stacks.
Adding a glyph
- Draw it in all four masters of its designspace (upright and italic are separate designspaces: a new letter needs eight drawings). Same contours, same point counts and order, same start points and directions, same components and anchors in every master; only coordinates differ.
- Set the advance, the code point (
<unicode hex="0410"/>; none for alternates) and anchors. Combining marks have advance 0. - Make sure each
glyphs/contents.plistlists it (any UFO library does this;build.pyfails on a.glifthat is not listed). Two branches adding different glyphs mergecontents.plistcleanly (merge=unionin.gitattributes). - Kerning: join the
public.kern1.*(left side of a pair, i.e. the glyph's right side) andpublic.kern2.*(its left side) groups whose shapes it shares — Cyrillic А, В, Е, К, М, Н, О, Р, С, Т, Х and Greek Α, Β, Ε, Ζ, Η, Ι, Κ, Μ, Ν, Ο, Ρ, Τ, Υ, Χ belong in the Latin groups (KO_A,KO_H,KO_O, …); the kern writer splits pairs per script. Groups and kerning live in each master'sgroups.plist(same in all masters) andkerning.plist(per master). - Features: edit
sources/features/<feature>.fea(one file, all 8 masters). A new script needs itslanguagesystemlines inlanguagesystems.fea(languagesystem cyrl dflt;,languagesystem cyrl BGR;,languagesystem cyrl SRB;,languagesystem grek dflt;), otherwise kerning andloclnever reach it. Add new combining top marks to@CombiningTopAccentsinccmp.feasoi/jlose their dot under them. build.py(it also checks accent clearance and colliding pairs;scripts/kerning.pykerns the collisions), thenproof.py, thenscripts/verify_upstream.py(must stay clean: a Latin change shows up by glyph name).
Glyph order, production names and GDEF classes for new glyphs are filled in by
build.py; lib.plist does not need editing. A glyph whose Glyphs category is
wrong can be overridden with com.schriftgestaltung.Glyphs.category /
.subCategory in the glyph's lib.
Greek
Modern monotonic Greek, upright and italic, all masters: Α–Ω, α–ω with ς, the
tonos and dialytika letters (Ά Έ Ή Ί Ό Ύ Ώ Ϊ Ϋ, ά έ ή ί ό ύ ώ ϊ ϋ ΐ ΰ), tonos
΄, dialytika-tonos ΅, ano teleia ·, the Greek question mark ; and the numeral
signs ʹ ͵ (numeral-greek, lowernumeral-greek); beside them MICRO SIGN µ (micro,
the Greek μ) and HORIZONTAL BAR ―, the Greek dialogue dash (horizontalbar,
the em dash, with a .case form).
The Greek glyphs are generated. scripts/greek/gen.py draws every one of
them into all eight masters from that master's own Latin parts and measured
stems, and overwrites their .glif files. Change a Greek glyph in the
generator, never in the .glif, or the next run loses the edit:
cd scripts/greek
uv run --with ufoLib2 --with fonttools python gen.py [glyph ...] # all, or the named glyphs
uv run kern.py [--report] # Greek kerning groups and pairs, all masters
uv run view.py out.png --masters Bold,CondensedBold --glyphs "beta xi" # sources, no build
uv run check.py # after build.py: pair kerning, substitutions, accent overhang
- Shared shapes: Α Β Ε Ζ Η Ι Κ Μ Ν Ο Ρ Τ Υ Χ and ο are components of the Latin glyphs; η comes from n, μ from u, κ from k, ι and τ from t, α from the single-storey a with t's tail, ρ σ from o, ν γ from v, λ's long stroke from y's turned over, Γ Δ Λ Π Ξ from F, A, H and E's stems and bars (Δ's base ends flush with the diagonals). Bowls (δ θ φ Φ) are the master's o or O stretched, so they carry Instrument's curve tension, and in the italic its corrected italic o.
- Drawn shapes (β γ ε ζ ξ ς υ ψ ω Ψ Ω) are skeleton strokes outlined with an elliptical pen using o's vertical and horizontal weights; terminals are cut across the stroke like c's and j's, and the descender hooks of ζ ξ ς taper. Joins are thinned where strokes meet (β's waist and stem joins, δ's neck, ψ and ω's bottoms, φ's bowl where the stem crosses it), more in Bold. Heavy masters lighten small bowls (β's and ε's upper bowls, φ, ψ) and widen the letters whose counters would otherwise close (φ Φ ψ Ψ ω θ in Condensed Bold).
- ζ and ξ leave their top bar from its right end, the bar ending flush with the stroke; ζ's spine stays diagonal well down and turns late into the bowl, with a small round under the bar in the heavy masters. ε's and ξ's tongue is where the two bowls part, run on a little and cut leaning back, not a bar. β is one stroke: the stem rolls over into the upper bowl, pinches at the waist and the lower bowl runs back into the stem. γ's right arm runs on without a break into a tail that leans a little left, the left arm joining it above the baseline. λ's short leg joins the long stroke a little under half way up, its foot well in, so the base is narrower than Λ's. υ and ω lean their arms in over the top quarter (by a share of the counter, less in Condensed Bold); ω's middle stroke is short, light and set high. Ω's sides turn in below the middle and run, still curving and a little lighter, into the feet.
- Italic masters are drawn upright and sheared 13°. Strokes are pre-compensated
so the shear keeps the upright's weight distribution (a plain shear thickens
\strokes and thins/strokes by about 11%). - The tonos is the acute stood up. Over a capital it sits in front of the letter at cap height; Ε Η Ι Ο set the white beside it, Υ and Α (whose sides run away from it) match Ε's mean white, and Ω's arch comes a little closer than Ο. Υ's tonos drops beside the arm, its top under the arm's tip, so it holds the same white as Έ. In ΐ ΰ ΅ the tonos stands up between the dots, narrower and steeper in the heavy masters, with its foot between the dots' tops; the dots keep the dieresis spacing where they can (drawn in, and 8% smaller in Condensed Bold). No accent hangs more than about 36 units past the advance: ϊ ϋ Ϊ Ϋ ΐ ΰ take extra sidebearing where needed.
- Kerning (
scripts/greek/kern.py): each Greek glyph joins the Latin kerning groups of the Latin glyph its sides match (Γ's arm kerns like T's, Δ Λ like A, ρ like p then o, υ ω like o on the right, ι like t, λ like A on the right and l on the left, accented letters like the Latin letter with the same accent). The kern writer splits pairs per script, so Greek and Latin never kern against each other. Where Instrument kerns the Latin analogue as a single glyph (T-u is a glyph pair), the Greek pair gets the same value as a glyph pair, so Τυ Τω Τμ Γω Υω kern like Tu. Where a Greek side is only partly like its analogue the pair is set from the outlines against the analogue's white: ζ ξ before lowercase take c's white plus about 40 units (their body sits well left of the bar and hook); υ ω before ν γ χ take u's white before v and x; Γ's long arm closes over x-height letters to the white T's arm keeps over the same letter; an x-height bar (π σ on the right, π τ on the left) keeps at least the white between two t's bars, so πτ never reads as one bar. A collision guard then opens any Greek letter pair whose outlines come closer than nearly all Latin pairs do in that master, and a punctuation guard keeps a Greek letter at least 70% as far from brackets, quotes and figures as its Latin analogue ((ζ no longer gets (c's kern). Deliberate exceptions: φχ φλ λρ 0, T-like arms before a capital with tonos +20 to +40, opening brackets, quotes and guillemets before a capital with tonos −45 (the tonos hangs into the space: «Έκθεση, (Άδεια). - Features (
features/greek.fea):languagesystem grek dflt; the Greek question mark follows the capitals undercase;ss01(round dots) covers ϊ ϋ Ϊ Ϋ ΐ ΰ ΅, the ano teleia and the question mark; andloclfor Greek gives U+00B7 MIDDLE DOT the ano teleia's form when it follows a Greek letter directly, since NFC turns U+0387 into U+00B7 and Greek keyboards type it. A middle dot with a space before it is a separator (Αρχεία · Ρυθμίσεις,12 MB · 3 λεπτά) and stays centred.casealso covers ―. Dropping the tonos in all-caps text (and adding the dialytika it can call for) is left to the text layer's Greek uppercasing.
Editing
UFO is a plain-text format: any UFO editor works (Fontra, RoboFont, FontForge,
Glyphs.app's UFO import), as do scripts with
ufoLib2 and hand-edited .glif XML. Keep
coordinates integral. glyphsLib.filters.eraseOpenCorners runs at build time, so
Glyphs-style open corners are fine.
Cyrillic
Coverage: Russian, Ukrainian (Ґ Є І Ї), Belarusian (Ў), Bulgarian, Serbian and
Macedonian (Ђ Ѓ Ѕ Ј Љ Њ Ћ Ќ Ѝ Џ, Ѐ), Kazakh (Ә Ғ Қ Ң Ө Ұ Ү Һ) and №, upright
and italic, in all eight masters (134 glyphs). proof.py --set cyrillic
shows all of them.
- Shared forms are components of the Latin glyphs: А В Е К М Н О Р С Т Х, а е о р с у х, Ѕ І Ј Ү, ѕ і ј һ, and Ё Ѐ ё ѐ reuse the Latin composites' offsets.
- Native forms are built by
scripts/cyrillic/construct.pyfrom the same master's Latin drawings, never by plain scaling. Curved parts are either scaled as a whole (an affine map keeps every curve's tension) or redrawn from numbers measured off one Latin glyph of that master (parts.py):- the Ь bowl (Б Ь Ъ Ы Љ Њ, and at x-height ь ъ ы љ њ я) is P's bowl redrawn at the letter's size with P's bars, side and curve tensions; capitals use P's own height, lowercase bowls are about 60% of the x-height, a little taller and with lighter bars in the bold masters so the counter stays open. The counter's left edge is the stem's inner edge;
- Л л Љ љ have a vertical left leg whose foot is a short J tail
(
parts.foot_leg: J's bottom-stroke weight, turn proportions, terminal rise and handles, remapped segment by segment), reaching about one light-master stem left of the leg in every master; - Д д are two square stems on a base that runs past them and descends at both ends, the left stem meeting the base square as Ц's do; counter and overhang are shares of H's or n's counter, so Д keeps H's width and д n's in every master;
- Ђ ђ are Ћ ћ whose right leg runs on into J's or j's tail as one contour (Ђ's hook ends about half its counter clear of the stem);
- м is M's construction redrawn at x-height with M's proportion to H, M's diagonal weight, and the vertex 36 units up in every master;
- я is R's bowl and leg mirrored, the leg at R's stroke thickness leaving the bowl 0.3 of its width out from the stem;
- Ю ю and Ф ф narrow O, o and p's bowls as a whole (outer and counter each scaled); in the bold masters the rings and bowls are drawn from a lighter O, o, p or q, so that next to the full stem Ю ю keep 0.85 of O's or o's counter and each half of Ф keeps 0.45–0.49 of O's. Ф is 1.07 of O's width in every master (and grows with weight as O does);
- в is B zone-mapped to x-height, with bars and sides a little lighter in
the bold masters (the middle bar at most e's crossbar), widened until its
upper counter is 0.72 of n's counter (
VE_UPPER); ы keeps 0.8 of ь's counter; - Э Є э є are C or c whose terminals run as far round as C's and c's, cut back along their own curves only where they would come within 0.4 of the bar's thickness of it vertically; the bar sits inside the counter and stops 0.6 of its thickness clear of the terminal tips;
- Ґ ґ lengthen Г's arm where needed so it runs free for 0.7 of a stem between stem and upturn;
- б is o with a straight stroke rising into f's top curve, run on as a level flag;
- ї Ї get their own dieresis: dots 0.92 (light) to 0.7 (bold) of the Latin dieresis' with a gap of 0.55 of a dot between them, overhanging the advance by at most a quarter of that gap, so ї keeps і's width;
- Һ is H wide, with h's arch zone-mapped so its shoulder tops out at 0.6 of the cap height (the leg keeps its weight, the arch its thickness). И Я Э are mirrored N, R, C, З the 3, Ч ч u's bowl, Ж ж K's and k's arms, У the Y arms, Ц Щ Џ Қ Ң tails to −160. Overhangs and gaps that should not grow with the stems (tails, strokes, the Ю gap, Л's foot) are measured in the light master's stem. Italic masters are slanted back upright (13° about y = 251), constructed and slanted again. A mirrored italic curve would lean backwards, so the italic Э э Я and ∂ mirror the upright master's C, c, R and 6 and are slanted as a whole, and the italic ф takes its left bowl from q. Slanted as a whole, a leg running down to the left lies flatter than upright, so the italic Я я have their foot moved right by the slant over the leg's height and keep the upright angle; the condensed italic Ж spreads its outer arm ends 12–26 units so they keep a diagonal.
- Checks: before writing,
construct.pychecks every glyph it built in every master and writes nothing if one fails (--checkonly reports,--forcewrites anyway): no curve tighter than Instrument's own (tension above 0.81, 0.86 in the condensed masters), no handle outside its segment's box (upright; italic: vertically only), counters probed along scanlines against the n, H, o or O counter of that master (PROBES), ђ has its bar, ї Ї keep the gap inside their dieresis and overhang the advance by no more than they may; no contour crosses itself (no area wound against the contour's direction; Instrument's own overlapping strokes wind the same way and pass); lowercase stems end at 510 and capital stems at 720; in the italic no curve side leans backwards; Ґ ґ keep their free arm; and every letter widens from the light to the bold master within 8% of its Latin model (n or H, or the Latin letters it is made of,GROWTH_REF). Running the script rewrites those glyphs (and the Cyrillic kerning), so refine a glyph in the script, or stop passing it to the script once it is edited by hand. - Italic uses the cursive forms: и й ѝ п т = u ŭ ù n m, ц ш щ on u, д = ∂ (the 6 mirrored).
locl(features/cyrillic.fea): Bulgarian (BGR) д ж и й ѝ к л п т ц ш щ ю (*.loclBGR: g-shaped д, ascending ж and ю, u n m shapes, Λ л). These follow the Bulgarian convention and switch on for everylang=bgtext; they stay onlocl(not behind a stylistic set) because a Bulgarian UI is the one place they belong, but a Bulgarian reader has not approved them yet: if they are rejected, delete theBGRblock; Serbian and Macedonian (SRB,MKD) б г ѓ д п т (*.loclSRB), which differ only in the italic (δ-shaped б, ī and ī with acute, g-shaped д, n̄, m̄; the macrons a little lighter than i's dot, the one over ī at least 2.2 stems wide so it is not read as a dot, the ones over n̄ m̄ inset 0.3 of a stem). The upright Serbian б keeps the common form. The same file makes і and ј lose their dot under a top mark. Bulgarian з keeps the common form until a Bulgarian reader signs off a variant.- Anchors: every native letter has
topandbottom, so stress marks (а́ е́ и́ …) and the precomposed letters attach throughmark. - Kerning: look-alikes sit in their Latin base glyph's groups; other
Cyrillic sides join the Latin group with the same profile (Г Ґ Ѓ in
KO_T, У Ў inKO_Y, Ж inKO_K/KO_X, Д Ц Щ Ы on the right inKO_H, …). Sides without a Latin counterpart use Cyrillic groups whose values are taken, per master, from a Latin row or column:KO_Softsign-cy(the right of Ь Ъ Љ Њ: 0.55 × L's row before Т У Ч V Y and every quote group, which tuck in over the low bowl, B's row otherwise),KO_n-cy(left stems after Т Г У, fromn; ч joins it),KO_El-cy(Д Л Љ: 0.4 × A–O after A, else 0.25 ×KO_A),KO_Ya-cy(Я: 0.73 × O's column after T Y V W and quotes, nothing else, since its leg runs out at the foot),KO_el-cy/KO_de-cy(л љ д, fromn; the italic ∂ frome),KO_te-cy(т: r upright, n italic). Some sides tuck in like their Latin source in one part and must not in another: б ф and the italic ∂ are round below and rise into an ascender or flag; я Я ж reach out at the foot; Қ қ run a tail out. They get their own groups (KO_be-cy,KO_ef-cy,KO_de-cy,KO_ya-cy,KO_Ya-cy,KO_zhe-cy,KO_Kadescender-cy,KO_kadescender-cy) whose values come from the Latin source (o, e, x, K, k) but are held off (FLOOR1,FLOOR2inkern.py), in the band of heights where that part sits, so that every Cyrillic letter or punctuation mark beside them keeps a share of the white it keeps there from a Latin reference at Instrument's own kerning: 0.55 of what it keeps from h's ascender beside б ф ∂ (l on their right), 0.8 of what it keeps from x's or X's foot beside я Я ж, all of it beside Қ қ's tail as from K and k; and never less than 28 units. White wider than between two n's needs no holding off.scripts/cyrillic/ink.pymeasures this on the outlinesconstruct.pywrites, in every master. So Тб is −19 in Regular where Те is −106, and the italic Тд −7. ї next to ї or і gets a pair that keeps the space between their dots 1.5 times the space inside ї's dieresis; Ї Ї likewise. What still touches after that (ј under the tails of Ц Щ Д Ң, ї beside ђ ћ, the condensed Тў) is opened byscripts/kerning.pywith glyph pairs.
The fallback check is proof.py --set cyrillic --fallback onest (Onest with
ss02). Native readers have not reviewed these yet. Left as they are, on
purpose: the bold м keeps M's own joins (no notch, as Instrument's M has
none), the italic ф's q bowls meet the stem as q's do, and a lowercase
letter before a capital (ґТ) is not kerned, as nowhere else in the font.
Rebasing on a newer upstream
scripts/import_upstream.py --force regenerates sources/ from
UPSTREAM_COMMIT; it overwrites every master. Bump the commit, run it on a
branch, then re-apply the Tilo Sans glyphs from git history. It also resets two
artefacts of today's glyphsLib to what upstream shipped: the 0.13° slant on the
italic i/j components, and base classes on spacing accents.
Releases and how Tilo consumes them
Tilo does not build this repository. It vendors the default build's binaries
(fonts/default/*.ttf) in its UI toolkit's font assets
(crates/sdk/ui-core/assets/fonts/ in tilo/tilo) and records, in its font
catalog (crates/sdk/font-catalog/manifest.toml and the assets'
PROVENANCE.md), the tag and commit here they were built from and each
file's SHA-256. A change here reaches Tilo only when a release is copied in.
A release is a tag vMAJOR.MINOR matching the fonts' version (head.fontRevision,
name ID 5), e.g. v1.001. Build it reproducibly: build.py.lock fixes every
build dependency, and SOURCE_DATE_EPOCH set to the commit time fixes
head.modified, the only part of the output that otherwise follows the clock:
git checkout v1.001
SOURCE_DATE_EPOCH=$(git log -1 --format=%ct) uv run build.py
uv run scripts/shaping_tests.py
sha256sum fonts/default/*.ttf
| release | commit time (SOURCE_DATE_EPOCH) |
fonts/default/ SHA-256 |
|---|---|---|
v1.001 |
1791039400 | TiloSans[wdth,wght].ttf 140edddcc0ee4953e5120ac208757e88f48c786788bfdd092d2dba1bb49abef4, TiloSans-Italic[wdth,wght].ttf a4ca118abf92d41fa93f46a6833436bea98869eada3f07634a3594104dfad09c |
v1.002 |
1791160331 | TiloSans[wdth,wght].ttf 903aac545b421e1c050552980762613e9373b4a01b491e45c9737f42525888d6, TiloSans-Italic[wdth,wght].ttf 72d98aff60fd0447e5081c48895b08656aa9f2b219c0698866e97f3cd20c5743 |
To ship a release in Tilo: copy fonts/default/*.ttf and OFL.txt (as
LICENSES/Tilo-Sans-OFL.txt) into the toolkit's font assets, update both
copies of the manifest, PROVENANCE.md and LICENSES/FONT-COPYRIGHT-NOTICES.txt
there with the tag, commit and hashes, and run its tools/font-catalog.py
check. Regenerate the toolkit's feature test face with
scripts/toolkit_fixture.py from the same commit and copy it to
crates/sdk/ui-core/tests/fixtures/fonts/.