diff --git a/crates/render/src/text.rs b/crates/render/src/text.rs index 5054859961..995fceb36c 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 2d42b2628e..78e54ac1ba 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 7c8c8d086d..9a45e03995 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 139454a0b2..1f386a2cec 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 b362669859..c650439961 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 02f76bfb80..16c023a88a 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) + }) } }