pub(crate) fn build_tounicode_cmap(mappings: &[(u16, char)]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(
b"/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n\
/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n\
/CMapName /Adobe-Identity-UCS def\n/CMapType 2 def\n",
);
out.extend_from_slice(b"1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n");
let mut sorted: Vec<(u16, char)> = mappings.to_vec();
sorted.sort_unstable_by_key(|&(c, _)| c);
sorted.dedup_by_key(|(c, _)| *c);
let mut ranges: Vec<(u16, u16, char)> = Vec::new(); let mut singles: Vec<(u16, char)> = Vec::new();
let mut i = 0;
while i < sorted.len() {
let (lo, ch) = sorted[i];
let start_cp = ch as u32;
let mut hi = lo;
let mut j = i + 1;
while j < sorted.len() {
let (cj, chj) = sorted[j];
if cj == hi.saturating_add(1) && (chj as u32) == start_cp + (cj - lo) as u32 {
hi = cj;
j += 1;
} else {
break;
}
}
if hi > lo {
ranges.push((lo, hi, ch));
} else {
singles.push((lo, ch));
}
i = j;
}
for chunk in ranges.chunks(100) {
out.extend_from_slice(format!("{} beginbfrange\n", chunk.len()).as_bytes());
for &(lo, hi, ch) in chunk {
out.extend_from_slice(format!("<{lo:04X}> <{hi:04X}> <").as_bytes());
write_utf16_hex(&mut out, ch as u32);
out.extend_from_slice(b">\n");
}
out.extend_from_slice(b"endbfrange\n");
}
for chunk in singles.chunks(100) {
out.extend_from_slice(format!("{} beginbfchar\n", chunk.len()).as_bytes());
for &(code, ch) in chunk {
out.extend_from_slice(format!("<{code:04X}> <").as_bytes());
write_utf16_hex(&mut out, ch as u32);
out.extend_from_slice(b">\n");
}
out.extend_from_slice(b"endbfchar\n");
}
out.extend_from_slice(b"endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend\n");
out
}
fn write_utf16_hex(out: &mut Vec<u8>, code_point: u32) {
let mut buf = [0u16; 2];
let s: &[u16] = match char::from_u32(code_point) {
Some(c) => c.encode_utf16(&mut buf),
None => &buf[..0],
};
for &u in s {
out.extend_from_slice(format!("{u:04X}").as_bytes());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bmp_singleton_round_trips_through_parser() {
let cmap = build_tounicode_cmap(&[(65, 'A')]);
let parsed = zpdf_font::cmap::ToUnicodeMap::parse(&cmap);
assert_eq!(parsed.lookup(65), Some("A"));
}
#[test]
fn cjk_range_packs_into_bfrange() {
let mappings = vec![
(100, '\u{4E00}'),
(101, '\u{4E01}'),
(102, '\u{4E02}'),
(103, '\u{4E03}'),
];
let cmap = build_tounicode_cmap(&mappings);
let body = String::from_utf8_lossy(&cmap);
assert!(body.contains("beginbfrange"), "body: {body}");
assert!(
!body.contains("beginbfchar"),
"expected no singletons: {body}"
);
let parsed = zpdf_font::cmap::ToUnicodeMap::parse(&cmap);
for (gid, ch) in &mappings {
assert_eq!(parsed.lookup(*gid as u32), Some(ch.to_string().as_str()));
}
}
#[test]
fn supplementary_plane_emits_surrogate_pair() {
let cmap = build_tounicode_cmap(&[(1, '\u{1F600}')]);
let body = String::from_utf8_lossy(&cmap);
assert!(body.contains("<D83DDE00>"), "body: {body}");
let parsed = zpdf_font::cmap::ToUnicodeMap::parse(&cmap);
assert_eq!(parsed.lookup(1), Some("\u{1F600}"));
}
}