Tiny TrueType Workbook
Build a TrueType renderer yourself
Built on Writing a tiny TrueType parser and renderer from scratch by Yohei Yamasaki. Read the post alongside; each assignment links to the part it covers.
Run it locallyStart here
- Get the workbook onto your computer: clone it, or hand the prompt under Run it locally to your coding assistant. The book runs your code on your machine.
- Open the book and set up your C++ project. In the book, each assignment's Start here card lists the files to write and your job in each.
- Begin with assignment 01 (Read big-endian numbers) and go in order: each one adds files to the project, and later ones use them. Read it now.
| # | Title | Difficulty |
|---|---|---|
| 01 | Read big-endian numbers | Easy |
| 02 | Read the table directory | Easy |
| 03 | Map characters to glyphs (cmap format 12) | Medium |
| 04 | Locate glyph records (loca) | Medium |
| 05 | Read the horizontal metrics | Easy |
| 06 | Draw on a framebuffer | Medium |
| 07 | Decode simple glyphs | Medium |
| 08 | Draw an outline with straight lines | Easy |
| 09 | Resolve compound glyphs | Hard |
| 10 | Lerp and quadratic Bezier curves | Easy |
| 11 | Draw an outline with curves | Medium |
| 12 | Intersect two segments | Medium |
| 13 | Intersect a line and a quadratic curve | Medium |
| 14 | Fill a glyph: even-odd rule | Hard |
| 15 | Lowest point of a quadratic curve | Easy |
| 16 | Fill a polygon: non-zero rule | Medium |
| 17 | Fill a glyph: non-zero rule | Hard |
| 18 | Render a line of text | Hard |
The project you build
Your C++ project lives in solutions/ and grows one assignment at a time. The number by each file is the assignment that creates it, and a + marks the assignments that add to it later.
TextRenderer.h 18 This class holds the whole pipeline. It loads a font's tables once, then maps each character of a line of text to its glyph, fills the glyph by the non-zero rule and moves the pen on by its advance. The checker's program and `main.cpp` both draw through it. TextRenderer.cpp 18 This class holds the whole pipeline. It loads a font's tables once, then maps each character of a line of text to its glyph, fills the glyph by the non-zero rule and moves the pen on by its advance. The checker's program and `main.cpp` both draw through it. main.cpp 18 This is the finished command-line program. `tiny_ttf <font.ttf> "<text>" <out.png>` draws a line of text with a TrueType font into a PNG sized to fit it, using your `TextRenderer`. An optional fourth argument sets the text height in pixels (64 by default). FrameBufferCanvas.h 06 The canvas holds a picture in memory, one colour per pixel. It can paint one pixel, fill a rectangle, draw a thick line and save the picture as a PNG. You draw every later picture on it: outlines in 08 and 11, filled glyphs in 14, 16 and 17, and a line of text in 18. FrameBufferCanvas.cpp 06 The canvas holds a picture in memory, one colour per pixel. It can paint one pixel, fill a rectangle, draw a thick line and save the picture as a PNG. You draw every later picture on it: outlines in 08 and 11, filled glyphs in 14, 16 and 17, and a line of text in 18. GlyphComponent.h 07 Every outline in the project takes this form: its points (x and y in font units, on or off the curve) and the index where each contour ends. Assignments 08 and 11 draw outlines in this form, 09 produces one per part of a compound glyph, and 14, 16 and 17 fill them. Glyph.h 07 This type holds a glyph as the glyf table stores it: whether it is empty, simple or compound, its contour count, its bounding box and, for a simple glyph, its outline. Your `GlyphParser` returns it, and in assignment 09 you use its kind to tell simple glyphs from compound ones. ByteReader.h 01 This header declares your reader for big-endian numbers at any position in a font file: unsigned and signed integers of 8, 16 and 32 bits, and F2Dot14 fixed point. Every later assignment that reads the font goes through it: assignment 02 reads the table directory, 03 to 05 the `cmap`, `loca` and metrics tables, and 07 and 09 the glyph outlines. ByteReader.cpp 01 This file implements the reader: it opens the font, moves the position and turns bytes into numbers. Every later assignment that reads the font runs this code, so a bug here makes their tests fail too. FontParser.h 02 This header declares the code that opens a font and reads its table directory: the tag, checksum, offset and length of each table (`cmap`, `glyf`, `loca`, ...). Assignments 03 to 05 find their tables by tag in these records, and assignment 18 opens the font through it. FontParser.cpp 02 This file reads the table directory from the first bytes of the font through your `ByteReader`. Each later assignment that needs a table looks up its offset in these records. Cmap.h 03 This header declares your reader for the `cmap` table, the font's map from each character to the glyph that draws it. It reads the format 12 subtable, which covers the full Unicode range, and assignment 18 looks up every character of a line of text in the result. Cmap.cpp 03 This file reads the platform 0 / encoding 4 subtable of `cmap` into a map from character to glyph. Assignment 18 builds this map once per font. Loca.h 04 This header declares your reader for the `loca` table, which gives the position and size in bytes of each glyph's record, by glyph id. Assignment 09 uses it to find the parts of a compound glyph, and assignment 18 to find the glyph of every character. Loca.cpp 04 This file reads `maxp`, `head` and `loca` and works out one location per glyph id. Assignments 09 and 18 look glyphs up in its result. Metrics.h 05 This header declares your reader for the `hhea` and `hmtx` tables: the font's ascent and descent, and each glyph's advance width and left side bearing. Assignment 18 moves the pen by each glyph's advance width and sizes the text from the ascent and descent. Metrics.cpp 05 This file reads `hhea`, `maxp` and `hmtx` into the ascent, the descent and one set of metrics per glyph id, including the glyphs that store a side bearing and no advance. Assignment 18 loads these metrics once per font. GlyphParser.h 07 The parser decodes one glyph record from the glyf table into the types in `Glyph.h`: the header of any glyph, and the contours and points of a simple one. In assignment 09 you call it for every simple glyph inside a compound glyph. GlyphParser.cpp 07 The parser decodes one glyph record from the glyf table into the types in `Glyph.h`: the header of any glyph, and the contours and points of a simple one. In assignment 09 you call it for every simple glyph inside a compound glyph. CompoundGlyph.h 09 This code turns any glyph into the simple outlines you draw it with. It reads a compound glyph's components, moves, scales or rotates each one, and resolves components nested at any depth. In assignment 18 you draw every character of a line of text through it, so your text renderer doesn't need to tell simple glyphs from compound ones. CompoundGlyph.cpp 09 This code turns any glyph into the simple outlines you draw it with. It reads a compound glyph's components, moves, scales or rotates each one, and resolves components nested at any depth. In assignment 18 you draw every character of a line of text through it, so your text renderer doesn't need to tell simple glyphs from compound ones. Scanline.h 14 It holds the steps both fill rules share: it turns an outline into edges in pixels and finds, row by row, where the line through the pixel centres crosses them. Your even-odd fill in this assignment and your non-zero fill in assignments 16 and 17 both run on it. Scanline.cpp 14 It holds the steps both fill rules share: it turns an outline into edges in pixels and finds, row by row, where the line through the pixel centres crosses them. Your even-odd fill in this assignment and your non-zero fill in assignments 16 and 17 both run on it. EvenOdd.h 14 It fills a glyph by the even-odd rule, on top of your `render/Scanline`. Assignment 16 adds the non-zero rule beside it, and the text renderer in assignment 18 uses that rule. EvenOdd.cpp 14 It fills a glyph by the even-odd rule, on top of your `render/Scanline`. Assignment 16 adds the non-zero rule beside it, and the text renderer in assignment 18 uses that rule. NonZero.h 16 +17 It fills a glyph by the non-zero rule, the rule TrueType uses, on top of your `render/Scanline`. This assignment handles straight edges, assignment 17 adds curves, and the text renderer in assignment 18 fills every glyph with it. NonZero.cpp 16 +17 It fills a glyph by the non-zero rule, the rule TrueType uses, on top of your `render/Scanline`. This assignment handles straight edges, assignment 17 adds curves, and the text renderer in assignment 18 fills every glyph with it. Placement.h 08 A placement is where a glyph goes on the canvas, given as three numbers: the pixel column of font x = 0, the pixel row of the baseline and the pixels per font unit. Together they turn font units (y up) into pixels (y down). You place glyphs with it in assignments 11, 14, 16 and 17, and in assignment 18 you move it along a line of text, one glyph at a time. Outline.h 08 +11 These files draw a glyph's outline on the canvas, with straight lines between its points and a marker on every point. In assignment 11 you add a second drawing, with the real curves, to the same files. Outline.cpp 08 +11 These files draw a glyph's outline on the canvas, with straight lines between its points and a marker on every point. In assignment 11 you add a second drawing, with the real curves, to the same files. Geometry.h 10 +12, 13, 15 This header holds the project's 2D maths: a point type, lerp, and the point at t on a quadratic Bezier curve. In assignment 11 you draw curves with it. Assignments 12, 13 and 15 add functions for where segments and curves meet and for a curve's lowest point, and the fills from assignment 14 on use them. Unicode.h 18 It turns UTF-8 text into Unicode code points, the numbers a font's cmap maps to glyphs. Your `TextRenderer` decodes every line of text with it. assignment_NN_….*allOne small program per assignment: it reads a test input, calls your project and writes the answer the checker compares.About this workbook
Each assignment has you write the program that one part of the blog post builds, from reading big-endian bytes to drawing a line of text. The checker feeds your program test inputs and compares what it writes with expected answers, most of them computed by fontTools. You choose how to structure the code and which language or libraries to use.
Fonts: JetBrains Mono (Nerd Font build) and Noto Sans JP, both under the SIL Open Font License. Most expected outputs come from fontTools.