1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
//! Encoding normalization for custom font encodings.
//!
//! This module provides lightweight encoding normalization to convert raw character
//! codes through font-specific encodings (standard or custom) before word boundary
//! detection. This ensures that word boundary detection works on actual Unicode
//! characters rather than raw byte codes.
//!
//! Per PDF Spec ISO 32000-1:2008, Section 9.6.6:
//! - Fonts can have standard encodings (WinAnsiEncoding, MacRomanEncoding, etc.)
//! - Fonts can have custom encodings with /Differences arrays
//! - /Differences override the base encoding for specific character codes
//!
//! # Example
//!
//! Given a custom encoding with /Differences [0x64 /rho], the byte 0x64 should
//! normalize to the Greek letter 'ρ' (U+03C1), not the ASCII 'd' (U+0064).
use super::Encoding;
/// Normalize character codes through font encoding.
///
/// This struct provides a lightweight wrapper around a font's encoding
/// to normalize raw character codes into Unicode code points.
#[derive(Debug, Clone)]
pub struct EncodingNormalizer {
/// The font's encoding (Standard, Custom, or Identity)
encoding: Encoding,
/// Font name for debugging/logging
font_name: String,
}
impl EncodingNormalizer {
/// Create an encoding normalizer from a font's encoding.
///
/// # Arguments
///
/// * `encoding` - The font's encoding (from FontInfo)
/// * `font_name` - The font's base name (for debugging)
///
/// # Examples
///
/// ```ignore
/// use xberg_native_pdf::fonts::{Encoding, encoding_normalizer::EncodingNormalizer};
///
/// let encoding = Encoding::Standard("WinAnsiEncoding".to_string());
/// let normalizer = EncodingNormalizer::new(encoding, "Helvetica".to_string());
/// ```
pub fn new(encoding: Encoding, font_name: String) -> Self {
Self { encoding, font_name }
}
/// Normalize a raw character code through the font's encoding.
///
/// Per PDF Spec ISO 32000-1:2008, Section 9.10:
/// Character-to-Unicode mapping priority:
/// 1. ToUnicode CMap (handled elsewhere, before this function is called)
/// 2. Font encoding (this function)
/// 3. Adobe Glyph List (fallback, handled elsewhere)
///
/// # Arguments
///
/// * `char_code` - The raw byte value from the PDF content stream
///
/// # Returns
///
/// The normalized Unicode code point, or the raw code if no mapping exists
pub fn normalize(&self, char_code: u8) -> u32 {
match &self.encoding {
Encoding::Custom(mappings) => {
if let Some(&mapped_char) = mappings.get(&char_code) {
mapped_char as u32
} else {
char_code as u32
}
}
Encoding::Standard(encoding_name) => self.normalize_standard_encoding(char_code, encoding_name),
Encoding::Identity => char_code as u32,
}
}
/// Apply standard encoding normalization rules.
///
/// Standard encodings like WinAnsiEncoding and MacRomanEncoding have
/// well-defined mappings from character codes to Unicode.
///
/// For now, this is a pass-through, as most standard encoding handling
/// happens in the ToUnicode CMap or character_mapper. This can be
/// enhanced in the future to provide explicit standard encoding tables.
fn normalize_standard_encoding(&self, char_code: u8, _encoding_name: &str) -> u32 {
// Per PDF 32000-1:2008 §9.10.2, ToUnicode CMap takes priority over font
// encoding for text extraction. Standard encoding tables serve as fallback. ~keep
char_code as u32
}
/// Get the encoding type as a string for debugging.
///
/// # Returns
///
/// - "Custom" for custom encodings with /Differences
/// - `"Standard(<name>)"` for standard encodings
/// - "Identity" for identity encodings
pub fn encoding_type(&self) -> String {
match &self.encoding {
Encoding::Custom(_) => "Custom".to_string(),
Encoding::Standard(name) => format!("Standard({})", name),
Encoding::Identity => "Identity".to_string(),
}
}
/// Get the font name associated with this normalizer.
pub fn font_name(&self) -> &str {
&self.font_name
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn test_custom_encoding_normalization() {
let mut mappings = HashMap::new();
mappings.insert(0x64, 'ρ');
let encoding = Encoding::Custom(mappings);
let normalizer = EncodingNormalizer::new(encoding, "CustomFont".to_string());
let normalized = normalizer.normalize(0x64);
assert_eq!(normalized, 0x03C1, "0x64 should normalize to Greek rho");
}
#[test]
fn test_custom_encoding_no_mapping() {
let mut mappings = HashMap::new();
mappings.insert(0x64, 'ρ');
let encoding = Encoding::Custom(mappings);
let normalizer = EncodingNormalizer::new(encoding, "CustomFont".to_string());
let normalized = normalizer.normalize(0x65);
assert_eq!(normalized, 0x65, "Unmapped code should return raw value");
}
#[test]
fn test_standard_encoding_passthrough() {
let encoding = Encoding::Standard("WinAnsiEncoding".to_string());
let normalizer = EncodingNormalizer::new(encoding, "Helvetica".to_string());
let normalized = normalizer.normalize(0x41);
assert_eq!(normalized, 0x41, "Standard encoding passes through");
}
#[test]
fn test_identity_encoding() {
let encoding = Encoding::Identity;
let normalizer = EncodingNormalizer::new(encoding, "CIDFont".to_string());
let normalized = normalizer.normalize(0x80);
assert_eq!(normalized, 0x80, "Identity encoding passes through");
}
#[test]
fn test_encoding_type_custom() {
let encoding = Encoding::Custom(HashMap::new());
let normalizer = EncodingNormalizer::new(encoding, "Test".to_string());
assert_eq!(normalizer.encoding_type(), "Custom");
}
#[test]
fn test_encoding_type_standard() {
let encoding = Encoding::Standard("WinAnsiEncoding".to_string());
let normalizer = EncodingNormalizer::new(encoding, "Test".to_string());
assert_eq!(normalizer.encoding_type(), "Standard(WinAnsiEncoding)");
}
#[test]
fn test_encoding_type_identity() {
let encoding = Encoding::Identity;
let normalizer = EncodingNormalizer::new(encoding, "Test".to_string());
assert_eq!(normalizer.encoding_type(), "Identity");
}
}