From d6b3bb13ab6b2097bd2f3729f4e665f4a62816be Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 2 Oct 2026 12:19:23 +0300 Subject: [PATCH] fix(text): copy a fallback face's font file only for an exporter #1580 built a GlyphSource for every fallback glyph, so drawing a CJK character on screen copied the whole fallback font (tens of MB) into a process-wide cache whether or not a PDF was ever exported. The source is now built only when fallback_source asks for it, through the same search. Co-Authored-By: Claude Opus 5.5 --- src/scene/text/ttf_glyph.rs | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/scene/text/ttf_glyph.rs b/src/scene/text/ttf_glyph.rs index 0ed67b5a3..b9fdd6be1 100644 --- a/src/scene/text/ttf_glyph.rs +++ b/src/scene/text/ttf_glyph.rs @@ -557,7 +557,7 @@ pub fn fallback_source(ch: char) -> Option { if let Some(hit) = cache.lock().unwrap().get(&ch) { return hit.clone(); } - let built = pick_fallback(ch).and_then(|(_, source)| source); + let built = pick_fallback(ch, true).and_then(|(_, source)| source); cache.lock().unwrap().insert(ch, built.clone()); built } @@ -688,15 +688,18 @@ mod fallback_tests { /// no stroke font provides. #[cfg(not(target_arch = "wasm32"))] fn build_fallback(ch: char) -> Option> { - pick_fallback(ch).map(|(glyph, _)| glyph) + pick_fallback(ch, false).map(|(glyph, _)| glyph) } -/// The fallback glyph for `ch` together with the face it came from -- one -/// search, so the glyph drawn on screen and the font an exporter embeds can -/// never disagree. +/// The fallback glyph for `ch` together with the face it came from -- the +/// same search for both, so the glyph drawn on screen and the font an +/// exporter embeds can never disagree. +/// +/// The source holds a copy of the whole font file, so it is built only when +/// asked for (`with_source`, an exporter): drawing on screen never keeps one. #[cfg(not(target_arch = "wasm32"))] -fn pick_fallback(ch: char) -> Option<(Arc, Option)> { - outline_from_fallback_face(ch).or_else(|| installed_family_glyph(ch)) +fn pick_fallback(ch: char, with_source: bool) -> Option<(Arc, Option)> { + outline_from_fallback_face(ch, with_source).or_else(|| installed_family_glyph(ch, with_source)) } /// Outline `ch` from a parsed face, in the 9-unit text space. @@ -711,6 +714,7 @@ fn outline_char( index: u32, face: &ttf_parser::Face, ch: char, + with_source: bool, ) -> Option<(Glyph, Option)> { let gid = face.glyph_index(ch)?; let k = if is_full_width(ch) { @@ -728,7 +732,8 @@ fn outline_char( advance, fill_tris, }; - Some((glyph, source_for(data, index, face, gid, k))) + let source = with_source.then(|| source_for(data, index, face, gid, k)).flatten(); + Some((glyph, source)) } /// Outline `ch` from the face cosmic-text picks for it: it knows the platform's @@ -738,7 +743,10 @@ fn outline_char( /// does, for every character, because ttf-parser reads no contours out of it — /// so an empty outline is not an answer and the caller has to keep looking. #[cfg(not(target_arch = "wasm32"))] -fn outline_from_fallback_face(ch: char) -> Option<(Arc, Option)> { +fn outline_from_fallback_face( + ch: char, + with_source: bool, +) -> Option<(Arc, Option)> { use cosmic_text::{Attrs, Buffer, Metrics, Shaping}; let mut fs = font_system().lock().unwrap(); // Default family → cosmic's own fallback search chooses a covering font. @@ -757,7 +765,9 @@ fn outline_from_fallback_face(ch: char) -> Option<(Arc, Option Option<(Arc, Option Option<(Arc, Option)> { +fn installed_family_glyph( + ch: char, + with_source: bool, +) -> Option<(Arc, Option)> { crate::scene::text::sysfont::families() .iter() .find_map(|family| { let (glyph, source) = sysfont::with_face_data(family, |data, index| { let face = ttf_parser::Face::parse(data, index).ok()?; - outline_char(data, index, &face, ch) + outline_char(data, index, &face, ch, with_source) }) .flatten()?; // A face that yields no outline (the system font, a space) is not a