Skip to content
C.W.K.
Lesson 10 of 10 · published

Nothing Burns as a Box

~14 min · typography, measurement, hard-checks, evolution

Level 0Cold Workshop
0 XP0/58 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

A Failure That Raises Nothing

On 2026-10-17 a standalone film opened on the subtitle "안녕하세요, 피파예요" followed by a box where a waving hand belonged. Nothing in the pipeline had failed. Every stage had exited cleanly, every check had passed, and the box went out in the first second of the film. The owner did not treat it as one episode's typo: "이거 역시 고질적인 문제야. 어느 텍스트든 (특히 자막) 이모지 깨지는 거 영상 파이프라인에 새겨넣어야 해. 자막이나 카드에서 깨지는 이모지. 확인 절차까지" ("this is a chronic problem again. Emoji breaking in any text, subtitles especially, has to be carved into the video pipeline. Broken emoji in subtitles or cards. And a procedure to check it").

He was right that it was chronic, and the reason is a property of the drawing library, not of any one script. PIL never falls back. When a face lacks a character, PIL draws that face's missing-glyph shape, usually a box, sometimes a blank, and raises nothing. It had happened before: the é in Grassé on an explainer (2026-09-02), the minus sign U+2212 in a conversation episode's graph nodes, which drew from him "이 마이너스 부호 깨지는 거 여러번째야" ("this is the umpteenth time the minus sign breaks"), and now the 👋. The library this workshop uses also has no complex-text layout, so even a face that held every piece of a family emoji would draw it as three separate people, a flag as two letters, and a keycap as a bare digit.

Lesson 1 of track 7 taught that exit codes lie. This is the same lie told in pixels: a frame is a frame whether or not its text came out right, and a check that reads geometry, timing and file shape has no way to know what a glyph should have looked like.

One Primitive, Cut by What a Reader Sees

The fix was not a per-episode patch, because each earlier patch had fixed one string in one script. It was one text-to-pixels primitive, beacon/glyphs.py, that every string headed for the screen now passes through: subtitles, chapter and notice cards, names and voice tags, effect captions, every dub card, badges, and the thumbnail lettering the workshop draws itself (73186e1).

It begins by cutting text into graphemes, what a reader sees as one character: a ZWJ sequence, a flag pair, a keycap, a skin tone, a combining mark. A grapheme that Apple presents as an emoji is shaped and drawn by CoreText in Apple Color Emoji, the way the owner's own screens draw it, and set on the text's baseline. Every other grapheme is taken apart again, and each of its code points goes to the first face in the caller's chain whose character map actually holds that code point: the Korean face, then Helvetica Neue for accented Latin, then Hiragino for kana and Han. Consecutive characters that land in the same face are drawn together as one run. Nothing is guessed and nothing is swapped. A grapheme that no face can draw whole is never drawn as a box and never replaced by something that looks close; the primitive refuses it.

A Gate Before the Spend

Refusing at draw time is too late on its own, because by then the voice has been paid for. So the same primitive answers a question earlier: can every string this production will put on screen actually be drawn? The coverage gate runs before anything is spent. In the talk and travel pipelines, cut writes its verdict at the end of the script sheet the owner reads, and beacon mobile approve, revoice, picture, dub-voice and dub-cards refuse a grapheme no face draws, naming the place, the grapheme and its code points. The scripted series run it in their own pre-spend checks. The remedy is written in the procedure: rewrite the text; never drop or substitute a character silently.

The first version of the gate had a hole of exactly the kind this quest keeps finding. It covered the surfaces someone had listed: a dub's voiced subtitles, its cards and its badge. The first dub to need typographic boards redrawn showed that an authored dub also burns names, voice tags, the disclaimer, the opening notice, chapter cards and every translated region. All of those were drawn through the primitive and none were gated before the spend or recorded for the readback. The list was extended the same evening (a5000e3). One primitive guarantees how text is drawn; it does not guarantee that the gate has heard of every place text is drawn.

Read the Ink Back From the Encoded Frame

The gate proves that a face exists for every grapheme. It cannot prove that the delivered film shows it. For that, every emoji the primitive drew, and every run of characters it drew in a fallback face, is recorded with its box, and after the render out/glyph-readback.json compares the decoded frame with the ink the primitive drew there. Only the opaque ink is compared, one pixel eroded off the antialiased edge, so the background under a subtitle does not matter and an emoji with no colour is checked as well as a coloured one. Measured on the #483 specimens: the right glyph reads 0.8 to 5.1 mean difference, a box or a blank reads 51 to 135, and the line sits at 14. A miss stops the video stage. OCR was rejected for the job because it does not read emoji at all, and a catalogue of box shapes would need one entry per font.

