M10: an unreadable glyph name reads as WinAnsi in the editor too (#3962)

Review on #3969: for a symbolic TrueType font without an encoding, the
editor overlaid only the codes whose glyph has a name with a Unicode
value on its Standard table. A code whose glyph name has none kept
Standard's reading (0xC4 `tilde`, 0x80 nothing), while extraction falls
back to WinAnsi (vendored hayro-interpret patch (53)): `Ä`, `€`. Such a
code now reads as WinAnsi in the editor as well, so editing and
extraction show the same text.

vendor/README.md: patch (53) names the function it adds,
`EmbeddedKind::symbolic_text`, states the glyph-name rules it shares with
the editor, and lists the pdfcraft-fonts test too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
This commit is contained in:
mtlcl410
2026-10-31 08:18:00 +08:00
co-authored by Claude Opus 5.5
parent 53d0e36533
commit 175d4e4de1
3 changed files with 16 additions and 5 deletions
+8 -2
View File
@@ -1232,7 +1232,9 @@ pub fn glyph_unicode(name: &str) -> Option<char> {
/// selects it: the code, then at 0xF000, 0xF100 and 0xF200). Such a font's codes need not be
/// Standard or WinAnsi: FrameMaker files distilled with Distiller 3 number them in the Macintosh
/// glyph order, 29 below ASCII, so `Solid` read as `6ROLG` and typing set the wrong glyphs (#3962).
/// Codes without a glyph, or whose glyph has no name with a Unicode value, are left out.
/// Codes without a glyph are left out. A code whose glyph has no name with a Unicode value reads
/// as WinAnsi, as the renderer reads it (vendored hayro-interpret patch (53)), so the editor and
/// extraction agree: `€` for code 0x80, which Standard leaves without a character.
fn symbolic_glyph_texts(doc: &Document, font: &Dict) -> Vec<(u32, String)> {
use skrifa::GlyphId;
use skrifa::raw::{FontData, FontRead as _, TableProvider as _, tables::cmap::PlatformId, tables::post::Post, types::Tag};
@@ -1276,7 +1278,11 @@ fn symbolic_glyph_texts(doc: &Document, font: &Dict) -> Vec<(u32, String)> {
offsets.iter().find_map(|o| subtable.map_codepoint(code + o).filter(|g| *g != GlyphId::NOTDEF))
})?;
let glyph = u16::try_from(glyph.to_u32()).ok()?;
Some((code, glyph_text(post_name(&post, &custom, glyph)?)?))
let winansi = || {
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)?))
})
.collect()
}
+7 -2
View File
@@ -224,6 +224,10 @@ fn a_symbolic_truetype_font_without_an_encoding_reads_its_glyph_names() {
let variant = (1..face.maxp().unwrap().num_glyphs())
.find(|&g| post.glyph_name(skrifa::raw::types::GlyphId16::new(g)) == Some("a.1"))
.expect("Inter has a.1");
// A glyph whose name has no Unicode value: its code reads as WinAnsi, as the renderer reads it.
let unnamed = (1..face.maxp().unwrap().num_glyphs())
.find(|&g| post.glyph_name(skrifa::raw::types::GlyphId16::new(g)).is_some_and(|n| !n.starts_with('.') && glyph_text(n).is_none()))
.expect("Inter has a glyph whose name has no text");
// A format 6 Mac cmap over codes 3..=0x57: each letter at its code minus 29.
let (first, last) = (3u16, 0x57u16);
let mut glyphs = vec![0u16; usize::from(last - first + 1)];
@@ -252,8 +256,8 @@ 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.
let symbol = format4(&[(0xF020, gid('k')), (0xF0C0, gid('é')), (0xF0C1, variant), (0xF0C2, gid('x')), (0xF0C3, gid('w'))]);
// 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)]);
// 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'))]);
@@ -315,6 +319,7 @@ fn a_symbolic_truetype_font_without_an_encoding_reads_its_glyph_names() {
let f = font(&mut doc, vec![("Subtype", Object::name("TrueType")), ("FontDescriptor", Object::Dict(descriptor))]);
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.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");
+1 -1
View File
File diff suppressed because one or more lines are too long