mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-31 13:37:35 -06:00
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
50f261d70c
commit
0b731453d3
@@ -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::<Vec<_>>();
|
||||
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
|
||||
|
||||
Vendored
+8
@@ -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
|
||||
|
||||
+47
@@ -669,6 +669,53 @@ pub(crate) fn glyph_name_to_unicode(name: &str) -> Option<char> {
|
||||
})
|
||||
}
|
||||
|
||||
/// 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<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();
|
||||
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<char> {
|
||||
let convert = |input: &str| u32::from_str_radix(input, 16).ok().and_then(char::from_u32);
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+6
-5
@@ -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),
|
||||
}
|
||||
|
||||
|
||||
+26
-5
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user