Files
vectorcraft/crates/engine/tests/font_folder.rs
T
dotCooCoo 5709e636a5 Fonts: missing fonts are listed when a document opens, with Find Fonts and a folder search
When a document opens with fonts that aren't available, fonts used
only in its symbols included, VectorCraft lists them in a Missing
Fonts dialog after the missing linked file questions, one document at
a time, with Find Fonts and Close. Find Fonts opens Type › Find Font
on the first missing font of the layers' type, to replace it. After
Find Fonts or Close the dialog doesn't open again for that open; when
another dialog replaces it, it opens again once that dialog closes.

Search Fonts Folder (the Fonts folder File › Package writes next to
the document) and Find in Folder… search a folder and its subfolders
for the fonts' files, and Add Fonts copies the files the user chooses
into a Fonts folder next to the preferences. The desktop app and
vectorcraft-cli read that folder with the system's font folders. Type
› Find Font has a Find in Folder… button too when fonts the document
uses are missing. The web app shows no dialog; a status message says
how many fonts aren't available.

- text.missingFonts lists each missing family and style once,
  whatever version the document's type names, and leaves out CSS
  generic families such as sans-serif. text.findFontFiles searches a
  folder on up to 8 threads and returns at once; text.findFontFiles {}
  reports the search, and {stop: true} stops it. A search reads only
  what the font scan reads of font files (their table directories and
  name, fvar and OS/2 tables) and, on macOS, of suitcase fonts, and
  lists a file for a font only when the file makes the font resolve
  exactly. It does not enter app packages, the Program Files and
  ProgramData folders, the system's folders at the root of a volume,
  the Shared and Public folders in Users, a home folder's Library,
  AppData, Applications, snap and dot folders, a Library folder's app
  data folders or the folders the environment names for apps, and it
  skips other hidden folders and does not follow links below the
  picked folder. It ends after 60 s by default, after 10,000,000
  entries listed, with more than 1,000,000 folders waiting, after
  10,000 font files read or 1,000 kept, or once every font has a file;
  Stop and Close end it too.
- text.addFontFiles copies only files the last search found. Of
  those, it copies only regular .ttf, .otf, .ttc, .otc, .woff and
  .woff2 files and suitcase fonts with their resource fork, at most
  256 MB a file and 1 GB in all. It never replaces a file: a file with
  the same contents is kept, and when another file has that name, the
  copy gets a number (Name 2.otf). The fonts are then scanned again,
  and type set in them redraws.
- ui.missingFontsDialog and ui.findFontsInFolder open the dialog for
  agents; docs/control-protocol.md lists its fields.
- File › Package gives a font file whose name has no font extension
  the extension that matches its data (.ttf, .otf or .ttc).
- vectorcraft-text adds set_app_font_dir, which system_font_dirs
  reads, WantedFonts, which reads a font file's names as the font scan
  does, is_font_file and is_suitcase. #762 split its face reader, which
  now takes read budgets: file_face_names_within tries the suitcase
  reader first, and reader_face_names_within applies the budgets to
  each font. vectorcraft-format adds write_new_with.

Docs: docs/mcp.md, docs/control-protocol.md, ROADMAP.
2026-10-10 08:42:32 +07:00