Then the instrument had to be measured too. The Chinese dub of the same film stopped at the readback on 466 of 876 cards, and not one glyph was missing. Name tags and board body of 25 to 32 pixels keep little opaque ink after the erosion, and chroma subsampling smears what is left, so present glyphs read 14 to 21 while an absent overlay read 44 to 77. Small type is now held to its own line of 26, and a mask shorter than 1/24 second is listed as skipped instead of failed (a8de3cb). The rule goes by duration alone: the 3 ms joint between two shots that prompted it landed on no decoded frame, but a span that short can still hold one, and it is skipped all the same. The readback also found a defect it was never built for: every bold Chinese line, and the Han of every bold Japanese line, had been drawn in a regular face, because an index into the font collection pointed at the wrong weight, and the thin strokes read barely over the line (971ad1f). And one miscalibration had a long tail. Dense Han and kana read barely over the strict line while perfectly present, sessions answered by growing the type until it passed, and the cards lost their hierarchy; track 5's lesson "Pick a Role, Not a Pixel" tells how that ended.

Holding the Line in New Code

A primitive only works if nothing draws around it. A test walks every brief script dated from 2026-10-18 and fails one that builds its own font or draws text directly, with the instruction to use the shared calls. Briefs from before the primitive are left as they ran, because they are the record of what produced those films. That is the whole pattern of this lesson in four parts: one path for every string, a refusal before the spend that names the exact character, a readback that measures the delivered pixels, and a rule that keeps the next script on the path.

When a library fails silently, put one door in front of it and measure what comes out. A renderer that substitutes a box for a missing character will do it in the one place no one looked. Route every call through a primitive that refuses what it cannot draw, check coverage before you spend, and read the result back from the delivered artifact, then calibrate that readback against cases you know are present and cases you know are missing.

Code

The coverage gate (verbatim, beacon/glyphs.py, as of 2026-10-27)·python
def coverage_errors(items: Iterable[tuple[str, str, Chain]]) -> list[str]:
    """One line per grapheme that cannot be drawn, for every `(where, text,
    chain)` headed for pixels: the place, the grapheme, its code points and
    the whole string. The caller stops on any line; nothing is dropped or
    substituted."""
    out, seen = [], set()
    for where, text, chain in items:
        if not text:
            continue
        for g, why in missing(text, chain):
            key = (where, g, text)
            if key in seen:
                continue
            seen.add(key)
            shown = text if len(text) <= 120 else text[:117] + "…"
            out.append(f"{where}: {g!r} [{codepoints(g)}] {why} — in {shown!r}")
    return out
Sketch: the readback and its calibration, numbers from the commits of 2026-10-17·text
compare      decoded frame vs the ink the primitive drew, opaque pixels only
             (alpha >= 250, one pixel eroded), inside each emoji's or fallback run's box

measured     right glyph            0.8 - 5.1   mean |delta|
(x264 crf18) box or blank           51 - 135
             line                   14.0

small type   present, 25-32 px      14 - 21     (Chinese dub: 466 of 876 cards stopped)
             overlay absent         44 - 77
             line for small type    26.0

sub-frame    a mask under 1/24 s -> skipped by its duration, not failed
             (the 3 ms joint behind the rule landed on no decoded frame;
              a span that short can still hold one, and is skipped anyway)

per check    one passing sample decides it; a failing check reads every sample

# the instrument was right about the box and wrong about small type.
# both were found by measuring it against glyphs known to be present.

External links

Exercise

List every place your system turns text into something a person sees without reading it back: generated images, PDFs, emails rendered by a template, charts, labels on a dashboard. For each, find out what happens when the font or renderer lacks a character. Then feed one of them a string with an emoji sequence, an accented Latin letter and a mathematical minus sign, and look at the output yourself.
Hint
The question that matters is not whether the output looks fine for your usual text, but whether the renderer raises or substitutes when it cannot draw. If it substitutes, every string that reaches it needs a coverage check before it goes, and the delivered artifact needs a readback that knows what the glyph should have looked like.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.