Skip to content

Commit 37e8298

Browse files
fschuttclaude
andcommitted
refactor: make parsing, multithreading, cache opt-in (not default)
Change default features from ["std", "parsing", "multithreading"] to just ["std"]. This lets consumers pull rust-fontconfig without allsorts (48 deps), rayon (5 deps), or serde/bincode/dirs (9 deps). Without parsing, build() now does filename-based scanning using the existing tokenizer (config::tokenize_font_stem, config::font_directories) instead of returning an empty cache. Refactored internals: - Split FcScanSingleDirectoryRecursive into FcCollectFontFilesRecursive (std-only, no allsorts) + FcParseFontFiles (parsing-only) - Widen PathBuf import and process_path from cfg(std,parsing) to cfg(std) - Three clean build() variants: no_std, std-only, std+parsing - pattern_from_filename uses existing tokenizer, no hardcoded names Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d7e35d8 commit 37e8298

2 files changed

Lines changed: 101 additions & 34 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ mmapio = { version = "0.9.1", default-features = false, optional = true }
3030
rayon = { version = "1.11.0", default-features = false, optional = true }
3131

3232
[features]
33-
default = ["std", "parsing", "multithreading"]
33+
default = ["std"]
3434
std = ["mmapio"]
3535
multithreading = ["rayon"]
3636
parsing = ["allsorts", "std"]

src/lib.rs

Lines changed: 100 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ use allsorts::tables::os2::Os2;
8585
use allsorts::tables::{FontTableProvider, HheaTable, HmtxTable, MaxpTable};
8686
#[cfg(all(feature = "std", feature = "parsing"))]
8787
use allsorts::tag;
88-
#[cfg(all(feature = "std", feature = "parsing"))]
88+
#[cfg(feature = "std")]
8989
use std::path::PathBuf;
9090

9191
pub mod utils;
@@ -1360,16 +1360,40 @@ impl FcFontCache {
13601360
}
13611361
}
13621362

1363-
/// Builds a new font cache
1364-
#[cfg(not(all(feature = "std", feature = "parsing")))]
1365-
pub fn build() -> Self {
1366-
Self::default()
1367-
}
1363+
/// Returns an empty font cache (no_std / no filesystem).
1364+
#[cfg(not(feature = "std"))]
1365+
pub fn build() -> Self { Self::default() }
1366+
1367+
/// Scans system font directories using filename heuristics (no allsorts).
1368+
#[cfg(all(feature = "std", not(feature = "parsing")))]
1369+
pub fn build() -> Self { Self::build_from_filenames() }
13681370

1369-
/// Builds a new font cache from all fonts discovered on the system
1371+
/// Scans and parses all system fonts via allsorts for full metadata.
13701372
#[cfg(all(feature = "std", feature = "parsing"))]
1371-
pub fn build() -> Self {
1372-
Self::build_inner(None)
1373+
pub fn build() -> Self { Self::build_inner(None) }
1374+
1375+
/// Filename-only scan: discovers fonts on disk, guesses metadata from
1376+
/// the filename using [`config::tokenize_font_stem`].
1377+
#[cfg(all(feature = "std", not(feature = "parsing")))]
1378+
fn build_from_filenames() -> Self {
1379+
let mut cache = Self::default();
1380+
for dir in crate::config::font_directories(OperatingSystem::current()) {
1381+
for path in FcCollectFontFilesRecursive(dir) {
1382+
let pattern = match pattern_from_filename(&path) {
1383+
Some(p) => p,
1384+
None => continue,
1385+
};
1386+
let id = FontId::new();
1387+
cache.disk_fonts.insert(id, FcFontPath {
1388+
path: path.to_string_lossy().to_string(),
1389+
font_index: 0,
1390+
});
1391+
cache.index_pattern_tokens(&pattern, id);
1392+
cache.metadata.insert(id, pattern.clone());
1393+
cache.patterns.insert(pattern, id);
1394+
}
1395+
}
1396+
cache
13731397
}
13741398

13751399
/// Builds a font cache with only specific font families (and their fallbacks).
@@ -3559,42 +3583,41 @@ fn FcScanDirectoriesInner(paths: &[(Option<String>, String)]) -> Vec<(FcPattern,
35593583
}
35603584
}
35613585

