use std::collections::HashMap;
pub fn parse_type1_encoding(font_data: &[u8]) -> Option<HashMap<u8, char>> {
let search_limit = font_data.len().min(65536);
let search_data = &font_data[..search_limit];
let encoding_pos = find_bytes(search_data, b"/Encoding")?;
let after_encoding = &search_data[encoding_pos + 9..];
let trimmed = skip_whitespace(after_encoding);
if trimmed.starts_with(b"StandardEncoding") {
tracing::debug!("Type 1 font uses StandardEncoding (predefined)");
return None;
}
let mut encoding_map = HashMap::new();
let mut pos = encoding_pos;
while pos < search_limit {
let remaining = &search_data[pos..];
let dup_offset = match find_bytes(remaining, b"dup") {
Some(off) => off,
None => break,
};
pos += dup_offset + 3;
let before_dup = &search_data[pos - 3..pos.min(search_limit)];
if before_dup == b"def" {
break;
}
let remaining = &search_data[pos..search_limit.min(search_data.len())];
if let Some((code, glyph_name, consumed)) = parse_dup_entry(remaining) {
if let Some(unicode_char) = super::font_dict::glyph_name_to_unicode(&glyph_name) {
encoding_map.insert(code, unicode_char);
}
pos += consumed;
}
let remaining = &search_data[pos..search_limit.min(search_data.len())];
let trimmed = skip_whitespace(remaining);
if trimmed.starts_with(b"readonly") || trimmed.starts_with(b"def") {
break;
}
}
if encoding_map.is_empty() {
None
} else {
tracing::debug!(
"Type 1 built-in encoding parsed: {} character mappings",
encoding_map.len()
);
Some(encoding_map)
}
}
fn parse_dup_entry(data: &[u8]) -> Option<(u8, String, usize)> {
let mut pos = 0;
while pos < data.len() && is_whitespace(data[pos]) {
pos += 1;
}
let code_start = pos;
while pos < data.len() && data[pos].is_ascii_digit() {
pos += 1;
}
if pos == code_start {
return None;
}
let code_str = std::str::from_utf8(&data[code_start..pos]).ok()?;
let code: u16 = code_str.parse().ok()?;
if code > 255 {
return None;
}
while pos < data.len() && is_whitespace(data[pos]) {
pos += 1;
}
if pos >= data.len() || data[pos] != b'/' {
return None;
}
pos += 1;
let name_start = pos;
while pos < data.len() && is_glyph_name_char(data[pos]) {
pos += 1;
}
if pos == name_start {
return None;
}
let glyph_name = std::str::from_utf8(&data[name_start..pos]).ok()?;
while pos < data.len() && is_whitespace(data[pos]) {
pos += 1;
}
if !data[pos..].starts_with(b"put") {
return None;
}
pos += 3;
Some((code as u8, glyph_name.to_string(), pos))
}
fn find_bytes(data: &[u8], needle: &[u8]) -> Option<usize> {
data.windows(needle.len()).position(|w| w == needle)
}
fn skip_whitespace(data: &[u8]) -> &[u8] {
let mut pos = 0;
while pos < data.len() && is_whitespace(data[pos]) {
pos += 1;
}
&data[pos..]
}
fn is_whitespace(b: u8) -> bool {
matches!(b, b' ' | b'\t' | b'\n' | b'\r')
}
fn is_glyph_name_char(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'.' || b == b'_'
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_type1_encoding_basic() {
let font_data = b"/Encoding 256 array\n\
0 1 255 {1 index exch /.notdef put} for\n\
dup 65 /A put\n\
dup 66 /B put\n\
dup 97 /a put\n\
readonly def\n";
let map = parse_type1_encoding(font_data).unwrap();
assert_eq!(map.get(&65), Some(&'A'));
assert_eq!(map.get(&66), Some(&'B'));
assert_eq!(map.get(&97), Some(&'a'));
}
#[test]
fn test_parse_type1_encoding_ligatures() {
let font_data = b"/Encoding 256 array\n\
0 1 255 {1 index exch /.notdef put} for\n\
dup 11 /ff put\n\
dup 12 /fi put\n\
dup 14 /ffi put\n\
readonly def\n";
let map = parse_type1_encoding(font_data).unwrap();
assert_eq!(map.get(&11), Some(&'\u{FB00}'));
assert_eq!(map.get(&12), Some(&'\u{FB01}'));
assert_eq!(map.get(&14), Some(&'\u{FB03}'));
}
#[test]
fn test_parse_type1_encoding_standard() {
let font_data = b"/Encoding StandardEncoding def\n";
assert!(parse_type1_encoding(font_data).is_none());
}
#[test]
fn test_parse_type1_encoding_empty() {
let font_data = b"no encoding here";
assert!(parse_type1_encoding(font_data).is_none());
}
#[test]
fn test_parse_type1_cmr_style_encoding() {
let font_data = b"%!PS-AdobeFont-1.0: CMR9 003.002\n\
/Encoding 256 array\n\
0 1 255 {1 index exch /.notdef put} for\n\
dup 11 /ff put\n\
dup 12 /fi put\n\
dup 13 /fl put\n\
dup 14 /ffi put\n\
dup 15 /ffl put\n\
dup 65 /A put\n\
dup 97 /a put\n\
dup 48 /zero put\n\
dup 58 /colon put\n\
dup 123 /endash put\n\
readonly def\n\
currentdict end\n\
currentfile eexec\n";
let map = parse_type1_encoding(font_data).unwrap();
assert_eq!(map.len(), 10);
assert_eq!(map.get(&11), Some(&'\u{FB00}'));
assert_eq!(map.get(&12), Some(&'\u{FB01}'));
assert_eq!(map.get(&13), Some(&'\u{FB02}'));
assert_eq!(map.get(&14), Some(&'\u{FB03}'));
assert_eq!(map.get(&15), Some(&'\u{FB04}'));
assert_eq!(map.get(&65), Some(&'A'));
assert_eq!(map.get(&97), Some(&'a'));
assert_eq!(map.get(&48), Some(&'0'));
assert_eq!(map.get(&58), Some(&':'));
assert_eq!(map.get(&123), Some(&'\u{2013}'));
}
}