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). |