mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-13 00:18:34 +00:00
M10: glyph names in a symbolic TrueType font's post table read as text only in the glyph list's uni and u forms (#3962)
The glyph list specification (ISO 32000-2 §9.10.2) writes the hex digits of its `uniXXXX` and `uXXXX`-`uXXXXXX` forms in uppercase, `uni` taking groups of exactly four and `u` four to six, outside the surrogate range. `glyph_name_text` (hayro-interpret patch 79) took lowercase digits too, so a glyph named `uni1eae` read as U+1EAE; it now names nothing and the code reads as WinAnsi, as for any glyph without a name. The editor's reading of the same `post` names in pdfcraft-fonts applies the same rule, so both agree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9dfd49cc94
commit
63a6f8ace6
+15
-1
@@ -1282,11 +1282,25 @@ fn symbolic_glyph_texts(doc: &Document, font: &Dict) -> Vec<(u32, String)> {
|
||||
let u = crate::encodings::WIN_ANSI.get(usize::try_from(code).ok()?).copied().filter(|u| *u != 0)?;
|
||||
char::from_u32(u).map(String::from)
|
||||
};
|
||||
Some((code, post_name(&post, &custom, glyph).and_then(glyph_text).or_else(winansi)?))
|
||||
let text = post_name(&post, &custom, glyph).filter(|n| uppercase_hex_forms(n)).and_then(glyph_text);
|
||||
Some((code, text.or_else(winansi)?))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Whether the `uni` and `u` components of a glyph name spell their hex digits in uppercase, as
|
||||
/// the glyph list specification's forms do (ISO 32000-2 §9.10.2): `uni1eae` names nothing, so
|
||||
/// its glyph's code reads as WinAnsi, as the renderer reads it (vendored hayro-interpret patch
|
||||
/// (79)). A component that is a list name (`a`, `ecircumflex`) is read as such whatever its case.
|
||||
fn uppercase_hex_forms(name: &str) -> bool {
|
||||
let stem = name.split('.').next().unwrap_or_default();
|
||||
stem.split('_').all(|part| {
|
||||
// Outside the list, a component `glyph_text` reads is `u` or `uni` and hex digits, so a
|
||||
// lowercase `a` to `f` in it is a lowercase digit.
|
||||
crate::encodings::NAMES.binary_search_by(|(n, _)| (*n).cmp(part)).is_ok() || !part.bytes().any(|b| matches!(b, b'a'..=b'f'))
|
||||
})
|
||||
}
|
||||
|
||||
/// The names a version 2.0 `post` table spells out, in order, read once. `Post::glyph_name` walks
|
||||
/// this data from its start for each glyph it is asked about, which over a font program of up to
|
||||
/// 16 MiB, asked about 256 codes, is work an untrusted file can make large. Reading stops at the
|
||||
|
||||
@@ -256,8 +256,22 @@ fn a_symbolic_truetype_font_without_an_encoding_reads_its_glyph_names() {
|
||||
.collect();
|
||||
words.iter().flat_map(|w| w.to_be_bytes()).collect::<Vec<u8>>()
|
||||
};
|
||||
// Symbol (3,0): 0xF020 → k, 0xF0C0 → é, 0xF0C1 → a.1, 0xF0C2 → x, 0xF0C4 → the unnamed glyph.
|
||||
let symbol = format4(&[(0xF020, gid('k')), (0xF0C0, gid('é')), (0xF0C1, variant), (0xF0C2, gid('x')), (0xF0C3, gid('w')), (0xF0C4, unnamed)]);
|
||||
// A glyph whose name the table spells `uni1eae`, lowercase, below: the glyph list
|
||||
// specification's hex digits are uppercase (ISO 32000-2 §9.10.2), so it names nothing.
|
||||
let lowercase = (1..face.maxp().unwrap().num_glyphs())
|
||||
.find(|&g| post.glyph_name(skrifa::raw::types::GlyphId16::new(g)) == Some("uni1EAE"))
|
||||
.expect("Inter has uni1EAE");
|
||||
// Symbol (3,0): 0xF020 → k, 0xF0C0 → é, 0xF0C1 → a.1, 0xF0C2 → x, 0xF0C4 → the unnamed glyph,
|
||||
// 0xF0C5 → the one spelt in lowercase.
|
||||
let symbol = format4(&[
|
||||
(0xF020, gid('k')),
|
||||
(0xF0C0, gid('é')),
|
||||
(0xF0C1, variant),
|
||||
(0xF0C2, gid('x')),
|
||||
(0xF0C3, gid('w')),
|
||||
(0xF0C4, unnamed),
|
||||
(0xF0C5, lowercase),
|
||||
]);
|
||||
// Unicode (3,1), after it: 0xC2 → z. The renderer reads the (3,0) record, offsets and
|
||||
// all, before this one, so code 0xC2 is `x`.
|
||||
let unicode = format4(&[(0xC2, gid('z'))]);
|
||||
@@ -288,6 +302,11 @@ fn a_symbolic_truetype_font_without_an_encoding_reads_its_glyph_names() {
|
||||
subset[entry + 8..entry + 12].copy_from_slice(&offset.to_be_bytes());
|
||||
subset[entry + 12..entry + 16].copy_from_slice(&(cmap.len() as u32).to_be_bytes());
|
||||
subset.extend_from_slice(&cmap);
|
||||
// The `post` table's own spelling of that name, a Pascal string, lowercased in place.
|
||||
let spelt = b"\x07uni1EAE";
|
||||
assert_eq!(subset.windows(spelt.len()).filter(|w| w == spelt).count(), 1);
|
||||
let spot = subset.windows(spelt.len()).position(|w| w == spelt).unwrap();
|
||||
subset[spot..spot + spelt.len()].copy_from_slice(b"\x07uni1eae");
|
||||
let codes: Vec<u8> = TEXT.bytes().map(|b| b - 29).collect();
|
||||
let metrics = |flags: i64, encoding: Option<Object>| {
|
||||
let mut doc = Document::new_empty();
|
||||
@@ -320,6 +339,7 @@ fn a_symbolic_truetype_font_without_an_encoding_reads_its_glyph_names() {
|
||||
assert_eq!(Metrics::from_dict(&doc, &f).decode(&codes), TEXT, "a swapped post version");
|
||||
assert_eq!(symbolic.decode(&[0x20, 0xC0, 0xC1, 0xC2, 0xC3]), "kéaxw");
|
||||
assert_eq!(symbolic.decode(&[0xC4]), "Ä", "a glyph name with no text reads as WinAnsi, not Standard's `tilde`");
|
||||
assert_eq!(symbolic.decode(&[0xC5]), "Å", "`uni1eae` names nothing, so the code reads as WinAnsi, not U+1EAE");
|
||||
assert_eq!(symbolic.encode("Solid").as_deref(), Some(&codes[..5]), "typing sets the same glyphs");
|
||||
// A named encoding, or the Nonsymbolic flag, says what the codes are: they read through it.
|
||||
assert_eq!(metrics(4, Some(Object::name("WinAnsiEncoding"))).decode(&codes), "6ROLGVWDWH");
|
||||
@@ -346,6 +366,18 @@ fn a_symbolic_truetype_font_without_an_encoding_reads_its_glyph_names() {
|
||||
assert_eq!(Metrics::from_dict(&doc, &f).decode(&codes), "");
|
||||
}
|
||||
|
||||
/// The glyph list specification's `uni` and `u` forms spell their hex digits in uppercase
|
||||
/// (ISO 32000-2 §9.10.2); list names keep their own case.
|
||||
#[test]
|
||||
fn glyph_names_with_lowercase_hex_digits_name_nothing() {
|
||||
for name in ["uni1EAE", "u1F600", "a", "ecircumflex", "f_i", "a.ss01", "uni0041_uni0042"] {
|
||||
assert!(uppercase_hex_forms(name), "{name}");
|
||||
}
|
||||
for name in ["uni1eae", "u1f600", "uni20ac.alt", "f_uni20ac", "uni0041_uni00e9"] {
|
||||
assert!(!uppercase_hex_forms(name), "{name}");
|
||||
}
|
||||
}
|
||||
|
||||
fn metrics_with_flags(flags: Option<Object>) -> Metrics {
|
||||
let mut doc = Document::new_empty();
|
||||
let mut descriptor = Dict::new();
|
||||
|
||||
@@ -2109,6 +2109,10 @@ fn a_symbolic_truetype_font_without_an_encoding_reads_its_glyph_names() {
|
||||
const UNI: u16 = 0x3E;
|
||||
const UNNAMED: u16 = 0x3F;
|
||||
const LIGATURE: u16 = 0x40;
|
||||
// 0x41 selects a glyph whose name is spelt `uni01cd`, lowercase: the glyph list
|
||||
// specification's hex digits are uppercase (ISO 32000-2 §9.10.2), so the name stands for no
|
||||
// text and the code reads as WinAnsi (`A`), not as U+01CD.
|
||||
const LOWERCASE: u16 = 0x41;
|
||||
let font = include_bytes!("../../../../assets/fonts/JetBrainsMono-Regular.ttf");
|
||||
let face = skrifa::FontRef::new(font).expect("the test font parses");
|
||||
let charmap = face.charmap();
|
||||
@@ -2131,6 +2135,9 @@ fn a_symbolic_truetype_font_without_an_encoding_reads_its_glyph_names() {
|
||||
map.push((0xF000 + UNI, uni));
|
||||
map.push((0xF000 + UNNAMED, named("EMquad")));
|
||||
map.push((0xF000 + LIGATURE, named("less_equal.liga")));
|
||||
let lowercase = named("uni01CD");
|
||||
assert_ne!(lowercase, uni, "the lowercased name is not the `uniXXXX` glyph read above");
|
||||
map.push((0xF000 + LOWERCASE, lowercase));
|
||||
map.sort_unstable();
|
||||
map.dedup();
|
||||
// A format 4 subtable: one segment per code, then the closing 0xFFFF segment.
|
||||
@@ -2159,10 +2166,15 @@ fn a_symbolic_truetype_font_without_an_encoding_reads_its_glyph_names() {
|
||||
let entry = (0..tables).map(|i| 12 + 16 * i).find(|&e| &font[e..e + 4] == b"cmap").expect("the test font has a cmap");
|
||||
program[entry + 8..entry + 12].copy_from_slice(&(at as u32).to_be_bytes());
|
||||
program[entry + 12..entry + 16].copy_from_slice(&(cmap.len() as u32).to_be_bytes());
|
||||
// The `post` table's own spelling of the name, a Pascal string, lowercased in place.
|
||||
let spelt = b"\x07uni01CD";
|
||||
let spot = program.windows(spelt.len()).position(|w| w == spelt).expect("the post table spells the name");
|
||||
assert_eq!(program.windows(spelt.len()).filter(|w| w == spelt).count(), 1);
|
||||
program[spot..spot + spelt.len()].copy_from_slice(b"\x07uni01cd");
|
||||
|
||||
let text_of = |encoding: &str| -> String {
|
||||
let codes: String = TEXT.chars().map(|c| format!("{:02X}", c as u8 - 29)).collect::<String>()
|
||||
+ &format!("{SUFFIXED:02X}{UNI:02X}{UNNAMED:02X}{LIGATURE:02X}");
|
||||
+ &format!("{SUFFIXED:02X}{UNI:02X}{UNNAMED:02X}{LIGATURE:02X}{LOWERCASE:02X}");
|
||||
let content = format!("BT /F1 14 Tf 10 40 Td <{codes}> Tj ET");
|
||||
let objects: Vec<Vec<u8>> = vec![
|
||||
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
|
||||
@@ -2191,11 +2203,11 @@ fn a_symbolic_truetype_font_without_an_encoding_reads_its_glyph_names() {
|
||||
let page = r.filtered_text(0, crate::text::TextFilter::default()).expect("the page has text");
|
||||
page.glyphs.iter().map(|g| g.text.as_str()).collect()
|
||||
};
|
||||
assert_eq!(text_of(""), format!("Solid stateW{uni_char}?<="));
|
||||
assert_eq!(text_of(""), format!("Solid stateW{uni_char}?<=A"));
|
||||
// An encoding says what the codes are, so they read through it.
|
||||
assert_eq!(text_of("/Encoding /WinAnsiEncoding "), "6ROLGVWDWH=>?@");
|
||||
assert_eq!(text_of("/Encoding /WinAnsiEncoding "), "6ROLGVWDWH=>?@A");
|
||||
// `/Differences` still names its code: 0x36 reads `A`, not the glyph's `S`.
|
||||
assert_eq!(text_of("/Encoding << /Differences [54 /A] >> "), format!("Aolid stateW{uni_char}?<="));
|
||||
assert_eq!(text_of("/Encoding << /Differences [54 /A] >> "), format!("Aolid stateW{uni_char}?<=A"));
|
||||
}
|
||||
|
||||
/// Issue #102: an A4 page (595.28×841.89 pt) rendered 595×841 at 72 dpi, dropping the last
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+25
-10
@@ -202,7 +202,9 @@ impl TrueTypeFont {
|
||||
// PdfCraft patch (#3169): a name outside the glyph list reads by the glyph list
|
||||
// specification's full mapping (`f_i`, `hyphen.case`).
|
||||
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))
|
||||
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),
|
||||
@@ -230,8 +232,8 @@ struct EmbeddedKind {
|
||||
cached_mappings: RefCell<HashMap<u8, GlyphId>>,
|
||||
/// PostScript name from the PDF.
|
||||
postscript_name: Option<String>,
|
||||
/// PdfCraft patch: (see vendor/README.md hayro-interpret (53)) the text each glyph's `post`
|
||||
/// table name stands for, for the names that stand for any (`glyph_name_text`).
|
||||
/// PdfCraft patch (79): the text each glyph's `post` table name stands for, for the names
|
||||
/// that stand for any (`glyph_name_text`).
|
||||
text_names: HashMap<GlyphId, String>,
|
||||
}
|
||||
|
||||
@@ -462,15 +464,17 @@ pub(crate) enum Width {
|
||||
}
|
||||
|
||||
/// PdfCraft patch (79): the text a glyph name stands for, by the rules of the Adobe Glyph List
|
||||
/// specification, as pdfcraft-fonts' `glyph_text` reads them for the editor: drop any suffix
|
||||
/// from the first `.` (`a.ss01` is `a`), then each `_`-separated component is a list name,
|
||||
/// `uni` and groups of four hex digits, or `u` and four to six. `None` when any component is
|
||||
/// none of these. Each component gives at least one character, so the text is never empty.
|
||||
/// specification (ISO 32000-2 §9.10.2): drop any suffix from the first `.` (`a.ss01` is `a`),
|
||||
/// then each `_`-separated component is a list name, `uni` and groups of exactly four
|
||||
/// uppercase hex digits, or `u` and four to six. A value in the surrogate range or past
|
||||
/// U+10FFFF is no character. `None` when any component is none of these (`uni20ac`, `uni65`,
|
||||
/// `u65`). Each component gives at least one character, so the text is never empty.
|
||||
fn glyph_name_text(name: &str) -> Option<String> {
|
||||
// An empty stem (`.notdef`) is no list name and no number, so it gives `None` below.
|
||||
let stem = name.split('.').next()?;
|
||||
// `from_str_radix` alone would also take a sign, as in `uni+041`.
|
||||
let hex = |h: &str| !h.is_empty() && h.bytes().all(|b| b.is_ascii_hexdigit());
|
||||
// `from_str_radix` alone would also take a sign (`uni+041`) and lowercase digits, which the
|
||||
// specification's forms don't have.
|
||||
let hex = |h: &str| !h.is_empty() && h.bytes().all(|b| matches!(b, b'0'..=b'9' | b'A'..=b'F'));
|
||||
let mut out = String::new();
|
||||
for part in stem.split('_') {
|
||||
if let Some(text) = glyph_names::get(part) {
|
||||
@@ -587,7 +591,7 @@ pub(crate) fn read_encoding(dict: &Dict<'_>) -> (Encoding, HashMap<u8, String>)
|
||||
mod tests {
|
||||
use super::glyph_name_text;
|
||||
|
||||
/// PdfCraft patch (79): glyph names read as pdfcraft-fonts' `glyph_text` reads them.
|
||||
/// PdfCraft patch (79): glyph names read by the glyph list specification's rules.
|
||||
#[test]
|
||||
fn glyph_names_read_by_the_adobe_glyph_list_rules() {
|
||||
for (name, expected) in [
|
||||
@@ -616,6 +620,17 @@ mod tests {
|
||||
"u0000041",
|
||||
"u004G",
|
||||
"a_EMquad",
|
||||
// PdfCraft patch (79): the glyph list specification's hex digits are uppercase only.
|
||||
"uni20ac",
|
||||
"uni004100e9",
|
||||
"u1f600",
|
||||
"u20ac",
|
||||
"a_uni20ac",
|
||||
"uni65",
|
||||
"u65",
|
||||
"uni+065",
|
||||
"uDFFF",
|
||||
"u110000",
|
||||
] {
|
||||
assert_eq!(glyph_name_text(name), None, "{name}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user