
Android showed some Hangul as blank gaps: NanumSquare's cmap claimed 11,172 syllables but drew only 2,479
When a font's cmap says it has a character it cannot draw, Android uses that font anyway and paints a blank instead of falling back to the system font. Removing the empty entries from the cmap fixed it.
On the night of 2026-09-15, the operator of our Korean spelling checker, SugarSpeller, noticed that the rare syllable 밣 showed up in the input box but turned into an empty gap inside the chat bubble. The cause was the bundled NanumSquare Light font. Its cmap (the table that maps character codes to glyph numbers) listed all 11,172 precomposed Hangul syllables, yet 8,693 of those glyphs had zero bytes of outline data. We removed 13,948 outline-less entries from the cmap in commit f27f36a, shipped in version 1.6.017.
Sources: commit f27f36a, the CHANGELOG entry for 1.6.017 and the session log in SugarMount's app repository (sugarspeller). All numbers were re-measured on 2026-10-22 from the font files before and after the fix, using only the Python standard library, and match the commit.
Contents (10)
- The glyph for 밣 had 0 bytes and a width of 910
- Re-measured: only 2,479 syllables had outlines
- Only the chat bubble broke because it was a TextView
- The agent's first check said 밣 was there
- The font was probably shipped this way
- We trimmed the cmap instead of swapping fonts or adding a fallback chain
- When this check does not work
- How to check the fonts in your own app
- What is left
- FAQ
The glyph for 밣 had 0 bytes and a width of 910
Android's text layout engine, Minikin, picks the font for each character by reading the cmap. It never checks whether the outline is empty. Following 밣 (U+BC23) through the file gives a four-step chain.
- The cmap maps U+BC23 to glyph 12552.
- The loca table (where each glyph's data starts and ends) gives glyph 12552 a length of 0.
- The hmtx table still gives it an advance width of 910, the same as a normal syllable such as 받. So the reader sees a one-character gap, not a tofu box. All 8,693 empty syllables had width 910.
- Since the app font already answered yes, no fallback to the system font happens.
Once the entry is out of the cmap, the app font says no and Android finds the character in the system font (Figure 1). The cmap went from 17,559 entries to 3,611.

Re-measured: only 2,479 syllables had outlines
These values come from reading the tables directly with Python's struct module, without fontTools.
| Item | Before | After |
|---|---|---|
| Hangul syllables in the cmap | 11,172 | 2,479 |
| Of those, 0-byte outlines | 8,693 | 0 |
| Visible characters with no outline (spaces and controls excluded) | 13,948 | 0 |
| Characters with outlines | 3,575 | 3,575 |
| Glyph count | 18,155 | 18,155 |
| File size (bytes) | 750,932 | 715,448 |
The 13,948 split into 8,693 Hangul syllables, 4,890 Han characters (hanja) and 365 others, including the bullet • and several arrows. Not a single hanja could be drawn by this font. The 2,479 surviving syllables are all 2,350 syllables of KS X 1001, the legacy Korean character set that covers everyday text, plus 129 more. Those 129 include 뷁, which is outside KS X 1001. A test that used 뷁 as its "rare syllable" would have passed.
Only the chat bubble broke because it was a TextView
The symptom as first described: visible in the input box, blank in the bubble, fine on the Classic screen. The app theme applied this font to every TextView through android:textViewStyle, and the bubble was a TextView. The input box and the Classic result box were EditTexts. According to the commit, AppCompat's EditText does not pick up the theme's android:editTextStyle, so they used the system font. The Classic screen survived by accident. Every TextView in the app was affected.
The agent's first check said 밣 was there
An AI coding agent found the cause, but its first measurement pointed the wrong way. It only checked whether 밣 was in each font's cmap: NanumSquare said yes, and the Gamja Flower subset used for greeting cards said no. What turned it around was file size. A font with 17,559 cmap entries in 750 KB looked too small to hold every syllable, so the agent measured outline lengths. The 8,693 figure came out 1 minute 35 seconds after the problem was raised.
The first trial fix was wrong too. It removed 13,991 entries without filtering, including 32 control characters and 3 space characters that are supposed to be empty. Right after the operator approved the plan, the agent re-filtered by Unicode category before applying it, and the final count became 13,948.
The third mistake was caught by a person. After committing, the agent said it could not check rendering on a device and handed the visual check to the operator, even though it had booted an Android 14 emulator in software mode the day before in the same session. The operator asked for a deep review and cross-check before release, and granted the KVM permission the agent asked for. At 08:37 the next morning, an Android 14 emulator showed both 밣받다 and • fully drawn.
The font was probably shipped this way
The commit message guessed that "a subsetting tool removed the glyphs but kept the cmap". While writing this note we ran the same measurement on the NanumSquare files the site uses to draw figures. NanumSquare ExtraBold matched the app's file exactly: 8,693 empty syllables, 13,948 visible characters without outlines. NanumSquare Regular had 8,822 empty syllables (Figure 2).

The site's files are weights the app never had, so they are not copies. All three carry a 2016 "Version 1.000" build string, and the app's file was already in the repository's first commit on 2020-12-16. We have not compared them with the official download, so the commit's guess stays unconfirmed. For contrast, the properly cut Gamja Flower subset listed only 2,349 syllables in its cmap.
We trimmed the cmap instead of swapping fonts or adding a fallback chain
NanumGothic, already in the app at 4,691,820 bytes, draws all 11,172 syllables, but switching to it changes the look of the whole app. Replacing the file with a complete NanumSquare Light was estimated at about 4 MB more, and the measurements above make it unclear whether such a file exists. Outside the session we also considered a fallback chain with Typeface.CustomFallbackBuilder (API 29). It does not help, because the font for each character is still chosen by cmap.
Trimming the cmap keeps every glyph in place, so the result can be checked one to one against the original. The cost is that rare syllables and hanja appear in the system typeface. If you bundle a font in a Korean app, do not trust cmap coverage to tell you it "supports Hangul". Add a test that measures outline lengths.
When this check does not work
- CFF-based OTF fonts have no loca table. The app's regression test treats the length as -1 there and passes silently.
- Fonts with only a format 12 cmap: the test reads format 4 only and passes on an empty map.
- Characters the system font also lacks still show tofu after the trim.
- Screens where every character must share one typeface need a different font.
We checked View-based TextViews on one Android 14 emulator. We did not check Compose Text.
How to check the fonts in your own app
- List every font in
res/fontandassets. - For each font, read glyph numbers from the cmap and lengths from loca, and count entries with length 0. Skip Unicode categories Cc, Cf, Zs, Zl, Zp and the three Hangul fillers (U+115F, U+1160, U+3164).
- Compare the number of drawable syllables with the 2,350 of KS X 1001.
- Pick on-screen test characters from the step 2 output.
Step 2 takes the 24 lines below. On the original file it prints blank 13948 / hangul 11172, drawn 2479. On a CFF-based OTF font it stops with KeyError: 'loca', because there is no loca table, and on a font without a format 4 subtable it prints hangul 0 (checked on 2026-10-23 with Noto Sans KR OTF and with a copy whose format 4 subtables were relabeled). If you see hangul 0, check the subtable formats first.
import struct, sys, unicodedata
d = open(sys.argv[1], "rb").read()
u16 = lambda p: struct.unpack_from(">H", d, p)[0]
u32 = lambda p: struct.unpack_from(">I", d, p)[0]
T = {d[12+16*i:16+16*i].decode("latin1"): u32(20+16*i) for i in range(u16(4))}
def glyph_len(g): # loca: short(0) / long(1)
lo = T["loca"]
if u16(T["head"] + 50): return u32(lo+4*g+4) - u32(lo+4*g)
return 2 * (u16(lo+2*g+2) - u16(lo+2*g))
cmap = {}
for i in range(u16(T["cmap"] + 2)): # format 4 subtables only
s = T["cmap"] + u32(T["cmap"] + 8 + 8*i)
if u16(s) != 4: continue
n = u16(s + 6); e = s + 14; st = e + n + 2; de = st + n; ro = de + n
for k in range(0, n, 2):
a, b, dl, r = u16(st+k), u16(e+k), struct.unpack_from(">h", d, de+k)[0], u16(ro+k)
for c in range(a, min(b, 0xFFFE) + 1):
g = (c + dl) & 0xFFFF if r == 0 else u16(ro + k + r + 2*(c - a))
if r and g: g = (g + dl) & 0xFFFF
if g: cmap[c] = g
ok = lambda c: c in (0x115F, 0x1160, 0x3164) or unicodedata.category(chr(c)) in ("Cc", "Cf", "Zs", "Zl", "Zp")
blank = [c for c, g in cmap.items() if glyph_len(g) == 0 and not ok(c)]
han = [c for c in cmap if 0xAC00 <= c <= 0xD7A3]
print(f"blank {len(blank)} / hangul {len(han)}, drawn {sum(glyph_len(cmap[c]) > 0 for c in han)}")
The cleanup itself was tools/trim_blank_cmap.py from commit f27f36a. With fontTools it deletes, in every cmap subtable, the entries that point to outline-less glyphs, and keeps the characters skipped in step 2. It refuses fonts without a glyf table.
font = TTFont(src)
glyf = font["glyf"]
for table in font["cmap"].tables:
cmap = getattr(table, "cmap", None) or {}
for cp, name in list(cmap.items()):
if glyf[name].numberOfContours == 0 and not is_blank_ok(cp):
del cmap[cp]
font.save(dst)
- After trimming, confirm every removed entry was empty and the count of characters with outlines is unchanged (3,575 here).
- Put the original font back and make sure the test fails, as in 3,066 green tests hid a line that breaks opening the database: checking tests by deleting the guard.
- Check that
base/res/fontin the release AAB holds the fixed file.
What is left
The site's figure and share-image tools use those same NanumSquare files, so any syllable outside KS X 1001 would come out blank in a figure, so this note keeps 밣 out of its title and figures. Swapping the figure font and teaching the app's test about CFF and format 12 are still open.
FAQ
Why do some Korean characters show as blank spaces in my Android app?
Most likely your bundled font lists them in its cmap but has empty outlines for them. Measuring the outline length behind each cmap entry shows it at once.
How do I tell a tofu box (□) from a blank gap?
A tofu box means no font, fallbacks included, has the character. A gap one character wide means the cmap lists it but the outline is empty. Every empty syllable in this font had width 910.
Which characters should I use to test a Korean font?
Ones the font actually cannot draw, found by inspecting it. This font drew 뷁 but not 밣.
This note is written from values SugarMount measured itself; an AI coding agent was used to draft it.
More AX notes
- Pre-gzipping Android assets, measured: zero APK savings, and .pack.gz became .pack with a FileNotFoundException
- A market:// link opens a store chooser on SKT-sold Samsung phones: set the Play package first
- A splash that never moved on: addOnCompleteListener(this) and onStop
- "6.7" < "6.11" is false: comparing Remote Config versions as strings froze content updates
- A reverse-construction generator and the gate behind 1,100 arrow-puzzle levels
- Daily users rose after the redesign while organic D1 retention fell from 15.2% to 11.2%