use std::borrow::Cow;
use std::collections::HashMap;
use crate::core::objects::decode_utf16be;
use crate::error::{PdfError, PdfResult};
use crate::parser::lexer::hex_digit;
#[derive(Debug, Clone)]
pub struct ToUnicodeCMap {
single_mappings: HashMap<Vec<u8>, String>,
range_mappings: Vec<CMapRange>,
}
#[derive(Debug, Clone)]
struct CMapRange {
start: Vec<u8>,
end: Vec<u8>,
target: CMapTarget,
}
#[derive(Debug, Clone)]
enum CMapTarget {
Base(Vec<u8>),
Array(Vec<String>),
}
impl ToUnicodeCMap {
pub fn parse(data: &[u8]) -> PdfResult<Self> {
let tokens = tokenize_cmap(data);
let mut cmap = ToUnicodeCMap {
single_mappings: HashMap::new(),
range_mappings: Vec::new(),
};
let mut i = 0;
while i < tokens.len() {
match tokens[i].as_str() {
"beginbfchar" => {
i += 1;
i = cmap.parse_bfchar(&tokens, i)?;
}
"beginbfrange" => {
i += 1;
i = cmap.parse_bfrange(&tokens, i)?;
}
_ => {
i += 1;
}
}
}
Ok(cmap)
}
pub fn map_code(&self, code: &[u8]) -> Option<Cow<'_, str>> {
if let Some(s) = self.single_mappings.get(code) {
return Some(Cow::Borrowed(s));
}
for range in &self.range_mappings {
if code.len() != range.start.len() {
continue;
}
if code >= range.start.as_slice() && code <= range.end.as_slice() {
let offset = code_offset(code, &range.start);
match &range.target {
CMapTarget::Base(base) => {
let mut buf = [0u8; 4];
offset_hex_bytes_into(base, offset, &mut buf);
return Some(Cow::Owned(hex_bytes_to_unicode(&buf[..base.len()])));
}
CMapTarget::Array(arr) => {
if (offset as usize) < arr.len() {
return Some(Cow::Borrowed(&arr[offset as usize]));
}
}
}
}
}
None
}
fn parse_bfchar(&mut self, tokens: &[CMapToken], mut i: usize) -> PdfResult<usize> {
while i + 1 < tokens.len() {
if tokens[i].as_str() == "endbfchar" {
return Ok(i + 1);
}
let src = parse_hex_token(&tokens[i])?;
let dst_str = hex_bytes_to_unicode(&parse_hex_token(&tokens[i + 1])?);
self.single_mappings.insert(src, dst_str);
i += 2;
}
Ok(i)
}
fn parse_bfrange(&mut self, tokens: &[CMapToken], mut i: usize) -> PdfResult<usize> {
while i + 2 < tokens.len() {
if tokens[i].as_str() == "endbfrange" {
return Ok(i + 1);
}
let start = parse_hex_token(&tokens[i])?;
let end = parse_hex_token(&tokens[i + 1])?;
let target = if tokens[i + 2].is_array() {
let arr = parse_array_token(&tokens[i + 2])?;
CMapTarget::Array(arr)
} else {
let base = parse_hex_token(&tokens[i + 2])?;
CMapTarget::Base(base)
};
self.range_mappings.push(CMapRange { start, end, target });
i += 3;
}
Ok(i)
}
}
#[derive(Debug, Clone)]
enum CMapToken {
HexString(String),
Keyword(String),
Array(Vec<String>),
}
impl CMapToken {
fn as_str(&self) -> &str {
match self {
CMapToken::HexString(s) | CMapToken::Keyword(s) => s,
CMapToken::Array(_) => "[]",
}
}
fn is_array(&self) -> bool {
matches!(self, CMapToken::Array(_))
}
}
fn tokenize_cmap(data: &[u8]) -> Vec<CMapToken> {
let mut tokens = Vec::new();
let mut i = 0;
while i < data.len() {
if data[i].is_ascii_whitespace() {
i += 1;
continue;
}
if data[i] == b'%' {
while i < data.len() && data[i] != b'\n' && data[i] != b'\r' {
i += 1;
}
continue;
}
if data[i] == b'<' {
i += 1;
let mut hex = String::new();
while i < data.len() && data[i] != b'>' {
if !data[i].is_ascii_whitespace() {
hex.push(data[i] as char);
}
i += 1;
}
if i < data.len() {
i += 1; }
tokens.push(CMapToken::HexString(hex));
continue;
}
if data[i] == b'[' {
i += 1;
let mut arr = Vec::new();
while i < data.len() && data[i] != b']' {
if data[i] == b'<' {
i += 1;
let mut hex = String::new();
while i < data.len() && data[i] != b'>' {
if !data[i].is_ascii_whitespace() {
hex.push(data[i] as char);
}
i += 1;
}
if i < data.len() {
i += 1; }
arr.push(hex);
} else {
i += 1;
}
}
if i < data.len() {
i += 1; }
tokens.push(CMapToken::Array(arr));
continue;
}
let start = i;
while i < data.len()
&& !data[i].is_ascii_whitespace()
&& data[i] != b'<'
&& data[i] != b'['
&& data[i] != b']'
{
i += 1;
}
if i > start {
let word = String::from_utf8_lossy(&data[start..i]).to_string();
tokens.push(CMapToken::Keyword(word));
}
}
tokens
}
fn parse_hex_token(token: &CMapToken) -> PdfResult<Vec<u8>> {
let hex = match token {
CMapToken::HexString(h) => h,
_ => {
return Err(PdfError::EncodingError(
"Expected hex string in CMap".to_string(),
))
}
};
hex_string_to_bytes(hex)
}
fn parse_array_token(token: &CMapToken) -> PdfResult<Vec<String>> {
let arr = match token {
CMapToken::Array(a) => a,
_ => {
return Err(PdfError::EncodingError(
"Expected array in CMap".to_string(),
))
}
};
arr.iter()
.map(|hex| {
let bytes = hex_string_to_bytes(hex)?;
Ok(hex_bytes_to_unicode(&bytes))
})
.collect()
}
fn hex_string_to_bytes(hex: &str) -> PdfResult<Vec<u8>> {
let raw = hex.as_bytes();
let mut bytes = Vec::with_capacity(raw.len() / 2);
let mut i = 0;
while i + 1 < raw.len() {
let high = hex_digit(raw[i]).ok_or_else(|| {
PdfError::EncodingError(format!("Invalid hex char in CMap: {}", raw[i] as char))
})?;
let low = hex_digit(raw[i + 1]).ok_or_else(|| {
PdfError::EncodingError(format!("Invalid hex char in CMap: {}", raw[i + 1] as char))
})?;
bytes.push((high << 4) | low);
i += 2;
}
if i < raw.len() {
let high = hex_digit(raw[i]).ok_or_else(|| {
PdfError::EncodingError(format!("Invalid hex char in CMap: {}", raw[i] as char))
})?;
bytes.push(high << 4);
}
Ok(bytes)
}
fn hex_bytes_to_unicode(bytes: &[u8]) -> String {
const REPLACEMENT: &str = "\u{FFFD}";
if bytes.is_empty() {
return String::new();
}
if !bytes.len().is_multiple_of(2) {
return char::from_u32(bytes[0] as u32)
.map(|c| c.to_string())
.unwrap_or_else(|| {
tracing::debug!("Invalid single-byte codepoint in CMap: 0x{:02X}", bytes[0]);
REPLACEMENT.to_string()
});
}
decode_utf16be(bytes).unwrap_or_else(|| {
tracing::debug!("Failed to decode UTF-16BE in CMap: {:02X?}", bytes);
REPLACEMENT.to_string()
})
}
fn code_offset(code: &[u8], start: &[u8]) -> u32 {
let code_val = bytes_to_u32(code);
let start_val = bytes_to_u32(start);
code_val.wrapping_sub(start_val)
}
fn bytes_to_u32(bytes: &[u8]) -> u32 {
let mut val: u32 = 0;
for &b in bytes {
val = (val << 8) | (b as u32);
}
val
}
fn offset_hex_bytes_into(base: &[u8], offset: u32, buf: &mut [u8; 4]) {
buf[..base.len()].copy_from_slice(base);
let mut carry = offset;
for byte in buf[..base.len()].iter_mut().rev() {
let sum = *byte as u32 + carry;
*byte = (sum & 0xFF) as u8;
carry = sum >> 8;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_bfchar_single() {
let cmap_data = b"1 beginbfchar\n<0041> <0061>\nendbfchar";
let cmap = ToUnicodeCMap::parse(cmap_data).unwrap();
assert_eq!(cmap.map_code(&[0x00, 0x41]).as_deref(), Some("a"));
}
#[test]
fn parse_bfchar_multiple() {
let cmap_data = b"2 beginbfchar\n<0041> <0061>\n<0042> <0062>\nendbfchar";
let cmap = ToUnicodeCMap::parse(cmap_data).unwrap();
assert_eq!(cmap.map_code(&[0x00, 0x41]).as_deref(), Some("a"));
assert_eq!(cmap.map_code(&[0x00, 0x42]).as_deref(), Some("b"));
}
#[test]
fn parse_bfrange_with_base() {
let cmap_data = b"1 beginbfrange\n<0041> <0043> <0061>\nendbfrange";
let cmap = ToUnicodeCMap::parse(cmap_data).unwrap();
assert_eq!(cmap.map_code(&[0x00, 0x41]).as_deref(), Some("a"));
assert_eq!(cmap.map_code(&[0x00, 0x42]).as_deref(), Some("b"));
assert_eq!(cmap.map_code(&[0x00, 0x43]).as_deref(), Some("c"));
}
#[test]
fn parse_bfrange_with_array() {
let cmap_data = b"1 beginbfrange\n<01> <03> [<0041> <0042> <0043>]\nendbfrange";
let cmap = ToUnicodeCMap::parse(cmap_data).unwrap();
assert_eq!(cmap.map_code(&[0x01]).as_deref(), Some("A"));
assert_eq!(cmap.map_code(&[0x02]).as_deref(), Some("B"));
assert_eq!(cmap.map_code(&[0x03]).as_deref(), Some("C"));
}
#[test]
fn map_code_not_found() {
let cmap_data = b"1 beginbfchar\n<0041> <0061>\nendbfchar";
let cmap = ToUnicodeCMap::parse(cmap_data).unwrap();
assert_eq!(cmap.map_code(&[0x00, 0x99]).as_deref(), None);
}
#[test]
fn parse_with_comments() {
let cmap_data = b"% This is a comment\n1 beginbfchar\n<0041> <0061>\nendbfchar";
let cmap = ToUnicodeCMap::parse(cmap_data).unwrap();
assert_eq!(cmap.map_code(&[0x00, 0x41]).as_deref(), Some("a"));
}
#[test]
fn multibyte_unicode_mapping() {
let cmap_data = b"1 beginbfchar\n<0041> <00E9>\nendbfchar";
let cmap = ToUnicodeCMap::parse(cmap_data).unwrap();
assert_eq!(cmap.map_code(&[0x00, 0x41]).as_deref(), Some("\u{00E9}"));
}
#[test]
fn single_byte_codes() {
let cmap_data = b"1 beginbfrange\n<20> <7E> <0020>\nendbfrange";
let cmap = ToUnicodeCMap::parse(cmap_data).unwrap();
assert_eq!(cmap.map_code(&[0x20]).as_deref(), Some(" "));
assert_eq!(cmap.map_code(&[0x41]).as_deref(), Some("A"));
assert_eq!(cmap.map_code(&[0x7E]).as_deref(), Some("~"));
}
#[test]
fn hex_bytes_to_unicode_basic() {
assert_eq!(hex_bytes_to_unicode(&[0x00, 0x41]), "A");
assert_eq!(hex_bytes_to_unicode(&[0x00, 0xE9]), "\u{00E9}");
}
#[test]
fn code_offset_basic() {
assert_eq!(code_offset(&[0x00, 0x43], &[0x00, 0x41]), 2);
assert_eq!(code_offset(&[0x01, 0x00], &[0x00, 0xFF]), 1);
}
}