AIFontGen research · 2026-11-05-v1
Unicode Combining Characters: A 61-Font NFC/NFD Audit
Font files observed on . This is the measurement time.
This audit tests Unicode combining characters in a dated snapshot of 61 public AIFontGen font files. It compares each file's character map with HarfBuzz output for composed and decomposed text. Use the results to investigate a specific font and sequence; download the input strings, file hashes and method to repeat the checks.
What a missing character-map entry tells you
A missing combining-mark entry does not, by itself, tell you the output of every sequence containing that mark. A shaping engine may use a precomposed character available in the font for a particular sequence. Another sequence with the same mark can still produce a missing glyph.
That distinction is the focus of this audit. We record nominal character-map coverage, output containing glyph 0, and equality of the resulting glyph IDs and positions as separate observations. None of these is a certificate of complete language support.
The test set contains 497 Latin-letter canonical pairs derived from Unicode 15.0.0 and four explicit controls without a precomposed form. Each of 61 files is tested against all 501 cases, giving 30,561 font–case combinations. The downloadable results identify every file and case rather than treating that total as a quality score.
Across the 30,317 canonical font–case pairs, 4,816 pairs in 51 files lacked complete direct NFD character-map coverage yet produced the same nonmissing glyph IDs and positions as NFC. Eight pairs in two files had nonmissing but different output. Five pairs in two other files had no missing glyph from NFC but did from NFD. Separately, 232 of the 244 control font–case pairs produced glyph 0.
NFC, NFD and the actual shaping output
The same abstract letter can have different codepoint sequences. For example, é can be U+00E9, or U+0065 followed by U+0301 COMBINING ACUTE ACCENT. Unicode's NFC and NFD normalization forms provide defined ways to compose and decompose canonically equivalent text. They do not make every arbitrary base-and-mark sequence into a precomposed character. Unicode Standard Annex #15
A font's cmap table answers a narrower question: which nominal glyph does this font map to a codepoint? HarfBuzz also processes the sequence and its shaping context. Its normalization implementation can use a supported precomposed form when a combining mark lacks its own nominal mapping. That is a possible mechanism for a measured result, rather than evidence that a font supports the mark in every context. HarfBuzz 14.4.0 normalization implementation
A measured pair and a control
In Bubble Heart Sprinkle version 0.1.3, the tested file has a nominal mapping for U+00E9 and none for U+0301. HarfBuzz 14.4.0 nevertheless returned glyph 79 for both U+00E9 and U+0065 U+0301, with an x advance of 604 font units, zero y advance and zero offsets. The file uses 1,000 units per em.
The control q́ — U+0071 U+0301 — returned glyph 121 followed by glyph 0 in the same environment. It has no precomposed counterpart in this test. Successful output for é therefore did not establish support for arbitrary acute-accent combinations.
File SHA-256: e12d2184032f9690a69b5aac2ca2aa30784899dade09f2f6c9864e5a2a4753f9.
There is also a reason to keep “no glyph 0” separate from “identical output.” In Acorn Birch version 0.1.2, four canonical pairs produced no glyph 0 in either form but different glyph sequences. One is U+01EC versus U+004F U+0328 U+0304. The composed input returned glyphs 114, 99 and 90; the decomposed input returned glyphs 329 and 99. Both had a total horizontal advance of 311 font units. These observations do not establish whether their rendered outlines look the same.
The opposite limitation also appeared: five canonical cases produced no glyph 0 from the NFC input but did produce glyph 0 from NFD. They were U+1EB4 and U+1EB5 in Amber Brook, and U+1EAF, U+1EB1 and U+1EB5 in Gothic Tattoo Iron. These counterexamples are part of the results, so the audit does not claim universal NFC/NFD compatibility even within the measured snapshot.
Unicode combining characters: results by font
Each canonical-case column has a denominator of 497 per file. “Same, no glyph 0” requires both forms to avoid the missing glyph and return the same glyph IDs, advances and offsets. “Missing cmap, same output” is its subset with incomplete nominal NFD coverage. The last column covers four separate controls. These are finite observations, not font quality scores.
| Font / manifest version | NFC no glyph 0 / 497 | NFD no glyph 0 / 497 | Same, no glyph 0 / 497 | Missing cmap, same output / 497 | Different, no glyph 0 / 497 | Controls with glyph 0 / 4 |
|---|---|---|---|---|---|---|
| abbey-textura0.1.3 | 53 | 53 | 53 | 53 | 0 | 4 |
| acorn-birch0.1.2 | 333 | 333 | 329 | 0 | 4 | 1 |
| acorn-vetch0.1.1 | 161 | 161 | 161 | 161 | 0 | 4 |
| agate-acorn0.1.1 | 0 | 0 | 0 | 0 | 0 | 4 |
| agate-gantry0.1.1 | 58 | 58 | 58 | 58 | 0 | 4 |
| amber-brook0.1.1 | 179 | 177 | 177 | 177 | 0 | 4 |
| amore-nib0.1.2 | 53 | 53 | 53 | 53 | 0 | 4 |
| amour-shadow0.1.3 | 53 | 53 | 53 | 53 | 0 | 4 |
| anchor-script0.1.2 | 53 | 53 | 53 | 53 | 0 | 4 |
| atelier-light0.1.6 | 91 | 91 | 91 | 57 | 0 | 1 |
| atelier-wide0.1.2 | 91 | 91 | 91 | 57 | 0 | 1 |
| atrium-sans0.1.4 | 53 | 53 | 53 | 53 | 0 | 4 |
| bandit-slab0.1.2 | 53 | 53 | 53 | 53 | 0 | 4 |
| banner-fraktur0.1.2 | 53 | 53 | 53 | 53 | 0 | 4 |
| basalt-lumen0.1.1 | 333 | 333 | 329 | 0 | 4 | 1 |
| beacon-slab0.1.3 | 53 | 53 | 53 | 53 | 0 | 4 |
| bellbottom-serif0.1.3 | 53 | 53 | 53 | 53 | 0 | 4 |
| birch-quill0.1.1 | 0 | 0 | 0 | 0 | 0 | 4 |
| birthday-candle-caps0.1.2 | 222 | 222 | 222 | 222 | 0 | 4 |
| bobbin-trillium0.1.1 | 161 | 161 | 161 | 161 | 0 | 4 |
| bouquet-script0.1.2 | 53 | 53 | 53 | 53 | 0 | 4 |
| bracken-knoll0.1.1 | 0 | 0 | 0 | 0 | 0 | 4 |
| bracken-quill0.1.1 | 161 | 161 | 161 | 161 | 0 | 4 |
| brigade-wide-stencil0.1.2 | 53 | 53 | 53 | 53 | 0 | 4 |
| brook-thistle0.1.1 | 58 | 58 | 58 | 58 | 0 | 4 |
| brush-roman-press0.1.2 | 205 | 205 | 205 | 205 | 0 | 4 |
| brush-shelf-slab0.1.2 | 216 | 216 | 216 | 216 | 0 | 4 |
| bubble-balloon-party0.1.3 | 53 | 53 | 53 | 53 | 0 | 4 |
| bubble-confetti-round0.1.3 | 53 | 53 | 53 | 53 | 0 | 4 |
| bubble-cookie-soft0.1.3 | 53 | 53 | 53 | 53 | 0 | 4 |
| bubble-heart-sprinkle0.1.3 | 53 | 53 | 53 | 53 | 0 | 4 |
| bubble-jelly-bean0.1.3 | 53 | 53 | 53 | 53 | 0 | 4 |
| bubble-pebble-sans0.1.3 | 230 | 230 | 230 | 230 | 0 | 4 |
| bubble-pillow-square0.1.2 | 53 | 53 | 53 | 53 | 0 | 4 |
| bubble-puffy-serif0.1.2 | 53 | 53 | 53 | 53 | 0 | 4 |
| bubble-ribbon-knot0.1.2 | 53 | 53 | 53 | 53 | 0 | 4 |
| bubble-squircle-block0.1.2 | 53 | 53 | 53 | 53 | 0 | 4 |
| buttress-bowl-sans0.1.2 | 220 | 220 | 220 | 220 | 0 | 4 |
| candy-heart-slab0.1.2 | 213 | 213 | 213 | 213 | 0 | 4 |
| crest-fraktur0.1.2 | 53 | 53 | 53 | 53 | 0 | 4 |
| cursive-letterpress-slant0.1.2 | 53 | 53 | 53 | 53 | 0 | 4 |
| cursive-ribbon-flow0.1.3 | 53 | 53 | 53 | 53 | 0 | 4 |
| cursive-school-upright0.1.2 | 53 | 53 | 53 | 53 | 0 | 4 |
| cursive-soft-bounce0.1.2 | 53 | 53 | 53 | 53 | 0 | 4 |
| dovetail-quill0.1.1 | 58 | 58 | 58 | 58 | 0 | 4 |
| dugout-script0.1.2 | 53 | 53 | 53 | 53 | 0 | 4 |
| executive-sign0.1.2 | 53 | 53 | 53 | 53 | 0 | 4 |
| gothic-blackline0.3.2 | 0 | 0 | 0 | 0 | 0 | 4 |
| gothic-fraktur-split0.1.2 | 53 | 53 | 53 | 53 | 0 | 4 |
| gothic-lombardic-round0.3.1 | 0 | 0 | 0 | 0 | 0 | 4 |
| gothic-modern-open0.1.2 | 207 | 207 | 207 | 207 | 0 | 4 |
| gothic-rotunda-broad0.1.2 | 215 | 215 | 215 | 215 | 0 | 4 |
| gothic-tattoo-iron0.1.3 | 206 | 203 | 203 | 203 | 0 | 4 |
| gothic-tattoo-spire0.1.3 | 53 | 53 | 53 | 53 | 0 | 4 |
| hull-block0.1.2 | 53 | 53 | 53 | 53 | 0 | 4 |
| kite-grotesk0.1.2 | 0 | 0 | 0 | 0 | 0 | 4 |
| margrave-fraktur0.1.2 | 53 | 53 | 53 | 53 | 0 | 4 |
| marker-broad-flat0.3.2 | 0 | 0 | 0 | 0 | 0 | 4 |
| marker-quick0.3.2 | 0 | 0 | 0 | 0 | 0 | 4 |
| needle-formal0.1.2 | 241 | 241 | 241 | 241 | 0 | 4 |
| saga-rotunda-shadow0.1.2 | 53 | 53 | 53 | 53 | 0 | 4 |
How to read the per-font results
Download the per-font summary or all per-case flags. The page table reports each of the 61 files.
For each font, the downloadable summary distinguishes direct nominal NFC and NFD coverage from shaped output. The page table shows NFC and NFD output without glyph 0, identical nonmissing glyph-position output, incomplete NFD cmap with equal output, differing nonmissing paths and the controls. Open the per-case results to see the actual input codepoints and output records.
An equality result compares glyph ID, x/y advance and x/y offset within the same file. Glyph IDs are local to a font: matching numbers in two different fonts have no comparative meaning. Raw records retain cluster information and glyph flags, but those fields are not used to require identical input representation from strings with different codepoint counts. Identical output containing a missing glyph is not counted as a successful canonical match.
The four controls are reported separately from the canonical pairs. Their inputs are unchanged by NFC/NFD; they test specified nonprecomposable combinations rather than adding four more canonical equivalents.
Reproduce the audit
The public manifest identifies each measured file, its font version and SHA-256. The cases file records the UnicodeData source hash, construction rule and exact input strings. The results include nominal mappings and full shaped output for each case.
The recorded runtime uses Python 3.12.13, FontTools 4.65.0, uharfbuzz 0.56.1 and HarfBuzz 14.4.0. The Python binding and shaping engine versions are different fields. Shaping uses the OpenType shaper, left-to-right direction, Latin script, English language, default buffer flags and features, and no variation overrides. Both font-scale axes use units per em. Each call uses one font without cross-font fallback. HarfBuzz default normalization and fallback mark positioning remain enabled; this audit does not attribute every position to font GPOS data.
Download the manifest, test cases, methodology, runtime, compressed raw output and reproduction script. From a directory containing the frozen inputs, run python reproduce.py --root . --output ./recomputed --download in the recorded environment. The script downloads the pinned public preview files and rejects a hash mismatch; the data package does not redistribute font binaries.
The full experiment was repeated from the frozen inputs, with all 66 generated output files matching byte for byte. Independent checking also re-downloaded six files and compared all 501 cases for each.
Scope and limitations
This is an audit of an owned commercial inventory, not a random sample of fonts or a comparison of AI font services. The results apply to the hashed files, finite test set and recorded shaping settings. Replacing a file or changing the engine, features, language or script can change an observation.
We did not score rasterized appearance, accent aesthetics, browser behavior or application compatibility. HarfBuzz selects and positions glyphs; the output still needs to be rendered. A nonzero glyph can have an unsuitable outline, and different glyph sequences can draw similar shapes. HarfBuzz manual
The audit does not establish complete language coverage, all combining-character combinations, every NFD string, or industry-wide prevalence. Before using a font in a finished design, test the actual text in the intended application.
Data versions and corrections
Snapshot: 5 November 2026, 12:56:33 UTC. Data version: 2026-11-05-v1. Checksums identify the published supporting files. The limitations and exception records remain part of the result. Report a reproducible correction by email at support@aifontgenerator.net, including the file hash and case ID.
The snapshot includes the 61 files linked from public catalog pages 1–3, using one primary current-version preview file per family. Sixty detail pages were in the sitemap; Bubble Pillow Square was publicly listed but omitted from it. Sitemap membership was not used as a substitute for public availability.
The earlier language character reference compares CLDR-derived character sets with a synthetic ASCII baseline. This audit uses real font files and shaped sequence output. Its measurements are independent of that reference's simulated coverage counts.
Data downloads and verification
Download the files into one directory. The manifest pins the original public preview URLs and byte hashes; font binaries are not redistributed here. The raw gzip contains 61 font headers and 30,561 shape records. The checksum file covers every other attachment.
- Dataset guide and population (dataset.json)
- Exact font versions, URLs and SHA-256 (manifest.json)
- All 501 input cases (cases.json)
- Per-font results (results.json)
- Per-font results (CSV) (font-summary.csv)
- All 30,561 font–case rows (CSV) (results.csv)
- Complete raw glyph output (gzip JSONL) (raw-shapes.jsonl.gz)
- All 13 counterexamples and selected examples (exceptions.json)
- Methodology (methodology.json)
- Limitations (limitations.json)
- Runtime versions and settings (runtime.json)
- Sources and attribution (sources.json)
- Shaping reproduction script (reproduce.py)
- Test-case construction script (build_cases.py)
- Attachment SHA-256 checksums (checksums.json)