From 49fa11942ffc4568be879d85c1834568bdf75d57 Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Mon, 19 Oct 2026 12:22:00 +0800 Subject: [PATCH] M10: glyph names with lowercase hex digits map to nothing (#3170) Review on #3239: the Adobe Glyph List Specification's `uni` and `u` forms take uppercase hexadecimal digits only (section 2), and its examples map `uni20ac` to an empty string. glyph_name_to_text accepted lowercase too. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q --- crates/render/src/text.rs | 24 ++++++++++++++++++++++++ vendor/hayro-interpret/src/font/mod.rs | 7 +++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/crates/render/src/text.rs b/crates/render/src/text.rs index 41071d1d5..5775e239a 100644 --- a/crates/render/src/text.rs +++ b/crates/render/src/text.rs @@ -1926,6 +1926,30 @@ mod tests { assert_eq!(texts, ["fi", "fl", "-", "\u{2019}", "a", "\u{fb01}", "fi", "="]); } + /// Review on #3239: the glyph list specification's `uni` and `u` forms take uppercase hex + /// digits only, and map a name with lowercase ones to nothing ("uni20ac" in its examples). + /// Names upstream's own lookup can't read (several groups, a suffix) show it: lowercase reads + /// as nothing, uppercase as its characters. A bare `uni20ac` or `u1040c` is still read by + /// upstream's lenient `unicode_from_name`, outside this mapping. + #[test] + fn glyph_names_with_lowercase_hex_read_as_nothing() { + use lopdf::{Object, dictionary}; + let name = |n: &str| Object::Name(n.as_bytes().to_vec()); + let differences = Object::Dictionary(dictionary! { + "Type" => "Encoding", + "Differences" => vec![Object::Integer(65), name("uni20ac0308"), name("uni20AC0308"), name("u1040c.alt"), name("u1040C.alt"), name("f_uni20ac"), name("f_uni20AC")], + }); + let pdf = encoded_pages(&[( + differences, + &b"BT /F1 20 Tf 20 180 Td (A) Tj 0 -25 Td (B) Tj 0 -25 Td (C) Tj 0 -25 Td (D) Tj 0 -25 Td (E) Tj 0 -25 Td (F) Tj ET"[..], + )]); + // One glyph per line, so each name's text stays its own (and a name read as nothing + // leaves no line). + let text = extract_page(&pdf, 0, &InterpreterSettings::default()).unwrap().plain_text(); + let lines: Vec<&str> = text.lines().map(str::trim).filter(|l| !l.is_empty()).collect(); + assert_eq!(lines, ["\u{20ac}\u{308}", "\u{1040c}", "f", "f\u{20ac}"], "{text:?}"); + } + #[test] fn narrow_ligatures_keep_every_letter() { // Split evenly, this "ffi" put its two "f"s so close that the second was dropped as a diff --git a/vendor/hayro-interpret/src/font/mod.rs b/vendor/hayro-interpret/src/font/mod.rs index 4eeb3a065..2a3d3f4dc 100644 --- a/vendor/hayro-interpret/src/font/mod.rs +++ b/vendor/hayro-interpret/src/font/mod.rs @@ -677,6 +677,9 @@ pub(crate) fn glyph_name_to_unicode(name: &str) -> Option { /// isn't a character, adds nothing; `None` when nothing is left. pub(crate) fn glyph_name_to_text(name: &str) -> Option { let hex = |h: &str| u32::from_str_radix(h, 16).ok().and_then(char::from_u32); + // The specification's digits are uppercase only (`uni20ac` maps to nothing, review on + // #3239). + let upper_hex = |d: &str| d.bytes().all(|b| matches!(b, b'0'..=b'9' | b'A'..=b'F')); let base = name.split('.').next().unwrap_or_default(); let mut text = String::new(); for part in base.split('_') { @@ -684,7 +687,7 @@ pub(crate) fn glyph_name_to_text(name: &str) -> Option { text.push_str(s); } else if let Some(digits) = part .strip_prefix("uni") - .filter(|d| !d.is_empty() && d.len() % 4 == 0 && d.bytes().all(|b| b.is_ascii_hexdigit())) + .filter(|d| !d.is_empty() && d.len() % 4 == 0 && upper_hex(d)) { // 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. @@ -695,7 +698,7 @@ pub(crate) fn glyph_name_to_text(name: &str) -> Option { 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())) + .filter(|d| (4..=6).contains(&d.len()) && upper_hex(d)) .and_then(hex) { text.push(c);