M3: numbered glyph names read as nothing again, not as a StandardEncoding guess (#3169, #3170)

Review on #3239: a name that only numbers the glyph (C39, c251) says
nothing about the character. Reading it as the code's StandardEncoding
character invents text wherever the font's /Encoding puts the glyph
elsewhere: with /C1 at 233 it extracted O-slash for a glyph drawn as
e-acute. numbered_glyph_name and the fallback are removed; the AGL
specification's full mapping stays.

The test puts one glyph per line: the test font has none of these glyphs,
so they have no width, and on one line a later 'fi' sat on the first and
was dropped as its fake-bold copy once the guessed glyphs between them
were gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3niW7kVeT8mi7TpWc8eAJ
This commit is contained in:
mtlcl410
2026-10-20 15:54:51 +08:00
co-authored by Claude Opus 5.5
parent 49fa11942f
commit f6255c0ed6
4 changed files with 26 additions and 37 deletions
+18 -8
View File
@@ -1904,9 +1904,10 @@ mod tests {
/// #3169, #3170: glyph names outside the Adobe Glyph List still read as text. Ligature
/// (`f_i`) and suffixed (`hyphen.case`) names go by the glyph list specification's full
/// mapping; a name that only numbers the glyph (`c251` small caps, `C39`, `C174`) reads as
/// the code in StandardEncoding, where 39 is a right quote and 174 the fi ligature. A
/// ZapfDingbats name (`a12`) and a name with no meaning left (`foo`) still read as nothing.
/// mapping. A name that only numbers the glyph (`c251` small caps, `C39`, `C174`, `C39.sc`)
/// reads as nothing, not as a guess from the code (review on #3239: with `/C1` at 233,
/// StandardEncoding's 233 would read `Ø` for a glyph drawn as `é`), and so do a ZapfDingbats
/// name (`a12`) and a name with no meaning left (`foo`).
#[test]
fn glyph_names_outside_the_glyph_list_still_read_as_text() {
use lopdf::{Object, dictionary};
@@ -1917,13 +1918,22 @@ 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"),
Object::Integer(61), name("C39.sc"), Object::Integer(233), name("C1"),
],
});
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", "="]);
// One glyph per line: the test font has none of these glyphs, so they have no width, and
// on one line a later `fi` would sit on the first and be dropped as its fake-bold copy.
let mut content = b"BT /F1 20 Tf 20 290 Td".to_vec();
for code in *b"\x1e\x1f\x02'a\xaeXYZ=\xe9" {
content.extend_from_slice(b" (");
content.push(code);
content.extend_from_slice(b") Tj 0 -25 Td");
}
content.extend_from_slice(b" ET");
let pdf = encoded_pages(&[(differences, &content[..])]);
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, ["fi", "fl", "-", "fi"], "{text:?}");
}
/// Review on #3239: the glyph list specification's `uni` and `u` forms take uppercase hex
+3 -5
View File
@@ -47,11 +47,9 @@ 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`; 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`.
character maps to nothing), so `f_i`, `f_l` and `hyphen.case` read as text. A name that only
numbers the glyph (`C39`, `c251`) still reads as nothing: the code's StandardEncoding character
would be a guess, wrong wherever the font's /Encoding puts the glyph elsewhere (review on #3239). Covered by `text::tests::glyph_names_outside_the_glyph_list_still_read_as_text`.
Text extraction: hayro-interpret exposes `OutlineGlyph::is_vertical` from a Type0 font’s CMap writing mode. PdfCraft follows negative glyph-space y for WMode 1 so upright vertical columns can be searched (#126).
-13
View File
@@ -712,19 +712,6 @@ pub(crate) fn glyph_name_to_text(name: &str) -> Option<BfString> {
}
}
/// PdfCraft patch: whether a glyph name only numbers the glyph, one or two letters and then
/// 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
&& name.bytes().skip(letters).all(|b| b.is_ascii_digit())
&& !(letters == 1 && name.starts_with('a'))
}
pub(crate) fn unicode_from_name(name: &str) -> Option<char> {
let convert = |input: &str| u32::from_str_radix(input, 16).ok().and_then(char::from_u32);
+5 -11
View File
@@ -3,7 +3,7 @@ use crate::font::standard_font::{StandardFont, StandardKind, select_standard_fon
use crate::font::true_type::{Width, read_encoding, read_widths};
use crate::font::{
Encoding, FallbackFontQuery, glyph_name_to_text, glyph_name_to_unicode, normalized_glyph_name,
numbered_glyph_name, read_to_unicode,
read_to_unicode,
};
use crate::{CMapResolverFn, CacheKey, FontResolverFn};
use hayro_cmap::{BfString, CMap};
@@ -128,21 +128,15 @@ impl Type1Font {
}
// PdfCraft patch (#3169, #3170): a name outside the glyph list still reads by the
// glyph list specification's full mapping (`f_i`, `hyphen.case`). A name that only
// numbers the glyph (`C39`, `c251`) reads as the code's character in StandardEncoding,
// the encoding a Type 1 program is written against: a book set in such fonts extracted
// as nothing at all, though every page draws.
// numbers the glyph (`C39`, `c251`) says nothing about the character and reads as
// nothing: the code's StandardEncoding character would be a guess, wrong wherever the
// font's /Encoding puts the glyph elsewhere (review on #3239).
let name = match &self.1 {
Kind::Standard(s) => s.code_to_ps_name(code),
Kind::Cff(c) => c.code_to_ps_name(code),
Kind::Type1(t) => t.code_to_ps_name(code),
}?;
glyph_name_to_text(name).or_else(|| {
numbered_glyph_name(name)
.then(|| Encoding::Standard.map_code(code))
.flatten()
.and_then(glyph_name_to_unicode)
.map(BfString::Char)
})
glyph_name_to_text(name)
}
}