draw Symbol and Wingdings bullets as Unicode when the font is missing (#29) (#37)

This commit is contained in:
MarekSpodBiedry
2026-10-09 08:48:57 -04:00
committed by GitHub
parent 5a94aa77d9
commit 0edb87e3da
3 changed files with 74 additions and 2 deletions
+41
View File
@@ -87,6 +87,34 @@ pub fn resolve(family: &str, bold: bool, italic: bool) -> Resolved {
r
}
/// The standard Unicode character for a Symbol or Wingdings character stored the way Word stores
/// it, as the private-use code U+F000 + the font's own code (U+F0B7 is the Symbol bullet). Used
/// when that font isn't installed: a substitute draws something else, or nothing, at the
/// private-use code. `None` for other fonts and for codes not in the table.
pub fn symbol_font_char(family: &str, c: char) -> Option<char> {
let code = u32::from(c).checked_sub(0xF000)?;
let u = match (family.to_ascii_lowercase().as_str(), code) {
// Adobe Symbol encoding.
("symbol", 0xA7) => '\u{2663}',
("symbol", 0xA8) => '\u{2666}',
("symbol", 0xA9) => '\u{2665}',
("symbol", 0xAA) => '\u{2660}',
("symbol", 0xAE) => '\u{2192}',
("symbol", 0xB7) => '\u{2022}',
("symbol", 0xD7) => '\u{22C5}',
("symbol", 0xE0) => '\u{25CA}',
// Wingdings characters common as list bullets.
("wingdings", 0x6C) => '\u{25CF}',
("wingdings", 0x6E) => '\u{25A0}',
("wingdings", 0x76) => '\u{2756}',
("wingdings", 0xA7) => '\u{25AA}',
("wingdings", 0xD8) => '\u{27A2}',
("wingdings", 0xFC) => '\u{2714}',
_ => return None,
};
Some(u)
}
/// Line metrics for a face in font units: (ascent, descent), both positive, the way word
/// processors measure single line spacing (OS/2 usWinAscent/usWinDescent, falling back to the
/// hhea values plus line gap).
@@ -131,6 +159,19 @@ mod tests {
assert!(a > 0.0 && d >= 0.0);
}
#[test]
fn symbol_font_chars_map_to_unicode() {
assert_eq!(symbol_font_char("Symbol", '\u{F0B7}'), Some('\u{2022}'));
assert_eq!(symbol_font_char("symbol", '\u{F0B7}'), Some('\u{2022}'));
assert_eq!(symbol_font_char("Wingdings", '\u{F0A7}'), Some('\u{25AA}'));
assert_eq!(symbol_font_char("Wingdings", '\u{F0D8}'), Some('\u{27A2}'));
// Same code, other font; unknown codes; ordinary characters.
assert_eq!(symbol_font_char("Arial", '\u{F0B7}'), None);
assert_eq!(symbol_font_char("Symbol", '\u{F041}'), None);
assert_eq!(symbol_font_char("Symbol", 'a'), None);
assert_eq!(symbol_font_char("Symbol", '\u{10FFFF}'), None);
}
#[test]
fn synthesizes_missing_styles() {
// JetBrains Mono ships only Regular here.
+13 -2
View File
@@ -499,9 +499,20 @@ pub fn layout_para(p: &Paragraph, env: &ParaEnv) -> ParaLayout {
let label = env.label.as_ref().map(|(text, lvl)| {
let lc = p.mark.clone().overlaid(&lvl.chr);
let rc = resolve(&lc);
// A Symbol/Wingdings bullet (U+F0B7…) without that font installed, or in a face that has
// no glyph at the private-use code: draw the standard character instead, since a
// substitute has another glyph, or none, there.
let r = wordcraft_fonts::word::resolve(&rc.font, rc.bold, rc.italic);
let text: String = text
.chars()
.map(|c| match wordcraft_fonts::word::symbol_font_char(&rc.font, c) {
Some(u) if r.substituted || !r.face.covers(c) => u,
_ => c,
})
.collect();
let before_c = b.clusters.len();
let before_g = b.glyphs.len();
b.shape(text, 0, &rc, None);
b.shape(&text, 0, &rc, None);
let cl: Vec<Cluster> = b.clusters.drain(before_c..).collect();
let mut glyphs = Vec::new();
let mut x = 0.0;
@@ -514,7 +525,7 @@ pub fn layout_para(p: &Paragraph, env: &ParaEnv) -> ParaLayout {
x += c.adv;
}
b.glyphs.truncate(before_g);
Label { text: text.clone(), style, glyphs, x: 0.0, width: x }
Label { text, style, glyphs, x: 0.0, width: x }
});
let mut pl = ParaLayout {
+20
View File
@@ -120,6 +120,26 @@ fn lists_get_labels_and_indent() {
assert!((x - 108.0).abs() < 0.5, "{x}");
}
#[test]
fn symbol_font_bullet_without_the_font_draws_a_bullet() {
// Word's default bullet: U+F0B7 in Symbol.
let mut d = Document::from_text("one");
let id = d.numbering.add_list(wordcraft_doc::ListKind::BulletChar('\u{F0B7}'));
for a in &mut d.numbering.abstracts {
if let Some(l) = a.levels.first_mut() {
l.chr.font = Some("Symbol".into());
}
}
d.format_paragraphs(&Pos::body(0, 0), &Pos::body(0, 0), &|p| p.numbering = Some(wordcraft_doc::props::NumRef { num: id, level: 0 })).unwrap();
let l = lay(&d);
let label = l.pages[0].items.iter().find_map(|it| if let Placed::Lines { para, .. } = it { para.label.clone() } else { None }).unwrap();
// Where a real Symbol font with the private-use code is installed (Windows), the code stays.
let symbol = wordcraft_fonts::word::resolve("Symbol", false, false);
let expected = if symbol.substituted || !symbol.face.covers('\u{F0B7}') { "\u{2022}" } else { "\u{F0B7}" };
assert_eq!(label.text, expected);
assert_eq!(label.glyphs.len(), 1);
}
#[test]
fn tables_lay_out_cells() {
let mut d = Document::from_text("before\nafter");