mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 14:41:51 +00:00
Merge #6521: fix(render): glyph names read as text only in the glyph list's uni and u forms (hayro-interpret patch 92)
# Conflicts: # vendor/README.md
This commit is contained in:
+18
-2
@@ -1402,8 +1402,9 @@ fn glyph_component(part: &str) -> Option<String> {
|
||||
if let Ok(i) = crate::encodings::NAMES.binary_search_by(|(n, _)| (*n).cmp(part)) {
|
||||
return char::from_u32(crate::encodings::NAMES[i].1).map(String::from);
|
||||
}
|
||||
// `from_str_radix` accepts a leading `+`, which is not a glyph name, so the digits are checked.
|
||||
let hexish = |h: &str| !h.is_empty() && h.bytes().all(|b| b.is_ascii_hexdigit());
|
||||
// `from_str_radix` accepts a leading `+` and lowercase digits, and the glyph list
|
||||
// specification's digits are uppercase only (`uni20ac` names nothing), so they are checked.
|
||||
let hexish = |h: &str| !h.is_empty() && h.bytes().all(|b| matches!(b, b'0'..=b'9' | b'A'..=b'F'));
|
||||
if let Some(hex) = part.strip_prefix("uni").filter(|h| h.len() >= 4 && h.len() % 4 == 0 && hexish(h)) {
|
||||
let mut s = String::new();
|
||||
for i in (0..hex.len()).step_by(4) {
|
||||
@@ -1560,6 +1561,21 @@ mod encoding_tests {
|
||||
Object::Dict(enc)
|
||||
}
|
||||
|
||||
/// The glyph list specification's hex forms: `uni` and groups of exactly four uppercase
|
||||
/// digits, `u` and four to six.
|
||||
#[test]
|
||||
fn glyph_names_take_uppercase_hex_in_whole_groups() {
|
||||
assert_eq!(glyph_text("uni20ac"), None, "the specification's hex digits are uppercase");
|
||||
assert_eq!(glyph_text("u1f600"), None);
|
||||
assert_eq!(glyph_text("uni65"), None, "`uni` takes groups of exactly four digits");
|
||||
assert_eq!(glyph_text("u65"), None, "`u` takes four to six digits");
|
||||
assert_eq!(glyph_text("u0010FFFF"), None);
|
||||
assert_eq!(glyph_text("uni0065").as_deref(), Some("e"));
|
||||
assert_eq!(glyph_text("u0065").as_deref(), Some("e"), "`u` and four digits");
|
||||
assert_eq!(glyph_text("u1F600").as_deref(), Some("\u{1f600}"), "`u` and five");
|
||||
assert_eq!(glyph_text("u10FFFF").as_deref(), Some("\u{10ffff}"), "`u` and six, the last code point");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glyph_names_follow_the_naming_algorithm() {
|
||||
// Annex D names and the two hex forms, as before.
|
||||
|
||||
@@ -698,8 +698,8 @@ fn glyph_names_outside_the_glyph_list_still_read_as_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.
|
||||
/// as nothing, uppercase as its characters. A bare `uni20ac` or `u1040c` reads as nothing too
|
||||
/// (`agl_unicode_from_name_rule_reads_text`).
|
||||
#[test]
|
||||
fn glyph_names_with_lowercase_hex_read_as_nothing() {
|
||||
use lopdf::{Object, dictionary};
|
||||
@@ -719,6 +719,34 @@ fn glyph_names_with_lowercase_hex_read_as_nothing() {
|
||||
assert_eq!(lines, ["\u{20ac}\u{308}", "\u{1040c}", "f", "f\u{20ac}"], "{text:?}");
|
||||
}
|
||||
|
||||
/// The glyph list specification's `uni` form is `uni` and groups of exactly four uppercase hex
|
||||
/// digits, its `u` form `u` and four to six, outside the surrogate range: `uni65`, `u65`,
|
||||
/// `uni20ac`, `u1f600`, `uni+065` and `uniD800` name no character, and their glyphs read as
|
||||
/// nothing rather than as `e`, `€` or an emoji. The well-formed names read as before.
|
||||
#[test]
|
||||
fn agl_unicode_from_name_rule_reads_text() {
|
||||
use lopdf::{Object, dictionary};
|
||||
let name = |n: &str| Object::Name(n.as_bytes().to_vec());
|
||||
let names = ["uni65", "u65", "uni20ac", "u1f600", "uni+065", "uniD800", "u10FFFF0", "uni0065", "u0066", "u1F600", "uni20AC"];
|
||||
let differences = Object::Dictionary(dictionary! {
|
||||
"Type" => "Encoding",
|
||||
"Differences" => std::iter::once(Object::Integer(65)).chain(names.iter().map(|n| name(n))).collect::<Vec<_>>(),
|
||||
});
|
||||
// One glyph per line, so each name's text stays its own (and a name read as nothing
|
||||
// leaves no line).
|
||||
let mut content = b"BT /F1 20 Tf 20 290 Td".to_vec();
|
||||
for code in (b'A'..).take(names.len()) {
|
||||
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, ["e", "f", "\u{1f600}", "\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
+1
-1
File diff suppressed because one or more lines are too long
+24
-1
@@ -675,7 +675,7 @@ pub(crate) fn glyph_name_to_unicode(name: &str) -> Option<char> {
|
||||
return unicode_str.chars().next();
|
||||
}
|
||||
|
||||
unicode_from_name(name).or_else(|| {
|
||||
agl_unicode_from_name(name).or_else(|| {
|
||||
warn!("failed to map glyph name {} to unicode", name);
|
||||
|
||||
None
|
||||
@@ -725,6 +725,29 @@ pub(crate) fn glyph_name_to_text(name: &str) -> Option<BfString> {
|
||||
}
|
||||
}
|
||||
|
||||
/// PdfCraft patch (92): the character a `uniXXXX` or `uXXXX` to `uXXXXXX` glyph name stands
|
||||
/// for, by the Adobe Glyph List Specification: `uni` and exactly four uppercase hex digits, or
|
||||
/// `u` and four to six; a value in the surrogate range or past U+10FFFF is no character. The
|
||||
/// text of a glyph goes by this rule, so `uni65`, `uni20ac` and `u41` read as nothing, as they
|
||||
/// do in [`glyph_name_to_text`].
|
||||
pub(crate) fn agl_unicode_from_name(name: &str) -> Option<char> {
|
||||
let upper_hex = |d: &str| d.bytes().all(|b| matches!(b, b'0'..=b'9' | b'A'..=b'F'));
|
||||
let digits = name
|
||||
.strip_prefix("uni")
|
||||
.filter(|d| d.len() == 4)
|
||||
.or_else(|| {
|
||||
name.strip_prefix('u')
|
||||
.filter(|d| (4..=6).contains(&d.len()))
|
||||
})
|
||||
.filter(|d| upper_hex(d))?;
|
||||
u32::from_str_radix(digits, 16)
|
||||
.ok()
|
||||
.and_then(char::from_u32)
|
||||
}
|
||||
|
||||
/// Upstream's lenient reading of a `uni…` or `u…` name (any number of hex digits, either case),
|
||||
/// kept for finding a glyph in a font: PdfCraft patch (92) reads text by
|
||||
/// [`agl_unicode_from_name`] instead.
|
||||
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);
|
||||
|
||||
|
||||
+6
-2
@@ -4,7 +4,7 @@ use crate::font::generated::{glyph_names, metrics, standard, symbol, zapf_dings}
|
||||
use crate::font::true_type::{Width, read_encoding, read_widths};
|
||||
use crate::font::{
|
||||
Encoding, FontData, FontQuery, glyph_name_to_unicode, normalized_glyph_name, stretch_glyph,
|
||||
strip_subset_prefix,
|
||||
strip_subset_prefix, unicode_from_name,
|
||||
};
|
||||
use hayro_syntax::object::Dict;
|
||||
use hayro_syntax::object::Name;
|
||||
@@ -769,7 +769,11 @@ fn accented_parts(name: &str) -> Option<(&'static str, &'static str, Place)> {
|
||||
.map(|&(_, base, accent, place)| (base, accent, place))
|
||||
};
|
||||
by_name(name).or_else(|| {
|
||||
let c = glyph_name_to_unicode(name)?;
|
||||
// Drawing keeps upstream's lenient reading of `uni` names; text goes by the glyph list
|
||||
// specification (PdfCraft patch 92).
|
||||
let c = glyph_names::get(name)
|
||||
.and_then(|s| s.chars().next())
|
||||
.or_else(|| unicode_from_name(name))?;
|
||||
match COMMA_BELOW_BY_CHAR.iter().find(|(letter, _)| *letter == c) {
|
||||
Some((_, comma_below)) => by_name(comma_below),
|
||||
None => by_name(glyph_names::get_reverse(c)?),
|
||||
|
||||
Reference in New Issue
Block a user