mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
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:
co-authored by
Claude Opus 5.5
parent
0b731453d3
commit
ecb990c8ce
@@ -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]
|
||||
|
||||
Vendored
+4
-3
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user