Files
figcraft/apps/figcraft-cli/tests/fonts.rs
T
bflatastic f2802cca02 M7: headless renders use system fonts
Headless runs (`figcraft-cli export`, `render`, `convert`, `run`, and the headless MCP server) never indexed the installed fonts, so a text layer in an installed family (Segoe UI, Inter installed per user, ...) was measured and drawn with a fallback face, while the desktop app used the real one. Auto layout sizes from MCP agents therefore differed from what users see in the app.

- `figcraft_text::scan_system_fonts_on_demand()`: the font database indexes the system fonts the first time it is used (first text layout, font lookup or font listing). `figcraft-cli` turns it on at startup for every subcommand, so a run that touches no text never pays for the scan. The desktop app keeps its background scan.
- `scan_system_fonts()` now runs once per process (a `OnceLock`): racing callers wait for the one directory walk instead of each walking the folders.
- Opt-out for reproducible renders: `--no-system-fonts` on any subcommand, or `FIGCRAFT_NO_SYSTEM_FONTS=1` (honoured by the desktop app too). Text then uses only the built-in fonts, as before.
- The scan is cheaper: it reads each file's headers, table directories and `name` tables instead of whole files (about 460 MB on this Windows machine), and takes file metadata from the directory listing instead of a lookup per file. This also speeds up the desktop app's background scan.
- Browser/wasm build unchanged (the scan is a no-op there).
- Docs: `docs/cli.md` (new Fonts section) and `docs/mcp.md` (Fonts under the backends table); CLI usage text mentions the flag.
2026-10-12 09:32:42 +07:00

48 lines
2.3 KiB
Rust

//! Headless runs lay text out with the installed fonts, like the desktop app, unless
//! `--no-system-fonts` or `FIGCRAFT_NO_SYSTEM_FONTS=1` opts out.
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use std::process::Command;
use serde_json::{Value, json};
/// The result of one `figcraft-cli run` step (script file `name`); `opt_out` sets the environment
/// variable.
fn run_one(name: &str, step: Value, args: &[&str], opt_out: bool) -> Value {
let d = std::env::temp_dir().join(format!("figcraft-cli-fonts-{}", std::process::id()));
std::fs::create_dir_all(&d).unwrap();
let script = d.join(format!("{name}.json"));
std::fs::write(&script, json!([step]).to_string()).unwrap();
let mut cmd = Command::new(env!("CARGO_BIN_EXE_figcraft-cli"));
cmd.arg("run").arg(&script).args(args);
if opt_out {
cmd.env("FIGCRAFT_NO_SYSTEM_FONTS", "1");
} else {
cmd.env_remove("FIGCRAFT_NO_SYSTEM_FONTS");
}
let out = cmd.output().unwrap();
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
let line: Value = serde_json::from_slice(&out.stdout).unwrap();
line["result"].clone()
}
fn families(args: &[&str], opt_out: bool) -> Vec<String> {
serde_json::from_value(run_one("families", json!({"command": "text.fonts"}), args, opt_out)).unwrap()
}
#[test]
fn headless_runs_resolve_installed_families_unless_opted_out() {
let built_in = families(&["--no-system-fonts"], false);
assert_eq!(families(&[], true), built_in, "FIGCRAFT_NO_SYSTEM_FONTS=1 opts out like the flag");
// Any family this machine adds (scanned here, in the test); none (CI): nothing to check.
figcraft_text::scan_system_fonts();
let Some(installed) = figcraft_text::family_names().into_iter().find(|f| !built_in.contains(f)) else {
eprintln!("skipped: no installed fonts beyond the built-in ones");
return;
};
assert!(families(&[], false).contains(&installed), "headless runs list {installed}");
let face = json!({"command": "text.fonts", "params": {"family": installed, "axes": true}});
assert_eq!(run_one("face", face.clone(), &[], false)["family"], json!(installed), "text uses the installed family");
assert_ne!(run_one("face", face, &["--no-system-fonts"], false)["family"], json!(installed), "opted out, it falls back");
}