diff --git a/crates/canvas/Cargo.toml b/crates/canvas/Cargo.toml index a73d0b670249dd67faba67f2095dbd1bd5134b49..0bfc76a723b64c0a23baafca804be0f69bea8f86 100644 --- a/crates/canvas/Cargo.toml +++ b/crates/canvas/Cargo.toml @@ -18,6 +18,7 @@ parley.workspace = true serde = { version = "1.0.229", features = ["derive"] } serde_json = "1.0.151" icu_normalizer = "2.3" +miniz_oxide = "0.8" skrifa = "0.44.0" # std's clock where there is one; the browser's on wasm32-unknown-unknown, whose std has none. web-time = "1.1" @@ -39,11 +40,13 @@ path = "src/bin/page_probe.rs" [dev-dependencies] draw = { path = "../draw", features = ["wgpu"] } -miniz_oxide = "0.8" accesskit_consumer = "0.39.0" pollster = "0.4" wgpu.workspace = true +[build-dependencies] +miniz_oxide = "0.8" + [[example]] name = "render_page" required-features = ["gpu"] diff --git a/crates/canvas/build.rs b/crates/canvas/build.rs new file mode 100644 index 0000000000000000000000000000000000000000..66fb802fe25c7bc805d42d038c7b89468a0438c7 --- /dev/null +++ b/crates/canvas/build.rs @@ -0,0 +1,71 @@ +//! Deflates the font clones the target embeds into `bundled.rs`, a table `layout.rs` includes, +//! leaving out each whose family the target's system ships. + +/// Metric-compatible clones, by the family each stands in for. +const CLONES: [(&str, &[&str]); 4] = [ + ( + "Calibri", + &[ + "Carlito-Regular", + "Carlito-Bold", + "Carlito-Italic", + "Carlito-BoldItalic", + ], + ), + ("Arial", &["Arimo", "Arimo-Italic"]), + ( + "Times New Roman", + &[ + "Tinos-Regular", + "Tinos-Bold", + "Tinos-Italic", + "Tinos-BoldItalic", + ], + ), + ( + "Courier New", + &[ + "Cousine-Regular", + "Cousine-Bold", + "Cousine-Italic", + "Cousine-BoldItalic", + ], + ), +]; + +fn main() { + println!("cargo::rerun-if-changed=build.rs"); + println!("cargo::rerun-if-changed=assets/fonts"); + let os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + let browser = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap() == "wasm32"; + let out = std::path::PathBuf::from(std::env::var_os("OUT_DIR").unwrap()); + let mut table = String::from("&[\n"); + for (family, faces) in CLONES { + // The browser fetches them beside the module instead. + let shipped = browser + || match os.as_str() { + "windows" => true, + "macos" | "ios" => family != "Calibri", + _ => false, + }; + if shipped { + continue; + } + table += &format!(" ({family:?}, &[\n"); + for face in faces { + let font = std::fs::read(format!("assets/fonts/{face}.ttf")).unwrap(); + let deflated = format!("{face}.ttf.deflate"); + std::fs::write( + out.join(&deflated), + miniz_oxide::deflate::compress_to_vec(&font, 10), + ) + .unwrap(); + // Relative to OUT_DIR, so a target folder reached by another path still finds them. + table += + &format!(" include_bytes!(concat!(env!(\"OUT_DIR\"), \"/{deflated}\")),\n"); + } + table += " ]),\n"; + } + table += "]\n"; + std::fs::write(out.join("bundled.rs"), table).unwrap(); +} diff --git a/crates/canvas/src/layout.rs b/crates/canvas/src/layout.rs index 8ccc0d142788d356f5b18f759c379513c21571c8..32dd575984df69355992d084f63f7a756df42829 100644 --- a/crates/canvas/src/layout.rs +++ b/crates/canvas/src/layout.rs @@ -12,7 +12,7 @@ use std::{ fmt, ops::Range, sync::{ - Arc, + Arc, OnceLock, atomic::{AtomicU64, Ordering}, }, }; @@ -113,55 +113,18 @@ pub(crate) const DEFAULT_FONT: &str = "Arial"; pub(crate) const DEFAULT_FONT_SIZE: f32 = 11.0; /// Metric-compatible faces for the fonts OneNote pages use most, under the SIL Open Font -/// Licence files beside them, by the family each stands in for. Each is left out where the -/// system ships the family it stands in for: Windows ships all four, macOS and iOS all but -/// Calibri. The browser fetches them beside the module instead (`register_substitute`), -/// keeping them out of it. -#[cfg(not(any(windows, target_arch = "wasm32")))] -const BUNDLED: &[(&str, &[&[u8]])] = &[ - ( - "Calibri", - &[ - include_bytes!("../assets/fonts/Carlito-Regular.ttf"), - include_bytes!("../assets/fonts/Carlito-Bold.ttf"), - include_bytes!("../assets/fonts/Carlito-Italic.ttf"), - include_bytes!("../assets/fonts/Carlito-BoldItalic.ttf"), - ], - ), - #[cfg(not(any(target_os = "macos", target_os = "ios")))] - ( - "Arial", - &[ - include_bytes!("../assets/fonts/Arimo.ttf"), - include_bytes!("../assets/fonts/Arimo-Italic.ttf"), - ], - ), - #[cfg(not(any(target_os = "macos", target_os = "ios")))] - ( - "Times New Roman", - &[ - include_bytes!("../assets/fonts/Tinos-Regular.ttf"), - include_bytes!("../assets/fonts/Tinos-Bold.ttf"), - include_bytes!("../assets/fonts/Tinos-Italic.ttf"), - include_bytes!("../assets/fonts/Tinos-BoldItalic.ttf"), - ], - ), - #[cfg(not(any(target_os = "macos", target_os = "ios")))] - ( - "Courier New", - &[ - include_bytes!("../assets/fonts/Cousine-Regular.ttf"), - include_bytes!("../assets/fonts/Cousine-Bold.ttf"), - include_bytes!("../assets/fonts/Cousine-Italic.ttf"), - include_bytes!("../assets/fonts/Cousine-BoldItalic.ttf"), - ], - ), -]; +/// Licence files beside them, deflated, by the family each stands in for. The build script +/// leaves out each whose family the system ships, and every one from the browser's module, +/// which fetches them beside it instead (`register_substitute`). +const BUNDLED: &[(&str, &[&[u8]])] = include!(concat!(env!("OUT_DIR"), "/bundled.rs")); + +/// Each bundled family's faces, inflated the first time an engine lacks the family. +static INFLATED: [OnceLock>>; BUNDLED.len()] = + [const { OnceLock::new() }; BUNDLED.len()]; impl Default for TextEngine { /// An engine that lays out each bundled family in its substitute where it is missing. fn default() -> Self { - #[cfg_attr(any(windows, target_arch = "wasm32"), allow(unused_mut))] let mut engine = Self { // Clones share loaded font files, so glyphs one lays out draw from the same cache. fonts: FontContext { @@ -172,14 +135,23 @@ impl Default for TextEngine { arial_substitutes: BTreeSet::new(), substitutes: BTreeMap::new(), }; - #[cfg(not(any(windows, target_arch = "wasm32")))] - for &(family, faces) in BUNDLED { - if engine.fonts.collection.family_id(family).is_none() { - for face in faces { - engine - .register(Blob::new(Arc::new(*face)), true) - .expect("bundled substitutes register"); - } + for (&(family, faces), inflated) in BUNDLED.iter().zip(&INFLATED) { + if engine.fonts.collection.family_id(family).is_some() { + continue; + } + let faces = inflated.get_or_init(|| { + faces + .iter() + .map(|face| { + let face = miniz_oxide::inflate::decompress_to_vec(face); + Blob::new(Arc::new(face.expect("bundled substitutes inflate"))) + }) + .collect() + }); + for face in faces { + engine + .register(face.clone(), true) + .expect("bundled substitutes register"); } } engine