3562-
#[cfg(all(feature = "std", feature = "parsing"))]
3563-
fn FcScanSingleDirectoryRecursive(dir: PathBuf) -> Vec<(FcPattern, FcFontPath)> {
3564-
let mut files_to_parse = Vec::new();
3586+
/// Recursively collect all files from a directory (no parsing, no allsorts).
3587+
#[cfg(feature = "std")]
3588+
fn FcCollectFontFilesRecursive(dir: PathBuf) -> Vec<PathBuf> {
3589+
let mut files = Vec::new();
35653590
let mut dirs_to_parse = vec![dir];
35663591

3567-
'outer: loop {
3568-
let mut new_dirs_to_parse = Vec::new();
3569-
3570-
'inner: for dir in dirs_to_parse.clone() {
3571-
let dir = match std::fs::read_dir(dir) {
3592+
loop {
3593+
let mut new_dirs = Vec::new();
3594+
for dir in &dirs_to_parse {
3595+
let entries = match std::fs::read_dir(dir) {
35723596
Ok(o) => o,
3573-
Err(_) => continue 'inner,
3597+
Err(_) => continue,
35743598
};
3575-
3576-
for (path, pathbuf) in dir.filter_map(|entry| {
3577-
let entry = entry.ok()?;
3599+
for entry in entries.flatten() {
35783600
let path = entry.path();
3579-
let pathbuf = path.to_path_buf();
3580-
Some((path, pathbuf))
3581-
}) {
35823601
if path.is_dir() {
3583-
new_dirs_to_parse.push(pathbuf);
3602+
new_dirs.push(path);
35843603
} else {
3585-
files_to_parse.push(pathbuf);
3604+
files.push(path);
35863605
}
35873606
}
35883607
}
3589-
3590-
if new_dirs_to_parse.is_empty() {
3591-
break 'outer;
3592-
} else {
3593-
dirs_to_parse = new_dirs_to_parse;
3608+
if new_dirs.is_empty() {
3609+
break;
35943610
}
3611+
dirs_to_parse = new_dirs;
35953612
}
35963613

3597-
FcParseFontFiles(&files_to_parse)
3614+
files
3615+
}
3616+
3617+
#[cfg(all(feature = "std", feature = "parsing"))]
3618+
fn FcScanSingleDirectoryRecursive(dir: PathBuf) -> Vec<(FcPattern, FcFontPath)> {
3619+
let files = FcCollectFontFilesRecursive(dir);
3620+
FcParseFontFiles(&files)
35983621
}
35993622

36003623
#[cfg(all(feature = "std", feature = "parsing"))]
@@ -3621,7 +3644,7 @@ fn FcParseFontFiles(files_to_parse: &[PathBuf]) -> Vec<(FcPattern, FcFontPath)>
36213644
result.into_iter().flat_map(|f| f.into_iter()).collect()
36223645
}
36233646

3624-
#[cfg(all(feature = "std", feature = "parsing"))]
3647+
#[cfg(feature = "std")]
36253648
/// Takes a path & prefix and resolves them to a usable path, or `None` if they're unsupported/unavailable.
36263649
///
36273650
/// Behaviour is based on: https://www.freedesktop.org/software/fontconfig/fontconfig-user.html
@@ -4065,3 +4088,47 @@ fn detect_monospace(
40654088

40664089
Some(monospace)
40674090
}
4091+
4092+
/// Guess font metadata from a filename using the existing tokenizer.
4093+
///
4094+
/// Uses [`config::tokenize_font_stem`] and [`config::FONT_STYLE_TOKENS`]
4095+
/// to extract the family name and detect style hints from the filename.
4096+
#[cfg(feature = "std")]
4097+
fn pattern_from_filename(path: &std::path::Path) -> Option<FcPattern> {
4098+
let ext = path.extension()?.to_str()?.to_lowercase();
4099+
match ext.as_str() {
4100+
"ttf" | "otf" | "ttc" | "woff" | "woff2" => {}
4101+
_ => return None,
4102+
}
4103+
4104+
let stem = path.file_stem()?.to_str()?;
4105+
let all_tokens = crate::config::tokenize_lowercase(stem);
4106+
4107+
// Style detection: check if any token matches a known style keyword
4108+
let has_token = |kw: &str| all_tokens.iter().any(|t| t == kw);
4109+
let is_bold = has_token("bold") || has_token("heavy");
4110+
let is_italic = has_token("italic");
4111+
let is_oblique = has_token("oblique");
4112+
let is_mono = has_token("mono") || has_token("monospace");
4113+
let is_condensed = has_token("condensed");
4114+
4115+
// Family = non-style tokens joined
4116+
let family_tokens = crate::config::tokenize_font_stem(stem);
4117+
if family_tokens.is_empty() { return None; }
4118+
let family = family_tokens.join(" ");
4119+
4120+
Some(FcPattern {
4121+
name: Some(stem.to_string()),
4122+
family: Some(family),
4123+
bold: if is_bold { PatternMatch::True } else { PatternMatch::False },
4124+
italic: if is_italic { PatternMatch::True } else { PatternMatch::False },
4125+
oblique: if is_oblique { PatternMatch::True } else { PatternMatch::DontCare },
4126+
monospace: if is_mono { PatternMatch::True } else { PatternMatch::DontCare },
4127+
condensed: if is_condensed { PatternMatch::True } else { PatternMatch::DontCare },
4128+
weight: if is_bold { FcWeight::Bold } else { FcWeight::Normal },
4129+
stretch: if is_condensed { FcStretch::Condensed } else { FcStretch::Normal },
4130+
unicode_ranges: Vec::new(),
4131+
metadata: FcFontMetadata::default(),
4132+
render_config: FcFontRenderConfig::default(),
4133+
})
4134+
}

0 commit comments

Comments
 (0)