Skip to content

feat(fonts): bundle families for Georgian, Armenian and Korean - #539

Open
DemchaAV wants to merge 3 commits into
feat/rtl-arabic-shapingfrom
feat/fonts-more-scripts
Open

feat(fonts): bundle families for Georgian, Armenian and Korean#539
DemchaAV wants to merge 3 commits into
feat/rtl-arabic-shapingfrom
feat/fonts-more-scripts

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Why

Georgian, Armenian and Korean had no covering family in the bundled set. A document
written in any of them rendered as rows of question marks — the substitution is logged at
WARN and the PDF still opens, so nothing about the output announced that the font was the
problem — unless the author registered a font of their own.

What

Three families join the catalog, shipping in the same graph-compose-fonts 1.1.0 as the
Arabic and Hebrew pair: NOTO_SANS_GEORGIAN, NOTO_SANS_ARMENIAN and GOTHIC_A1.

Each covers the second case its script has, which is the part that is easy to lose:
Armenian capitals and the և ligature that is the word "and"; Georgian Mtavruli, the
capitals a heading is set in, which Unicode encodes in a block of its own (U+1C90) far from
the lowercase range; and for Korean both the 11 172 precomposed syllables and the
conjoining jamo a decomposed (NFD) string is made of.

Gothic A1 was chosen over the better-known Korean families for its Latin. A paragraph is
drawn in one family, so a Korean sentence holding a European name is drawn entirely in the
Korean font — Nanum Gothic covers 17 % of Latin-1 and no Cyrillic, which turns Müller
into M?ller while the Korean around it renders perfectly. Gothic A1 covers Latin-1,
Latin Extended-A, Cyrillic and Greek in full.

Georgian and Armenian upstream publish only as variable fonts with no static face to take.
The bundled file is that variable font; a PDF applies no instancing and draws the fvar
default, which for these two is regular — so bold resolves to it and renders unemboldened.
That default is now asserted rather than assumed: the Noto CJK variable fonts default to
weight 100, so the same vendoring done with one of those would have shipped a family that
renders every page hairline Thin with full coverage and nothing failing.

Chinese and Japanese stay unbundled and the docs say why rather than leaving it looking
like an omission: the official static Noto CJK faces use CFF outlines, which the PDF
backend cannot embed at all, and the variable ones carry the weight-100 default above.

fonts/README.md gains a "which script needs which family" table, measured against the
shipped binaries. Two rows written from reputation rather than from the cmap were wrong
and are corrected: Hebrew is carried by Tinos and Cousine as well as David Libre, and
Latin Extended-A and Vietnamese are not covered by "any bundled family" — nine families
cover almost none of either.

Three guards also stop keying on the absence of a marker and start keying on the signal
they exist for — the script demos assert their script reached the page rather than only
that no ? did; the stale-artifact diagnostic is driven through every family that needs
it instead of one sample; and the fonts and emoji version pins are discovered across the
poms that declare them rather than listed two at a time, which was already two short.

Tests

  • BundledScriptCoverageTest — holds every row of the README table to the binaries, so
    the table fails a build rather than going stale.
  • KoreanFontCoverageTest — all 11 172 syllables in both faces, both jamo forms, and the
    Latin-1 / Latin Extended-A / Cyrillic / Greek the docs promise; bold is a distinct face.
  • GeorgianArmenianFontCoverageTest — both cases of each script, and that the variable
    fonts resolve to their regular instance.
  • BundledFamiliesTest — the families are listed, the faces upstream does not ship
    collapse onto a real resource, and every family needing the stale-artifact diagnostic
    gets it.
  • KoreanGlyphsDemoTest, GeorgianArmenianGlyphsDemoTest — render each script (Korean
    alongside accented Latin, and decomposed) and read the PDF back to prove the glyphs
    reached the page.
  • available_fonts_preview layout snapshot refreshed: the showcase grows from five pages
    to six.
  • Full reactor gate green (clean verify across core, render-pdf/docx/pptx, templates,
    testing, qa, coverage).

@DemchaAV DemchaAV changed the title feat(fonts): bundle families for Georgian and Armenian feat(fonts): bundle families for Georgian, Armenian and Korean Aug 11, 2026
@DemchaAV
DemchaAV force-pushed the feat/fonts-more-scripts branch from 6efa059 to d9466db Compare August 11, 2026 07:15
@DemchaAV

Copy link
Copy Markdown
Owner Author

Rebased onto the updated feat/rtl-arabic-shaping and pushed d9466db7 with the
coverage and runbook corrections.

Script blocks are swept by letter, not by hand-picked range. The Georgian assertions
stopped at U+10FA and U+1CBA, where those blocks used to end; Unicode has since assigned
letters past both and the bundled family carries them, so a test reporting complete
coverage had not looked at them. Extending the ranges then failed on U+1CBB and U+1CBC —
which are unassigned. A block is not a solid run of letters: it has gaps, and punctuation
and modifiers sit among them. Instead of a hand-kept list of holes needing an edit every
Unicode release, the sweep asks whether each code point is a letter and skips the rest.

