use crate::cache::MutexExt;
use crate::error::Result;
use regex::Regex;
use std::collections::HashMap;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::sync::{Arc, Mutex};
#[derive(Clone, Debug)]
struct RangeEntry {
start: u32,
end: u32,
target: u32,
}
#[derive(Clone, Debug)]
pub struct CMap {
chars: HashMap<u32, String>,
ranges: Vec<RangeEntry>,
notdef_ranges: Vec<RangeEntry>,
pub code_width: u8,
pub wmode: u8,
}
impl CMap {
pub fn get(&self, code: &u32) -> Option<std::borrow::Cow<'_, str>> {
if let Some(s) = self.chars.get(code) {
return Some(std::borrow::Cow::Borrowed(s));
}
if let Ok(pos) = self.ranges.binary_search_by(|r| {
if r.end < *code {
std::cmp::Ordering::Less
} else if r.start > *code {
std::cmp::Ordering::Greater
} else {
std::cmp::Ordering::Equal
}
}) {
let r = &self.ranges[pos];
let cp = r.target.wrapping_add(*code - r.start);
if let Some(ch) = char::from_u32(cp) {
return Some(std::borrow::Cow::Owned(ch.to_string()));
}
}
for range in &self.notdef_ranges {
if range.start <= *code
&& *code <= range.end
&& let Some(s) = self.chars.get(&range.target)
{
return Some(std::borrow::Cow::Borrowed(s));
}
}
None
}
fn compress_sequential_ranges(&mut self) {
const MIN_RUN: usize = 256;
let notdef_targets: std::collections::HashSet<u32> = self.notdef_ranges.iter().map(|r| r.target).collect();
let mut singles: Vec<(u32, u32)> = self
.chars
.iter()
.filter(|(c, _)| !notdef_targets.contains(c))
.filter_map(|(&c, s)| {
let mut it = s.chars();
match (it.next(), it.next()) {
(Some(ch), None) => Some((c, ch as u32)),
_ => None,
}
})
.collect();
if singles.len() < MIN_RUN {
return;
}
singles.sort_unstable_by_key(|&(c, _)| c);
let mut i = 0;
while i < singles.len() {
let mut j = i;
while j + 1 < singles.len() && singles[j + 1].0 == singles[j].0 + 1 && singles[j + 1].1 == singles[j].1 + 1
{
j += 1;
}
if j - i + 1 >= MIN_RUN {
self.ranges.push(RangeEntry {
start: singles[i].0,
end: singles[j].0,
target: singles[i].1,
});
for &(c, _) in &singles[i..=j] {
self.chars.remove(&c);
}
}
i = j + 1;
}
self.ranges.sort_unstable_by_key(|r| r.start);
}
pub fn is_empty(&self) -> bool {
self.chars.is_empty() && self.ranges.is_empty() && self.notdef_ranges.is_empty()
}
pub fn len(&self) -> usize {
self.chars.len() + self.ranges.len() + self.notdef_ranges.len()
}
fn new() -> Self {
CMap {
chars: HashMap::new(),
ranges: Vec::new(),
notdef_ranges: Vec::new(),
code_width: 1,
wmode: 0,
}
}
fn insert(&mut self, code: u32, unicode: String) {
self.chars.insert(code, unicode);
}
}
#[derive(Hash, Eq, PartialEq, Clone, Copy, Debug)]
pub struct CMapKey(u64);
fn compute_stream_hash(data: &[u8]) -> CMapKey {
let mut hasher = DefaultHasher::new();
data.hash(&mut hasher);
CMapKey(hasher.finish())
}
const MAX_CMAP_CACHE_ENTRIES: usize = 1024;
static CMAP_CACHE: std::sync::LazyLock<Mutex<crate::cache::BoundedEntryCache<CMapKey, Arc<CMap>>>> =
std::sync::LazyLock::new(|| Mutex::new(crate::cache::BoundedEntryCache::new(MAX_CMAP_CACHE_ENTRIES)));
pub fn clear_cmap_cache() {
CMAP_CACHE.lock_or_recover().clear();
}
pub fn cmap_cache_size() -> usize {
CMAP_CACHE.lock_or_recover().len()
}
#[derive(Debug, Clone)]
pub struct LazyCMap {
raw_stream: Vec<u8>,
cache_key: CMapKey,
parsed: Arc<Mutex<Option<Option<Arc<CMap>>>>>,
}
impl LazyCMap {
pub fn new(raw_stream: Vec<u8>) -> Self {
let cache_key = compute_stream_hash(&raw_stream);
LazyCMap {
raw_stream,
cache_key,
parsed: Arc::new(Mutex::new(None)),
}
}
pub fn new_for_font(raw_stream: Vec<u8>, _font_name: String) -> Self {
LazyCMap::new(raw_stream)
}
pub fn raw_data(&self) -> &[u8] {
&self.raw_stream
}
pub fn code_width(&self) -> u8 {
self.get().map(|cmap| cmap.code_width).unwrap_or(1)
}
pub fn wmode(&self) -> u8 {
self.get().map(|cmap| cmap.wmode).unwrap_or(0)
}
pub fn get(&self) -> Option<Arc<CMap>> {
let mut parsed_guard = self.parsed.lock_or_recover();
if let Some(attempted) = parsed_guard.as_ref() {
return attempted.as_ref().map(Arc::clone);
}
{
let mut global = CMAP_CACHE.lock_or_recover();
if let Some(cached) = global.get(&self.cache_key) {
let arc = Arc::clone(cached);
*parsed_guard = Some(Some(Arc::clone(&arc)));
tracing::debug!("CMap cache hit (global) for stream hash {:?}", self.cache_key);
return Some(arc);
}
}
match parse_tounicode_cmap(&self.raw_stream) {
Ok(cmap) => {
let cmap_arc = Arc::new(cmap);
*parsed_guard = Some(Some(Arc::clone(&cmap_arc)));
{
let mut global = CMAP_CACHE.lock_or_recover();
global.insert(self.cache_key, Arc::clone(&cmap_arc));
}
tracing::debug!("CMap parsed and cached (stream hash {:?})", self.cache_key);
Some(cmap_arc)
}
Err(error) => {
*parsed_guard = Some(None);
crate::error::trace_recovery("parse_tounicode_cmap", &error);
None
}
}
}
}
fn parse_escape_sequence(token: &str) -> Option<String> {
let token = token.trim();
let token = if token.starts_with('<') && token.ends_with('>') {
&token[1..token.len() - 1]
} else {
token
};
let token_lower = token.to_lowercase();
match token_lower.trim() {
"space" => Some(" ".to_string()),
"tab" => Some("\t".to_string()),
"newline" => Some("\n".to_string()),
"carriage return" => Some("\r".to_string()),
_ => None,
}
}
fn decode_utf16_surrogate_pair(value: u32) -> Option<String> {
let high = (value >> 16) as u16;
let low = (value & 0xFFFF) as u16;
if (0xD800..=0xDBFF).contains(&high) && (0xDC00..=0xDFFF).contains(&low) {
let codepoint = 0x10000 + (((high & 0x3FF) as u32) << 10) + ((low & 0x3FF) as u32);
char::from_u32(codepoint).map(|ch| ch.to_string())
} else {
char::from_u32(value).map(|ch| ch.to_string())
}
}
const CMAP_STRUCTURAL_KEYWORDS: [&str; 5] = [
"begincmap",
"beginbfchar",
"beginbfrange",
"begincodespacerange",
"beginnotdefrange",
];
fn has_any_cmap_structural_keyword(content: &str) -> bool {
CMAP_STRUCTURAL_KEYWORDS.iter().any(|keyword| content.contains(keyword))
}
pub fn parse_tounicode_cmap(data: &[u8]) -> Result<CMap> {
let mut cmap = CMap::new();
let content = String::from_utf8_lossy(data);
if !data.is_empty() && !has_any_cmap_structural_keyword(&content) {
return Err(crate::error::Error::Font(format!(
"ToUnicode CMap stream ({} byte(s)) has none of begincmap/beginbfchar/beginbfrange/\
begincodespacerange/beginnotdefrange; not a valid CMap",
data.len()
)));
}
if let Some(parsed_wmode) = parse_wmode_directive(&content) {
cmap.wmode = parsed_wmode;
if parsed_wmode == 1 {
tracing::trace!("CMap declares /WMode 1 (vertical writing)");
}
}
apply_codespacerange_sections(&mut cmap, &content);
for (kind, section) in bf_sections_in_document_order(&content) {
match kind {
BfSectionKind::Char => apply_bfchar_section(&mut cmap, section),
BfSectionKind::Range => apply_bfrange_section(&mut cmap, section),
}
}
apply_notdefrange_sections(&mut cmap, &content);
cmap.compress_sequential_ranges();
Ok(cmap)
}
fn apply_codespacerange_sections(cmap: &mut CMap, content: &str) {
for section in extract_sections(content, "begincodespacerange", "endcodespacerange") {
for line in section.lines() {
let width = parse_codespacerange_line_width(line);
if width > cmap.code_width {
cmap.code_width = width;
tracing::trace!("ToUnicode codespacerange: code_width set to {}", cmap.code_width);
}
}
}
}
fn apply_bfchar_section(cmap: &mut CMap, section: &str) {
let mut attempted = 0usize;
let mut malformed = 0usize;
for line in significant_lines(section) {
attempted += 1;
let pairs = parse_bfchar_line(line);
if pairs.is_empty() {
malformed += 1;
continue;
}
for (src, dst) in pairs {
tracing::trace!("ToUnicode bfchar: 0x{:02X} -> {:?}", src, dst);
cmap.insert(src, dst);
}
}
warn_on_malformed_lines("bfchar", attempted, malformed);
}
fn apply_bfrange_section(cmap: &mut CMap, section: &str) {
let mut attempted = 0usize;
let mut malformed = 0usize;
for line in significant_lines(section) {
attempted += 1;
let Some(mappings) = parse_bfrange_line(line) else {
malformed += 1;
continue;
};
tracing::trace!("ToUnicode bfrange: {} mappings parsed", mappings.len());
for (src, dst) in mappings {
cmap.insert(src, dst);
}
}
warn_on_malformed_lines("bfrange", attempted, malformed);
}
fn apply_notdefrange_sections(cmap: &mut CMap, content: &str) {
for section in extract_sections(content, "beginnotdefrange", "endnotdefrange") {
apply_notdefrange_section(cmap, section);
}
}
fn apply_notdefrange_section(cmap: &mut CMap, section: &str) {
let mut attempted = 0usize;
let mut malformed = 0usize;
for line in significant_lines(section) {
attempted += 1;
let Some(mappings) = parse_notdefrange_line(line) else {
malformed += 1;
continue;
};
tracing::trace!("ToUnicode notdefrange: {} mappings parsed", mappings.len());
for (src, dst) in mappings {
if !cmap.chars.contains_key(&src) {
cmap.insert(src, dst);
}
}
}
warn_on_malformed_lines("notdefrange", attempted, malformed);
}
enum BfSectionKind {
Char,
Range,
}
pub(crate) fn significant_lines(section: &str) -> impl Iterator<Item = &str> {
section
.lines()
.map(str::trim)
.filter(|line| !line.is_empty() && !line.starts_with('%'))
}
fn warn_on_malformed_lines(section_kind: &str, attempted: usize, malformed: usize) {
if malformed > 0 {
tracing::warn!(
"ToUnicode CMap: {} of {} {} line(s) failed to parse (malformed hex or unmappable \
code point); those entries are skipped",
malformed,
attempted,
section_kind
);
}
}
fn bf_sections_in_document_order(content: &str) -> impl Iterator<Item = (BfSectionKind, &str)> {
let mut remaining = content;
std::iter::from_fn(move || {
loop {
let pos = remaining.find("beginbf")?;
let after = &remaining[pos + "beginbf".len()..];
if let Some(body) = after.strip_prefix("char") {
if let Some(end) = body.find("endbfchar") {
remaining = &body[end + "endbfchar".len()..];
return Some((BfSectionKind::Char, &body[..end]));
}
tracing::warn!(
"ToUnicode CMap: `beginbfchar` has no matching `endbfchar`; dropping this \
truncated block"
);
} else if let Some(body) = after.strip_prefix("range") {
if let Some(end) = body.find("endbfrange") {
remaining = &body[end + "endbfrange".len()..];
return Some((BfSectionKind::Range, &body[..end]));
}
tracing::warn!(
"ToUnicode CMap: `beginbfrange` has no matching `endbfrange`; dropping this \
truncated block"
);
}
remaining = after;
}
})
}
pub(crate) fn extract_sections<'a>(content: &'a str, begin: &str, end: &str) -> Vec<&'a str> {
let mut sections = Vec::new();
let mut remaining = content;
while let Some(begin_pos) = remaining.find(begin) {
let after_begin = &remaining[begin_pos + begin.len()..];
if let Some(end_pos) = after_begin.find(end) {
sections.push(&after_begin[..end_pos]);
remaining = &after_begin[end_pos + end.len()..];
} else {
tracing::warn!(
"ToUnicode CMap: `{}` has no matching `{}`; the rest of the stream is dropped \
for this section type",
begin,
end
);
break;
}
}
sections
}
pub(crate) fn parse_wmode_directive_public(content: &str) -> Option<u8> {
parse_wmode_directive(content)
}
fn parse_wmode_directive(content: &str) -> Option<u8> {
static RE: std::sync::LazyLock<Regex> =
std::sync::LazyLock::new(|| Regex::new(r"/WMode\s+([0-9]+)\s+def").unwrap());
let cleaned: String = content
.lines()
.map(|line| match line.find('%') {
Some(idx) => &line[..idx],
None => line,
})
.collect::<Vec<_>>()
.join("\n");
let caps = RE.captures(&cleaned)?;
let value: u32 = caps[1].parse().ok()?;
match value {
0 => Some(0),
1 => Some(1),
other => {
tracing::warn!(
"Non-standard /WMode {} in CMap stream; falling back to horizontal (WMode 0)",
other
);
None
}
}
}
fn parse_codespacerange_line_width(line: &str) -> u8 {
static RE: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| Regex::new(r"<([^>]*)>\s*<([^>]*)>").unwrap());
let mut max_width: u8 = 1;
for caps in RE.captures_iter(line) {
let lo_hex = caps[1].trim().replace(char::is_whitespace, "");
let hi_hex = caps[2].trim().replace(char::is_whitespace, "");
if lo_hex.len() >= 4 || hi_hex.len() >= 4 {
max_width = 2;
}
}
max_width
}
fn is_ascii_hex_digits(s: &str) -> bool {
!s.is_empty() && s.bytes().all(|b| b.is_ascii_hexdigit())
}
fn parse_bfchar_line(line: &str) -> Vec<(u32, String)> {
static RE: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| Regex::new(r"<([^>]*)>\s*<([^>]*)>").unwrap());
let mut results = Vec::new();
for caps in RE.captures_iter(line) {
let parsed = (|| -> Option<(u32, String)> {
let src_str = caps[1].trim().replace(char::is_whitespace, "");
let src = u32::from_str_radix(&src_str, 16).ok()?;
let dst_str = caps[2].trim();
let dst = if let Some(escape) = parse_escape_sequence(&format!("<{}>", dst_str)) {
escape
} else {
let dst_hex = dst_str.replace(char::is_whitespace, "");
if !is_ascii_hex_digits(&dst_hex) {
return None;
}
if dst_hex.len() <= 4 {
let dst_code = u32::from_str_radix(&dst_hex, 16).ok()?;
char::from_u32(dst_code)?.to_string()
} else if dst_hex.len() <= 6 {
let dst_code = u32::from_str_radix(&dst_hex, 16).ok()?;
char::from_u32(dst_code)?.to_string()
} else if dst_hex.len() == 8 {
let dst_code = u32::from_str_radix(&dst_hex, 16).ok()?;
if let Some(decoded) = decode_utf16_surrogate_pair(dst_code) {
decoded
} else {
let mut result = String::new();
if let Ok(code1) = u32::from_str_radix(&dst_hex[0..4], 16)
&& let Some(ch) = char::from_u32(code1)
{
result.push(ch);
}
if let Ok(code2) = u32::from_str_radix(&dst_hex[4..8], 16)
&& let Some(ch) = char::from_u32(code2)
{
result.push(ch);
}
if result.is_empty() {
return None;
}
result
}
} else {
let mut result = String::new();
for i in (0..dst_hex.len()).step_by(4) {
let end = (i + 4).min(dst_hex.len());
if let Ok(code) = u32::from_str_radix(&dst_hex[i..end], 16)
&& let Some(ch) = char::from_u32(code)
{
result.push(ch);
}
}
if result.is_empty() {
return None;
}
result
}
};
Some((src, dst))
})();
if let Some(pair) = parsed {
results.push(pair);
}
}
results
}
fn parse_bfrange_line(line: &str) -> Option<Vec<(u32, String)>> {
static RE_SEQ: std::sync::LazyLock<Regex> =
std::sync::LazyLock::new(|| Regex::new(r"<([^>]*)>\s*<([^>]*)>\s*<([^>]*)>").unwrap());
static RE_ARRAY: std::sync::LazyLock<Regex> =
std::sync::LazyLock::new(|| Regex::new(r"<([^>]*)>\s*<([^>]*)>\s*\[((?:\s*<[^>]+>\s*)+)\]").unwrap());
if let Some(caps) = RE_ARRAY.captures(line) {
return parse_bfrange_array_form(&caps);
}
if let Some(caps) = RE_SEQ.captures(line) {
return parse_bfrange_sequential_form(&caps);
}
None
}
fn parse_bfrange_array_form(caps: ®ex::Captures) -> Option<Vec<(u32, String)>> {
let start_str = caps[1].trim().replace(char::is_whitespace, "");
let end_str = caps[2].trim().replace(char::is_whitespace, "");
let start = u32::from_str_radix(&start_str, 16).ok()?;
let end = u32::from_str_radix(&end_str, 16).ok()?;
let array_str = &caps[3];
static RE_HEX: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| Regex::new(r"<([^>]*)>").unwrap());
let dst_hexes: Vec<String> = RE_HEX
.captures_iter(array_str)
.filter_map(|cap| {
let s = cap.get(1).unwrap().as_str().trim().replace(char::is_whitespace, "");
if is_ascii_hex_digits(&s) { Some(s) } else { None }
})
.collect();
let mut result = Vec::new();
let range_size = end.saturating_sub(start).saturating_add(1) as usize;
if dst_hexes.len() != range_size {
tracing::warn!(
"ToUnicode bfrange array size mismatch: expected {} entries for range 0x{:X}-0x{:X}, got {}",
range_size,
start,
end,
dst_hexes.len()
);
}
for (i, dst_hex) in dst_hexes.iter().take(range_size).enumerate() {
let src = start + i as u32;
let Some(dst) = decode_bfrange_array_entry(dst_hex)? else {
continue;
};
result.push((src, dst));
}
Some(result)
}
fn decode_bfrange_array_entry(dst_hex: &str) -> Option<Option<String>> {
if dst_hex.len() <= 4 {
let dst_code = u32::from_str_radix(dst_hex, 16).ok()?;
return Some(Some(char::from_u32(dst_code)?.to_string()));
}
if dst_hex.len() <= 6 {
let dst_code = u32::from_str_radix(dst_hex, 16).ok()?;
return Some(char::from_u32(dst_code).map(|ch| ch.to_string()));
}
if dst_hex.len() == 8 {
let dst_code = u32::from_str_radix(dst_hex, 16).ok()?;
if let Some(decoded) = decode_utf16_surrogate_pair(dst_code) {
return Some(Some(decoded));
}
let mut unicode_string = String::new();
if let Ok(code) = u32::from_str_radix(&dst_hex[0..4], 16)
&& let Some(ch) = char::from_u32(code)
{
unicode_string.push(ch);
}
if let Ok(code) = u32::from_str_radix(&dst_hex[4..8], 16)
&& let Some(ch) = char::from_u32(code)
{
unicode_string.push(ch);
}
return Some(if unicode_string.is_empty() {
None
} else {
Some(unicode_string)
});
}
let mut unicode_string = String::new();
for chunk_start in (0..dst_hex.len()).step_by(4) {
let chunk_end = (chunk_start + 4).min(dst_hex.len());
if let Ok(code) = u32::from_str_radix(&dst_hex[chunk_start..chunk_end], 16)
&& let Some(ch) = char::from_u32(code)
{
unicode_string.push(ch);
}
}
Some(if unicode_string.is_empty() {
None
} else {
Some(unicode_string)
})
}
fn parse_bfrange_sequential_form(caps: ®ex::Captures) -> Option<Vec<(u32, String)>> {
let start_str = caps[1].trim().replace(char::is_whitespace, "");
let end_str = caps[2].trim().replace(char::is_whitespace, "");
let dst_start_str = caps[3].trim().replace(char::is_whitespace, "");
let start = u32::from_str_radix(&start_str, 16).ok()?;
let end = u32::from_str_radix(&end_str, 16).ok()?;
let dst_start = u32::from_str_radix(&dst_start_str, 16).ok()?;
let mut result = Vec::new();
let range_size = end.saturating_sub(start).min(10000);
let base_codepoint = if dst_start > 0xFFFF {
if let Some(decoded) = decode_utf16_surrogate_pair(dst_start) {
decoded.chars().next().map(|c| c as u32)
} else {
Some(dst_start)
}
} else {
Some(dst_start)
};
if let Some(base_cp) = base_codepoint {
for i in 0..=range_size {
let src = start.wrapping_add(i);
let cp = base_cp.wrapping_add(i);
if let Some(ch) = char::from_u32(cp) {
result.push((src, ch.to_string()));
}
}
}
Some(result)
}
fn parse_notdefrange_line(line: &str) -> Option<Vec<(u32, String)>> {
static RE_SEQ: std::sync::LazyLock<Regex> =
std::sync::LazyLock::new(|| Regex::new(r"<([^>]*)>\s*<([^>]*)>\s*<([^>]*)>").unwrap());
if let Some(caps) = RE_SEQ.captures(line) {
let start_str = caps[1].trim().replace(char::is_whitespace, "");
let end_str = caps[2].trim().replace(char::is_whitespace, "");
let dst_str = caps[3].trim();
let start = u32::from_str_radix(&start_str, 16).ok()?;
let end = u32::from_str_radix(&end_str, 16).ok()?;
let dst = if let Some(escape) = parse_escape_sequence(&format!("<{}>", dst_str)) {
escape
} else {
let dst_hex = dst_str.replace(char::is_whitespace, "");
let dst_code = u32::from_str_radix(&dst_hex, 16).ok()?;
if dst_code > 0xFFFF {
decode_utf16_surrogate_pair(dst_code).or_else(|| char::from_u32(dst_code).map(|ch| ch.to_string()))?
} else {
char::from_u32(dst_code)?.to_string()
}
};
let mut result = Vec::new();
let range_size = end.saturating_sub(start).min(10000);
for i in 0..=range_size {
let src = start.wrapping_add(i);
result.push((src, dst.clone()));
}
return Some(result);
}
None
}
pub fn parse_cid_to_unicode(data: &[u8]) -> Result<CMap> {
parse_tounicode_cmap(data)
}
#[cfg(test)]
mod tests;