// lesson: drawing-text-bitmap-fonts

Drawing Text: Bitmap Fonts First

An editor's rendering budget is 99% text, so this is where the framebuffer starts earning its keep. Real font rendering is one of the deepest rabbit holes in systems programming β€” outlines, hinting, subpixel antialiasing, shaping β€” so we take the classic route: get a bitmap font working end-to-end first, with the right abstractions, then survey how the real stack slots into the same interfaces.

The vocabulary: baseline, ascent, descent, advance

Text is not positioned by its top-left corner. Every professional text API β€” FreeType, DirectWrite, CoreText, the lot β€” positions glyphs on the baseline: the invisible line the letters sit on. Above it, a font reserves the ascent (tall letters, capitals); below, the descent (the tails of g, y, p). Per glyph, the advance says how far the pen moves rightward for the next glyph β€” for proportional fonts it varies (i narrow, W wide); for monospace it doesn't.

Why baseline-relative? Because mixed content must align. If you ever draw two fonts on one line β€” or an emoji next to text β€” their baselines must coincide, not their tops. Adopting the convention now costs one subtraction (top = baseline - ascent) and saves a rewrite later. A line of text in a box of height line_height = ascent + descent + line_gap puts its baseline at box_top + ascent; that formula appears in every paint function you'll write from here to the final challenge.

The humble bitmap font

Our font is the time-honored terminal format: every printable ASCII glyph is an 8Γ—16 monochrome bitmap β€” 16 bytes per glyph, one byte per row, bit 7 the leftmost pixel. (This is exactly the format of the VGA text-mode fonts and of X11's classic misc-fixed family; public-domain dumps abound, or draw your own.) The whole ASCII range is 95 glyphs Γ— 16 bytes = 1520 bytes you embed straight into the executable as a constexpr array β€” no font file, no loader, no I/O failure modes:

// 'A' β€” one byte per row, MSB = leftmost pixel   bits, visualized:
constexpr uint8_t kGlyphA[16] = {
    0x00, 0x00,                     //  . . . . . . . .
    0x10,                           //  . . . # . . . .
    0x28,                           //  . . # . # . . .
    0x44,                           //  . # . . . # . .
    0x82, 0x82,                     //  # . . . . . # .   (x2)
    0xFE,                           //  # # # # # # # .
    0x82, 0x82, 0x82,               //  # . . . . . # .   (x3)
    0x00, 0x00, 0x00, 0x00, 0x00,   //  padding to the descent
};

Drawing a glyph is a masked blit: for each set bit, write the foreground color; for each clear bit, touch nothing (the background was already painted by fill_rect β€” this is what lets selection highlights show through text). And because a glyph at the window edge is half outside the buffer, the blit clips exactly like fill_rect did. For our font, ascent = 12 and descent = 4: the baseline runs through glyph row 12.

How the real stack fits the same holes

Swap the bitmap font out later and nothing above the font abstraction changes. What goes in its place, on any modern OS, is a pipeline:

  • FreeType (Linux; also inside Android, game engines) parses TrueType/ OpenType files and rasterizes outlines β€” quadratic/cubic BΓ©zier contours β€” into exactly the kind of small bitmaps we're blitting, one per glyph per size, which you cache. It also reports the real metrics: per-glyph advance, ascent/descent, and kerning β€” per-pair spacing adjustments, because "AV" set at plain advances looks like "A V" (the diagonals leave a hole; the kern pair pulls V leftward a few pixels).
  • HarfBuzz answers a question we've quietly dodged: which glyphs? For English, charβ†’glyph is a table lookup. For Arabic (letters change shape by position), Devanagari (characters reorder), or "fi" ligatures, a shaping engine converts a codepoint sequence into a glyph sequence with positions. That's why serious text APIs take whole runs, never one char at a time β€” an interface lesson our draw_text (which takes a string) respects, and a per-char draw_glyph wouldn't.
  • DirectWrite (Windows) and CoreText (macOS) are those two layers fused into one platform service each β€” plus rasterization tuned for their compositor (ClearType subpixel AA; macOS's grayscale AA).

The seam design writes itself: the editor core measures text through a FontMetrics value (ascent, descent, per-char advances, kern pairs) and never asks how pixels get made. This lesson's second challenge builds that measuring kit; the layout engine in lesson 9 consumes it unchanged whether the numbers came from a constexpr table or from FreeType.

One habit to start now: never position text by summing widths as you render. Measure with the same code you'll hit-test with. If measurement and rendering ever disagree β€” one applies kerning, the other forgets β€” the caret lands between pixels of a glyph and users file bugs that say "the cursor is drunk". One function, one truth.

β€Ί Blit a Glyph

12 pts

Implement draw_glyph and draw_text for the 8Γ—16 bitmap format. The starter ships the geometry kit and a working Framebuffer from lesson 4.

Rules:

  • A glyph is 16 bytes, one per row, bit 7 leftmost: pixel column c of row r is set iff rows[r] & (0x80 >> c).
  • draw_glyph(fb, rows, x, baseline, fg) puts the glyph's top-left at (x, baseline - kAscent). Set bits are painted fg; clear bits leave the buffer untouched. Everything clips to the buffer (the tests run glyphs off all four edges under AddressSanitizer).
  • draw_text(fb, font, s, x, baseline, fg) draws s left to right starting at pen position x, advancing kGlyphW per character. Characters in ' '..'~' use font[ch - 32]; anything else draws nothing but still advances (the classic "tofu" slot would go here). Returns the final pen x.

Log in to submit a solution and earn points.

β€Ί Measure Before You Draw

12 pts

Build the measuring kit: the FontMetrics value type the layout engine and hit-testing will consume. This one is proportional-font ready β€” per-character advances plus kerning pairs β€” so that when FreeType replaces the bitmap font, only the numbers change.

Semantics:

  • line_height(m) = ascent + descent + line_gap.
  • advance_of(m, c): the advance for c from the table (advances[c - 32] for ' '..'~'); any other character measures as '?' does.
  • kern_between(m, l, r): the adjustment for the exact pair (l, r), or 0 if the pair isn't listed.
  • measure(m, s): total advance of s β€” the sum of every character's advance plus the kern between each adjacent pair.
  • caret_xs(m, s): the n+1 caret positions in s: caret_xs(s)[i] is measure of the first i characters (so [0] is 0 and [n] is measure(m, s)). A kern pair contributes only once both its characters are inside the prefix.

Log in to submit a solution and earn points.