use crate::error::Result;
use std::collections::HashMap;
use super::cmap::{extract_sections, significant_lines};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct CodespaceRange {
low: u32,
high: u32,
n_bytes: u8,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct CidRangeEntry {
start: u32,
end: u32,
start_cid: u16,
}
#[doc(hidden)]
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct CidCMap {
chars: HashMap<u32, u16>,
ranges: Vec<CidRangeEntry>,
codespace: Vec<CodespaceRange>,
}
impl CidCMap {
pub(crate) fn is_empty(&self) -> bool {
self.chars.is_empty() && self.ranges.is_empty()
}
pub(crate) fn lookup(&self, code: u32) -> Option<u16> {
if let Some(&cid) = self.chars.get(&code) {
return Some(cid);
}
self.ranges.iter().find_map(|r| {
if code < r.start || code > r.end {
return None;
}
let offset = u16::try_from(code - r.start).ok()?;
Some(r.start_cid.wrapping_add(offset))
})
}
pub(crate) fn codespace_widths(&self) -> Vec<u8> {
let mut widths: Vec<u8> = self.codespace.iter().map(|r| r.n_bytes).collect();
widths.sort_unstable();
widths.dedup();
widths
}
pub(crate) fn code_length(&self, bytes: &[u8], pos: usize) -> usize {
let remaining = bytes.len().saturating_sub(pos);
if remaining == 0 {
return 0;
}
let widths = self.codespace_widths();
if widths.is_empty() {
return 1;
}
for width in &widths {
let w = *width as usize;
if w > remaining {
continue;
}
let mut code: u32 = 0;
for &b in &bytes[pos..pos + w] {
code = (code << 8) | u32::from(b);
}
let matches = self
.codespace
.iter()
.any(|r| r.n_bytes == *width && code >= r.low && code <= r.high);
if matches {
return w;
}
}
(widths[0] as usize).min(remaining)
}
}
const CID_CMAP_STRUCTURAL_KEYWORDS: [&str; 4] = ["begincmap", "begincidchar", "begincidrange", "begincodespacerange"];
fn has_any_structural_keyword(content: &str) -> bool {
CID_CMAP_STRUCTURAL_KEYWORDS
.iter()
.any(|keyword| content.contains(keyword))
}
fn parse_codespacerange_entry(line: &str) -> Option<CodespaceRange> {
static RE: std::sync::LazyLock<regex::Regex> =
std::sync::LazyLock::new(|| regex::Regex::new(r"<([0-9A-Fa-f]+)>\s*<([0-9A-Fa-f]+)>").unwrap());
let caps = RE.captures(line)?;
let lo_hex = &caps[1];
let hi_hex = &caps[2];
let n_bytes = lo_hex.len().max(hi_hex.len()).div_ceil(2).clamp(1, 4) as u8;
let low = u32::from_str_radix(lo_hex, 16).ok()?;
let high = u32::from_str_radix(hi_hex, 16).ok()?;
if low > high {
return None;
}
Some(CodespaceRange { low, high, n_bytes })
}
fn parse_cidchar_line(line: &str) -> Vec<(u32, u16)> {
static RE: std::sync::LazyLock<regex::Regex> =
std::sync::LazyLock::new(|| regex::Regex::new(r"<([0-9A-Fa-f]+)>\s*(\d+)").unwrap());
RE.captures_iter(line)
.filter_map(|caps| {
let code = u32::from_str_radix(&caps[1], 16).ok()?;
let cid: u32 = caps[2].parse().ok()?;
let cid = u16::try_from(cid).ok()?;
Some((code, cid))
})
.collect()
}
fn parse_cidrange_line(line: &str) -> Option<CidRangeEntry> {
static RE: std::sync::LazyLock<regex::Regex> =
std::sync::LazyLock::new(|| regex::Regex::new(r"<([0-9A-Fa-f]+)>\s*<([0-9A-Fa-f]+)>\s*(\d+)").unwrap());
let caps = RE.captures(line)?;
let start = u32::from_str_radix(&caps[1], 16).ok()?;
let end = u32::from_str_radix(&caps[2], 16).ok()?;
let start_cid: u32 = caps[3].parse().ok()?;
let start_cid = u16::try_from(start_cid).ok()?;
if start > end {
return None;
}
Some(CidRangeEntry { start, end, start_cid })
}
pub(crate) fn parse_cid_cmap(data: &[u8]) -> Result<CidCMap> {
let mut map = CidCMap::default();
let content = String::from_utf8_lossy(data);
if !data.is_empty() && !has_any_structural_keyword(&content) {
return Err(crate::error::Error::Font(format!(
"Encoding CMap stream ({} byte(s)) has none of begincmap/begincidchar/begincidrange/\
begincodespacerange; not a valid CID CMap",
data.len()
)));
}
for section in extract_sections(&content, "begincodespacerange", "endcodespacerange") {
for line in significant_lines(section) {
if let Some(range) = parse_codespacerange_entry(line) {
map.codespace.push(range);
}
}
}
for section in extract_sections(&content, "begincidchar", "endcidchar") {
for line in significant_lines(section) {
for (code, cid) in parse_cidchar_line(line) {
map.chars.insert(code, cid);
}
}
}
for section in extract_sections(&content, "begincidrange", "endcidrange") {
for line in significant_lines(section) {
if let Some(entry) = parse_cidrange_line(line) {
map.ranges.push(entry);
}
}
}
Ok(map)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_a_single_cidrange() {
let data = b"1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n\
1 begincidrange\n<0000> <005E> 1\nendcidrange\n";
let map = parse_cid_cmap(data).unwrap();
assert_eq!(map.lookup(0x0000), Some(1));
assert_eq!(map.lookup(0x0041), Some(0x42));
assert_eq!(map.lookup(0x005E), Some(0x5F));
assert_eq!(map.lookup(0x005F), None);
assert_eq!(map.codespace_widths(), vec![2]);
}
#[test]
fn parses_cidchar_single_pairs() {
let data = b"1 begincidchar\n<0041> 34\nendcidchar\n";
let map = parse_cid_cmap(data).unwrap();
assert_eq!(map.lookup(0x41), Some(34));
}
#[test]
fn cidchar_takes_precedence_over_overlapping_cidrange() {
let data = b"1 begincidrange\n<0000> <00FF> 100\nendcidrange\n\
1 begincidchar\n<0041> 999\nendcidchar\n";
let map = parse_cid_cmap(data).unwrap();
assert_eq!(map.lookup(0x41), Some(999));
assert_eq!(map.lookup(0x42), Some(100 + 0x42));
}
#[test]
fn variable_width_codespace_segments_per_code() {
let data = b"2 begincodespacerange\n<00> <80>\n<8140> <9FFC>\nendcodespacerange\n";
let map = parse_cid_cmap(data).unwrap();
assert_eq!(map.codespace_widths(), vec![1, 2]);
let bytes = [0x41u8, 0x81, 0x40, 0x20];
assert_eq!(map.code_length(&bytes, 0), 1);
assert_eq!(map.code_length(&bytes, 1), 2);
assert_eq!(map.code_length(&bytes, 3), 1);
assert_eq!(map.code_length(&bytes, 4), 0);
}
#[test]
fn fixed_two_byte_codespace_reports_single_width() {
let data = b"1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n";
let map = parse_cid_cmap(data).unwrap();
assert_eq!(map.codespace_widths(), vec![2]);
assert_eq!(map.code_length(&[0x00, 0x41], 0), 2);
}
#[test]
fn usecmap_only_stream_parses_to_empty_map() {
let data = b"/Identity-H usecmap\nbegincmap\nendcmap\n";
let map = parse_cid_cmap(data).unwrap();
assert!(map.is_empty());
assert!(map.codespace_widths().is_empty());
}
#[test]
fn non_cmap_stream_is_rejected() {
let data = b"this is not a cmap at all, just some random text of enough length";
assert!(parse_cid_cmap(data).is_err());
}
#[test]
fn empty_stream_is_legitimately_empty() {
let map = parse_cid_cmap(b"").unwrap();
assert!(map.is_empty());
}
#[test]
fn truncated_cidrange_block_is_dropped_without_panicking() {
let data = b"1 begincidrange\n<0000> <005E> 1\n"; let map = parse_cid_cmap(data).unwrap();
assert!(map.is_empty());
}
}