text: add Symbol euro and Apple alternatives; keep covered bullets

Adobe symbol.txt also maps 0xA0 to the euro sign (U+20AC), which
symbol_unicode lacked. Apple's Symbol maps a few codes to different
Unicode than Adobe (0xA4 to U+2044, 0xE1/0xF1 to U+3008/U+3009), so
symbol_font_draw now tries those alternatives after the Adobe character
when the installed face doesn't cover it. The Adobe aliases for
0x44/0x57/0x6D (U+2206/U+2126/U+00B5) are not Apple alternatives and
stay out.

symbol_font_char masked every bullet character to its low byte before
checking direct coverage, so a 'bullet' (U+2022) bullet drew an
unrelated Symbol code. It now maps only symbol-font codes (low byte or
U+F0xx) and falls back to the original character when the face covers
it.
This commit is contained in:
Bryson Meiling
2026-10-20 12:23:50 -06:00
parent f8c8223ff0
commit e5a407d6e2
2 changed files with 99 additions and 20 deletions
+38 -18
View File
@@ -1168,13 +1168,14 @@ fn symbol_map_char(face: &FontFace, ch: char, sym: Option<&str>) -> Option<char>
}
/// The real-Unicode character the Symbol font's code `code` (0x00–0xFF) maps to, in the Adobe
/// Symbol encoding (Unicode, Inc.'s `MAPPINGS/VENDORS/ADOBE/symbol.txt`; Apple's Mac OS Symbol is
/// laid out identically): the codes a document stores as U+F000 + code draw the same glyphs at
/// these Unicode points. Apple's "Symbol" on macOS has no private-use mapping at all — it maps the
/// glyphs to these real characters instead — so a document's U+F0xx symbol characters must be
/// remapped to them there (PowerPoint does the same against whatever the installed Symbol font
/// covers). Only the non-ASCII, non-CUS entries are listed: printable ASCII passes through as its
/// own code, and the CUS (Corporate Use Subarea) fragment glyphs have no standard Unicode point.
/// Symbol encoding (Unicode, Inc.'s `MAPPINGS/VENDORS/ADOBE/symbol.txt`; Apple's Mac OS Symbol
/// shares this layout, with a few alternative Unicode choices — see `symbol_unicode_alts`): the
/// codes a document stores as U+F000 + code draw the same glyphs at these Unicode points. Apple's
/// "Symbol" on macOS has no private-use mapping at all — it maps the glyphs to these real characters
/// instead — so a document's U+F0xx symbol characters must be remapped to them there (PowerPoint
/// does the same against whatever the installed Symbol font covers). Only the non-ASCII, non-CUS
/// entries are listed: printable ASCII passes through as its own code, and the CUS (Corporate Use
/// Subarea) fragment glyphs have no standard Unicode point.
const fn symbol_unicode(code: u32) -> Option<u32> {
Some(match code {
// 0x20–0x7E: the few mathematical ones among the ASCII-range characters.
@@ -1241,7 +1242,8 @@ const fn symbol_unicode(code: u32) -> Option<u32> {
0x78 => 0x03BE,
0x79 => 0x03C8,
0x7A => 0x03B6,
// 0xA1–0xAF: Greek upsilon with hook, primes, math, suits and arrows.
// 0xA0–0xAF: euro, Greek upsilon with hook, primes, math, suits and arrows.
0xA0 => 0x20AC,
0xA1 => 0x03D2,
0xA2 => 0x2032,
0xA3 => 0x2264,
@@ -1315,11 +1317,25 @@ const fn symbol_unicode(code: u32) -> Option<u32> {
})
}
/// Apple's Mac OS Symbol (which shares the Adobe layout) diverges from the Adobe table on a few
/// codes: it maps the fraction slash to U+2044 and the angle brackets to U+3008/U+3009 instead of
/// Adobe's U+2215/U+2329/U+232A. `symbol_font_draw` tries these alternatives after the Adobe
/// mapping when the installed face doesn't cover the Adobe character. The additional Adobe aliases
/// for 0x44/0x57/0x6D (U+2206/U+2126/U+00B5) are not Apple alternatives and are deliberately absent.
const fn symbol_unicode_alts(code: u32) -> &'static [u32] {
match code {
0xA4 => &[0x2044],
0xE1 => &[0x3008],
0xF1 => &[0x3009],
_ => &[],
}
}
/// The character the symbol font draws the code `code` at, when the installed `face` is that font:
/// its private-use code U+F000 + code, or, when the face has no private-use mapping (Apple's
/// "Symbol" maps its glyphs to real Unicode), the Adobe Symbol encoding's real character. Printable
/// ASCII with no table entry (the `< = > ?` codes among them) passes through as its own character
/// when the face covers it.
/// "Symbol" maps its glyphs to real Unicode), the Adobe Symbol encoding's real character (then its
/// Apple alternative, then, for printable ASCII with no table entry — the `< = > ?` codes among
/// them — the ASCII character itself) when the face covers it.
fn symbol_font_draw(face: &FontFace, code: u32) -> Option<char> {
let code = code & 0xFF;
let pua = char::from_u32(0xF000 | code)?;
@@ -1327,11 +1343,7 @@ fn symbol_font_draw(face: &FontFace, code: u32) -> Option<char> {
return Some(pua);
}
if let Some(real) = symbol_unicode(code) {
let real = char::from_u32(real)?;
if face.covers(real) {
return Some(real);
}
return None;
return std::iter::once(real).chain(symbol_unicode_alts(code).iter().copied()).filter_map(char::from_u32).find(|c| face.covers(*c));
}
// 0x20–0x7E with no table entry draw the ASCII character itself (the Symbol font's glyphs for
// "< = > ?" etc. sit at their own ASCII codes, not at a remapped Unicode point).
@@ -1352,8 +1364,16 @@ fn symbol_font_char(face: &FontFace, c: &str, font: Option<&str>) -> Option<char
return None;
}
let first = c.chars().next()?;
let code = (first as u32) & 0xFF;
symbol_font_draw(face, code).or_else(|| face.covers(first).then_some(first))
let cp = first as u32;
// Only symbol-font codes (low byte, or the U+F000 + code private-use form) take the mapping.
// A bullet whose character is a plain Unicode codepoint (e.g. U+2022 •) draws its own glyph
// when the face covers it, never a low-byte remap to an unrelated Symbol code.
if (cp < 0x100 || (0xF000..=0xF0FF).contains(&cp))
&& let Some(c) = symbol_font_draw(face, cp & 0xFF)
{
return Some(c);
}
face.covers(first).then_some(first)
}
/// The character that draws `c`, a character of text set in the symbol font `font` (Wingdings,
+61 -2
View File
@@ -937,6 +937,27 @@ fn an_installed_symbol_font_draws_its_own_bullet() {
assert_eq!(symbol_font_char(&face, "\u{ea}", Some("Arial")), None);
}
#[test]
fn symbol_font_char_keeps_covered_non_symbol_bullets() {
// `Bullet::Char.char` is unrestricted text; the PPTX reader stores `buChar` directly. A bullet
// such as U+2022 • must draw its own covered glyph, never a low-byte remap (0x22 → FOR ALL)
// to an unrelated Symbol code. Skipped when the face doesn't cover U+2022.
let db = FontDb::global();
if !db.has_family("Symbol") {
eprintln!("skipped: no Symbol font installed");
return;
}
let face = db.face("Symbol", "Regular");
if !face.covers('•') {
eprintln!("skipped: installed Symbol face lacks U+2022");
return;
}
assert_eq!(symbol_font_char(&face, "•", Some("Symbol")), Some('•'), "U+2022 draws itself, not a low-byte remap");
// A real symbol code still takes the mapping.
let f0de = String::from_utf8(vec![0xEF, 0x83, 0x9E]).unwrap(); // U+F0DE
assert_eq!(symbol_font_char(&face, &f0de, Some("Symbol")), symbol_font_draw(&face, 0xDE));
}
#[test]
fn symbol_char_maps_symbol_font_codes() {
// Inter is not a symbol font, so the codes map to look-alikes.
@@ -998,6 +1019,44 @@ fn symbol_font_draw_falls_back_to_ascii() {
}
}
#[test]
fn symbol_font_draw_tries_apple_alternatives() {
// Apple's Symbol maps a few codes differently from Adobe: 0xA4 (fraction slash) to U+2044
// instead of Adobe's U+2215, and 0xE1/0xF1 (angle brackets) to U+3008/U+3009 instead of
// U+2329/U+232A. When the installed face doesn't cover the Adobe character, the alternative is
// tried before giving up. Skipped without an installed Symbol face.
let db = FontDb::global();
if !db.has_family("Symbol") {
eprintln!("skipped: no Symbol font installed");
return;
}
let face = db.face("Symbol", "Regular");
for &(code, adobe, alt) in &[(0xA4, '\u{2215}', '\u{2044}'), (0xE1, '\u{2329}', '\u{3008}'), (0xF1, '\u{232A}', '\u{3009}')] {
let pua = char::from_u32(0xF000 | code).unwrap();
let drawn = symbol_font_draw(&face, code);
if face.covers(pua) {
assert_eq!(drawn, Some(pua), "the face's private-use glyph wins for 0x{code:02X}");
} else if face.covers(adobe) {
assert_eq!(drawn, Some(adobe), "Adobe U+{:04X} wins for 0x{code:02X} when covered", adobe as u32);
} else {
assert_eq!(drawn, Some(alt), "Apple alternative U+{:04X} is tried for 0x{code:02X}", alt as u32);
}
}
}
#[test]
fn symbol_unicode_maps_the_euro() {
// Adobe symbol.txt: 20AC A0 # EURO SIGN. The euro is the one currency symbol in the encoding.
assert_eq!(symbol_unicode(0xA0), Some(0x20AC));
// Apple alternatives are tried in order for 0xA4/0xE1/0xF1.
assert_eq!(symbol_unicode_alts(0xA4), &[0x2044u32]);
assert_eq!(symbol_unicode_alts(0xE1), &[0x3008u32]);
assert_eq!(symbol_unicode_alts(0xF1), &[0x3009u32]);
assert_eq!(symbol_unicode_alts(0x44), &[] as &[u32]);
assert_eq!(symbol_unicode_alts(0x57), &[] as &[u32]);
assert_eq!(symbol_unicode_alts(0x6D), &[] as &[u32]);
}
/// Every entry of the Adobe Symbol encoding table, in full. Pure and platform-independent: the
/// table maps a Symbol code to its real Unicode character regardless of any installed font, so this
/// runs on every machine. Guarded against typos in `symbol_unicode` (e.g. 0x40 must be U+2245, the
@@ -1019,8 +1078,8 @@ fn symbol_unicode_table_covers_the_adobe_encoding() {
(0x61, 0x03B1), (0x62, 0x03B2), (0x63, 0x03C7), (0x64, 0x03B4), (0x65, 0x03B5), (0x66, 0x03C6), (0x67, 0x03B3), (0x68, 0x03B7), (0x69, 0x03B9), (0x6A, 0x03D5),
(0x6B, 0x03BA), (0x6C, 0x03BB), (0x6D, 0x03BC), (0x6E, 0x03BD), (0x6F, 0x03BF), (0x70, 0x03C0), (0x71, 0x03B8), (0x72, 0x03C1), (0x73, 0x03C3), (0x74, 0x03C4),
(0x75, 0x03C5), (0x76, 0x03D6), (0x77, 0x03C9), (0x78, 0x03BE), (0x79, 0x03C8), (0x7A, 0x03B6),
// 0xA1–0xAF: Greek upsilon with hook, primes, math, suits and arrows.
(0xA1, 0x03D2), (0xA2, 0x2032), (0xA3, 0x2264), (0xA4, 0x2215), (0xA5, 0x221E), (0xA6, 0x0192), (0xA7, 0x2663), (0xA8, 0x2666), (0xA9, 0x2665), (0xAA, 0x2660),
// 0xA0–0xAF: euro, Greek upsilon with hook, primes, math, suits and arrows.
(0xA0, 0x20AC), (0xA1, 0x03D2), (0xA2, 0x2032), (0xA3, 0x2264), (0xA4, 0x2215), (0xA5, 0x221E), (0xA6, 0x0192), (0xA7, 0x2663), (0xA8, 0x2666), (0xA9, 0x2665), (0xAA, 0x2660),
(0xAB, 0x2194), (0xAC, 0x2190), (0xAD, 0x2191), (0xAE, 0x2192), (0xAF, 0x2193),
// 0xB0–0xBC: degree, plus-minus, double prime, math and ellipsis.
(0xB0, 0x00B0), (0xB1, 0x00B1), (0xB2, 0x2033), (0xB3, 0x2265), (0xB4, 0x00D7), (0xB5, 0x221D), (0xB6, 0x2202), (0xB7, 0x2022), (0xB8, 0x00F7), (0xB9, 0x2260),