From 55fadb6afef8c51b4e14dd67e2001030c46b7275 Mon Sep 17 00:00:00 2001 From: Lucas Leroux <128176798+lcslrx@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:49:14 +0200 Subject: [PATCH 1/2] Text: a `bundled-fonts` feature (on by default) lets embedders that bring their own fonts leave ours out The nine OFL faces in `assets/fonts` are compiled into every build of vectorcraft-text: 3.4 MB of the binary, about 1.6 MB of a gzipped wasm. An embedder that loads every font at run time with `FontDb::add_font` (a web app serving its own font catalogue, say) ships them twice and can't turn them off. - vectorcraft-text: `bundled-fonts`, a default feature, holds the `include_bytes!` list. Off, a database starts without them (craft-fonts and system fonts are unaffected) and text has no glyphs until a font is added, as when the bundled fonts fail to parse. - vectorcraft-engine: a `bundled-fonts` default feature forwards to it, so the apps and any embedder using the engine keep the fonts unless they build it with `default-features = false`. - The workspace declares vectorcraft-text without default features, so no library crate turns the fonts back on. The tests of cad, metafile and tools, which lay type out, ask for the feature in their dev-dependencies; the other crates' tests get it through testkit. `cargo test --workspace` builds with the fonts, as before. A new integration test, `without_bundled_fonts`, covers the configuration without them (`cargo test -p vectorcraft-text --no-default-features --test without_bundled_fonts`): the database starts without them, text in no font lays out with no glyphs, and a font added later is used and is the fallback for families the database lacks. Co-Authored-By: Claude Opus 5.5 --- Cargo.toml | 4 ++- crates/cad/Cargo.toml | 2 ++ crates/engine/Cargo.toml | 6 ++++ crates/metafile/Cargo.toml | 4 +++ crates/text/Cargo.toml | 6 ++++ crates/text/src/fontdb.rs | 10 ++++-- crates/text/src/lib.rs | 3 +- crates/text/tests/without_bundled_fonts.rs | 42 ++++++++++++++++++++++ crates/tools/Cargo.toml | 3 +- docs/development.md | 18 ++++++++++ 10 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 crates/text/tests/without_bundled_fonts.rs diff --git a/Cargo.toml b/Cargo.toml index a04729748..b001535c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,9 @@ vectorcraft-doc = { path = "crates/doc" } vectorcraft-pathops = { path = "crates/pathops" } vectorcraft-brush = { path = "crates/brush" } vectorcraft-trace = { path = "crates/trace" } -vectorcraft-text = { path = "crates/text" } +# Without its default `bundled-fonts`: the engine turns them on (its own default feature), so a +# library crate never forces them on an embedder that supplies its fonts at run time. +vectorcraft-text = { path = "crates/text", default-features = false } vectorcraft-effects = { path = "crates/effects" } vectorcraft-three-d = { path = "crates/three-d" } vectorcraft-render = { path = "crates/render" } diff --git a/crates/cad/Cargo.toml b/crates/cad/Cargo.toml index e8a8ea158..4886d9ea5 100644 --- a/crates/cad/Cargo.toml +++ b/crates/cad/Cargo.toml @@ -21,3 +21,5 @@ image = { workspace = true } [dev-dependencies] serde_json = { workspace = true } +# The tests lay type out in the bundled fonts (the workspace leaves turning them on to the engine). +vectorcraft-text = { workspace = true, features = ["bundled-fonts"] } diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index 1adf22155..c51728ef2 100644 --- a/crates/engine/Cargo.toml +++ b/crates/engine/Cargo.toml @@ -8,6 +8,12 @@ description = "VectorCraft engine: sessions, history, selection, the command reg [lints] workspace = true +[features] +default = ["bundled-fonts"] +# The fonts vectorcraft-text bundles (see its `bundled-fonts`). Embedders that supply every font at +# run time (`vectorcraft_text::FontDb::add_font`) build with `default-features = false`. +bundled-fonts = ["vectorcraft-text/bundled-fonts"] + [dependencies] vectorcraft-geom = { workspace = true } vectorcraft-color = { workspace = true } diff --git a/crates/metafile/Cargo.toml b/crates/metafile/Cargo.toml index 86ade96df..fe54a67f6 100644 --- a/crates/metafile/Cargo.toml +++ b/crates/metafile/Cargo.toml @@ -17,3 +17,7 @@ vectorcraft-brush = { workspace = true } vectorcraft-text = { workspace = true } kurbo = { workspace = true } image = { workspace = true } + +[dev-dependencies] +# The tests lay type out in the bundled fonts (the workspace leaves turning them on to the engine). +vectorcraft-text = { workspace = true, features = ["bundled-fonts"] } diff --git a/crates/text/Cargo.toml b/crates/text/Cargo.toml index 011d0fed6..7f3b52b1d 100644 --- a/crates/text/Cargo.toml +++ b/crates/text/Cargo.toml @@ -33,6 +33,12 @@ unicode-segmentation = "1.13" write-fonts = { version = "0.48", optional = true } [features] +default = ["bundled-fonts"] +# The OFL fonts of `assets/fonts` (Source Sans 3, Source Serif 4, Inter, JetBrains Mono) compiled +# in: 3.4 MB of the binary. Off, the font database starts without them (craft-fonts and system +# fonts are unaffected), for an embedder that adds its own with `FontDb::add_font` and would ship +# these twice. vectorcraft-engine turns it on by default (its `bundled-fonts` feature). +bundled-fonts = [] # `vectorcraft_text::test_fonts` for other crates' tests (dev-dependencies only). test-fonts = ["dep:write-fonts"] diff --git a/crates/text/src/fontdb.rs b/crates/text/src/fontdb.rs index bbcdef2bb..bb83f44f0 100644 --- a/crates/text/src/fontdb.rs +++ b/crates/text/src/fontdb.rs @@ -16,6 +16,8 @@ use skrifa::{GlyphId, MetadataProvider, Tag}; /// The family used when a requested family is unknown (Illustrator's Myriad Pro analogue). pub const FALLBACK_FAMILY: &str = "Source Sans 3"; +/// The fonts every database starts with (feature `bundled-fonts`, on by default). +#[cfg(feature = "bundled-fonts")] static BUNDLED: &[&[u8]] = &[ include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf"), include_bytes!("../../../assets/fonts/SourceSans3-Semibold.ttf"), @@ -27,6 +29,10 @@ static BUNDLED: &[&[u8]] = &[ include_bytes!("../../../assets/fonts/Inter-SemiBold.ttf"), include_bytes!("../../../assets/fonts/JetBrainsMono-Regular.ttf"), ]; +/// Without `bundled-fonts` a database starts with none: the embedder adds its fonts +/// ([`FontDb::add_font`]); until then text has no glyphs. +#[cfg(not(feature = "bundled-fonts"))] +static BUNDLED: &[&[u8]] = &[]; /// An axis setting of a variable font: the axis tag (`wght`) and its value in user units (700). pub type Variation = ([u8; 4], f32); @@ -1919,8 +1925,8 @@ impl FontDb { /// Resolve a family + style to a face, falling back to the closest style of the family, then to /// Source Sans 3 Regular. Installed system fonts are found by name whatever ran before. `None` - /// only if no font at all is loaded (the bundled fonts failed to parse), in which case text has - /// no glyphs. + /// only if no font at all is loaded (the bundled fonts failed to parse, or were left out with + /// the `bundled-fonts` feature and none was added), in which case text has no glyphs. pub fn face(&self, family: &str, style: &str) -> Option> { let (family, style) = self.canonical(family, style); let (family, style) = (family.as_str(), style.as_str()); diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index fb241f1d5..e65c1f79f 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -1,7 +1,8 @@ //! VectorCraft text: font database, shaping, layout, glyph outlines. //! //! API contract used by `vectorcraft-render`, `vectorcraft-tools` and the UI: -//! - [`FontDb::global`]: process-wide database preloaded with the bundled OFL fonts. +//! - [`FontDb::global`]: process-wide database preloaded with the bundled OFL fonts (feature +//! `bundled-fonts`, on by default; without it the embedder adds fonts with [`FontDb::add_font`]). //! - [`layout`]: lays out a [`TextObject`] into glyph outlines in *text space* (apply `t.xf` to //! get document coordinates), plus line and caret information. //! - [`caret_position`] / [`hit_byte`]: caret geometry and hit testing for the Type tool. diff --git a/crates/text/tests/without_bundled_fonts.rs b/crates/text/tests/without_bundled_fonts.rs new file mode 100644 index 000000000..1fe58035f --- /dev/null +++ b/crates/text/tests/without_bundled_fonts.rs @@ -0,0 +1,42 @@ +//! The `bundled-fonts` feature turned off, as by an embedder that supplies its own fonts at run +//! time: a database starts without the bundled fonts, text in no font lays out with no glyphs +//! instead of failing, and the fonts added later are used. Run with +//! `cargo test -p vectorcraft-text --no-default-features --test without_bundled_fonts`. +#![cfg(not(feature = "bundled-fonts"))] +// Integration tests: unwrapping and panicking on failure is fine here, unlike in shipped code (AGENTS.md › Robustness). +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] + +use kurbo::Point; +use vectorcraft_doc::CharStyle; +use vectorcraft_text::{CRAFT_FONTS, FALLBACK_FAMILY, FontDb, TextObject, layout}; + +#[test] +fn fonts_come_from_the_embedder() { + // No system fonts, as on the web. + let db = FontDb::with_font_dirs(vec![]); + db.set_system_fallback(false); + let families = db.families(); + for bundled in ["Source Sans 3", "Source Serif 4", "Inter", "JetBrains Mono"] { + assert!(!families.iter().any(|f| f == bundled), "{bundled} is bundled: {families:?}"); + } + let t = TextObject::point(Point::ZERO, "Hello", CharStyle { size: 24.0, ..CharStyle::default() }); + if CRAFT_FONTS.is_empty() { + assert!(families.is_empty(), "{families:?}"); + assert!(db.face(FALLBACK_FAMILY, "Regular").is_none()); + assert!(layout(&db, &t).glyphs.is_empty(), "no font, no glyphs"); + } + // The embedder adds its fonts: text lays out in them, and a family the database lacks (the + // default style's Source Sans 3 too) falls back to them. + let font = |file: &str| std::fs::read(format!("{}/../../assets/fonts/{file}", env!("CARGO_MANIFEST_DIR"))).unwrap(); + assert_eq!(db.add_font(font("Inter-Regular.ttf")), 1); + assert!(db.families().iter().any(|f| f == "Inter")); + if CRAFT_FONTS.is_empty() { + assert_eq!(db.face("No Such Font", "Regular").unwrap().family, "Inter"); + } + let l = layout(&db, &t); + assert_eq!(l.glyphs.len(), 5); + assert!(l.glyphs.iter().all(|g| g.advance > 0.0)); + // With Source Sans 3 added, the usual fallback is back. + assert_eq!(db.add_font(font("SourceSans3-Regular.ttf")), 1); + assert_eq!(db.face("No Such Font", "Regular").unwrap().family, FALLBACK_FAMILY); +} diff --git a/crates/tools/Cargo.toml b/crates/tools/Cargo.toml index 4731d2c95..b835974a4 100644 --- a/crates/tools/Cargo.toml +++ b/crates/tools/Cargo.toml @@ -20,4 +20,5 @@ serde_json = { workspace = true } [dev-dependencies] image = { workspace = true } -vectorcraft-text = { workspace = true, features = ["test-fonts"] } +# The tests lay type out in the bundled fonts (the workspace leaves turning them on to the engine). +vectorcraft-text = { workspace = true, features = ["test-fonts", "bundled-fonts"] } diff --git a/docs/development.md b/docs/development.md index 522f465be..0265d2306 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,5 +1,6 @@ # Development +> **Last updated:** 2026-10-14 · **Change:** leaving out the bundled fonts (`bundled-fonts` feature). > **Last updated:** 2026-10-14 · **Change:** document-font registration for all explicitly supplied faces. ## Web build @@ -148,6 +149,22 @@ CRAFT_FONTS_DIR="$PWD/../craft-fonts" cargo xtask ci # also runs the Japanese- CI job and every release job (`release.yml`) check craft-fonts out at a pinned commit; release packages carry each embedded font's `OFL-.txt`. +### Leaving out the bundled fonts (embedders) + +The Latin fonts in `assets/fonts/` are compiled into `vectorcraft-text` by its `bundled-fonts` +feature (3.4 MB of the binary). It is on by default, and `vectorcraft-engine`'s default +`bundled-fonts` feature turns it on, so the apps and anything built on the engine have them. An +embedder that brings every font itself at run time (`FontDb::add_font`, say a web app serving its +own font catalogue) depends on the engine with `default-features = false` and doesn't ship them +twice. Its font database starts without them: on the web and without craft-fonts it starts empty, +and text has no glyphs until a font is added. + +- The workspace declares `vectorcraft-text` without default features, so no library crate turns the + fonts back on; a crate whose tests lay type out asks for `bundled-fonts` in its + dev-dependencies (or gets it through `vectorcraft-testkit`, which uses the engine). +- `cargo test -p vectorcraft-text --no-default-features --test without_bundled_fonts` checks the + configuration without them. + ### Arabic fonts and the web build Desktop builds load craft-fonts' Arabic families into the document font database (Noto Sans @@ -346,5 +363,6 @@ do not replace expected hashes automatically in response to a download mismatch. | Date | Change | Summary | |---|---|---| +| 2026-10-14 | minor | Document the `bundled-fonts` feature of vectorcraft-text and vectorcraft-engine (Fonts › Leaving out the bundled fonts). | | 2026-10-14 | minor | Document registration of every explicitly supplied craft-fonts face after the existing script-specific prefix; empty input and web selection remain unchanged. | | 2026-10-13 | minor | Document pinned PngSuite files, integrity checks and download budgets (#1729). | From 2abc1d61f7547faf32c3a4e9ea529025429ed690 Mon Sep 17 00:00:00 2001 From: Lucas Leroux <128176798+lcslrx@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:54:26 +0200 Subject: [PATCH 2/2] xtask ci: test vectorcraft-text without its bundled fonts `cargo test --workspace` builds vectorcraft-text with `bundled-fonts` (the engine turns it on), so the configuration embedders use without it was tested only by hand. A `no-bundled-fonts` step after `test` runs its integration test. Co-Authored-By: Claude Opus 5.5 --- xtask/src/main.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 7aa6117da..65f5ac5a1 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -28,7 +28,7 @@ commands: pdf-deps verify immutable shared PDF writer and patched Krilla sources layers enforce the crate dependency layering (plan/architecture.md §3) wasm cargo check --target wasm32-unknown-unknown for the wasm-safe crates and the web app - ci fmt --check, clippy -D warnings, test, pdf-deps, assets, brands, layers, wasm (stops at first failure) + ci fmt --check, clippy -D warnings, test, no-bundled-fonts, pdf-deps, assets, brands, layers, wasm (stops at first failure) corpus [--download] [--affinity] show where test corpora live; --download fetches PngSuite into corpus/pngsuite, --affinity the pinned public Affinity documents into corpus/affinity @@ -203,6 +203,15 @@ fn cmd_ci() -> Result<(), String> { run(c, "cargo test --workspace") }), ), + ( + "no-bundled-fonts", + Box::new(|| { + // vectorcraft-text as embedders that bring their own fonts build it. + let mut c = cargo(); + c.args(["test", "-p", "vectorcraft-text", "--no-default-features", "--test", "without_bundled_fonts"]); + run(c, "cargo test -p vectorcraft-text --no-default-features --test without_bundled_fonts") + }), + ), ("pdf-deps", Box::new(|| pdf_deps::run(&root()))), ("assets", Box::new(|| assets::run(&root()))), ("brands", Box::new(|| brands::run(&root()))),