authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-03 14:19:55-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-03 15:13:32-07:00
log7865e773c24cb9a24fbe145bf63fc3fee561fc49
treeee5914bf4c50d4475ed9376af8ebe0c29342ecc8
parent36add75c8aac03116e2ce66440c25e5c8b9bc2de
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

chore: the font clones a build embeds are deflated, and inflated the first time an engine lacks their family

A build script deflates the clones the target keeps into the table layout.rs includes; miniz_oxide was already linked for PNG and PDF. Release executables after the debug split: Linux x86_64 52.35 to 48.78 MB (-3.57), macOS aarch64 36.42 to 34.85 MB (-1.57). Inflating all fourteen faces takes 18 ms on an M3 Ultra, Carlito alone 6 ms. Assisted-by: claude-opus-5.5

3 files changed, 101 insertions(+), 55 deletions(-)

crates/canvas/Cargo.toml+4-1
......@@ -18,6 +18,7 @@ parley.workspace = true
1818serde = { version = "1.0.229", features = ["derive"] }
1919serde_json = "1.0.151"
2020icu_normalizer = "2.3"
21miniz_oxide = "0.8"
2122skrifa = "0.44.0"
2223# std's clock where there is one; the browser's on wasm32-unknown-unknown, whose std has none.
2324web-time = "1.1"
......@@ -39,11 +40,13 @@ path = "src/bin/page_probe.rs"
3940
4041[dev-dependencies]
4142draw = { path = "../draw", features = ["wgpu"] }
42miniz_oxide = "0.8"
4343accesskit_consumer = "0.39.0"
4444pollster = "0.4"
4545wgpu.workspace = true
4646
47[build-dependencies]
48miniz_oxide = "0.8"
49
4750[[example]]
4851name = "render_page"
4952required-features = ["gpu"]
crates/canvas/build.rs created+71
......@@ -0,0 +1,71 @@
1//! Deflates the font clones the target embeds into `bundled.rs`, a table `layout.rs` includes,
2//! leaving out each whose family the target's system ships.
3
4/// Metric-compatible clones, by the family each stands in for.
5const CLONES: [(&str, &[&str]); 4] = [
6 (
7 "Calibri",
8 &[
9 "Carlito-Regular",
10 "Carlito-Bold",
11 "Carlito-Italic",
12 "Carlito-BoldItalic",
13 ],
14 ),
15 ("Arial", &["Arimo", "Arimo-Italic"]),
16 (
17 "Times New Roman",
18 &[
19 "Tinos-Regular",
20 "Tinos-Bold",
21 "Tinos-Italic",
22 "Tinos-BoldItalic",
23 ],
24 ),
25 (
26 "Courier New",
27 &[
28 "Cousine-Regular",
29 "Cousine-Bold",
30 "Cousine-Italic",
31 "Cousine-BoldItalic",
32 ],
33 ),
34];
35
36fn main() {
37 println!("cargo::rerun-if-changed=build.rs");
38 println!("cargo::rerun-if-changed=assets/fonts");
39 let os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
40 let browser = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap() == "wasm32";
41 let out = std::path::PathBuf::from(std::env::var_os("OUT_DIR").unwrap());
42 let mut table = String::from("&[\n");
43 for (family, faces) in CLONES {
44 // The browser fetches them beside the module instead.
45 let shipped = browser
46 || match os.as_str() {
47 "windows" => true,
48 "macos" | "ios" => family != "Calibri",
49 _ => false,
50 };
51 if shipped {
52 continue;
53 }
54 table += &format!(" ({family:?}, &[\n");
55 for face in faces {
56 let font = std::fs::read(format!("assets/fonts/{face}.ttf")).unwrap();
57 let deflated = format!("{face}.ttf.deflate");
58 std::fs::write(
59 out.join(&deflated),
60 miniz_oxide::deflate::compress_to_vec(&font, 10),
61 )
62 .unwrap();
63 // Relative to OUT_DIR, so a target folder reached by another path still finds them.
64 table +=
65 &format!(" include_bytes!(concat!(env!(\"OUT_DIR\"), \"/{deflated}\")),\n");
66 }
67 table += " ]),\n";
68 }
69 table += "]\n";
70 std::fs::write(out.join("bundled.rs"), table).unwrap();
71}
crates/canvas/src/layout.rs+26-54
......@@ -12,7 +12,7 @@ use std::{
1212 fmt,
1313 ops::Range,
1414 sync::{
15 Arc,
15 Arc, OnceLock,
1616 atomic::{AtomicU64, Ordering},
1717 },
1818};
......@@ -113,55 +113,18 @@ pub(crate) const DEFAULT_FONT: &str = "Arial";
113113pub(crate) const DEFAULT_FONT_SIZE: f32 = 11.0;
114114
115115/// Metric-compatible faces for the fonts OneNote pages use most, under the SIL Open Font
116/// Licence files beside them, by the family each stands in for. Each is left out where the
117/// system ships the family it stands in for: Windows ships all four, macOS and iOS all but
118/// Calibri. The browser fetches them beside the module instead (`register_substitute`),
119/// keeping them out of it.
120#[cfg(not(any(windows, target_arch = "wasm32")))]
121const BUNDLED: &[(&str, &[&[u8]])] = &[
122 (
123 "Calibri",
124 &[
125 include_bytes!("../assets/fonts/Carlito-Regular.ttf"),
126 include_bytes!("../assets/fonts/Carlito-Bold.ttf"),
127 include_bytes!("../assets/fonts/Carlito-Italic.ttf"),
128 include_bytes!("../assets/fonts/Carlito-BoldItalic.ttf"),
129 ],
130 ),
131 #[cfg(not(any(target_os = "macos", target_os = "ios")))]
132 (
133 "Arial",
134 &[
135 include_bytes!("../assets/fonts/Arimo.ttf"),
136 include_bytes!("../assets/fonts/Arimo-Italic.ttf"),
137 ],
138 ),
139 #[cfg(not(any(target_os = "macos", target_os = "ios")))]
140 (
141 "Times New Roman",
142 &[
143 include_bytes!("../assets/fonts/Tinos-Regular.ttf"),
144 include_bytes!("../assets/fonts/Tinos-Bold.ttf"),
145 include_bytes!("../assets/fonts/Tinos-Italic.ttf"),
146 include_bytes!("../assets/fonts/Tinos-BoldItalic.ttf"),
147 ],
148 ),
149 #[cfg(not(any(target_os = "macos", target_os = "ios")))]
150 (
151 "Courier New",
152 &[
153 include_bytes!("../assets/fonts/Cousine-Regular.ttf"),
154 include_bytes!("../assets/fonts/Cousine-Bold.ttf"),
155 include_bytes!("../assets/fonts/Cousine-Italic.ttf"),
156 include_bytes!("../assets/fonts/Cousine-BoldItalic.ttf"),
157 ],
158 ),
159];
116/// Licence files beside them, deflated, by the family each stands in for. The build script
117/// leaves out each whose family the system ships, and every one from the browser's module,
118/// which fetches them beside it instead (`register_substitute`).
119const BUNDLED: &[(&str, &[&[u8]])] = include!(concat!(env!("OUT_DIR"), "/bundled.rs"));
120
121/// Each bundled family's faces, inflated the first time an engine lacks the family.
122static INFLATED: [OnceLock<Vec<Blob<u8>>>; BUNDLED.len()] =
123 [const { OnceLock::new() }; BUNDLED.len()];
160124
161125impl Default for TextEngine {
162126 /// An engine that lays out each bundled family in its substitute where it is missing.
163127 fn default() -> Self {
164 #[cfg_attr(any(windows, target_arch = "wasm32"), allow(unused_mut))]
165128 let mut engine = Self {
166129 // Clones share loaded font files, so glyphs one lays out draw from the same cache.
167130 fonts: FontContext {
......@@ -172,14 +135,23 @@ impl Default for TextEngine {
172135 arial_substitutes: BTreeSet::new(),
173136 substitutes: BTreeMap::new(),
174137 };
175 #[cfg(not(any(windows, target_arch = "wasm32")))]
176 for &(family, faces) in BUNDLED {
177 if engine.fonts.collection.family_id(family).is_none() {
178 for face in faces {
179 engine
180 .register(Blob::new(Arc::new(*face)), true)
181 .expect("bundled substitutes register");
182 }
138 for (&(family, faces), inflated) in BUNDLED.iter().zip(&INFLATED) {
139 if engine.fonts.collection.family_id(family).is_some() {
140 continue;
141 }
142 let faces = inflated.get_or_init(|| {
143 faces
144 .iter()
145 .map(|face| {
146 let face = miniz_oxide::inflate::decompress_to_vec(face);
147 Blob::new(Arc::new(face.expect("bundled substitutes inflate")))
148 })
149 .collect()
150 });
151 for face in faces {
152 engine
153 .register(face.clone(), true)
154 .expect("bundled substitutes register");
183155 }
184156 }
185157 engine