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.
This commit is contained in:
dotCooCoo
2026-10-10 08:42:32 +07:00
committed by bflatastic
parent 796accffc0
commit 5709e636a5
40 changed files with 4673 additions and 49 deletions
+21
View File
@@ -375,6 +375,27 @@ folder; `ui.dialog.set {field: "discard", value: true}` then confirm ignores it
Then, with the preference `updateLinks: "askWhenModified"`, modified linked files are offered for update in a
`confirm` dialog whose `ui.dialog.confirm` runs `links.update`.
Missing fonts: once the missing linked file questions are answered, or at once when there are none, `app.open` of a
document whose type uses fonts that aren't available (`text.missingFonts`) opens the `missingFonts` dialog, one document
at a time, and so does `links.editOriginal` of a placed document. When another dialog takes its place (Import PDF for a
file opened next, for example), the dialog opens again once that one closes. Fields: `document` (the document's uid),
`fonts` (`[{family, style, status, resolved}]`), `fontsNextToDocument` (the `Fonts` folder next to the document, or
empty), `state` (empty, `searching`, `done`, `stopped` or `failed`), `search` (the search's `id`), `folder`, `found`
(`text.findFontFiles`'s `fonts`, with the files found), `searched`, `skipped`, `unreadable`, `stopped`, `error` and
`chosen` (the files to add; a font's first file is chosen when the search finds it). Its buttons are Find Fonts and
Close, with Search Fonts Folder, Find in Folder… and, once files are found, Add Fonts in the dialog.
`ui.dialog.set {field: "discard", value: true}` then `ui.dialog.confirm` is Find Fonts: the dialog closes and Type ›
Find Font opens (dialog `findFont`) with the first missing font selected. `ui.dialog.set {field: "searchFolder",
value}` then `ui.dialog.confirm` searches that folder (`text.findFontFiles`), keeps the dialog open and clears
`searchFolder`, also when the search doesn't start. The fields change as frames draw the dialog; `text.findFontFiles
{}` reports the search directly. `ui.dialog.confirm` without `discard` or `searchFolder` is Add Fonts: it copies the
files in `chosen` (`text.addFontFiles`) and closes the dialog. `ui.dialog.cancel` (Close) closes it and stops its
search. After Find Fonts or Close, the dialog doesn't open again for that open.
`ui.missingFontsDialog {folder?}` opens the dialog for the active document, searching `folder` when given.
`ui.findFontsInFolder {folder?}` (Find in Folder… in Type › Find Font…, shown when the document misses fonts) asks for a
folder, then opens the dialog searching it. The web shows a status line instead of the dialog, and both commands are
disabled there.
Text Import Options: placing a `.txt` file through the app (`file.place` with a file and no `text` options, the Place
dialog, a drop) opens the `textImport` dialog (fields `platform`: `windows`/`mac`, `characterSet`: `unicode`/`ansi`,
`removeLineReturns`, `removeParagraphReturns`, `replaceSpaces`, `spaces`). `ui.dialog.confirm` places the file as area
+49 -1
View File
@@ -2562,7 +2562,11 @@ the fonts CoreText's font manager lists outside them, such as those apps and fon
folders; Linux and BSD: `/usr/share/fonts`,
`/usr/local/share/fonts`, `~/.fonts` and the XDG data folders' `fonts`, `~/.local/share/fonts` among them, and in a
Flatpak sandbox the host's fonts). Faces without outlines VectorCraft draws (no `glyf`, `CFF`, `CFF2` or `VARC` table, such
as bitmap-only fonts) are left out. The installed fonts are cataloged once per session (in the background when the app starts, else on the first lookup
as bitmap-only fonts) are left out. The scan also reads the folder the `fontsFolder` preference names (Preferences › Type ›
Additional Fonts Folder) with its subfolders and, in the desktop app and `vectorcraft-cli`, VectorCraft's own `Fonts` folder
next to the preferences: `~/Library/Application Support/VectorCraft/Fonts` on macOS, `%APPDATA%\VectorCraft\Fonts` on
Windows, and `$XDG_CONFIG_HOME/vectorcraft/Fonts` or `~/.config/vectorcraft/Fonts` on Linux and BSD. `text.addFontFiles`
copies fonts into that folder. The installed fonts are cataloged once per session (in the background when the app starts, else on the first lookup
by family name), so opening, placing, pasting and importing files find them whatever ran before. `text.fontList`
lists every family available, the installed ones included, as the font menus do: without the system's hidden
families, whose names start with "." (macOS's ".SF NS", ".LastResort"), which still resolve when a document names
@@ -2578,6 +2582,50 @@ comes to the front and a font folder changed meanwhile.
{"name":"run_command","arguments":{"command":"text.rescanFonts","params":{}}}
```
`text.missingFonts` lists the fonts that the active document's type uses, in its layers and its symbols, and that aren't
available as named: `{fonts: [{family, style, status, resolved}], count, fontsNextToDocument?}`. `status` is `missing` or
`substitute`, as in `text.fonts`. CSS generic families such as `sans-serif`, which no font file provides, and names
longer than 256 bytes are left out. `fontsNextToDocument` is the `Fonts` folder next to the saved document
(File › Package writes it), when there is one and a search may start there. `text.findFontFiles {folder}` looks for
the fonts' files in an absolute folder and its subfolders on separate threads and returns at once with
`state: "searching"` and an `id`; `fonts: [{family, style?}]` names other fonts to look for. `text.findFontFiles {}`
returns the last search's state with the files found so far for each font: `searching`, `done`, `stopped` (with
`stopped`: `stop`, `time` or `limit`) or `failed` (with `error`); before the first search it returns
`{state: "idle", fonts: []}`, without an `id`. `{stop: true}` stops the search. A search reads the table directories
and the `name`, `fvar` and `OS/2` tables of `.ttf`, `.otf`, `.ttc` and `.otc` files and, on macOS, of suitcase fonts
(files without an extension whose fonts are in their resource fork), and it lists a file for a font only when adding
the file makes the font resolve exactly as `text.fonts` resolves it. It ends after `maxSeconds` (60 by default, 1 to
600), at its limits on the entries listed (10,000,000), the folders waiting to be listed (1,000,000), the font files
read (10,000) and the files kept (1,000), or once every font has a file. Folders more than 64 levels below the picked
one are skipped and counted in `skipped`.
A search does not enter app and media library packages (`.app`, `.bundle`, `.framework`, `.photoslibrary` and the like),
folders named `Program Files` (also `Program Files (x86)` and `Program Files (Arm)`), `ProgramData`, `$Recycle.Bin` or
`System Volume Information`, the system's folders at the root of a volume (a macOS, Linux, BSD or Windows root, told by
the folders it holds), the `Shared` and `Public` folders in `Users`, a home folder's `Library`, `AppData`,
`Applications` and `snap` folders and the folders in it whose names start with a dot, the app data folders in a
`Library` folder (`Application Support`, `Containers`, `Group Containers`, `Caches`, `Preferences`), or the folders the
environment names for apps and the system (such as `APPDATA`, `LOCALAPPDATA`, `ProgramFiles`, `SystemRoot` and
`XDG_CONFIG_HOME`), and a search of a folder inside one of them fails. The walk also skips other hidden folders and
folders whose contents are in the cloud, and it follows no links; a picked folder may be one of those, and a picked path
that goes through a link is searched at its target. Programs installed in other folders, such as a folder on another
drive, are searched as any other folder is.
`text.addFontFiles {files}` copies files that the session's last search found into VectorCraft's `Fonts` folder, then
scans the fonts again, and type set in them redraws. 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, and lists any
other file in `skipped`. It never replaces a file: a file with the same contents is kept (`kept`), and when another
file has the name, the copy gets a number (`Name 2.otf`). Copy only fonts you own or are licensed to install. `command.batch` returns before a search ends; later requests over MCP or the control channel poll
`text.findFontFiles {}`. `vectorcraft-cli run` stops the search when its last step ends. The web build has no Fonts
folder and no folder search.
```json
{"name":"run_command","arguments":{"command":"text.missingFonts","params":{}}}
{"name":"run_command","arguments":{"command":"text.findFontFiles","params":{"folder":"/Users/me/Downloads"}}}
{"name":"run_command","arguments":{"command":"text.findFontFiles","params":{}}}
{"name":"run_command","arguments":{"command":"text.addFontFiles","params":{"files":["/Users/me/Downloads/Example/Example-Regular.otf"]}}}
```
## Tool options
`tool.setOption` reads and sets a tool's options, headless too: `{key, value}` or `{values: {…}}` sets them on the