mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ecb990c8ce
commit
49fa11942f
@@ -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
|
||||
|
||||
+5
-2
@@ -677,6 +677,9 @@ pub(crate) fn glyph_name_to_unicode(name: &str) -> Option<char> {
|
||||
/// 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);
|
||||
// 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<BfString> {
|
||||
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<BfString> {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user