mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-25 10:56:48 -06:00
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.
48 lines
2.3 KiB
Rust
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");
|
|
}
|