collective

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

diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs--- a/crates/text/src/lib.rs+++ b/crates/text/src/lib.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") {diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs--- a/crates/text/src/tests.rs+++ b/crates/text/src/tests.rs@@ -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");