78 lines
4.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! VectorCraft's own Fonts folder: a missing font's file, found in a folder, is copied into it
//! without replacing a file there, and type set in the font redraws in it. One test in its own
//! process, so the process-wide font database and Fonts folder start fresh, as when the app starts.
// Integration tests: unwrapping and panicking on failure is fine here, unlike in shipped code (AGENTS.md › Robustness).
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use std::time::{Duration, Instant};
use serde_json::{Value, json};
use vectorcraft_engine::Session;
use vectorcraft_engine::cmd::findfiles::{Rules, plain};
/// `text.findFontFiles {}` once the search ended (10 seconds at most).
fn finished(s: &mut Session) -> Value {
let until = Instant::now() + Duration::from_secs(10);
loop {
let r = s.execute("text.findFontFiles", &json!({})).unwrap();
if r["state"] != "searching" || Instant::now() > until {
return r;
}
std::thread::sleep(Duration::from_millis(5));
}
}
#[test]
fn a_found_font_is_copied_without_replacing_a_file_and_its_type_redraws() {
let root = vectorcraft_testkit::temp_dir("font-folder");
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(root.join("Downloads/Deep")).unwrap();
let root = plain(std::fs::canonicalize(&root).unwrap());
let fonts = root.join("App Fonts");
vectorcraft_text::set_app_font_dir(fonts.clone());
assert!(vectorcraft_text::system_font_dirs().contains(&fonts));
let file = root.join("Downloads/Deep/Findme.otf");
std::fs::write(&file, vectorcraft_testkit::fonts::renamed("Findme Sans 3")).unwrap();
let serif = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../assets/fonts/SourceSerif4-Regular.ttf");
std::fs::copy(serif, root.join("other.ttf")).unwrap();
let mut s = Session::new();
s.search_rules = Some(Rules { user_content: vec![root.clone()], ..Rules::default() });
s.search_threads = Some(2);
s.execute("file.new", &json!({"width": 300, "height": 200})).unwrap();
s.execute("text.create", &json!({"x": 10, "y": 40, "text": "Findme", "font": "Findme Sans 3"})).unwrap();
let r = s.execute("text.missingFonts", &json!({})).unwrap();
assert_eq!((r["fonts"][0]["family"].as_str(), r["fonts"][0]["status"].as_str()), (Some("Findme Sans 3"), Some("missing")), "{r}");
s.doc_mut().unwrap().mark_saved();
let revision = s.doc().unwrap().revision;
s.execute("text.findFontFiles", &json!({"folder": root})).unwrap();
let r = finished(&mut s);
assert_eq!((&r["state"], &r["fonts"][0]["files"]), (&json!("done"), &json!([file])), "{r}");
// A file of that name is in the Fonts folder already: it stays, and the copy takes a number.
std::fs::create_dir_all(&fonts).unwrap();
std::fs::write(fonts.join("Findme.otf"), b"keep").unwrap();
let r = s.execute("text.addFontFiles", &json!({"files": [file]})).unwrap();
assert_eq!(r["copied"], json!([{"from": file, "to": fonts.join("Findme 2.otf")}]), "{r}");
assert!(r["faces"].as_u64().is_some_and(|n| n > 0) && r["families"].as_u64().is_some(), "{r}");
assert_eq!(std::fs::read(fonts.join("Findme.otf")).unwrap(), b"keep");
// The type is set in the font now, without editing the document.
let listed = s.execute("text.fonts", &json!({})).unwrap();
let findme = listed.as_array().unwrap().iter().find(|f| f["family"] == "Findme Sans 3").cloned().unwrap();
assert_eq!((findme["status"].as_str(), findme["resolved"]["family"].as_str()), (Some("exact"), Some("Findme Sans 3")), "{findme}");
let st = s.doc().unwrap();
assert!(st.revision > revision && !st.is_dirty());
assert_eq!(s.execute("text.missingFonts", &json!({})).unwrap()["count"], 0);
// The same file again is kept, not copied a third time.
let r = s.execute("text.addFontFiles", &json!({"files": [file]})).unwrap();
assert_eq!((r["copied"].clone(), r["kept"].clone()), (json!([]), json!([{"from": file, "to": fonts.join("Findme 2.otf")}])), "{r}");
assert!(!fonts.join("Findme 3.otf").exists());
// Only files the last search found are copied.
let r = s.execute("text.addFontFiles", &json!({"files": [root.join("other.ttf")]}));
assert!(r.as_ref().is_err_and(|e| e.to_string().contains("the last text.findFontFiles search")), "{r:?}");
let _ = std::fs::remove_dir_all(&root);
}