# craft-font-catalog The installed fonts for the Craft apps: discovery on every platform, lookup by family and style or by any other name, and loading shared between threads. ## What it finds A `Catalog` reads, in this order: | Where | Source | Platforms | |---|---|---| | The platform's font folders (`font_dirs`) | `System` | all | | The suite's font store (`font_store_dir`) | `Store` | all | | The app's own folders (`Catalog::set_app_dirs`, for example `office_font_dirs`) | `App` | all | | Files the platform's font service knows (`system` feature, through fontique) | `System` | macOS, Windows, Linux, FreeBSD | | The registry's font list | `System` | Windows | | Files from injected listers (`Catalog::with_lister`) | `System` | all | The font folders: - macOS: `/System/Library/Fonts`, `/Library/Fonts`, `/Network/Library/Fonts`, `~/Library/Fonts`, `/System/Library/Assets` and the font assets macOS downloads on demand (`AssetsV2/com_apple_MobileAsset_Font*`). - Windows: `%WINDIR%\Fonts` and `%LOCALAPPDATA%\Microsoft\Windows\Fonts`. - Linux and the BSDs: `/usr/share/fonts`, `/usr/local/share/fonts`, the Flatpak host folders (`/run/host/fonts`, `/run/host/local-fonts`, `/run/host/user-fonts`), `~/.local/share/fonts`, `~/.fonts`, `$XDG_DATA_HOME/fonts`, `$XDG_DATA_DIRS/*/fonts`, and the `` and `` entries of the fontconfig configuration with its includes and the user's own. Inside a Flatpak sandbox, also the host's folders from `/run/host/font-dirs.xml` and from the host's configuration under `/run/host/etc/fonts`. - Haiku: the system's and the user's `data/fonts` folders. The font service covers fonts outside those folders: - macOS: CoreText's available fonts, which include fonts that Adobe Fonts, RightFont and other font managers activate from their own folders. - Windows: DirectWrite's system font collection, which includes per-user fonts and the fonts that font services such as Adobe Fonts load at run time. The registry adds fonts that programs register by full path. - Linux and FreeBSD: fontconfig, loaded at run time (`fontconfig-dlopen`), so a system without it still runs. `FontEnv` holds the environment all of this depends on (`HOME`, the XDG variables, `FONTCONFIG_FILE`, `WINDIR`, `LOCALAPPDATA`, `CRAFT_FONT_STORE`, a file system root), so tests can describe any system without touching the process environment. ## The font store `font_store_dir` is the one per-user folder that the suite's font store installs fonts into and every app's catalog reads. No other program reads it. | Platform | Folder | |---|---| | macOS | `~/Library/Application Support/Craft Fonts` | | Windows | `%LOCALAPPDATA%\Craft Fonts` | | Linux, BSDs, Haiku | `$XDG_DATA_HOME/craft-fonts` (`~/.local/share/craft-fonts`) | `CRAFT_FONT_STORE` replaces it, for portable installs and tests. ## Scanning The scan reads each file's table directory and its `name`, `OS/2` and `fvar` tables, at most 1 MiB per table, never the whole file. Faces without outline tables (`glyf`, `CFF `, `CFF2`, `VARC`) are left out. Files in folders count when they have a font extension (`.ttf`, `.otf`, `.ttc`, `.otc`); files a font service or lister names count whatever their name. Mac suitcase fonts are read from their resource fork. Damaged files give no faces, never a panic. Each face (`FaceInfo`) has its family and style, legacy family and style, family names in other languages, full name, PostScript name, weight, width, italic, a named instance's axis settings, `fsType`, the OS/2 code page bits, its file and index, and its source. Each named instance of a variable font is a face of its own. ## Lookup - `families()` and `styles(family)`. - `family(name)` and `resolve(name, style)` accept any name of a family: its own name ignoring ASCII case, its names in other languages and its legacy names ignoring case and anything but letters and digits. `resolve` also accepts a legacy family with its legacy style (`Roboto Light` / `Regular`), a full name, and a PostScript name. - Within a family, `resolve` matches the style by name (any of its names). Otherwise it picks the nearest weight, with italic differing counting as 1000, then the nearest width. It returns `Match::Exact` or `Match::NearestStyle`, so the app decides about substitutes. - `by_postscript_name(name)`. ## Loading and threads - The first scan runs on first use, or on a background thread after `scan_in_background`. Lookups wait for it. - Lookups read an immutable snapshot, so a rescan never blocks them. - `load_family` reads a family's files once. Other threads asking for the same family wait and share the result; other families load at the same time. Faces of one file share its bytes (`Arc<[u8]>`). - `add_bytes` and `add_file` add fonts from documents, downloads or the app's bundle, within `Limits`. Added faces win over installed faces of the same family and style. `remove_added` removes a source's faces, for example when a document closes. ## Changes - `rescan` scans again, reading only new and changed files. - `installed_fonts_changed` compares the modification times of every folder the scan read (and the folders of files a font service named) and, on Windows, the registry's font list. It costs a few hundred `stat` calls, not a scan. - `rescan_if_changed` does both; apps call it when their window is activated. - `watch(interval, on_change)` polls on a background thread. - `generation` counts the catalog's changes. ## One enumeration for the UI and documents With the `system` feature, `system::collection()` returns the process's fontique `Collection`: the system's fonts and the font store's. The catalog lists its files, and craft-ui's font fallback can query the same collection instead of building its own, so the system's fonts are enumerated once per process. A rescan builds a new collection; `system::generation()` tells holders of an older clone to take a new one. ## Try it ```sh cargo run -p craft-font-catalog --example fonts # counts, folders, faces outside them cargo run -p craft-font-catalog --example fonts -- Arial # a family's faces cargo run -p craft-font-catalog --example fonts -- --watch # report fonts installed or removed ```