mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
Merge PR #3239: M3: extract text from glyph names outside the reference-app Glyph List (#3169, #3170)
This commit is contained in:
@@ -2086,6 +2086,64 @@ 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`, `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};
|
||||
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"),
|
||||
Object::Integer(61), name("C39.sc"), Object::Integer(233), name("C1"),
|
||||
],
|
||||
});
|
||||
// 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
|
||||
/// 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
|
||||
|
||||
Vendored
+7
@@ -43,6 +43,13 @@ 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`; 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 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).
|
||||
|
||||
Lab Bradford follow-up to #1237: Lab adapts XYZ to D65 with white-point ratios
|
||||
|
||||
+43
@@ -669,6 +669,49 @@ 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, 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);
|
||||
// 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('_') {
|
||||
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 && 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.
|
||||
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()) && upper_hex(d))
|
||||
.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)),
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
|
||||
|
||||
+20
-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,
|
||||
read_to_unicode,
|
||||
};
|
||||
use crate::{CMapResolverFn, CacheKey, FontResolverFn};
|
||||
use hayro_cmap::{BfString, CMap};
|
||||
@@ -117,11 +118,25 @@ 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`) 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user