From 3200a5078d9447b06eba892ec8c1966454c6f520 Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Thu, 22 Oct 2026 10:39:14 +0800 Subject: [PATCH] M10: the editor finds a symbolic font's glyph through the cmap as the renderer does (#3962) Review follow-up. symbolic_glyph_texts tried each offset across every cmap subtable, while the renderer reads the records in order, each with its offsets, and only Windows (3,0)/(3,1) and Macintosh or Unicode-platform encoding-0 records. A font with a (3,0) and a (3,1) cmap could give the editor another glyph, and so another name, than the page shows. Co-Authored-By: Claude Opus 5.5 --- crates/fonts/src/pdf.rs | 15 +++++++- crates/fonts/src/pdf/tests.rs | 69 ++++++++++++++++++++++++----------- 2 files changed, 61 insertions(+), 23 deletions(-) diff --git a/crates/fonts/src/pdf.rs b/crates/fonts/src/pdf.rs index 8e256f7f09..431d6832ec 100644 --- a/crates/fonts/src/pdf.rs +++ b/crates/fonts/src/pdf.rs @@ -1234,7 +1234,7 @@ pub fn glyph_unicode(name: &str) -> Option { /// 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. fn symbolic_glyph_texts(doc: &Document, font: &Dict) -> Vec<(u32, String)> { - use skrifa::{GlyphId, raw::TableProvider as _}; + use skrifa::{GlyphId, raw::TableProvider as _, raw::tables::cmap::PlatformId}; let Some(descriptor) = dict(doc, font.get(b"FontDescriptor")) else { return Vec::new() }; // Flag bit 6, Nonsymbolic (ISO 32000-2 Table 121). let flags = descriptor.get(b"Flags").and_then(|v| doc.resolve(v).as_f64()).unwrap_or(0.0) as i64; @@ -1249,7 +1249,18 @@ fn symbolic_glyph_texts(doc: &Document, font: &Dict) -> Vec<(u32, String)> { let (Ok(cmap), Ok(post)) = (face.cmap(), face.post()) else { return Vec::new() }; (0..=255u32) .filter_map(|code| { - let glyph = [0, 0xF000, 0xF100, 0xF200].into_iter().find_map(|o| cmap.map_codepoint(code + o).filter(|g| *g != GlyphId::NOTDEF))?; + // The records in order, as the renderer's `map_code` reads a symbolic font: a Windows + // symbol or Unicode cmap with the code and the code at 0xF000, 0xF100 and 0xF200, a + // Macintosh or Unicode-platform one with the code alone; the first glyph found wins. + let glyph = cmap.encoding_records().iter().find_map(|record| { + let offsets: &[u32] = match (record.platform_id(), record.encoding_id()) { + (PlatformId::Windows, 0 | 1) => &[0, 0xF000, 0xF100, 0xF200], + (PlatformId::Macintosh | PlatformId::Unicode, 0) => &[0], + _ => return None, + }; + let subtable = record.subtable(cmap.offset_data()).ok()?; + offsets.iter().find_map(|o| subtable.map_codepoint(code + o).filter(|g| *g != GlyphId::NOTDEF)) + })?; let glyph = skrifa::raw::types::GlyphId16::new(u16::try_from(glyph.to_u32()).ok()?); Some((code, glyph_text(post.glyph_name(glyph)?)?)) }) diff --git a/crates/fonts/src/pdf/tests.rs b/crates/fonts/src/pdf/tests.rs index 3de1980482..75b5649843 100644 --- a/crates/fonts/src/pdf/tests.rs +++ b/crates/fonts/src/pdf/tests.rs @@ -195,6 +195,8 @@ fn mac_subset_cmap_uses_pdf_unicode_without_changing_source_program() { /// `post` table names the glyphs its codes select, and typing encodes back to those codes. /// A (3,0) symbol cmap at 0xF000 + code is read too: code 0x20, `.notdef` in the Mac table, /// shows `k` there, code 0xC0 `é`, and code 0xC1 the variant `a.1`, whose suffix is dropped. +/// Records are read in order, each with its offsets, as the renderer reads them: 0xC2 is the +/// symbol cmap's `x`, not a later (3,1) cmap's `z`. #[test] fn a_symbolic_truetype_font_without_an_encoding_reads_its_glyph_names() { use skrifa::{MetadataProvider as _, raw::TableProvider as _}; @@ -214,26 +216,51 @@ fn a_symbolic_truetype_font_without_an_encoding_reads_its_glyph_names() { } let mac_len = 10 + 2 * glyphs.len() as u16; let gid = |c: char| u16::try_from(charmap.map(c).unwrap().to_u32()).unwrap(); - // Two subtables: the Mac one at 20, the symbol one after it. - let mut cmap = vec![0, 0, 0, 2, 0, 1, 0, 0, 0, 0, 0, 20, 0, 3, 0, 0]; - cmap.extend_from_slice(&(20 + u32::from(mac_len)).to_be_bytes()); - cmap.extend_from_slice(&[0, 6]); - cmap.extend_from_slice(&mac_len.to_be_bytes()); - cmap.extend_from_slice(&[0, 0]); - cmap.extend_from_slice(&first.to_be_bytes()); - cmap.extend_from_slice(&(glyphs.len() as u16).to_be_bytes()); - glyphs.iter().for_each(|g| cmap.extend_from_slice(&g.to_be_bytes())); - // Format 4: 0xF020 → k, 0xF0C0 → é, 0xF0C1 → a.1, then the closing 0xFFFF segment. - let segments = [(0xF020u16, gid('k')), (0xF0C0, gid('é')), (0xF0C1, variant), (0xFFFF, 0)]; - let words: Vec = [4, 16 + 8 * 4, 0, 8, 8, 2, 0] - .into_iter() - .chain(segments.iter().map(|s| s.0)) - .chain([0]) - .chain(segments.iter().map(|s| s.0)) - .chain(segments.iter().map(|&(code, glyph)| if code == 0xFFFF { 1 } else { glyph.wrapping_sub(code) })) - .chain([0, 0, 0, 0]) - .collect(); - words.iter().for_each(|w| cmap.extend_from_slice(&w.to_be_bytes())); + let mut mac = vec![0, 6]; + mac.extend_from_slice(&mac_len.to_be_bytes()); + mac.extend_from_slice(&[0, 0]); + mac.extend_from_slice(&first.to_be_bytes()); + mac.extend_from_slice(&(glyphs.len() as u16).to_be_bytes()); + glyphs.iter().for_each(|g| mac.extend_from_slice(&g.to_be_bytes())); + // A format 4 subtable: one segment per code, then the closing 0xFFFF segment. + let format4 = |map: &[(u16, u16)]| { + let segments: Vec<(u16, u16)> = map.iter().copied().chain([(0xFFFF, 0)]).collect(); + let n = segments.len() as u16; + let words: Vec = [4, 16 + 8 * n, 0, 2 * n, 2, 0, 2 * n - 2] + .into_iter() + .chain(segments.iter().map(|s| s.0)) + .chain([0]) + .chain(segments.iter().map(|s| s.0)) + .chain(segments.iter().map(|&(code, glyph)| if code == 0xFFFF { 1 } else { glyph.wrapping_sub(code) })) + .chain(segments.iter().map(|_| 0)) + .collect(); + words.iter().flat_map(|w| w.to_be_bytes()).collect::>() + }; + // 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'))]); + // 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'))]); + // A Unicode-platform (0,3) record, first, which the renderer doesn't read: 0xC3 → q there, + // so code 0xC3 is the symbol cmap's `w`. + let platform0 = format4(&[(0xC3, gid('q'))]); + // Four records, in platform order as the cmap table requires. + let at = |n: usize| (36 + n) as u32; + let mut cmap = vec![0, 0, 0, 4]; + for (platform, encoding, offset) in [ + (0u16, 3u16, at(0)), + (1, 0, at(platform0.len())), + (3, 0, at(platform0.len() + mac.len())), + (3, 1, at(platform0.len() + mac.len() + symbol.len())), + ] { + cmap.extend_from_slice(&platform.to_be_bytes()); + cmap.extend_from_slice(&encoding.to_be_bytes()); + cmap.extend_from_slice(&offset.to_be_bytes()); + } + cmap.extend(platform0); + cmap.extend(mac); + cmap.extend(symbol); + cmap.extend(unicode); let mut subset = original.to_vec(); let count = u16::from_be_bytes(subset[4..6].try_into().unwrap()) as usize; let entry = (0..count).map(|i| 12 + i * 16).find(|i| &subset[*i..*i + 4] == b"cmap").unwrap(); @@ -257,7 +284,7 @@ fn a_symbolic_truetype_font_without_an_encoding_reads_its_glyph_names() { }; let symbolic = metrics(4, None); assert_eq!(symbolic.decode(&codes), TEXT); - assert_eq!(symbolic.decode(&[0x20, 0xC0, 0xC1]), "kéa"); + assert_eq!(symbolic.decode(&[0x20, 0xC0, 0xC1, 0xC2, 0xC3]), "kéaxw"); 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");