Writing a TrueType font renderer
![](https://blinkingrobots.com/wp-content/uploads/2024/01/Writing-a-TrueType-font-renderer-1600x438.png)
Textual content is the medium of digital interplay, and textual content rendering has an outsized impression on the general match and end of any system. It due to this fact
pains me that axle has for years relied on a low-resolution 8×8 bitmap font. Let’s repair it!
Check out the characters composing this sentence. Notice the curves, the unfavourable house, using measurement and distance.
In the event you’ve received a magnifying glass helpful, you possibly can even see how ‘black-on-white’ textual content makes use of many shades of grey to make the textual content smoother on the eyes. I’ll present you:
![](https://blinkingrobots.com/wp-content/uploads/2024/01/Writing-a-TrueType-font-renderer.png)
Font rendering comes all the way down to telling the pc which pixels needs to be on or off, which needs to be darkened barely for impact and which ought to let the background shine by means of.
The problem comes from describing this to the pc in a means that’s environment friendly and resizable. Fonts shouldn’t take up an excessive amount of storage, and we should always be capable to scale a font to any measurement the person requests whereas preserving the character✱ of the font design. Maybe most significantly, we’ll wish to fulfill these constraints whereas additionally making the textual content nice to learn.
Minimalist Textual content Renderer
Let’s dispose of all of those necessities for now, and begin off with the best viable method to rendering characters. How may we go about coloring pixels into textual content?
Firstly, we’ll choose a set measurement for every character. We’ll agree that each character shall be precisely 8 pixels broad and eight pixels excessive. That provides us a grid of 64 pixels per character to work with.
We are able to use a easy information format that describes whether or not every pixel in our 8×8 grid needs to be on (coloured with the font coloration) or off (retain the background coloration). We are able to draw out our character grid like so:
_ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _
Since we’ve received 8 pixels in every row, and every pixel has two states, it’s pure to retailer every row in our grid as one byte. For instance, we may characterize a capital A
utilizing a illustration like the next:
Byte 1: 0 0 1 1 0 0 0 0
Byte 2: 0 1 1 1 1 0 0 0
Byte 3: 1 1 0 0 1 1 0 0
Byte 4: 1 1 0 0 1 1 0 0
Byte 5: 1 1 1 1 1 1 0 0
Byte 6: 1 1 0 0 1 1 0 0
Byte 7: 1 1 0 0 1 1 0 0
Byte 8: 0 0 0 0 0 0 0 0
![](https://blinkingrobots.com/wp-content/uploads/2024/01/1704159893_270_Writing-a-TrueType-font-renderer.png)
That is equal to the next hexadecimal illustration:
Byte 1: 0x0c
Byte 2: 0x1e
Byte 3: 0x33
Byte 4: 0x33
Byte 5: 0x3f
Byte 6: 0x33
Byte 7: 0x33
Byte 8: 0x00
… and permits us to very effectively retailer render maps for ASCII:
uint8_t ascii_map[256][8] = [
// 64 previous entries...
{ 0x0C, 0x1E, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x00}, // U+0041 (A)
{ 0x3F, 0x66, 0x66, 0x3E, 0x66, 0x66, 0x3F, 0x00}, // U+0042 (B)
{ 0x3C, 0x66, 0x03, 0x03, 0x03, 0x66, 0x3C, 0x00}, // U+0043 (C)
{ 0x1F, 0x36, 0x66, 0x66, 0x66, 0x36, 0x1F, 0x00}, // U+0044 (D)
{ 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x46, 0x7F, 0x00}, // U+0045 (E)
{ 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x0F, 0x00}, // U+0046 (F)
{ 0x3C, 0x66, 0x03, 0x03, 0x73, 0x66, 0x7C, 0x00}, // U+0047 (G)
{ 0x33, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x33, 0x00}, // U+0048 (H)
// ...
];
That is fairly a tidy encoding! To render a personality, we will immediately lookup its render map utilizing its ASCII illustration:
fn render(ch: char) {
let ch_render_map: [u8; 8] = ascii_bitmaps[ch as u8]; // Neat!
for row in ch_render_map.iter() {
for col in row.bits().iter() {
if col == 1 {
// This pixel belongs to the character
}
else {
// This pixel ought to show the background
}
}
}
}
Consider it or not, that’s it. axle has been utilizing the above font rendering approach for the higher a part of a decade.
![](https://blinkingrobots.com/wp-content/uploads/2024/01/1704159893_931_Writing-a-TrueType-font-renderer.png)
Though the font above solely natively offers 8px bitmaps, it’s fairly straightforward to scale it to any measurement we like. Nevertheless, the font will at all times seem jagged and low-resolution.
![](https://blinkingrobots.com/wp-content/uploads/2024/01/1704159893_837_Writing-a-TrueType-font-renderer.png)
Whereas the 8×8 bitmap font works, it positive isn’t fairly, and it’s fairly restricted: we will solely use no matter font we’re keen to encode into this little 8×8 grid, and
we’re unable to make use of any of the numerous fonts out there on-line and within the public area. Moreover, our 8×8 grid doesn’t give us a lot of a great way to explain curves, that are important in some fonts (take a look at this a
!).
Most significantly, writing a brand new font renderer feels like enjoyable.
TrueType
For the subsequent leap in our textual content rendering journey, we flip to TrueType: the de facto commonplace for encoding and distributing fonts. Not like our humble 8×8 bitmaps, TrueType fonts are distributed as structured binary information (.ttf
). We’ll must parse these information and render their information into pixels.
There’s few issues on this world I really like greater than taking an opaque binary and regularly uncovering the group that was current all alongside. Dissecting the underlying and basic construction of a binary is as gratifying as something I can think about.
The TrueType specification provides an excellent introduction to the issue house (I extremely advocate the overviews in Digitizing Letterforms
and particularly Instructing Fonts
), nevertheless it doesn’t give a lot in the best way of signposting “begin parsing right here”. I’ll prevent the difficulty.
TTFs are structured considerably equally to Mach-Os. That is fairly a pleasant shock, as a result of I’ve spent a lot of time parsing Mach-Os! Each file codecs have been developed at Apple✱ across the similar time, so I suppose they’d some design factors within the air.
TTFs and Mach-Os are each largely structured round a flat sequence of sections, every of which is described by a desk of contents originally of the file. Within the case of TTFs, every of those sections will include some metadata or different that we’ll want over the course of rendering the font.
![](https://blinkingrobots.com/wp-content/uploads/2024/01/1704159893_358_Writing-a-TrueType-font-renderer.png)
Earlier than looking at these sections turns into fruitful, although, we have to know the elemental course of for describing fonts utilized by TrueType. The specification does an excellent job, so I’ll hold myself temporary.
The gathering of strains and curves used to visually characterize a selected letter is known as a glyph. For instance, the glyph for b
consists of a vertical line and a semicircle.
With out additional info, although, our renderer gained’t know which glyph represents every character. We’ll want some extra metadata to transform from a personality inside a character map (akin to ASCII or Unicode) to the glyph {that a} specific font is utilizing to visually characterize that character. The job of the TTF is dually to explain how to attract all of the glyphs represented by the font, and to explain the correspondence between the character map and these glyphs.
TrueType describes how to attract every glyph by itemizing a sequence of factors, which collectively compose the define of every glyph. It’s as much as the renderer to attach these factors into strains, to show these strains into curves, and to ‘coloration contained in the strains’ as acceptable. There’s extra nuance right here, however that’s the naked bones.
![](https://blinkingrobots.com/wp-content/uploads/2024/01/1704159893_138_Writing-a-TrueType-font-renderer.png)
![](https://blinkingrobots.com/wp-content/uploads/2024/01/1704159894_745_Writing-a-TrueType-font-renderer.png)
Rendering factors with out connecting them into strains
Issues begin to look much more understandable when you join the dots.
![](https://blinkingrobots.com/wp-content/uploads/2024/01/1704159894_986_Writing-a-TrueType-font-renderer.png)
![](https://blinkingrobots.com/wp-content/uploads/2024/01/1704159894_134_Writing-a-TrueType-font-renderer.png)
Coloring contained in the strains seems to be harder than you may think✱.
My first implementation of this renderer didn’t draw any curves, however as a substitute immediately linked the factors with straight strains. My colleague Amos implemented the power for the renderer to interpolate the glyph define alongside the curves described within the font, which actually improved the look of issues. Thanks, Amos!
![](https://blinkingrobots.com/wp-content/uploads/2024/01/1704159894_515_Writing-a-TrueType-font-renderer.png)
Earlier than and after Amos’s work to interpolate alongside curves
Tangled Textual content Tables
Every part within the TTF is recognized by a 4-character ASCII identify. Completely different sections within the TTF include completely different vital metadata, akin to:
glyf
: The set of factors for every glyph define within the font (most vital!)hmtx
:✱ Spacing info, i.e. how far to advance the cursor after drawing every glyphcmap
: The correspondence of Unicode✱ codepoints to glyph indexes
Every of those sections accommodates a bunch of structured information describing their contents. Right here, our first mini problem presents itself. TTF was designed in an period through which neither RAM nor core cycles have been low cost. Subsequently, the designers of the format made positive it’d be as straightforward as doable for the renderer to finish its work. To this finish, they offloaded two obligations to the font file itself:
- Environment friendly information constructions that the renderer can stroll to retrieve data (for instance, lookup tables to map a personality code to a glyph index)
- Pre-computation of some values used throughout lookup (for instance, scaling parameters for binary looking a personality map)
These optimizations aren’t so related at this time, however we’ll must work with them nonetheless. The primary one is especially vital: TTF is designed to be loaded immediately into reminiscence and browse by the renderer in-place, with out the renderer populating its personal middleman information constructions. Inexplicably, although, the TTF shops all fields in massive endian, so we’ll undoubtedly need to do some work on our finish earlier than we will learn any sane values out of font constructions.
To have the ability to outline and browse from these font constructions with minimal boilerplate and repetition, I set issues up such that I may wrap any font construction fields in BigEndianValue
. After I name .into()
, the whole lot is finished and dusted and I can transfer on with my day. Just a little little bit of construction definition upfront for some straightforward breezy parsing code down the road!
rust_programs/ttf_renderer/src/parser.rs
/// Right here, we're modelling a construction that is outlined by the TTF specification
#[repr(C, packed)]
#[derive(Debug, Copy, Clone)]
struct OffsetSubtableRaw {
/// Notice every subject is wrapped in BigEndianValue
scalar_type: BigEndianValue<u32>,
num_tables: BigEndianValue<u16>,
search_range: BigEndianValue<u16>,
entry_selector: BigEndianValue<u16>,
range_shift: BigEndianValue<u16>,
}
/// This marker trait signifies that this construction sits immediately in TTF reminiscence
impl TransmuteFontBufInPlace for OffsetSubtableRaw {}
/// That is our 'high-level' parsed illustration
#[derive(Debug, Copy, Clone)]
struct OffsetSubtable {
scalar_type: u32,
num_tables: u16,
search_range: u16,
entry_selector: u16,
range_shift: u16,
}
/// Convert the 'uncooked' TTF reminiscence illustration into our high-level illustration with native byte order
impl FromFontBufInPlace<OffsetSubtableRaw> for OffsetSubtable {
fn from_in_place_buf(uncooked: &OffsetSubtableRaw) -> Self {
Self {
scalar_type: uncooked.scalar_type.into_value(),
num_tables: uncooked.num_tables.into_value(),
search_range: uncooked.search_range.into_value(),
entry_selector: uncooked.entry_selector.into_value(),
range_shift: uncooked.range_shift.into_value(),
}
}
}
Prepared for the payoff?
pub fn parse(&mut self) -> Font
rust_programs/ttf_renderer/src/parser.rs
let offset_table = OffsetSubtable::from_in_place_buf(self.read_with_cursor(&mut cursor));
Oh yeah! We are able to parse our high-level, byte-order-corrected illustration immediately from the TTF reminiscence with out even needing to point out the uncooked TTF illustration on the call-site. I’ll name {that a} win.
Parser
Useful APIs in hand, we’ll parse all of the part descriptions in the beginning of the file.
The head
part (or desk, as TTF prefers) shops some vital metadata in regards to the font as a complete. We all know that TTF describes glyph outlines through a sequence of linked factors. But when we see a degree like (371, 205)
, with out additional info we don’t have a reference level for understanding the place this sits visually. The head
desk provides us such a reference level, by describing the glyph_bounding_box
that every one factors sit inside.
Subsequent up, we’ll wish to parse a character map to learn the way we’ll correspond encoded characters to glyph indexes. Every character map will even be described in an extremely gnarly format that the parser might want to know tips on how to learn.
And now the thrilling bit: parsing glyphs! The maxp
desk will give us some metadata on what number of glyphs the font accommodates, whereas the glyph information itself is saved in glyf
.
Earlier than we will parse every glyf
entry, we’ll must know the place every entry begins. The loca
desk will give us these offsets. Nevertheless, nowhere is the measurement of every glyph’s information given explicitly within the font. As a substitute, the dimensions of every glyph’s information is implicitly given by the distinction with the beginning offset of the subsequent glyph.
The intrepid font parser will rapidly discover that some glyph descriptions seem to have no measurement. That is intentional: some glyphs, such because the one similar to the house character, haven’t any seen manifestation other than the unfavourable house they occupy. When the renderer tries to attract one in every of these zero-sized glyphs, we’ll simply advance the cursor based mostly on the corresponding htmx
measurement for this glyph.
We’ve really received three distinct sorts of glyphs:
- Polygon glyphs
- These glyphs are the traditional case, an ordered connection of edge factors and management factors that needs to be linked in a sequence of curves.
- Clean glyphs
- Particular case for glyphs akin to these similar to the house character.
- Compound glyphs
- Glyphs manufactured by combining and scaling different glyphs.
The third class is admittedly fascinating! The glyphs for i
and j
each share an equivalent dot on high, and there’s no must duplicate its description within the draw directions. As a substitute, we will simply say that i
and j
are every composed of a few glyph fragments laid out on the canvas at particular scales and positions.
![](https://blinkingrobots.com/wp-content/uploads/2024/01/1704159894_962_Writing-a-TrueType-font-renderer.png)
There are all types of counterintuitive edge instances with compound glyphs. For instance, we all know that conventional polygon glyphs specify how far the cursor ought to advance after drawing them through corresponding entries within the hmtx
and vmtx
tables. Nevertheless, how far ought to the cursor advance for a compound glyph? Generally the compound glyph has its personal metrics desk entries, and typically the compound glyph is annotated with a particular flag indicating that it ought to take its cursor advance metrics from a particular little one glyph.
rust_programs/ttf_renderer/src/glyph.rs
enum CompoundGlyphComponentFlag {
HeaderValuesAreWords,
HeaderValuesAreCoordinates,
RoundCoordinatesToGrid,
CustomScale,
MoreComponentsFollow,
HasDifferentScalesForXAndY,
HasTwoByTwoTransformation,
HasInstructions,
UseMetricsFromThisComponent,
ComponentsOverlap,
}
Compound glyphs don’t at all times immediately include polygon glyphs. As a substitute, a compound glyph could be composed of a tree of different compound glyphs, all stretched and scaled and repositioned throughout the bounding field.
Hinting
TrueType famously defines a hinting VM that’s supposed to assist retain the essence of a font when rendering at low resolutions to a restricted pixel grid. The job of this system working beneath this VM is to nudge the glyph’s management factors into a particular structure on low-resolution bitmaps✱.
Let’s say you’re making an attempt to scale down a glyph’s factors to suit on a small grid. If the render decision isn’t precisely divisible by the glyph’s level spacing, you’re going to get scaling artifacts whenever you spherical every glyph level to a pixel.
![]() |
![]() |
---|
The font can embed slightly program that, when executed, nudges the glyph’s structure factors to higher characterize the glyph throughout the out there grid.
![]() |
![]() |
---|
Our renderer might want to implement this VM, which is at all times a enjoyable endeavor.
The hinting VM is constructed round an ordinary (and extremely satisfying) interpreter loop that modifies a digital machine state based mostly on the operations indicated by the byte stream:
rust_programs/ttf_renderer/src/hints.rs
0x4b => {
// Measure pixels per em within the projection vector's axis
if operations.should_print() {
println!(
"MPPEMtMeasure pixels per em in {:?}",
graphics_state.projection_vector
);
}
if operations.should_execute() {
let val = match graphics_state.projection_vector {
Axis::X => graphics_state.font_size.width,
Axis::Y => graphics_state.font_size.peak,
};
graphics_state.push(val as u32);
}
}
0x50 => {
// Lower than
let e2 = graphics_state.pop();
let e1 = graphics_state.pop();
let outcome = e1 < e2;
if operations.should_print() {
println!("LTtLower than? {e1} < {e2} = {outcome}");
}
if operations.should_execute() {
graphics_state.push(if outcome { 1 } else { 0 });
}
}
There a slew of hilarious little engineering challenges that come up when making an attempt to execute capabilities concentrating on a font’s working atmosphere.
rust_programs/ttf_renderer/src/hints.rs
pub(crate) fn identify_functions(directions: &[u8]) -> Vec<FunctionDefinition> {
// We've got a bootstrapping drawback: Capabilities cannot be run to
// completion with out the context (arguments, and many others.) from the caller, however we have to
// parse perform boundaries earlier than we will run callers since callers deal with capabilities through
// identifiers that get created when the fpgm desk is executed.
// To resolve this round dependency, we use a easy heuristic to establish perform
// definitions, fairly than doing a full interpreter move.
// This has the draw back that our heuristic cannot inform the
// distinction between code and information.
// If some information is pushed to the stack with the identical worth because the
// perform definition opcode, we could interpret it as a perform definition.
//
// ...
}
TrueType Quirks
Occult Memorabilia
The hinting VM is all about nudging the glyph’s define factors in order that they appear higher on a small pixel grid. All of those factors that really compose part of the glyph define are within the ‘Glyph Zone’.
Nevertheless, the VM designers realized that it was typically helpful to have a degree that wasn’t actually a part of the define, however was simply used as a reference when doing distance calculations. These factors don’t belong within the Glyph Zone, so as a substitute TrueType locations them in a unique house referred to as… the Twilight Zone.
The TrueType designers have been clearly followers of the paranormal. TrueType offers handy help for describing emboldened and italicized variations of fonts by embedding offsets to use to every level in a glyph define. A font variation may want all the bounding field of a glyph to be modified too, so there are ‘phantom points’ similar to the corners of the bounding field that may be stretched and squeezed.
Implied Management Factors
The TrueType specification suggests to the reader, hey! Possibly it’d be doable to save lots of house by letting the renderer deduce lacking management factors?
Regardless of the informal tone of commentary, renderers are literally required to help this. Fonts depend on this on a regular basis! That is the one point out of it in the entire spec! Everybody must detect and insert implied management factors, all on the again of 1 introductory diagram.
Trace: Assume Nothing
Like we’ve seen, TrueType has refined help for manipulating the rendering course of in constrained font sizes. However like several good programming atmosphere, it additionally offers a lot of escape hatches. One such escape hatch is the bloc
desk, which basically says “if the font measurement is just too small, ignore the glyph curves and hinting applications and simply render these tiny bitmaps as a substitute.”
Firming Up a Renderer
There’s nonetheless work to do to combine this font renderer neatly in all of the stacks that wind up displaying textual content on axle’s desktop, however the foundations are prepared. Right here’s what issues seemed just like the final time I labored on propagating TrueType all through the desktop.
![](https://blinkingrobots.com/wp-content/uploads/2024/01/1704159894_807_Writing-a-TrueType-font-renderer.png)
The supply code for this renderer is here. Beneath are some screenshots from the course of this renderer’s improvement.