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:
bflatastic
2026-11-09 08:10:37 +07:00
5 changed files with 79 additions and 8 deletions
+18 -2
View File
@@ -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.
+30 -2
View File
@@ -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
+1 -1
View File
File diff suppressed because one or more lines are too long
+24 -1
View File
@@ -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
View File
@@ -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)?),