From 7660cfc84cdc1d56cede73023d9842c4ce31e29e Mon Sep 17 00:00:00 2001 From: jedarden Date: Mon, 6 Jul 2026 13:25:15 -0400 Subject: [PATCH] feat(bf-4zgpr): add unmapped_glyph_names config field to CMAP generator Add unmapped_glyph_names configuration field to DifferencesOverlay struct, which serves as the CMAP generator for Type1 font encoding differences. Changes: - Added unmapped_glyph_names: HashSet field to DifferencesOverlay - Added with_unmapped_glyph_names() constructor for custom configuration - Added is_unmapped_glyph_name() instance method for checking membership - Added unmapped_glyph_names() getter and set_unmapped_glyph_names() setter - Updated parse() to use instance method instead of global function - Defaults to global UNMAPPED_GLYPH_NAMES set when not explicitly configured Acceptance criteria: - CMAP generator struct has unmapped_glyph_names field - Field is HashSet for O(1) lookup efficiency - Field can be set via constructor or setter method - Code compiles and all tests pass (24 encoding tests) Closes bf-4zgpr. See notes/bf-4zgpr.md for verification details. --- crates/pdftract-core/src/font/encoding.rs | 64 ++++++++++++++++++++++- notes/bf-4zgpr.md | 54 +++++++++++++++++++ 2 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 notes/bf-4zgpr.md diff --git a/crates/pdftract-core/src/font/encoding.rs b/crates/pdftract-core/src/font/encoding.rs index c80e566..e7a676f 100644 --- a/crates/pdftract-core/src/font/encoding.rs +++ b/crates/pdftract-core/src/font/encoding.rs @@ -130,16 +130,43 @@ pub struct DifferencesOverlay { /// Sparse list of (code, glyph_name) overrides. /// Sorted by code for binary search, though linear search is fine for <32 entries. entries: Vec<(u8, Arc)>, + /// Glyph names that should be skipped during CMAP entry creation. + /// These glyphs have no valid Unicode mapping and should not appear in text extraction. + /// Defaults to the global UNMAPPED_GLYPH_NAMES set if not explicitly configured. + unmapped_glyph_names: std::collections::HashSet, } impl DifferencesOverlay { - /// Create an empty overlay. + /// Create an empty overlay with default unmapped glyph names. pub fn new() -> Self { Self { entries: Vec::new(), + unmapped_glyph_names: Self::default_unmapped_glyph_names(), } } + /// Create an empty overlay with custom unmapped glyph names. + /// + /// # Arguments + /// + /// * `unmapped_glyph_names` - Set of glyph names to skip during CMAP entry creation + pub fn with_unmapped_glyph_names(unmapped_glyph_names: std::collections::HashSet) -> Self { + Self { + entries: Vec::new(), + unmapped_glyph_names, + } + } + + /// Get the default set of unmapped glyph names. + /// + /// Returns the global UNMAPPED_GLYPH_NAMES set as a HashSet. + fn default_unmapped_glyph_names() -> std::collections::HashSet { + crate::font::unmapped::UNMAPPED_GLYPH_NAMES + .iter() + .map(|s| s.to_string()) + .collect() + } + /// Parse a /Differences array into an overlay. /// /// The array alternates between integers (starting codes) and names (glyph names). @@ -197,7 +224,7 @@ impl DifferencesOverlay { // Skip unmapped glyph names (e.g., .notdef) to prevent them from // appearing in text extraction output. These glyphs have no valid // Unicode mapping and should emit GLYPH_UNMAPPED diagnostics instead. - if !crate::font::is_unmapped_glyph_name(&name) { + if !overlay.is_unmapped_glyph_name(&name) { overlay.entries.push((cursor as u8, Arc::clone(name))); } } @@ -234,6 +261,39 @@ impl DifferencesOverlay { pub fn len(&self) -> usize { self.entries.len() } + + /// Check if a glyph name is in the unmapped glyph names set. + /// + /// # Arguments + /// + /// * `name` - The glyph name to check (with or without leading `/`) + /// + /// # Returns + /// + /// `true` if the glyph name is in the unmapped set, `false` otherwise. + fn is_unmapped_glyph_name(&self, name: &str) -> bool { + // Strip leading slash if present + let clean_name = if name.starts_with('/') { + &name[1..] + } else { + name + }; + self.unmapped_glyph_names.contains(clean_name) + } + + /// Get a reference to the unmapped glyph names set. + pub fn unmapped_glyph_names(&self) -> &std::collections::HashSet { + &self.unmapped_glyph_names + } + + /// Set the unmapped glyph names set. + /// + /// # Arguments + /// + /// * `unmapped_glyph_names` - New set of glyph names to skip during CMAP entry creation + pub fn set_unmapped_glyph_names(&mut self, unmapped_glyph_names: std::collections::HashSet) { + self.unmapped_glyph_names = unmapped_glyph_names; + } } impl Default for DifferencesOverlay { diff --git a/notes/bf-4zgpr.md b/notes/bf-4zgpr.md new file mode 100644 index 0000000..6c333b7 --- /dev/null +++ b/notes/bf-4zgpr.md @@ -0,0 +1,54 @@ +# Verification Note: bf-4zgpr + +## Task +Add unmapped_glyph_names config field to CMAP generator + +## Summary +Added `unmapped_glyph_names` configuration field to the `DifferencesOverlay` struct, which serves as the CMAP generator for Type1 font encoding differences. + +## Changes Made + +### File: `/home/coding/pdftract/crates/pdftract-core/src/font/encoding.rs` + +1. **Added `unmapped_glyph_names` field to `DifferencesOverlay` struct**: + - Type: `std::collections::HashSet` + - Stores glyph names that should be skipped during CMAP entry creation + - Defaults to global `UNMAPPED_GLYPH_NAMES` set if not explicitly configured + +2. **Added `with_unmapped_glyph_names()` constructor**: + - Allows creating an overlay with a custom unmapped glyph names set + - Enables the field to be populated from configuration + +3. **Added `is_unmapped_glyph_name()` helper method**: + - Instance method that checks if a glyph name is in the unmapped set + - Handles leading `/` stripping for consistency + +4. **Added getter and setter methods**: + - `unmapped_glyph_names()` - Get reference to the set + - `set_unmapped_glyph_names()` - Set a new set + +5. **Updated `parse()` method**: + - Changed from global `crate::font::is_unmapped_glyph_name()` to instance method `overlay.is_unmapped_glyph_name()` + - Now uses the configurable field instead of hardcoded global set + +## Acceptance Criteria + +- ✅ CMAP generator struct has `unmapped_glyph_names` field +- ✅ Field is of appropriate type (`HashSet` for efficient lookup) +- ✅ Field can be set from config/passed in constructor via `with_unmapped_glyph_names()` +- ✅ Code compiles without errors (verified with `cargo build --release`) +- ✅ All tests pass (24 encoding tests, including 2 specific unmapped glyph name tests) + +## Test Results +``` +running 24 tests +test font::encoding::tests::test_differences_overlay_skips_notdef ... ok +test font::encoding::tests::test_differences_overlay_skips_notdef_with_slash ... ok +test font::encoding::tests::test_font_encoding_lookup_order ... ok +... +test result: ok. 24 passed; 0 failed; 0 ignored; 0 measured +``` + +## Related Files +- `/home/coding/pdftract/crates/pdftract-core/src/font/encoding.rs` - Main implementation +- `/home/coding/pdftract/crates/pdftract-core/src/font/unmapped.rs` - Global UNMAPPED_GLYPH_NAMES set (used as default)