From 47be8a628f04a60a9d176ad0755aaf30e21d753a Mon Sep 17 00:00:00 2001 From: Velli20 Date: Sun, 11 Oct 2026 14:43:43 +0300 Subject: [PATCH] Look up symbolic TrueType codes in code-keyed cmap subtables A simple-font code without a /Differences entry was resolved only by Unicode, and the Unicode charmap ignores the (3,0) Symbol and (1,0) Macintosh Roman subtables. A symbolic TrueType subset whose only cmap is one of those resolved no glyphs, so its text was drawn with a fallback face instead of the embedded program. Add FontFace::glyph_for_code, which TrueType faces answer from the (3,0) subtable, directly and at 0xF000 + code, and then from the (1,0) subtable. Without an explicit selector, symbolic faces try the code before Unicode and other faces after it. Co-Authored-By: Claude Opus 5.5 --- crates/pdf-font/src/font.rs | 8 +++++ crates/pdf-font/src/true_type.rs | 33 +++++++++++++++++- crates/pdf-text-engine/src/pdf_text_layout.rs | 34 ++++++++++++------- 3 files changed, 62 insertions(+), 13 deletions(-) diff --git a/crates/pdf-font/src/font.rs b/crates/pdf-font/src/font.rs index 1810323e..6ea40f52 100644 --- a/crates/pdf-font/src/font.rs +++ b/crates/pdf-font/src/font.rs @@ -213,6 +213,14 @@ pub trait FontFace: Send + Sync { /// Resolves a PostScript glyph name when supported by the face. fn glyph_for_name(&self, name: &GlyphName) -> Option; + /// Resolves a one-byte simple-font character code through the face's code-keyed cmap + /// subtables, as symbolic TrueType fonts require. + /// + /// Faces without such subtables return `None`. + fn glyph_for_code(&self, _code: u8) -> Option { + None + } + /// Returns the glyph's horizontal advance in font design units when available. /// /// PDF layout uses this only when an unrelated fallback face supplies the glyph. Native PDF diff --git a/crates/pdf-font/src/true_type.rs b/crates/pdf-font/src/true_type.rs index ab319da6..99fc3863 100644 --- a/crates/pdf-font/src/true_type.rs +++ b/crates/pdf-font/src/true_type.rs @@ -8,7 +8,11 @@ use std::sync::Arc; use bytes::Bytes; -use read_fonts::{TableProvider, tables::post::DEFAULT_GLYPH_NAMES, types::Version16Dot16}; +use read_fonts::{ + TableProvider, + tables::{cmap::PlatformId, post::DEFAULT_GLYPH_NAMES}, + types::Version16Dot16, +}; use self_cell::self_cell; use skrifa::{ FontRef, GlyphNames, MetadataProvider, @@ -52,6 +56,22 @@ impl<'a> TrueTypeProgramRef<'a> { outlines, }) } + + /// Looks up a character code in the cmap subtable with the given platform and encoding. + /// + /// Glyph zero is `.notdef`, so it counts as a miss. + fn code_glyph(&self, platform: PlatformId, encoding: u16, code: u32) -> Option { + let cmap = self.font.cmap().ok()?; + let record = cmap + .encoding_records() + .iter() + .find(|record| record.platform_id() == platform && record.encoding_id() == encoding)?; + let glyph = record + .subtable(cmap.offset_data()) + .ok()? + .map_codepoint(code)?; + (glyph.to_u32() != 0).then(|| GlyphId(glyph.to_u32())) + } } self_cell!( @@ -230,6 +250,17 @@ impl FontFace for TrueTypeFontFace { self.resolve_glyph_name(name) } + /// Looks the code up in a (3,0) Microsoft Symbol subtable, directly and in the `0xF000` + /// private-use range, then in a (1,0) Macintosh Roman subtable. + fn glyph_for_code(&self, code: u8) -> Option { + let program = self.program.borrow_dependent(); + let code = u32::from(code); + program + .code_glyph(PlatformId::Windows, 0, code) + .or_else(|| program.code_glyph(PlatformId::Windows, 0, 0xF000 | code)) + .or_else(|| program.code_glyph(PlatformId::Macintosh, 0, code)) + } + fn horizontal_advance(&self, glyph: GlyphId) -> Result, FontError> { Ok(self .program diff --git a/crates/pdf-text-engine/src/pdf_text_layout.rs b/crates/pdf-text-engine/src/pdf_text_layout.rs index 5195243d..7b8ed30a 100644 --- a/crates/pdf-text-engine/src/pdf_text_layout.rs +++ b/crates/pdf-text-engine/src/pdf_text_layout.rs @@ -275,12 +275,7 @@ fn resolve_glyph( cache: &mut FallbackCache, decoded: &DecodedGlyph<'_>, ) -> Result { - if let Some(glyph_id) = select_glyph( - font.primary().as_ref(), - &decoded.selector, - decoded.unicode.as_slice(), - font.uses_substitute(), - ) { + if let Some(glyph_id) = select_glyph(font.primary().as_ref(), decoded, font.uses_substitute()) { return Ok(ResolvedGlyph { face: ResolvedFace::Primary, glyph_id, @@ -366,26 +361,41 @@ fn fallback_fit( /// /// Whole-font substitutes prefer Unicode because embedded glyph IDs, names, and CIDs generally do /// not address the substitute's glyph space. Native faces try their explicit PDF selector first, -/// then use the first Unicode scalar as a recovery path. +/// then use the first Unicode scalar as a recovery path. Without an explicit selector, a one-byte +/// code is looked up in the face's code-keyed cmap subtables: before Unicode for a symbolic font, +/// whose codes address those subtables directly, and after it otherwise. fn select_glyph( face: &dyn FontFace, - selector: &GlyphSelector<'_>, - unicode: &[char], + decoded: &DecodedGlyph<'_>, prefer_unicode: bool, ) -> Option { let unicode_glyph = || { - unicode + decoded + .unicode + .as_slice() .first() .and_then(|character| face.glyph_for_char(*character)) }; if prefer_unicode && let Some(glyph) = unicode_glyph() { return Some(glyph); } - let selected = match selector { + let selected = match &decoded.selector { GlyphSelector::GlyphId(glyph) => Some(*glyph), GlyphSelector::GlyphName(name) => face.glyph_for_name(name), GlyphSelector::Cid(Cid(cid)) => Some(GlyphId(*cid)), - GlyphSelector::Unspecified => None, + GlyphSelector::Unspecified => { + let code_glyph = || { + (decoded.source_code.byte_len() == 1) + .then(|| u8::try_from(decoded.source_code.value()).ok()) + .flatten() + .and_then(|code| face.glyph_for_code(code)) + }; + return if face.metadata().symbolic { + code_glyph().or_else(unicode_glyph) + } else { + unicode_glyph().or_else(code_glyph) + }; + } }; selected.or_else(unicode_glyph) }