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...@@ -18,6 +18,7 @@ parley.workspace = true
18serde = { version = "1.0.229", features = ["derive"] }18serde = { version = "1.0.229", features = ["derive"] }
19serde_json = "1.0.151"19serde_json = "1.0.151"
20icu_normalizer = "2.3"20icu_normalizer = "2.3"
21miniz_oxide = "0.8"
21skrifa = "0.44.0"22skrifa = "0.44.0"
22# std's clock where there is one; the browser's on wasm32-unknown-unknown, whose std has none.23# std's clock where there is one; the browser's on wasm32-unknown-unknown, whose std has none.
23web-time = "1.1"24web-time = "1.1"
...@@ -39,11 +40,13 @@ path = "src/bin/page_probe.rs"...@@ -39,11 +40,13 @@ path = "src/bin/page_probe.rs"
3940
40[dev-dependencies]41[dev-dependencies]
41draw = { path = "../draw", features = ["wgpu"] }42draw = { path = "../draw", features = ["wgpu"] }
42miniz_oxide = "0.8"
43accesskit_consumer = "0.39.0"43accesskit_consumer = "0.39.0"
44pollster = "0.4"44pollster = "0.4"
45wgpu.workspace = true45wgpu.workspace = true
4646
47[build-dependencies]
48miniz_oxide = "0.8"
49
47[[example]]50[[example]]
48name = "render_page"51name = "render_page"
49required-features = ["gpu"]52required-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::{...@@ -12,7 +12,7 @@ use std::{
12 fmt,12 fmt,
13 ops::Range,13 ops::Range,
14 sync::{14 sync::{
15 Arc,15 Arc, OnceLock,
16 atomic::{AtomicU64, Ordering},16 atomic::{AtomicU64, Ordering},
17 },17 },
18};18};
...@@ -113,55 +113,18 @@ pub(crate) const DEFAULT_FONT: &str = "Arial";...@@ -113,55 +113,18 @@ pub(crate) const DEFAULT_FONT: &str = "Arial";
113pub(crate) const DEFAULT_FONT_SIZE: f32 = 11.0;113pub(crate) const DEFAULT_FONT_SIZE: f32 = 11.0;
114114
115/// Metric-compatible faces for the fonts OneNote pages use most, under the SIL Open Font115/// 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 the116/// Licence files beside them, deflated, by the family each stands in for. The build script
117/// system ships the family it stands in for: Windows ships all four, macOS and iOS all but117/// leaves out each whose family the system ships, and every one from the browser's module,
118/// Calibri. The browser fetches them beside the module instead (`register_substitute`),118/// which fetches them beside it instead (`register_substitute`).
119/// keeping them out of it.119const BUNDLED: &[(&str, &[&[u8]])] = include!(concat!(env!("OUT_DIR"), "/bundled.rs"));
120#[cfg(not(any(windows, target_arch = "wasm32")))]120
121const BUNDLED: &[(&str, &[&[u8]])] = &[121/// Each bundled family's faces, inflated the first time an engine lacks the family.
122 (122static INFLATED: [OnceLock<Vec<Blob<u8>>>; BUNDLED.len()] =
123 "Calibri",123 [const { OnceLock::new() }; BUNDLED.len()];
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];
160124
161impl Default for TextEngine {125impl Default for TextEngine {
162 /// An engine that lays out each bundled family in its substitute where it is missing.126 /// An engine that lays out each bundled family in its substitute where it is missing.
163 fn default() -> Self {127 fn default() -> Self {
164 #[cfg_attr(any(windows, target_arch = "wasm32"), allow(unused_mut))]
165 let mut engine = Self {128 let mut engine = Self {
166 // Clones share loaded font files, so glyphs one lays out draw from the same cache.129 // Clones share loaded font files, so glyphs one lays out draw from the same cache.
167 fonts: FontContext {130 fonts: FontContext {
...@@ -172,14 +135,23 @@ impl Default for TextEngine {...@@ -172,14 +135,23 @@ impl Default for TextEngine {
172 arial_substitutes: BTreeSet::new(),135 arial_substitutes: BTreeSet::new(),
173 substitutes: BTreeMap::new(),136 substitutes: BTreeMap::new(),
174 };137 };
175 #[cfg(not(any(windows, target_arch = "wasm32")))]138 for (&(family, faces), inflated) in BUNDLED.iter().zip(&INFLATED) {
176 for &(family, faces) in BUNDLED {139 if engine.fonts.collection.family_id(family).is_some() {
177 if engine.fonts.collection.family_id(family).is_none() {140 continue;
178 for face in faces {141 }
179 engine142 let faces = inflated.get_or_init(|| {
180 .register(Blob::new(Arc::new(*face)), true)143 faces
181 .expect("bundled substitutes register");144 .iter()
182 }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");
183 }155 }
184 }156 }
185 engine157 engine