When you want to render text in a pure Rust project — a GUI app, a document generator, a terminal — you quickly find that "render some text" is actually four distinct problems stacked on top of each other. The C ecosystem has a library for each one. Rust has options too, but coverage is uneven, and the naming conventions obscure which crates actually depend on C.
The four layers
| Layer | C library | What it does |
|---|---|---|
| Font discovery | fontconfig | Finds installed fonts by name, family, or script |
| Text shaping | HarfBuzz | Converts a Unicode string → glyph IDs with positions |
| Rasterization | FreeType | Turns outline vectors → pixel bitmaps |
| Layout | Pango | Wraps all three, handles BiDi, line breaking |
On macOS/iOS the shaping and rasterization layers are CoreText. On Windows they're DirectWrite. In both cases, the system library is C or Objective-C, not Rust.
Each layer depends on the one below it: shaping needs the raw font bytes to read OpenType tables, rasterization needs the shaped glyph IDs, layout needs both. You can't swap in one layer without thinking about the others.
What text shaping actually does
Shaping is the step between "here is a Unicode string" and "here are the glyph IDs and positions to draw." Most developers don't know it exists until something renders wrong.
The string "fi" contains two Unicode codepoints (U+0066, U+0069). But most fonts substitute those two characters with a single ligature glyph — "fi". In Arabic, the same letter takes a different shape depending on whether it appears at the start, middle, or end of a word. Even Latin fonts have kerning rules that adjust spacing between specific letter pairs.
All of this is encoded in the font file's OpenType tables: GSUB (glyph substitution) and GPOS (glyph positioning). A shaping engine reads those tables and converts your input string into a list of (glyph ID, x, y) tuples. HarfBuzz is the de facto standard for this. Without a shaper, you get incorrect rendering for anything beyond the simplest Latin text.
The naming trap
Several font crates look like pure Rust but are C wrappers:
harfbuzz-rs — a safe Rust wrapper around libharfbuzz. The API feels like Rust, but libharfbuzz is compiled from C++. Run cargo tree and you'll see harfbuzz-sys in the graph.
freetype-rs — same pattern. Safe API, C underneath. Look for freetype-sys.
fontconfig — bindings to the C fontconfig library.
If you're targeting wasm32-unknown-unknown or cross-compiling without a C toolchain, these fail at build time. The -sys suffix in cargo tree output is the reliable signal.
Pure Rust, layer by layer
Font discovery: fontdb
fontdb reads font directories directly, parses font metadata from binary headers, and supports querying by family name, weight, style, and script coverage. No C. Works on Linux, macOS, Windows, and WASM.
It doesn't use fontconfig under the hood — it scans directories directly. For most use cases this is fine. If you need fontconfig's alias resolution ("sans-serif" mapping to a specific installed font), fontdb covers part of this logic but not all of it.
Text shaping: rustybuzz or harfrust
rustybuzz is a complete port of HarfBuzz v10.1.0 to Rust. It passes 2,221 out of 2,252 HarfBuzz shaping tests. Performance is 1.5–2x slower than C HarfBuzz. Maintained under the harfbuzz org. Used in cosmic-text.
harfrust is a newer port, matching HarfBuzz v13.0.0. It started as a fork of rustybuzz, migrating the font parser from ttf-parser to read-fonts (Google's fontations project). Less than 25% slower than HarfBuzz on common fonts.
For new projects, harfrust tracks HarfBuzz more closely (3 major versions ahead of rustybuzz). rustybuzz has more production field time — it's what cosmic-text ships.
Known limitations of both: Arabic rendering works for common patterns, but unusual ligature constructions that require building lookup rules at runtime aren't supported. Some variable font interpolation features (avar2) are also not implemented.
Rasterization: swash or fontdue
swash handles both glyph outline extraction and rasterization. Supports ligatures and color emoji (CBDT, COLRv1). Used in cosmic-text.
fontdue focuses on rasterization only — no shaping, no color emoji. Simpler API and lighter weight if you're only rasterizing pre-shaped glyphs for basic Latin text.
Layout: cosmic-text
cosmic-text assembles the full stack: fontdb for discovery, rustybuzz for shaping, swash for rasterization, and its own layout engine for line breaking and BiDi text. Version 0.14.2 was released April 2025.
If you're building a GUI, a terminal emulator, or anything that needs correct multi-line Unicode text, this is the fastest path to a working stack. It's used in COSMIC (the Pop!_OS desktop environment), Iced, and Floem.
When the stack breaks
Complex scripts. rustybuzz handles most scripts well. Arabic works for common patterns but fails on ligature constructions that require building font lookup rules on the fly. Indic scripts (Devanagari, Bengali, Tamil) handle standard shaping but haven't had the production volume that C HarfBuzz has accumulated over 15+ years.
Font fallback. If a character isn't covered by your selected font, neither fontdb nor cosmic-text handles the fallback automatically. cosmic-text's FontSystem gives you font-matching utilities and codepoint coverage queries, but you write the chain logic.
Variable fonts. Basic variation instance selection works. Complex interpolation between instances (avar2) is not yet fully supported.
Practical summary
| Goal | Crate(s) |
|---|---|
| Full text rendering pipeline | cosmic-text |
| Shaping only, modern (v13) | harfrust |
| Shaping only, battle-tested | rustybuzz |
| Rasterization with color emoji | swash |
| Lightweight rasterization (Latin only) | fontdue |
| Font metadata / discovery | fontdb |
Checklist before shipping:
- [ ]
cargo tree | grep -- -sys— verify no C dependency slipped in - [ ] Test on all target platforms (font path scanning differs between Linux, macOS, Windows)
- [ ] Check your target script against the known limitations for rustybuzz or harfrust
- [ ] If targeting WASM, fontdb won't find system fonts — bundle font bytes directly
If you're doing font subsetting rather than rendering — extracting just the glyphs you need to embed in a PDF or WASM module — the relevant crates are different. But knowing that the shaping layer reads GSUB and GPOS tables directly informs which tables you need to keep when subsetting. I've been building that tooling in harumi.
Questions welcome in the comments.
Top comments (0)