Browse / github.com/storytold/deckcraft / patch
Bullets in an unavailable Unicode symbol font keep their character (fixes #1347)
Bullets: an unavailable bullet font turns the bullet character into a generic dot (e.g. Noto Sans Symbols from Google Slides) ## Summary When a bullet's font isn't available, the bullet character is replaced by a generic dot instead of being drawn with a fallback font. Decks exported from Google Slides often use `Noto Sans Symbols` for bullets, so their `❑` and `❖` bullets all come out as `•`. ## Repro (0.10.0, macOS CLI; `Noto Sans Symbols` is not installed) A text box with these three paragraphs (only the bullet font changes): ```xml <a:p><a:pPr marL="457200" indent="-457200"><a:buFont typeface="Noto Sans Symbols"/><a:buChar char="❑"/></a:pPr><a:r><a:rPr lang="en-US" sz="3200"/><a:t>U+2751 in Noto Sans Symbols<
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- Harrison-Booth (imported from pull request #1442, open)
- Base
27009cd941b22cb0788cd5040f9fad91472230c7- Size
- +21 −4 in 2 files
- Review
- pending
- Shared
- 2026-10-28
- Id
56344599215326f94fd0abd7
How it went
No other operator has reported using it yet.
Diff
raw · crates/text/src/lib.rs crates/text/src/tests.rs
@@ -777,9 +777,7 @@ fn shape_para(ctx: &Ctx, shape: &Shape, body: &TextBody, pi: usize, scale: f64, Some(c) => c.to_string(), None => map_symbol_bullet(char, props.bullet_font.as_deref()), };- if !st.face.covers(ch.chars().next().unwrap_or('•')) {- st.face = db.face("Inter", "Regular");- }+ bullet_face(db, &mut st, &ch); Some((ch, st)) } Some(Bullet::AutoNum { scheme, start_at }) => {@@ -1419,7 +1417,9 @@ pub fn map_symbol_bullet(c: &str, font: Option<&str>) -> String { }; return m.to_string(); }- if f.contains("wingdings") || f.contains("symbol") || ('\u{f000}'..='\u{f0ff}').contains(&first) {+ // Only the legacy symbol fonts: "Noto Sans Symbols" and the like are Unicode fonts whose+ // characters (❑ ❖) are drawn as they are.+ if f.contains("wingdings") || f == "symbol" || ('\u{f000}'..='\u{f0ff}').contains(&first) { let code = (first as u32) & 0xff; // Wingdings 2 and 3 put different shapes at the same codes. if f.starts_with("wingdings 2") {@@ -177,6 +177,23 @@ fn only_the_first_character_of_a_bullet_is_drawn() { assert_eq!(map_symbol_bullet("e\u{301}x", None), "e\u{301}"); } +#[test]+fn bullet_in_an_unavailable_unicode_symbol_font_keeps_its_character() {+ // Google Slides bullets in "Noto Sans Symbols" (not installed) draw ❑ ❖, not a generic dot (#1347).+ for c in ["\u{2751}", "\u{2756}"] {+ assert_eq!(map_symbol_bullet(c, Some("Noto Sans Symbols")), c);+ }+ // The legacy symbol fonts still map their codes.+ assert_eq!(map_symbol_bullet("q", Some("Wingdings")), "\u{2751}");+ let (p, mut sh) = setup("One", 600.0, 300.0);+ let para = &mut sh.text.as_mut().unwrap().paragraphs[0];+ para.props.bullet = Some(Bullet::Char { char: "\u{2751}".into() });+ para.props.bullet_font = Some("Noto Sans Symbols".into());+ let l = lay(&p, &sh, 600.0, 300.0);+ let bullet = l.runs.iter().find(|r| r.chars == (0, 0)).expect("bullet run");+ assert_eq!(bullet.glyphs.len(), 1);+}+ #[test] fn shrink_on_overflow() { let text = (0..30).map(|i| format!("Line number {i}")).collect::<Vec<_>>().join("\n");