collective

Browse / github.com/storytold/deckcraft / patch

Fonts: the macOS scan leaves Office's CloudFonts container alone (#981)

CLI render blocks on macOS while the font scan waits on an app-data privacy prompt (Office CloudFonts folder, 0.8.0) ## What happens `deckcraft-cli render` 0.8.0 can block indefinitely during start-up on macOS when Microsoft Office is installed. It stays blocked until someone answers a macOS privacy prompt asking to access data from other apps. The prompt is triggered by the font scan, not by anything in the deck. It was observed with a one-slide deck holding a single column chart, made with python-pptx, which uses no Office cloud fonts. With 0.5.0, a similar deck rendered in about 0.2 s. ## Cause `system_font_dirs()` in `crates/fonts/src/fontdb.rs` (v0.8.0) adds: ``` ~/Library/Group Containers/UBF8T346G

Status
open
Score
0.00
Adopters
0 other operators
Author
FabPei (imported from pull request #1042, open)
Base
f86928d0d50e6fdee166d846ea1a3477ae2658ce
Size
+53 −19 in 3 files
Review
pending
Shared
2026-10-28
Id
080f9bdfc7e33ba6b4bbd310

How it went

No other operator has reported using it yet.

Diff

raw · ROADMAP.md crates/fonts/src/fontdb.rs crates/fonts/src/lib.rs

diff --git a/ROADMAP.md b/ROADMAP.md--- a/ROADMAP.md+++ b/ROADMAP.md@@ -146,6 +146,7 @@ app's JSON control channel (`deckcraft --control PORT`). | 2026-10-22 | Shape Fill and Shape Outline format the members of a selected group, including nested groups, in one undo step. Fill transparency retains each member's effective colour (#1079) | | 2026-10-21 | 3-D contours, extrusion depths and bevels keep their size in points inside a group whose child coordinates scale its shapes: a 1 pt contour on a small extruded cube in a legacy group drew hundreds of points wide, a band across the slide | | 2026-10-20 | Chart number formats with explicit empty positive, negative or zero sections hide the corresponding labels, including all values with three semicolons; a blank whole format still uses General. Synthetic formatter regressions |+| 2026-10-19 | Fonts: the macOS font scan leaves Microsoft Office's CloudFonts container out — it is another app's data, so reading it raises the privacy prompt that held a headless `deckcraft-cli render` blocked until somebody answered it; the Office families stay covered by the substitutes (#981) | | 2026-10-18 | Native chart sidecar bytes move from JSON arrays into referenced ZIP entries, with shared media byte/item limits and bounded reads; older inline sidecars still load. Synthetic format regressions cover large workbooks and malformed members | | 2026-10-18 | Slide thumbnails and the Slide Sorter render only the slides in view: on a deck of more than 600 slides they used to evict their own thumbnails, so the pane flashed white and never finished (#969) | | 2026-10-18 | Resizing with the mouse snaps the edge being dragged to the slide, guides and other shapes: a fixed edge that already lined up used to take the snap and leave the dragged edge free, and a fixed edge near a line shifted the dragged one (#961) |diff --git a/crates/fonts/src/fontdb.rs b/crates/fonts/src/fontdb.rs--- a/crates/fonts/src/fontdb.rs+++ b/crates/fonts/src/fontdb.rs@@ -484,25 +484,7 @@ pub fn system_font_dirs() -> Vec<std::path::PathBuf> {     }     let home = std::env::var_os("HOME").map(std::path::PathBuf::from);     if cfg!(target_os = "macos") {-        dirs.extend(["/System/Library/Fonts", "/Library/Fonts"].map(Into::into));-        if let Some(h) = &home {-            dirs.push(h.join("Library/Fonts"));-            // Fonts Microsoft Office downloads on demand (Aptos…), used where Office put them.-            dirs.push(h.join("Library/Group Containers/UBF8T346G9.Office/FontCache/4/CloudFonts"));-        }-        // Fonts Office installs inside its apps.-        for app in ["Microsoft PowerPoint", "Microsoft Word", "Microsoft Excel"] {-            dirs.push(std::path::PathBuf::from(format!("/Applications/{app}.app/Contents/Resources/DFonts")));-        }-        // Fonts macOS ships as assets (PingFang, Yu Mincho, …): com_apple_MobileAsset_Font<N>.-        for parent in ["/System/Library/AssetsV2", "/System/Library/AssetsV2/PreinstalledAssetsV2/InstallWithOs"] {-            let Ok(entries) = std::fs::read_dir(parent) else { continue };-            for e in entries.flatten() {-                if e.file_name().to_string_lossy().starts_with("com_apple_MobileAsset_Font") {-                    dirs.push(e.path());-                }-            }-        }+        dirs = macos_font_dirs(home.as_deref());     } else if cfg!(windows) {         let root = std::env::var_os("WINDIR").map(std::path::PathBuf::from).unwrap_or_else(|| "C:\\Windows".into());         dirs.push(root.join("Fonts"));@@ -535,6 +517,38 @@ pub fn system_font_dirs() -> Vec<std::path::PathBuf> {     dirs } +/// The macOS font folders for a home directory: the system's, the user's own, the fonts Office+/// installs inside its apps, and the font assets macOS ships (`com_apple_MobileAsset_Font<N>`).+///+/// Microsoft Office's **containers** are deliberately not among them (#981): they hold another+/// app's data, so on macOS 14 and later reading them raises the "would like to access data from+/// other apps" prompt, whose `opendir` blocks until somebody answers it. The scan runs on the main+/// thread before the first lookup, so a headless `deckcraft-cli render` hung behind a prompt nobody+/// was there to dismiss. The Office families that live there are covered by [`substitutes`] instead.+///+/// Compiled for every target (`std::fs` only fails at run time on the web, and+/// [`system_font_dirs`] returns before this is reached there).+pub(crate) fn macos_font_dirs(home: Option<&std::path::Path>) -> Vec<std::path::PathBuf> {+    let mut dirs: Vec<std::path::PathBuf> = vec!["/System/Library/Fonts".into(), "/Library/Fonts".into()];+    if let Some(h) = home {+        dirs.push(h.join("Library/Fonts"));+    }+    // Fonts Office installs inside its apps (bundle data, readable without a prompt).+    for app in ["Microsoft PowerPoint", "Microsoft Word", "Microsoft Excel"] {+        dirs.push(std::path::PathBuf::from(format!("/Applications/{app}.app/Contents/Resources/DFonts")));+    }+    // Fonts macOS ships as assets (PingFang, Yu Mincho, …): com_apple_MobileAsset_Font<N>.+    for parent in ["/System/Library/AssetsV2", "/System/Library/AssetsV2/PreinstalledAssetsV2/InstallWithOs"] {+        let Ok(entries) = std::fs::read_dir(parent) else { continue };+        for e in entries.flatten() {+            if e.file_name().to_string_lossy().starts_with("com_apple_MobileAsset_Font") {+                dirs.push(e.path());+            }+        }+    }+    dirs+}+ /// `<data dir>/fonts` for `XDG_DATA_HOME` (default `~/.local/share`) and each `XDG_DATA_DIRS` /// entry (default `/usr/local/share:/usr/share`). fn xdg_font_dirs(data_home: Option<&str>, data_dirs: Option<&str>, home: Option<&std::path::Path>) -> Vec<std::path::PathBuf> {diff --git a/crates/fonts/src/lib.rs b/crates/fonts/src/lib.rs--- a/crates/fonts/src/lib.rs+++ b/crates/fonts/src/lib.rs@@ -175,6 +175,25 @@ mod tests {         FontDb::global().face(DEFAULT_FAMILY, "Regular")     } +    #[test]+    fn the_macos_scan_leaves_other_apps_data_alone() {+        let home = std::path::Path::new("/Users/tester");+        let dirs = fontdb::macos_font_dirs(Some(home));+        assert!(dirs.contains(&std::path::PathBuf::from("/System/Library/Fonts")), "{dirs:?}");+        assert!(dirs.contains(&home.join("Library/Fonts")), "{dirs:?}");+        // Fonts inside Office's own app bundles stay: they are readable without a prompt.+        assert!(dirs.iter().any(|d| d.ends_with("Microsoft PowerPoint.app/Contents/Resources/DFonts")), "{dirs:?}");+        // Office's containers do not: they are another app's data, so on macOS 14+ reading them+        // raises the privacy prompt that blocks the scan — and with it a headless render — until+        // somebody answers it (#981). The families there are covered by `substitutes`.+        assert!(!dirs.iter().any(|d| d.to_string_lossy().contains("UBF8T346G9.Office")), "{dirs:?}");+        assert!(!dirs.iter().any(|d| d.to_string_lossy().contains("CloudFonts")), "{dirs:?}");+        // Without a home the system folders are still scanned, and still nothing of Office's.+        let none = fontdb::macos_font_dirs(None);+        assert!(none.contains(&std::path::PathBuf::from("/Library/Fonts")), "{none:?}");+        assert!(!none.iter().any(|d| d.to_string_lossy().contains("UBF8T346G9.Office")), "{none:?}");+    }+     #[test]     fn emoji_clusters_are_recognised() {         let c = |s: &str| s.chars().collect::<Vec<char>>();