rust-fontconfig
Pure-Rust rewrite of the Linux fontconfig library (no system dependencies). Enable the parsing feature to parse .woff, .woff2, .ttc, .otf and .ttf with allsorts.
NOTE: Also works on Windows, macOS and WASM - without external dependencies!
Motivation
There are a number of reasons why I want to have a pure-Rust version of fontconfig:
- fontconfig with all dependencies (expat and freetype) is ~190.000 lines of C (extremely bloated for what it does)
- fontconfig, freetype, expat and basically any kind of parsing in C is a common attack vector (via maliciously crafted fonts). The Rust version (allsorts) checks the boundaries before accessing memory, so attacks via font files should be less common.
- it gets rid of the cmake / cc dependencies necessary to build azul on Linux
- fontconfig isn't really a "hard" library to rewrite, it just parses fonts and selects fonts by name
- Rust has existing xml parsers and font parsers, just use those
- It allows fontconfig libraries to be purely statically linked
- Font parsing / loading can be easily multithreaded (parsing font files in parallel)
- It reduces the number of necessary non-Rust dependencies on Linux for azul to 0
- fontconfig (or at least the Rust bindings) do not allow you to store an in-memory cache, only an on-disk cache, requiring disk access on every query (= slow)
- in-memory ("bring your own font files") font loading for WASM and sandboxed environments
Now for the more practical reasons:
- libfontconfig 0.12.x sometimes hangs and crashes (see issue)
- libfontconfig introduces build issues with cmake / cc (see issue)
- To support font fallback in CSS selectors and text runs based on Unicode ranges, you have to do several calls into C, since fontconfig doesn't handle that
- The rust rewrite uses multithreading and memory mapping, since that is faster than reading each file individually
- The rust rewrite only parses the font tables necessary to select the name, not the entire font
- The rust rewrite uses very few allocations (some are necessary because of UTF-16 / UTF-8 conversions and multithreading lifetime issues)
Installation
[]
= { = "5.0", = ["parsing"] }
The default build (std only) discovers system fonts via filename
heuristics. Enable parsing to read the actual font tables with allsorts for
accurate family names, weights, Unicode coverage and
.woff/.woff2/.ttc/.otf/.ttf support.
Cargo features
| Feature | Default | Description |
|---|---|---|
std |
✅ | Filesystem scanning + mmap-backed font loading. Currently required - the crate is std-only as of v4.1. |
parsing |
Parse font tables via allsorts (accurate metadata; WOFF/WOFF2/TTC/OTF/TTF). Implies std. |
|
multithreading |
Parallel font scanning/parsing via rayon. | |
cache |
Persist the parsed cache to disk (serde + bincode + dirs). | |
async-registry |
FcFontRegistry for incremental/background font discovery. Implies parsing. |
|
ffi |
C API bindings. Implies parsing + async-registry. |
WASM:
wasm32-*targets build out of the box -mmapioandrayonare excluded automatically viacfg. Build with--features parsing.
Usage
Basic Font Query
use ;
Font Fallback Chain for CSS font-family
The new API separates font chain resolution from text querying:
resolve_font_chain()- Create a fallback chain from CSS font-family (without text)chain.resolve_text()- Query which fonts to use for specific text
use ;
Controlling fallback
Everything the chain builder knows about the host is injected through an
FcFallbackConfig: which families stand behind each generic, which fonts to
prefer for a script, what to substitute for a missing named family, and what
to draw when nothing covers a character. FcFontCache::build() parses the
platform configuration where there is one (Linux fonts.conf aliases) and
fills the gaps from FcFallbackConfig::os_defaults; FcFontCache::default()
starts empty.
use ;
let mut config = os_defaults;
// Prefer a specific font for Hiragana when the stack asks for sans-serif.
config.script_fallbacks.insert;
// The font whose .notdef is drawn for anything no font covers.
config.last_resort = vec!;
let cache = build.with_fallback_config;
// The scripts hint bounds what the chain precomputes: `Some(&[])` builds no
// script tier at all (ASCII-only documents pull in no CJK fonts), `None` uses
// `DEFAULT_UNICODE_FALLBACK_SCRIPTS`, and a list of blocks - usually derived
// from the document's text - builds exactly those.
let chain = cache.resolve_font_chain_with_scripts;
A chain resolves a character in three tiers, first hit wins: the CSS stack
(a generic's per-script preferred fonts before its base fonts), then the
coverage-gated group for the character's script block - configured
preferences first, then registered fonts ranked by coverage of that block,
style, and dedication to the script (a font that is mostly this script beats
one that merely includes it; breadth of coverage is never a bonus) - then
the configured last resort. resolve_char and query_for_text read only
the chain: no lock, no cache access per character.
For the async registry, FcFontRegistry::new_with_configs(scan, fallback)
injects both the scan side and the resolution side; the families it parses
ahead of a request are exactly config.candidate_families(stack, scripts),
so what is prefetched and what a chain can contain agree by construction.
Character-by-Character Font Resolution
For fine-grained control, use resolve_text() to get per-character font assignments:
use ;
List All Fonts Matching a Pattern
use ;
Using from C
Linking with the C API
The rust-fontconfig library provides C-compatible bindings that can be used from C/C++ applications.
Binary Downloads
You can download pre-built binary files from the latest GitHub release:
- Windows:
rust_fontconfig.dllandrust_fontconfig.lib - macOS:
librust_fontconfig.dylibandlibrust_fontconfig.a - Linux:
librust_fontconfig.soandlibrust_fontconfig.a
Building from Source
Alternatively, you can build the library from source:
# Clone the repository
# Build with FFI support
# The generated libraries will be in target/release
Including in Your C Project
- Copy the header file from
ffi/rust_fontconfig.hto your include directory - Link against the static or dynamic library
- Include the header file in your C code:
Minimal C Example
int
For a more comprehensive example, see the example.c file included in the repository.
Compiling the C Example
On Linux:
On macOS:
On Windows:
Performance
- cache building (cold start, multithreaded): ~1.2s for ~760 fonts
- cache building (from disk cache): ~12ms
- cache query: ~4µs
Features
- Font matching by name, family, style properties, or Unicode ranges
- CSS font-family resolution with
resolve_font_chain()for proper fallback handling - Per-character font resolution with
chain.resolve_text()for multilingual text - Font run grouping with
chain.query_for_text()for text shaping pipelines - Support for font weights (thin, light, normal, bold, etc.)
- Support for font stretches (condensed, normal, expanded, etc.)
- In-memory font loading and caching
- WASM support (
wasm32-*targets;mmapio/rayonauto-excluded viacfg) - C API for integration with non-Rust languages
License
MIT