Amiri is swept in all four faces it ships rather than three. The sibling assertion that
its faces never collapse onto one another meant the fourth was carrying the whole script
with nothing checking that it could draw it.

On the fonts version. graph-compose-fonts:1.1.0 is unreleased — fonts-v1.1.0 is
not tagged (only fonts-v1.0.0 exists) and Central returns 404 for it — so accumulating
Georgian, Armenian and Korean into that version is the intended shape, not a conflict
with the Arabic/Hebrew PR. What is real is the sequencing constraint, and the release
runbook now states it: the tag goes up only after every family meant for a version has
landed, because a published artifact is immutable while the catalog goes on naming that
version as the one a family arrived in.

The same runbook said three things about the fonts artifact that had stopped being true,
each in the direction of doing too little — that core carries no pin on it (it does, at
test scope, so a single-module build resolves it from the local repository), that the
engine needs no fonts bootstrap, and that two poms re-pin the version where six do.
Corrected, and VersionConsistencyGuardTest now discovers the declaring poms rather than
listing them, so the list cannot go short again.

Full reactor gate green.

Georgian and Armenian had no covering family in the bundled set, so a
document written in either rendered as rows of question marks unless the
author registered a font of their own.

Noto Sans Georgian and Noto Sans Armenian join the catalog, shipping in
the same graph-compose-fonts 1.1.0 as the Arabic and Hebrew pair. Both
cover their script in full — Georgian in Mkhedruli and in Mtavruli, the
capitals a heading is set in, which Unicode encodes in a block of its own
far from the lowercase range. Upstream publishes them as variable fonts
with no static weights to take, so the artifact carries the regular
instance and every other face resolves to it: bold renders unemboldened
rather than failing, which the tests state rather than leave to be found.

The family list the measurement/render width-parity test walks is now
read off the catalog instead of repeated, so a family added to the
catalog is covered the day it lands.
Korean had no covering family, so a document written in it rendered as
rows of question marks unless the author registered a font of their own.

Gothic A1 joins the catalog with a drawn regular and bold. It carries all
11 172 precomposed Hangul syllables, both jamo forms, and — unlike the
better-known Korean families — Latin-1, Latin Extended-A, Cyrillic and
Greek. That last part decided it: a paragraph is drawn in one family, so
a Korean sentence holding a European name is drawn in the Korean font,
and a family with ASCII but no accents turns Müller into M?ller with the
Korean around it rendering perfectly. Hanja are not covered.

Chinese and Japanese stay unbundled, and the docs now say why rather
than leaving it looking like an omission: the official static Noto CJK
faces use CFF outlines, which the PDF backend cannot embed at all, and
the variable ones default to weight 100 — the weight a PDF draws, since
it applies no instancing.

The script table in the fonts README was measured against the shipped
binaries and two of its rows were wrong. Hebrew was listed as reachable
only through David Libre, while Tinos and Cousine carry the whole block;
Latin Extended-A and Vietnamese were listed as covered by any bundled
family, when nine families cover almost none of either. Both rows are
corrected and the whole table is now held to the binaries by a test, so
it fails a build instead of going quietly stale.

Three guards stop keying on the absence of a marker and start keying on
the signal they exist for: the script demos assert their script actually
reached the page rather than only that no '?' did, the stale-artifact
diagnostic is driven through every family that needs it instead of one
sample, and the fonts and emoji version pins are discovered across the
poms that declare them rather than listed two at a time.

The Georgian and Armenian binaries are the variable fonts upstream
publishes, not static instances, and the comments describing them said
otherwise. What makes them safe is that their wght axis defaults to 400,
so that is now asserted — the same vendoring done with a Noto CJK
variable font would have shipped hairline Thin with nothing failing.
The Georgian assertions stopped at U+10FA and U+1CBA, where the blocks
used to end. Unicode has since assigned letters past both, and the
bundled family carries them — so a test that reported complete coverage
had not looked at them. Extending the ranges then failed on U+1CBB and
U+1CBC, which are unassigned: a block is not a solid run of letters, it
has gaps, and punctuation and modifiers sit among them.

Rather than carry a hand-kept list of holes — a list that needs an edit
every Unicode release — the sweep asks whether each code point is a
letter and skips the rest. The range stays whole and the claim stays
honest without anything to maintain.

Amiri is swept in all four of the faces it ships, not three. The sibling
assertion that the faces never collapse onto one another meant the
fourth carried the whole script with nothing checking it could draw it.

The release runbook said three things about the fonts artifact that had
stopped being true, each in the direction of doing too little: that core
carries no pin on it (it does, at test scope, so a single-module build
resolves it from the local repository), that the engine needs no fonts
bootstrap, and that two poms re-pin the version where six do. It also
now says when the tag may be cut — after every family meant for that
version has landed, since a published artifact is immutable while the
catalog goes on naming it as the version a family arrived in.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant