Pure-Rust rewrite of the Linux fontconfig library (no system dependencies) - using ttf-parser and allsorts
Rust
93
221 commits
updated Oct 6, 2026
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!
There are a number of reasons why I want to have a pure-Rust version of fontconfig:
Now for the more practical reasons:
[dependencies]
rust-fontconfig = { version = "5.0", features = ["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.
| 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.
use rust_fontconfig::{FcFontCache, FcPattern};
fn main() {
// Build the font cache (scans system fonts)
let cache = FcFontCache::build();
// Query a font by name
let mut trace = Vec::new();
let results = cache.query(
&FcPattern {
name: Some(String::from("Arial")),
..Default::default()
},
&mut trace
);
if let Some(font_match) = results {
println!("Font match ID: {:?}", font_match.id);
println!("Font unicode ranges: {:?}", font_match.unicode_ranges);
// Get font metadata
if let Some(meta) = cache.get_metadata_by_id(&font_match.id) {
println!("Family: {:?}", meta.family);
}
// Get font file path
if let Some(source) = cache.get_font_by_id(&font_match.id) {
match source {
rust_fontconfig::OwnedFontSource::Disk(path) => {
println!("Path: {}", path.path);
}
rust_fontconfig::OwnedFontSource::Memory(font) => {
println!("Memory font: {}", font.id);
}
}
}
} else {
println!("No matching font found");
}
}
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 textuse rust_fontconfig::{FcFontCache, FcWeight, PatternMatch};
fn main() {
let cache = FcFontCache::build();
// Step 1: Build font fallback chain (without text parameter)
let mut trace = Vec::new();
let font_chain = cache.resolve_font_chain(
&["Arial".to_string(), "sans-serif".to_string()],
FcWeight::Normal,
PatternMatch::DontCare, // italic
PatternMatch::DontCare, // oblique
&mut trace,
);
println!("CSS fallback groups: {}", font_chain.css_fallbacks.len());
for group in &font_chain.css_fallbacks {
println!(" CSS '{}' resolved to {} fonts", group.css_name, group.fonts.len());
}
// Step 2: Query which fonts to use for specific text
let text = "Hello 你好 Здравствуйте";
let font_runs = font_chain.query_for_text(&cache, text);
println!("\nText '{}' split into {} font runs:", text, font_runs.len());
for run in &font_runs {
println!(" '{}' -> font {:?}", run.text, run.font_id);
}
}
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 rust_fontconfig::{
FcFallbackConfig, FcFontCache, FcScriptFallback, FcWeight, GenericFamily,
OperatingSystem, PatternMatch, UnicodeRange,
};
let mut config = FcFallbackConfig::os_defaults(OperatingSystem::current());
// Prefer a specific font for Hiragana when the stack asks for sans-serif.
config.script_fallbacks.insert(0, FcScriptFallback {
range: UnicodeRange { start: 0x3040, end: 0x309F },
generic: Some(GenericFamily::SansSerif),
families: vec!["Noto Sans JP".to_string()],
});
// The font whose .notdef is drawn for anything no font covers.
config.last_resort = vec!["Noto Sans".to_string()];
let cache = FcFontCache::build().with_fallback_config(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(
&["sans-serif".to_string()],
FcWeight::Normal,
PatternMatch::DontCare,
PatternMatch::DontCare,
Some(&[UnicodeRange { start: 0x3040, end: 0x309F }]),
&mut Vec::new(),
);
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.
For fine-grained control, use resolve_text() to get per-character font assignments:
use rust_fontconfig::{FcFontCache, FcWeight, PatternMatch};
fn main() {
let cache = FcFontCache::build();
let chain = cache.resolve_font_chain(
&["sans-serif".to_string()],
FcWeight::Normal,
PatternMatch::False,
PatternMatch::False,
&mut Vec::new(),
);
// Get font assignment for each character
let text = "Hello 世界";
let resolved = chain.resolve_text(&cache, text);
for (ch, font_info) in resolved {
match font_info {
Some((font_id, css_source)) => {
let font_name = cache.get_metadata_by_id(&font_id)
.and_then(|m| m.name.clone().or(m.family.clone()))
.unwrap_or_default();
println!("'{}' -> {} (from CSS '{}')", ch, font_name, css_source);
}
None => println!("'{}' -> NO FONT FOUND", ch),
}
}
}
use rust_fontconfig::{FcFontCache, FcWeight};
fn main() {
let cache = FcFontCache::build();
// List all fonts - filter by properties
let bold_fonts: Vec<_> = cache.list().into_iter()
.filter(|(pattern, _id)| {
matches!(pattern.weight, FcWeight::Bold | FcWeight::ExtraBold)
})
.collect();
println!("Found {} bold fonts:", bold_fonts.len());
for (pattern, id) in bold_fonts.iter().take(5) {
println!(" {:?}: {:?}", id, pattern.name.as_ref().or(pattern.family.as_ref()));
}
}
The rust-fontconfig library provides C-compatible bindings that can be used from C/C++ applications.
You can download pre-built binary files from the latest GitHub release:
rust_fontconfig.dll and rust_fontconfig.liblibrust_fontconfig.dylib and librust_fontconfig.alibrust_fontconfig.so and librust_fontconfig.aAlternatively, you can build the library from source:
# Clone the repository
git clone https://github.com/fschutt/rust-fontconfig.git
cd rust-fontconfig
# Build with FFI support
cargo build --release --features ffi
# The generated libraries will be in target/release
ffi/rust_fontconfig.h to your include directory#include "rust_fontconfig.h"
#include <stdio.h>
#include "rust_fontconfig.h"
int main() {
// Build the font cache
FcFontCache cache = fc_cache_build();
if (!cache) {
fprintf(stderr, "Failed to build font cache\n");
return 1;
}
// Create a pattern to search for Arial
FcPattern* pattern = fc_pattern_new();
fc_pattern_set_name(pattern, "Arial");
// Search for the font
FcTraceMsg* trace = NULL;
size_t trace_count = 0;
FcFontMatch* match = fc_cache_query(cache, pattern, &trace, &trace_count);
if (match) {
char id_str[40];
fc_font_id_to_string(&match->id, id_str, sizeof(id_str));
printf("Found font! ID: %s\n", id_str);
// Get the font path
FcFontPath* font_path = fc_cache_get_font_path(cache, &match->id);
if (font_path) {
printf("Font path: %s (index: %zu)\n", font_path->path, font_path->font_index);
fc_font_path_free(font_path);
}
fc_font_match_free(match);
} else {
printf("Font not found\n");
}
// Clean up
fc_pattern_free(pattern);
if (trace) fc_trace_free(trace, trace_count);
fc_cache_free(cache);
return 0;
}
For a more comprehensive example, see the example.c file included in the repository.
On Linux:
gcc -I./include -L. -o font_example example.c -lrust_fontconfig
On macOS:
clang -I./include -L. -o font_example example.c -lrust_fontconfig
On Windows:
cl.exe /I./include /Fe:font_example.exe example.c rust_fontconfig.lib
resolve_font_chain() for proper fallback handlingchain.resolve_text() for multilingual textchain.query_for_text() for text shaping pipelineswasm32-* targets; mmapio/rayon auto-excluded via cfg)MIT
Overall
Maintained
Code review
crates.io
Latest release
Releases this year
149 followers · starred Apr 2021
279 followers · starred Jan 2022
194 followers · starred Mar 2025
81 followers · starred Apr 2021
Pure-Rust rewrite of the Linux fontconfig library (no system dependencies) - using ttf-parser and allsorts
Rust
93
221 commits
updated Oct 6, 2026
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!
There are a number of reasons why I want to have a pure-Rust version of fontconfig:
Now for the more practical reasons:
[dependencies]
rust-fontconfig = { version = "5.0", features = ["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.
| 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.
use rust_fontconfig::{FcFontCache, FcPattern};
fn main() {
// Build the font cache (scans system fonts)
let cache = FcFontCache::build();
// Query a font by name
let mut trace = Vec::new();
let results = cache.query(
&FcPattern {
name: Some(String::from("Arial")),
..Default::default()
},
&mut trace
);
if let Some(font_match) = results {
println!("Font match ID: {:?}", font_match.id);
println!("Font unicode ranges: {:?}", font_match.unicode_ranges);
// Get font metadata
if let Some(meta) = cache.get_metadata_by_id(&font_match.id) {
println!("Family: {:?}", meta.family);
}
// Get font file path
if let Some(source) = cache.get_font_by_id(&font_match.id) {
match source {
rust_fontconfig::OwnedFontSource::Disk(path) => {
println!("Path: {}", path.path);
}
rust_fontconfig::OwnedFontSource::Memory(font) => {
println!("Memory font: {}", font.id);
}
}
}
} else {
println!("No matching font found");
}
}
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 textuse rust_fontconfig::{FcFontCache, FcWeight, PatternMatch};
fn main() {
let cache = FcFontCache::build();
// Step 1: Build font fallback chain (without text parameter)
let mut trace = Vec::new();
let font_chain = cache.resolve_font_chain(
&["Arial".to_string(), "sans-serif".to_string()],
FcWeight::Normal,
PatternMatch::DontCare, // italic
PatternMatch::DontCare, // oblique
&mut trace,
);
println!("CSS fallback groups: {}", font_chain.css_fallbacks.len());
for group in &font_chain.css_fallbacks {
println!(" CSS '{}' resolved to {} fonts", group.css_name, group.fonts.len());
}
// Step 2: Query which fonts to use for specific text
let text = "Hello 你好 Здравствуйте";
let font_runs = font_chain.query_for_text(&cache, text);
println!("\nText '{}' split into {} font runs:", text, font_runs.len());
for run in &font_runs {
println!(" '{}' -> font {:?}", run.text, run.font_id);
}
}
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 rust_fontconfig::{
FcFallbackConfig, FcFontCache, FcScriptFallback, FcWeight, GenericFamily,
OperatingSystem, PatternMatch, UnicodeRange,
};
let mut config = FcFallbackConfig::os_defaults(OperatingSystem::current());
// Prefer a specific font for Hiragana when the stack asks for sans-serif.
config.script_fallbacks.insert(0, FcScriptFallback {
range: UnicodeRange { start: 0x3040, end: 0x309F },
generic: Some(GenericFamily::SansSerif),
families: vec!["Noto Sans JP".to_string()],
});
// The font whose .notdef is drawn for anything no font covers.
config.last_resort = vec!["Noto Sans".to_string()];
let cache = FcFontCache::build().with_fallback_config(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(
&["sans-serif".to_string()],
FcWeight::Normal,
PatternMatch::DontCare,
PatternMatch::DontCare,
Some(&[UnicodeRange { start: 0x3040, end: 0x309F }]),
&mut Vec::new(),
);
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.
For fine-grained control, use resolve_text() to get per-character font assignments:
use rust_fontconfig::{FcFontCache, FcWeight, PatternMatch};
fn main() {
let cache = FcFontCache::build();
let chain = cache.resolve_font_chain(
&["sans-serif".to_string()],
FcWeight::Normal,
PatternMatch::False,
PatternMatch::False,
&mut Vec::new(),
);
// Get font assignment for each character
let text = "Hello 世界";
let resolved = chain.resolve_text(&cache, text);
for (ch, font_info) in resolved {
match font_info {
Some((font_id, css_source)) => {
let font_name = cache.get_metadata_by_id(&font_id)
.and_then(|m| m.name.clone().or(m.family.clone()))
.unwrap_or_default();
println!("'{}' -> {} (from CSS '{}')", ch, font_name, css_source);
}
None => println!("'{}' -> NO FONT FOUND", ch),
}
}
}
use rust_fontconfig::{FcFontCache, FcWeight};
fn main() {
let cache = FcFontCache::build();
// List all fonts - filter by properties
let bold_fonts: Vec<_> = cache.list().into_iter()
.filter(|(pattern, _id)| {
matches!(pattern.weight, FcWeight::Bold | FcWeight::ExtraBold)
})
.collect();
println!("Found {} bold fonts:", bold_fonts.len());
for (pattern, id) in bold_fonts.iter().take(5) {
println!(" {:?}: {:?}", id, pattern.name.as_ref().or(pattern.family.as_ref()));
}
}
The rust-fontconfig library provides C-compatible bindings that can be used from C/C++ applications.
You can download pre-built binary files from the latest GitHub release:
rust_fontconfig.dll and rust_fontconfig.liblibrust_fontconfig.dylib and librust_fontconfig.alibrust_fontconfig.so and librust_fontconfig.aAlternatively, you can build the library from source:
# Clone the repository
git clone https://github.com/fschutt/rust-fontconfig.git
cd rust-fontconfig
# Build with FFI support
cargo build --release --features ffi
# The generated libraries will be in target/release
ffi/rust_fontconfig.h to your include directory#include "rust_fontconfig.h"
#include <stdio.h>
#include "rust_fontconfig.h"
int main() {
// Build the font cache
FcFontCache cache = fc_cache_build();
if (!cache) {
fprintf(stderr, "Failed to build font cache\n");
return 1;
}
// Create a pattern to search for Arial
FcPattern* pattern = fc_pattern_new();
fc_pattern_set_name(pattern, "Arial");
// Search for the font
FcTraceMsg* trace = NULL;
size_t trace_count = 0;
FcFontMatch* match = fc_cache_query(cache, pattern, &trace, &trace_count);
if (match) {
char id_str[40];
fc_font_id_to_string(&match->id, id_str, sizeof(id_str));
printf("Found font! ID: %s\n", id_str);
// Get the font path
FcFontPath* font_path = fc_cache_get_font_path(cache, &match->id);
if (font_path) {
printf("Font path: %s (index: %zu)\n", font_path->path, font_path->font_index);
fc_font_path_free(font_path);
}
fc_font_match_free(match);
} else {
printf("Font not found\n");
}
// Clean up
fc_pattern_free(pattern);
if (trace) fc_trace_free(trace, trace_count);
fc_cache_free(cache);
return 0;
}
For a more comprehensive example, see the example.c file included in the repository.
On Linux:
gcc -I./include -L. -o font_example example.c -lrust_fontconfig
On macOS:
clang -I./include -L. -o font_example example.c -lrust_fontconfig
On Windows:
cl.exe /I./include /Fe:font_example.exe example.c rust_fontconfig.lib
resolve_font_chain() for proper fallback handlingchain.resolve_text() for multilingual textchain.query_for_text() for text shaping pipelineswasm32-* targets; mmapio/rayon auto-excluded via cfg)MIT
Overall
Maintained
Code review
crates.io
Latest release
Releases this year
149 followers · starred Apr 2021
279 followers · starred Jan 2022
194 followers · starred Mar 2025
81 followers · starred Apr 2021