From 0b731453d377d462cfd55b2f9b01c523b256ba02 Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Sun, 18 Oct 2026 19:46:39 +0800 Subject: [PATCH 1/4] M3: extract text from glyph names outside the Adobe Glyph List (#3169, #3170) A simple font without a usable /ToUnicode reads its text from glyph names, and a name missing from the glyph list dropped the glyph from the extracted text while the page still drew. An 821-page book set in Type1C fonts named C39, C40, ... extracted 4 KB of front matter and nothing else. - glyph_name_to_text applies the Adobe Glyph List Specification's full mapping: drop from the first '.', split on '_', map each component through the list or as uniXXXX / uXXXX. f_i, f_l and hyphen.case now read as fi, fl and '-'. Type 1, CFF, standard and embedded TrueType simple fonts use it. - A Type 1 / CFF / standard font name that only numbers the glyph (one or two letters then digits, ZapfDingbats' aN excepted) reads as the code's StandardEncoding character, so C174 is the fi ligature and C208 an em dash. The book now extracts 1.9 MB (pdftotext: 1.85 MB), with fi and em dashes where pdftotext writes Latin-1 mojibake. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q --- crates/render/src/text.rs | 22 +++++++++ vendor/README.md | 8 ++++ vendor/hayro-interpret/src/font/mod.rs | 47 +++++++++++++++++++ .../hayro-interpret/src/font/standard_font.rs | 2 +- vendor/hayro-interpret/src/font/true_type.rs | 11 +++-- vendor/hayro-interpret/src/font/type1.rs | 31 ++++++++++-- 6 files changed, 110 insertions(+), 11 deletions(-) diff --git a/crates/render/src/text.rs b/crates/render/src/text.rs index 505485996..995fceb36 100644 --- a/crates/render/src/text.rs +++ b/crates/render/src/text.rs @@ -1902,6 +1902,28 @@ mod tests { assert_eq!(texts(2), ["a", "\u{ad}", "b", "\u{a0}", "c"], "a named glyph keeps its glyph list value"); } + /// #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. + #[test] + fn glyph_names_outside_the_glyph_list_still_read_as_text() { + 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(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"), + ], + }); + 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::>(); + assert_eq!(texts, ["fi", "fl", "-", "\u{2019}", "a", "\u{fb01}", "fi"]); + } + #[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/README.md b/vendor/README.md index 2d42b2628..78e54ac1b 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -44,6 +44,14 @@ Covered by `raster::tests::tiff_predictor_packed_rgb_rows_keep_their_byte_bounda horizontal differencing and an 8-bit control) and `raster::tests::tiff_predictor_packed_rgb_image_renders_every_row`. +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, +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`. + 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). Lab Bradford follow-up to #1237: Lab adapts XYZ to D65 with white-point ratios diff --git a/vendor/hayro-interpret/src/font/mod.rs b/vendor/hayro-interpret/src/font/mod.rs index 7c8c8d086..9a45e0399 100644 --- a/vendor/hayro-interpret/src/font/mod.rs +++ b/vendor/hayro-interpret/src/font/mod.rs @@ -669,6 +669,53 @@ pub(crate) fn glyph_name_to_unicode(name: &str) -> Option { }) } +/// PdfCraft patch: the text of a glyph name `glyph_name_to_unicode` doesn't know, by the +/// 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. +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); + let base = name.split('.').next().unwrap_or_default(); + let mut text = String::new(); + for part in base.split('_') { + if let Some(s) = glyph_names::get(part) { + 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())) + { + // 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)); + } + } else if let Some(c) = part + .strip_prefix('u') + .filter(|d| (4..=6).contains(&d.len()) && d.bytes().all(|b| b.is_ascii_hexdigit())) + .and_then(hex) + { + text.push(c); + } + } + let mut chars = text.chars(); + match (chars.next(), chars.next()) { + (None, _) => None, + (Some(c), None) => Some(BfString::Char(c)), + _ => Some(BfString::String(text)), + } +} + +/// 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 { + 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 { let convert = |input: &str| u32::from_str_radix(input, 16).ok().and_then(char::from_u32); diff --git a/vendor/hayro-interpret/src/font/standard_font.rs b/vendor/hayro-interpret/src/font/standard_font.rs index 139454a0b..1f386a2ce 100644 --- a/vendor/hayro-interpret/src/font/standard_font.rs +++ b/vendor/hayro-interpret/src/font/standard_font.rs @@ -397,7 +397,7 @@ impl StandardKind { }) } - fn code_to_ps_name(&self, code: u8) -> Option<&str> { + pub(crate) fn code_to_ps_name(&self, code: u8) -> Option<&str> { let bf = self.base_font; self.encodings diff --git a/vendor/hayro-interpret/src/font/true_type.rs b/vendor/hayro-interpret/src/font/true_type.rs index b36266985..c65043996 100644 --- a/vendor/hayro-interpret/src/font/true_type.rs +++ b/vendor/hayro-interpret/src/font/true_type.rs @@ -2,7 +2,7 @@ use crate::font::blob::{CffFontBlob, OpenTypeFontBlob}; use crate::font::generated::{glyph_names, mac_os_roman, mac_roman, standard, win_ansi}; use crate::font::standard_font::StandardKind; use crate::font::{ - Encoding, FallbackFontQuery, FontFlags, glyph_name_to_unicode, read_to_unicode, + Encoding, FallbackFontQuery, FontFlags, glyph_name_to_text, glyph_name_to_unicode, read_to_unicode, strip_subset_prefix, unicode_from_name, }; use crate::util::OptionLog; @@ -190,10 +190,11 @@ impl TrueTypeFont { } match &self.kind { - Kind::Embedded(e) => e - .code_to_unicode_name(code as u8) - .and_then(glyph_name_to_unicode) - .map(BfString::Char), + // PdfCraft patch (#3169): a name outside the glyph list reads by the glyph list + // specification's full mapping (`f_i`, `hyphen.case`). + Kind::Embedded(e) => e.code_to_unicode_name(code as u8).and_then(|name| { + glyph_name_to_unicode(name).map(BfString::Char).or_else(|| glyph_name_to_text(name)) + }), Kind::Standard(s) => s.char_code_to_unicode(code as u8).map(BfString::Char), } diff --git a/vendor/hayro-interpret/src/font/type1.rs b/vendor/hayro-interpret/src/font/type1.rs index 02f76bfb8..16c023a88 100644 --- a/vendor/hayro-interpret/src/font/type1.rs +++ b/vendor/hayro-interpret/src/font/type1.rs @@ -2,7 +2,8 @@ use crate::font::blob::{CffFontBlob, Type1FontBlob}; use crate::font::standard_font::{StandardFont, StandardKind, select_standard_font}; use crate::font::true_type::{Width, read_encoding, read_widths}; use crate::font::{ - Encoding, FallbackFontQuery, glyph_name_to_unicode, normalized_glyph_name, read_to_unicode, + Encoding, FallbackFontQuery, glyph_name_to_text, glyph_name_to_unicode, normalized_glyph_name, + numbered_glyph_name, read_to_unicode, }; use crate::{CMapResolverFn, CacheKey, FontResolverFn}; use hayro_cmap::{BfString, CMap}; @@ -117,11 +118,31 @@ impl Type1Font { } let code = char_code as u8; - match &self.1 { - Kind::Standard(s) => s.char_code_to_unicode(code).map(BfString::Char), - Kind::Cff(c) => c.char_code_to_unicode(code).map(BfString::Char), - Kind::Type1(t) => t.char_code_to_unicode(code).map(BfString::Char), + let found = match &self.1 { + Kind::Standard(s) => s.char_code_to_unicode(code), + Kind::Cff(c) => c.char_code_to_unicode(code), + Kind::Type1(t) => t.char_code_to_unicode(code), + }; + if let Some(c) = found { + return Some(BfString::Char(c)); } + // 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. + 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) + }) } } From ecb990c8ce9398ac8cf4d3a274ca8cd871af7592 Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Mon, 19 Oct 2026 01:57:14 +0800 Subject: [PATCH 2/4] 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 Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q --- crates/render/src/text.rs | 6 ++++-- vendor/README.md | 7 ++++--- vendor/hayro-interpret/src/font/mod.rs | 16 +++++++++++----- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/crates/render/src/text.rs b/crates/render/src/text.rs index 995fceb36..41071d1d5 100644 --- a/crates/render/src/text.rs +++ b/crates/render/src/text.rs @@ -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::>(); - assert_eq!(texts, ["fi", "fl", "-", "\u{2019}", "a", "\u{fb01}", "fi"]); + assert_eq!(texts, ["fi", "fl", "-", "\u{2019}", "a", "\u{fb01}", "fi", "="]); } #[test] diff --git a/vendor/README.md b/vendor/README.md index 78e54ac1b..314525c57 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -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`. diff --git a/vendor/hayro-interpret/src/font/mod.rs b/vendor/hayro-interpret/src/font/mod.rs index 9a45e0399..4eeb3a065 100644 --- a/vendor/hayro-interpret/src/font/mod.rs +++ b/vendor/hayro-interpret/src/font/mod.rs @@ -673,7 +673,8 @@ pub(crate) fn glyph_name_to_unicode(name: &str) -> Option { /// 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 { 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 { .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 = (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 { /// 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 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 3/4] 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); From f6255c0ed609b6b094d6f66c876648ea8fb75d38 Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:54:51 +0800 Subject: [PATCH 4/4] 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 Claude-Session: https://claude.ai/code/session_01R3niW7kVeT8mi7TpWc8eAJ --- crates/render/src/text.rs | 26 ++++++++++++++++-------- vendor/README.md | 8 +++----- vendor/hayro-interpret/src/font/mod.rs | 13 ------------ vendor/hayro-interpret/src/font/type1.rs | 16 +++++---------- 4 files changed, 26 insertions(+), 37 deletions(-) diff --git a/crates/render/src/text.rs b/crates/render/src/text.rs index 5775e239a..5f0987a8a 100644 --- a/crates/render/src/text.rs +++ b/crates/render/src/text.rs @@ -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::>(); - 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 diff --git a/vendor/README.md b/vendor/README.md index 314525c57..c20e6a253 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -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). diff --git a/vendor/hayro-interpret/src/font/mod.rs b/vendor/hayro-interpret/src/font/mod.rs index 2a3d3f4dc..103ee3961 100644 --- a/vendor/hayro-interpret/src/font/mod.rs +++ b/vendor/hayro-interpret/src/font/mod.rs @@ -712,19 +712,6 @@ pub(crate) fn glyph_name_to_text(name: &str) -> Option { } } -/// 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 { let convert = |input: &str| u32::from_str_radix(input, 16).ok().and_then(char::from_u32); diff --git a/vendor/hayro-interpret/src/font/type1.rs b/vendor/hayro-interpret/src/font/type1.rs index 16c023a88..4ef5f953f 100644 --- a/vendor/hayro-interpret/src/font/type1.rs +++ b/vendor/hayro-interpret/src/font/type1.rs @@ -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) } }