M10: glyph names: a uni part with a surrogate group maps to nothing; numbered names read under a suffix (#3170)

Review follow-ups on #3239.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
This commit is contained in:
mtlcl410
2026-10-19 01:57:14 +08:00
co-authored by Claude Opus 5.5
parent 0b731453d3
commit ecb990c8ce
3 changed files with 19 additions and 10 deletions
+4 -2
View File
@@ -1917,11 +1917,13 @@ mod tests {
Object::Integer(30), name("f_i"), name("f_l"), Object::Integer(2), name("hyphen.case"),
Object::Integer(39), name("C39"), Object::Integer(97), name("c251"), Object::Integer(174), name("C174"),
Object::Integer(88), name("a12"), name("foo"), name("uni00660069"),
// #3239 review: a numbered name keeps its meaning under a suffix.
Object::Integer(61), name("C39.sc"),
],
});
let pdf = encoded_pages(&[(differences, &b"BT /F1 20 Tf 1 0 0 1 20 150 Tm (\x1e\x1f\x02'a\xaeXYZ) Tj ET"[..])]);
let pdf = encoded_pages(&[(differences, &b"BT /F1 20 Tf 1 0 0 1 20 150 Tm (\x1e\x1f\x02'a\xaeXYZ=) Tj ET"[..])]);
let texts = extract_page(&pdf, 0, &InterpreterSettings::default()).unwrap().glyphs.iter().map(|g| g.text.clone()).collect::<Vec<_>>();
assert_eq!(texts, ["fi", "fl", "-", "\u{2019}", "a", "\u{fb01}", "fi"]);
assert_eq!(texts, ["fi", "fl", "-", "\u{2019}", "a", "\u{fb01}", "fi", "="]);
}
#[test]
+4 -3
View File
@@ -46,9 +46,10 @@ horizontal differencing and an 8-bit control) and
Glyph names outside the Adobe Glyph List (#3169, #3170): hayro-interpret's `glyph_name_to_text`
maps a name by the glyph list specification's full mapping (drop from the first `.`, split on `_`,
each part through the list, `uniXXXX…` or `uXXXX`), so `f_i`, `f_l` and `hyphen.case` read as
text. A simple Type 1 or CFF font whose name only numbers the glyph (`C39`, `c251`; one or two
letters then digits, ZapfDingbats' `aN` excepted) reads as the code's StandardEncoding character,
each part through the list, `uniXXXX…` or `uXXXX`; a `uni` part with any group that isn't a
character maps to nothing), so `f_i`, `f_l` and `hyphen.case` read as text. A simple Type 1 or
CFF font whose name only numbers the glyph (`C39`, `c251`, also with a suffix such as `C39.sc`;
one or two letters then digits, ZapfDingbats' `aN` excepted) reads as the code's StandardEncoding character,
so a book set in such fonts extracts instead of returning nothing. Embedded TrueType fonts take
the full mapping only. Covered by `text::tests::glyph_names_outside_the_glyph_list_still_read_as_text`.
+11 -5
View File
@@ -673,7 +673,8 @@ pub(crate) fn glyph_name_to_unicode(name: &str) -> Option<char> {
/// Adobe Glyph List Specification's full mapping: drop everything from the first `.`
/// (`hyphen.case`, `a.sc`), split the rest on `_` into components (`f_i`, `f_f_l`), and map each
/// one through the glyph list, as `uniXXXX[XXXX…]` (BMP values, four hex digits each) or as
/// `uXXXX` to `uXXXXXX`. A component it can't map adds nothing; `None` when nothing is left.
/// `uXXXX` to `uXXXXXX`. A component it can't map, or a `uni` component with any group that
/// isn't a character, adds nothing; `None` when nothing is left.
pub(crate) fn glyph_name_to_text(name: &str) -> Option<BfString> {
let hex = |h: &str| u32::from_str_radix(h, 16).ok().and_then(char::from_u32);
let base = name.split('.').next().unwrap_or_default();
@@ -685,10 +686,13 @@ pub(crate) fn glyph_name_to_text(name: &str) -> Option<BfString> {
.strip_prefix("uni")
.filter(|d| !d.is_empty() && d.len() % 4 == 0 && d.bytes().all(|b| b.is_ascii_hexdigit()))
{
// All ASCII, so every fourth byte is a char boundary.
for at in (0..digits.len()).step_by(4) {
text.extend(digits.get(at..at + 4).and_then(hex));
}
// All ASCII, so every fourth byte is a char boundary. A group that isn't a
// character (a surrogate) makes the whole component map to nothing.
let chars: Option<String> = (0..digits.len())
.step_by(4)
.map(|at| digits.get(at..at + 4).and_then(hex))
.collect();
text.extend(chars);
} else if let Some(c) = part
.strip_prefix('u')
.filter(|d| (4..=6).contains(&d.len()) && d.bytes().all(|b| b.is_ascii_hexdigit()))
@@ -709,6 +713,8 @@ pub(crate) fn glyph_name_to_text(name: &str) -> Option<BfString> {
/// digits (`C39`, `c251`, `g12`), so it says nothing about the character. ZapfDingbats' own
/// names (`a1` to `a191`) are left out: they name dingbats, not the code's letter.
pub(crate) fn numbered_glyph_name(name: &str) -> bool {
// As in the glyph list's mapping, a suffix (`C39.sc`) is dropped first.
let name = name.split('.').next().unwrap_or_default();
let letters = name.bytes().take_while(u8::is_ascii_alphabetic).count();
(1..=2).contains(&letters)
&& name.len() > letters