use log::{debug, warn};
use lopdf::{Document, Object, ObjectId};
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
#[cfg(not(target_arch = "wasm32"))]
use std::path::PathBuf;
use crate::glyph_names::glyph_name_to_string;
static BUILTIN_CMAPS: include_dir::Dir<'_> =
include_dir::include_dir!("$CARGO_MANIFEST_DIR/external/bcmaps");
#[derive(Debug, Default, Clone)]
pub struct ToUnicodeCMap {
pub char_map: HashMap<u16, String>,
pub ranges: Vec<(u16, u16, u32)>,
pub code_byte_length: u8,
pub cid_passthrough: bool,
pub(crate) gap_fills: HashMap<u16, char>,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct CidDecodeStats {
pub codes: u32,
pub interpolated: u32,
pub unmapped: u32,
}
impl CidDecodeStats {
pub fn add(&mut self, other: CidDecodeStats) {
self.codes = self.codes.saturating_add(other.codes);
self.interpolated = self.interpolated.saturating_add(other.interpolated);
self.unmapped = self.unmapped.saturating_add(other.unmapped);
}
pub fn has_gaps(&self) -> bool {
self.interpolated > 0 || self.unmapped > 0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct CidDecoding {
pub(crate) text: String,
pub(crate) contributing: usize,
pub(crate) stats: CidDecodeStats,
}
const MAX_GAP_FILL_WIDTH: u32 = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GapFillClass {
Digit,
Upper(u8),
Lower(u8),
}
fn cased_script_block(c: char) -> Option<u8> {
Some(match c as u32 {
0x41..=0x5A | 0x61..=0x7A => 0,
0xC0..=0xFF => 1,
0x100..=0x17F => 2,
0x180..=0x24F => 3,
0x1E00..=0x1EFF => 4,
0x370..=0x3FF => 5,
0x400..=0x4FF => 6,
0x500..=0x52F => 7,
0x531..=0x587 => 8,
0x10A0..=0x10FF | 0x1C90..=0x1CBF => 9,
_ => return None,
})
}
fn gap_fill_class(c: char) -> Option<GapFillClass> {
if c.is_ascii_digit() {
return Some(GapFillClass::Digit);
}
let block = cased_script_block(c)?;
if c.is_uppercase() {
Some(GapFillClass::Upper(block))
} else if c.is_lowercase() {
Some(GapFillClass::Lower(block))
} else {
None
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum CodeMapping {
Text(String),
ControlDestination,
Unmapped,
}
fn is_control_destination_char(ch: char) -> bool {
matches!(
ch,
'\u{01}'..='\u{08}' | '\u{0B}' | '\u{0C}' | '\u{0E}'..='\u{1F}' | '\u{7F}'
)
}
fn destination_is_control(text: &str) -> bool {
if text.is_empty() {
return false;
}
let mut chars = text.chars().filter(|&ch| ch != '\0').peekable();
match chars.peek() {
None => true,
Some(_) => chars.all(is_control_destination_char),
}
}
fn destination_span_is_control_block(base: u32, len: u32) -> bool {
base.saturating_add(len) <= 0x1F || (base == 0x7F && len == 0)
}
fn repair_control_destinations<'p>(
target: &mut ToUnicodeCMap,
fallback: Option<&ToUnicodeCMap>,
program_reading: impl FnOnce() -> Option<&'p ToUnicodeCMap>,
program: impl FnOnce() -> Option<&'p [u8]>,
font_dict: &lopdf::Dictionary,
doc: &Document,
) {
let before = target.control_destination_codes();
if before.is_empty() {
return;
}
let named = differences_named_codes(font_dict, doc, &before);
let left = |target: &ToUnicodeCMap| -> Vec<u16> {
target
.control_destination_codes()
.into_iter()
.filter(|code| !named.contains(code))
.collect()
};
let mut codes = left(target);
if let Some(fallback) = fallback.filter(|_| !codes.is_empty()) {
target.recover_codes(&codes, fallback);
codes = left(target);
}
if !codes.is_empty() {
if let Some(reading) = program_reading() {
target.recover_codes(&codes, reading);
codes = left(target);
}
}
if !codes.is_empty() {
if let Some(program) = program() {
for (code, text) in blank_cid_glyph_spaces(font_dict, doc, program, &codes) {
target.char_map.insert(code, text);
}
}
}
if target.control_destination_codes() != before {
target.refresh_gap_fills();
}
}
fn cmap_entry(
mut primary: ToUnicodeCMap,
mut remapped: Option<ToUnicodeCMap>,
mut fallback: Option<ToUnicodeCMap>,
primary_entries: usize,
obj_num: u32,
promote: bool,
) -> CMapEntry {
if primary_entries < 10 {
if let Some(fb) = fallback.take() {
debug!(
"ToUnicode CMap obj={} too sparse ({} entries); using fallback",
obj_num, primary_entries
);
remapped = Some(primary);
primary = fb;
}
}
if promote && remapped.is_some() {
if let Some(ref fb) = fallback {
let fb_entries = fb.char_map.len() + fb.ranges.len();
if fb_entries > primary_entries {
debug!(
"ToUnicode CMap obj={}: TrueType fallback ({} entries) > primary ({}); promoting over sequential remap",
obj_num, fb_entries, primary_entries
);
let old_remap = remapped.take().unwrap();
remapped = fallback.take();
fallback = Some(old_remap);
}
}
}
CMapEntry {
primary,
remapped,
fallback,
}
}
fn differences_named_codes(
font_dict: &lopdf::Dictionary,
doc: &Document,
codes: &[u16],
) -> HashSet<u16> {
if font_subtype(font_dict) == Some(&b"Type0"[..]) {
return HashSet::new();
}
let Some(encoding) = crate::extractor::fonts::parse_font_encoding(doc, font_dict) else {
return HashSet::new();
};
codes
.iter()
.copied()
.filter(|&code| u8::try_from(code).is_ok_and(|byte| encoding.named_codes.contains(&byte)))
.collect()
}
fn blank_cid_glyph_spaces(
font_dict: &lopdf::Dictionary,
doc: &Document,
program: &[u8],
codes: &[u16],
) -> Vec<(u16, String)> {
let Some(cid_font_dict) = get_descendant_cid_font(font_dict, doc) else {
return Vec::new();
};
let Some(glyphs) = ProgramGlyphs::parse(program) else {
return Vec::new();
};
let cid_to_gid = get_cid_to_gid_map(cid_font_dict, doc);
let glyph_of = |cid: u16| -> Option<u16> {
match (&glyphs.charset, cid_to_gid.as_ref()) {
(Some(by_cid), _) => by_cid.get(&cid).copied(),
(None, None) => Some(cid),
(None, Some(map)) => map.get(usize::from(cid)).copied().filter(|&gid| gid != 0),
}
};
let mut with_glyph = 0usize;
let blanks: Vec<(u16, String)> = codes
.iter()
.copied()
.filter(|&cid| {
let Some(gid) = glyph_of(cid).filter(|&gid| glyphs.has(gid)) else {
return false;
};
with_glyph += 1;
glyphs.blank(gid) && cid_advance(cid_font_dict, doc, cid) > 0.0
})
.map(|cid| (cid, " ".to_string()))
.collect();
if !blanks.is_empty() && with_glyph > 2 && !glyphs.any_outline() {
return Vec::new();
}
blanks
}
struct ProgramGlyphs<'a> {
outlines: Outlines<'a>,
charset: Option<HashMap<u16, u16>>,
}
enum Outlines<'a> {
Sfnt(Box<ttf_parser::Face<'a>>),
Cff(Box<ttf_parser::cff::Table<'a>>),
}
impl<'a> ProgramGlyphs<'a> {
fn parse(program: &'a [u8]) -> Option<Self> {
let (outlines, cff) = if let Ok(face) = ttf_parser::Face::parse(program, 0) {
let tables = face.tables();
if tables.glyf.is_none() && tables.cff.is_none() && tables.cff2.is_none() {
return None;
}
let cff = tables.cff;
(Outlines::Sfnt(Box::new(face)), cff)
} else {
let cff = ttf_parser::cff::Table::parse(program)?;
(Outlines::Cff(Box::new(cff)), Some(cff))
};
let charset = cff
.filter(|cff| cff.glyph_cid(ttf_parser::GlyphId(0)).is_some())
.map(|cff| cff_cid_to_gid(&cff));
Some(Self { outlines, charset })
}
fn has(&self, gid: u16) -> bool {
match &self.outlines {
Outlines::Sfnt(face) => gid < face.number_of_glyphs(),
Outlines::Cff(cff) => gid < cff.number_of_glyphs(),
}
}
fn any_outline(&self) -> bool {
match &self.outlines {
Outlines::Sfnt(face) => (0..face.number_of_glyphs())
.any(|gid| face.glyph_bounding_box(ttf_parser::GlyphId(gid)).is_some()),
Outlines::Cff(cff) => (0..cff.number_of_glyphs()).any(|gid| {
cff.outline(ttf_parser::GlyphId(gid), &mut NoOutline)
.is_ok()
}),
}
}
fn blank(&self, gid: u16) -> bool {
let glyph = ttf_parser::GlyphId(gid);
match &self.outlines {
Outlines::Sfnt(face) => {
gid < face.number_of_glyphs() && face.glyph_bounding_box(glyph).is_none()
}
Outlines::Cff(cff) => {
gid < cff.number_of_glyphs()
&& matches!(
cff.outline(glyph, &mut NoOutline),
Err(ttf_parser::CFFError::ZeroBBox)
)
}
}
}
}
struct NoOutline;
impl ttf_parser::OutlineBuilder for NoOutline {
fn move_to(&mut self, _x: f32, _y: f32) {}
fn line_to(&mut self, _x: f32, _y: f32) {}
fn quad_to(&mut self, _x1: f32, _y1: f32, _x: f32, _y: f32) {}
fn curve_to(&mut self, _x1: f32, _y1: f32, _x2: f32, _y2: f32, _x: f32, _y: f32) {}
fn close(&mut self) {}
}
fn cff_cid_to_gid(cff: &ttf_parser::cff::Table<'_>) -> HashMap<u16, u16> {
(0..cff.number_of_glyphs())
.filter_map(|gid| {
cff.glyph_cid(ttf_parser::GlyphId(gid))
.map(|cid| (cid, gid))
})
.collect()
}
fn font_program_stream<'a>(
font: &'a lopdf::Dictionary,
doc: &'a Document,
) -> Option<&'a lopdf::Stream> {
let descriptor = match font.get(b"FontDescriptor").ok()? {
Object::Reference(r) => doc.get_dictionary(*r).ok()?,
Object::Dictionary(d) => d,
_ => return None,
};
let font_file = [&b"FontFile2"[..], &b"FontFile3"[..]]
.into_iter()
.find_map(|key| descriptor.get(key).ok().and_then(|o| o.as_reference().ok()))?;
doc.get_object(font_file).ok()?.as_stream().ok()
}
fn program_bytes(stream: &lopdf::Stream, limit: Option<usize>) -> Option<Vec<u8>> {
let decoded = match limit {
Some(limit) => stream.decompressed_content_with_limit(limit),
None => stream.decompressed_content(),
};
match decoded {
Ok(data) if !data.is_empty() => Some(data),
Err(lopdf::Error::Decompress(lopdf::DecompressError::MemoryLimitExceeded { .. })) => None,
_ => limit
.is_none_or(|limit| stream.content.len() <= limit)
.then(|| stream.content.clone()),
}
}
fn font_subtype(font_dict: &lopdf::Dictionary) -> Option<&[u8]> {
font_dict.get(b"Subtype").ok()?.as_name().ok()
}
fn identity_type0_descendant<'a>(
font_dict: &'a lopdf::Dictionary,
doc: &'a Document,
) -> Option<&'a lopdf::Dictionary> {
let encoding = match font_dict.get(b"Encoding").ok()? {
Object::Reference(r) => doc.get_object(*r).ok()?,
other => other,
};
let encoding = encoding.as_name().ok()?;
if encoding != b"Identity-H" && encoding != b"Identity-V" {
return None;
}
get_descendant_cid_font(font_dict, doc)
}
fn embedded_font_program(font_dict: &lopdf::Dictionary, doc: &Document) -> Option<Vec<u8>> {
program_bytes(embedded_font_program_stream(font_dict, doc)?, None)
}
fn embedded_font_program_stream<'a>(
font_dict: &'a lopdf::Dictionary,
doc: &'a Document,
) -> Option<&'a lopdf::Stream> {
match font_subtype(font_dict)? {
b"Type0" => font_program_stream(identity_type0_descendant(font_dict, doc)?, doc),
_ => font_program_stream(font_dict, doc),
}
}
struct LazyProgram<'a> {
font_dict: &'a lopdf::Dictionary,
doc: &'a Document,
bytes: std::cell::OnceCell<Option<Vec<u8>>>,
reading: std::cell::OnceCell<Option<ToUnicodeCMap>>,
}
impl<'a> LazyProgram<'a> {
fn new(font_dict: &'a lopdf::Dictionary, doc: &'a Document) -> Self {
Self {
font_dict,
doc,
bytes: std::cell::OnceCell::new(),
reading: std::cell::OnceCell::new(),
}
}
fn bytes(&self) -> Option<&[u8]> {
self.bytes
.get_or_init(|| embedded_font_program(self.font_dict, self.doc))
.as_deref()
}
fn bytes_within(&self, limit: usize) -> Option<&[u8]> {
if self.bytes.get().is_none() {
let bytes = embedded_font_program_stream(self.font_dict, self.doc)
.and_then(|stream| program_bytes(stream, Some(limit)))?;
let _ = self.bytes.set(Some(bytes));
}
self.bytes.get()?.as_deref()
}
fn reading(&self) -> Option<&ToUnicodeCMap> {
self.reading
.get_or_init(|| program_fallback_cmap(self.font_dict, self.doc, self.bytes()))
.as_ref()
}
}
#[derive(Clone, Copy)]
struct EntryBuild {
promote: bool,
program_fallback: ProgramFallback,
program_repair: bool,
}
#[derive(Clone, Copy)]
enum ProgramFallback {
Always,
WhenSparse,
Never,
}
fn font_cmap_entry(
cmap: ToUnicodeCMap,
font_dict: &lopdf::Dictionary,
doc: &Document,
obj_num: u32,
build: EntryBuild,
) -> CMapEntry {
let program = LazyProgram::new(font_dict, doc);
let (mut primary, mut remapped) = try_remap_subset_cmap(cmap, font_dict, doc, obj_num, || {
program.bytes_within(crate::MAX_STREAM_DECOMPRESSED_BYTES)
});
let primary_entries = primary.char_map.len() + primary.ranges.len();
let sparse = primary_entries < 10;
let encoding_fallback = build_fallback_tounicode_from_encoding(font_dict, doc);
let program_fallback = match build.program_fallback {
ProgramFallback::Always => true,
ProgramFallback::WhenSparse => sparse,
ProgramFallback::Never => false,
};
let program_read = program_fallback && encoding_fallback.is_none();
let fallback = encoding_fallback.or_else(|| {
program_fallback
.then(|| program.reading().cloned())
.flatten()
});
for target in std::iter::once(&mut primary).chain(remapped.as_mut()) {
repair_control_destinations(
target,
fallback.as_ref(),
|| {
(build.program_repair && !program_read)
.then(|| program.reading())
.flatten()
},
|| build.program_repair.then(|| program.bytes()).flatten(),
font_dict,
doc,
);
}
cmap_entry(
primary,
remapped,
fallback,
primary_entries,
obj_num,
build.promote,
)
}
fn program_fallback_cmap(
font_dict: &lopdf::Dictionary,
doc: &Document,
program: Option<&[u8]>,
) -> Option<ToUnicodeCMap> {
if font_subtype(font_dict)? == b"Type0" {
let cid_font_dict = identity_type0_descendant(font_dict, doc)?;
if let Some(cmap) = program.and_then(build_cmap_from_truetype) {
if let Some(cid_to_gid) = get_cid_to_gid_map(cid_font_dict, doc) {
if let Some(repaired) = build_cmap_with_cid_to_gid_map(&cmap, &cid_to_gid) {
debug!(
"Fallback TrueType CMap repaired with CIDToGIDMap: {} entries",
repaired.char_map.len()
);
return Some(repaired);
}
}
debug!(
"Fallback TrueType CMap (Type0+ToUnicode) char_map={}",
cmap.char_map.len()
);
return Some(cmap);
}
let cmap = build_cmap_from_cid_system_info(cid_font_dict, doc)?;
debug!(
"Fallback CIDSystemInfo CMap (Type0+ToUnicode) char_map={}",
cmap.char_map.len()
);
Some(cmap)
} else {
let cmap = build_simple_cmap_from_truetype(program?)?;
debug!(
"Fallback simple font cmap (ToUnicode present) char_map={}",
cmap.char_map.len()
);
Some(cmap)
}
}
fn cid_advance(cid_font_dict: &lopdf::Dictionary, doc: &Document, cid: u16) -> f64 {
w_array_width(cid_font_dict, doc, cid).unwrap_or_else(|| {
cid_font_dict
.get(b"DW")
.ok()
.and_then(|o| match o {
Object::Reference(r) => doc.get_object(*r).ok().and_then(object_number),
other => object_number(other),
})
.unwrap_or(1000.0)
})
}
fn object_number(o: &Object) -> Option<f64> {
match o {
Object::Integer(n) => Some(*n as f64),
Object::Real(n) => Some(f64::from(*n)),
_ => None,
}
}
pub(crate) fn build_cmap_entry_from_stream(
data: &[u8],
font_dict: &lopdf::Dictionary,
doc: &Document,
obj_num: u32,
) -> Option<CMapEntry> {
if let Some(cmap) = ToUnicodeCMap::parse(data) {
return Some(font_cmap_entry(
cmap,
font_dict,
doc,
obj_num,
EntryBuild {
promote: true,
program_fallback: ProgramFallback::Always,
program_repair: true,
},
));
}
let fallback = build_fallback_cmap_for_type0(font_dict, doc)
.or_else(|| build_fallback_cmap_for_simple(font_dict, doc))?;
debug!(
"ToUnicode CMap obj={} parse failed; using fallback (entries={})",
obj_num,
fallback.char_map.len()
);
Some(CMapEntry {
primary: fallback,
remapped: None,
fallback: None,
})
}
impl ToUnicodeCMap {
pub fn new() -> Self {
Self::default()
}
pub fn parse(content: &[u8]) -> Option<Self> {
let text = String::from_utf8_lossy(content);
let mut cmap = ToUnicodeCMap::new();
let mut src_hex_lengths: Vec<usize> = Vec::new();
let mut use_cmap_name: Option<String> = None;
let mut codespace_byte_len: Option<u8> = None;
if let Some(cs_start) = text.find("begincodespacerange") {
let section_start = cs_start + "begincodespacerange".len();
if let Some(cs_end) = text[section_start..].find("endcodespacerange") {
let section = &text[section_start..section_start + cs_end];
let mut in_hex = false;
let mut hex_len = 0;
for c in section.chars() {
if c == '<' {
in_hex = true;
hex_len = 0;
} else if c == '>' {
if in_hex && hex_len > 0 {
let byte_len = (hex_len + 1) / 2; codespace_byte_len = Some(byte_len as u8);
}
in_hex = false;
} else if in_hex && c.is_ascii_hexdigit() {
hex_len += 1;
}
}
}
}
if let Some(name) = find_usecmap_name(&text) {
use_cmap_name = Some(name);
}
let mut pos = 0;
while let Some(start) = text[pos..].find("beginbfchar") {
let section_start = pos + start + "beginbfchar".len();
if let Some(end) = text[section_start..].find("endbfchar") {
let section = &text[section_start..section_start + end];
cmap.parse_bfchar_section(section, &mut src_hex_lengths);
pos = section_start + end;
} else {
break;
}
}
pos = 0;
while let Some(start) = text[pos..].find("beginbfrange") {
let section_start = pos + start + "beginbfrange".len();
if let Some(end) = text[section_start..].find("endbfrange") {
let section = &text[section_start..section_start + end];
cmap.parse_bfrange_section(section, &mut src_hex_lengths);
pos = section_start + end;
} else {
break;
}
}
if cmap.char_map.is_empty() && cmap.ranges.is_empty() {
return None;
}
cmap.code_byte_length = if let Some(cs_len) = codespace_byte_len {
if cs_len == 2 && !src_hex_lengths.is_empty() && src_hex_lengths.iter().all(|&l| l <= 2)
{
1
} else {
cs_len
}
} else if !src_hex_lengths.is_empty() {
let max_hex_len = src_hex_lengths.iter().max().copied().unwrap_or(4);
if max_hex_len <= 2 {
1
} else {
2
}
} else {
2 };
cmap.ranges.sort_unstable_by_key(|&(start, _, _)| start);
if let Some(name) = use_cmap_name {
if let Some(base) = load_builtin_cmap_by_name(&name) {
cmap = merge_cmaps(base, cmap);
} else {
warn!("usecmap={} could not be loaded", name);
}
}
cmap.refresh_gap_fills();
Some(cmap)
}
fn parse_bfchar_section(&mut self, section: &str, src_hex_lengths: &mut Vec<usize>) {
let mut chars = section.chars().peekable();
loop {
while chars.peek().is_some_and(|c| c.is_whitespace()) {
chars.next();
}
if chars.peek() != Some(&'<') {
break;
}
chars.next();
let mut src_hex = String::new();
while chars.peek().is_some_and(|&c| c != '>') {
if let Some(c) = chars.next() {
src_hex.push(c);
}
}
chars.next();
let trimmed_src = src_hex.trim();
if !trimmed_src.is_empty() {
src_hex_lengths.push(trimmed_src.len());
}
while chars.peek().is_some_and(|c| c.is_whitespace()) {
chars.next();
}
if chars.peek() != Some(&'<') {
continue;
}
chars.next();
let mut dst_hex = String::new();
while chars.peek().is_some_and(|&c| c != '>') {
if let Some(c) = chars.next() {
dst_hex.push(c);
}
}
chars.next();
if let (Some(src), Some(dst)) =
(parse_hex_u16(&src_hex), hex_to_unicode_string(&dst_hex))
{
self.char_map.insert(src, dst);
}
}
}
fn parse_bfrange_section(&mut self, section: &str, src_hex_lengths: &mut Vec<usize>) {
let mut chars = section.chars().peekable();
loop {
while chars.peek().is_some_and(|c| c.is_whitespace()) {
chars.next();
}
if chars.peek() != Some(&'<') {
break;
}
chars.next();
let mut start_hex = String::new();
while chars.peek().is_some_and(|&c| c != '>') {
if let Some(c) = chars.next() {
start_hex.push(c);
}
}
chars.next();
let trimmed_start = start_hex.trim();
if !trimmed_start.is_empty() {
src_hex_lengths.push(trimmed_start.len());
}
while chars.peek().is_some_and(|c| c.is_whitespace()) {
chars.next();
}
if chars.peek() != Some(&'<') {
continue;
}
chars.next();
let mut end_hex = String::new();
while chars.peek().is_some_and(|&c| c != '>') {
if let Some(c) = chars.next() {
end_hex.push(c);
}
}
chars.next();
while chars.peek().is_some_and(|c| c.is_whitespace()) {
chars.next();
}
if chars.peek() == Some(&'<') {
chars.next();
let mut base_hex = String::new();
while chars.peek().is_some_and(|&c| c != '>') {
if let Some(c) = chars.next() {
base_hex.push(c);
}
}
chars.next();
if let (Some(start), Some(end), Some(base)) = (
parse_hex_u16(&start_hex),
parse_hex_u16(&end_hex),
hex_to_unicode_scalar(&base_hex),
) {
self.ranges.push((start, end, base));
}
} else if chars.peek() == Some(&'[') {
chars.next(); if let (Some(start), Some(end)) =
(parse_hex_u16(&start_hex), parse_hex_u16(&end_hex))
{
let mut cid = start;
loop {
while chars.peek().is_some_and(|c| c.is_whitespace()) {
chars.next();
}
if chars.peek() == Some(&']') {
chars.next();
break;
}
if chars.peek() != Some(&'<') {
break;
}
chars.next(); let mut hex = String::new();
while chars.peek().is_some_and(|&c| c != '>') {
if let Some(c) = chars.next() {
hex.push(c);
}
}
chars.next(); if let Some(unicode_str) = hex_to_unicode_string(&hex) {
self.char_map.insert(cid, unicode_str);
}
if cid >= end {
while chars.peek().is_some_and(|&c| c != ']') {
chars.next();
}
if chars.peek() == Some(&']') {
chars.next();
}
break;
}
cid = cid.saturating_add(1);
}
} else {
while chars.peek().is_some_and(|&c| c != ']') {
chars.next();
}
if chars.peek() == Some(&']') {
chars.next();
}
}
}
}
}
pub fn lookup(&self, cid: u16) -> Option<String> {
match self.lookup_code(cid) {
CodeMapping::Text(text) => Some(text),
CodeMapping::ControlDestination | CodeMapping::Unmapped => None,
}
}
pub(crate) fn lookup_code(&self, cid: u16) -> CodeMapping {
match self.destination(cid) {
Some(text) if destination_is_control(&text) => CodeMapping::ControlDestination,
Some(text) => CodeMapping::Text(text),
None => CodeMapping::Unmapped,
}
}
pub(crate) fn control_destination_codes(&self) -> Vec<u16> {
let mut codes: std::collections::BTreeSet<u16> = self
.char_map
.iter()
.filter(|(_, text)| destination_is_control(text))
.map(|(&code, _)| code)
.collect();
for &(start, end, base) in &self.ranges {
if start > end || !destination_span_is_control_block(base, u32::from(end - start)) {
continue;
}
for code in start..=end {
if matches!(self.lookup_code(code), CodeMapping::ControlDestination) {
codes.insert(code);
}
}
}
codes.into_iter().collect()
}
fn recover_codes(&mut self, codes: &[u16], source: &ToUnicodeCMap) {
for &code in codes {
if let Some(text) = source
.lookup(code)
.filter(|text| !text.is_empty() && !text.contains('\u{FFFD}'))
{
self.char_map.insert(code, text);
}
}
}
fn destination(&self, cid: u16) -> Option<String> {
if let Some(s) = self.char_map.get(&cid) {
return Some(s.clone());
}
let idx = self
.ranges
.binary_search_by(|&(start, _, _)| start.cmp(&cid))
.unwrap_or_else(|i| i);
if idx < self.ranges.len() {
let (start, end, base) = self.ranges[idx];
if cid >= start && cid <= end {
let unicode = base + (cid - start) as u32;
if let Some(c) = char::from_u32(unicode) {
return Some(c.to_string());
}
}
}
if idx > 0 {
let (start, end, base) = self.ranges[idx - 1];
if cid >= start && cid <= end {
let unicode = base + (cid - start) as u32;
if let Some(c) = char::from_u32(unicode) {
return Some(c.to_string());
}
}
}
None
}
pub fn lookup_bytes(&self, bytes: &[u8]) -> Vec<(u8, Option<String>)> {
bytes
.iter()
.map(|&b| {
let code = b as u16;
let result = self.lookup(code).filter(|s| !s.contains('\u{FFFD}'));
(b, result)
})
.collect()
}
pub fn decode_cids(&self, bytes: &[u8]) -> String {
self.decode_cids_with(bytes, |out, label| out.push_str(label))
.text
}
pub fn decode_cids_with_stats(&self, bytes: &[u8]) -> (String, CidDecodeStats) {
let decoded = self.decode_cids_with(bytes, |out, label| out.push_str(label));
(decoded.text, decoded.stats)
}
pub(crate) fn decode_cids_with(
&self,
bytes: &[u8],
mut append: impl FnMut(&mut String, &str),
) -> CidDecoding {
let mut result = String::new();
let mut stats = CidDecodeStats::default();
let mut contributing = 0usize;
let mut control_destinations = 0u32;
if self.code_byte_length == 1 {
for &b in bytes {
stats.codes += 1;
let code = b as u16;
match self.lookup_code(code) {
CodeMapping::Text(s) if !s.contains('\u{FFFD}') => {
append(&mut result, &s);
contributing += 1;
}
CodeMapping::ControlDestination => {
result.push('\u{FFFD}');
contributing += 1;
stats.unmapped += 1;
control_destinations += 1;
}
_ => {
if b >= 0x20 {
result.push(b as char);
contributing += 1;
}
stats.unmapped += 1;
}
}
}
} else {
for chunk in bytes.chunks(2) {
if chunk.len() == 2 {
stats.codes += 1;
let cid = u16::from_be_bytes([chunk[0], chunk[1]]);
match self.lookup_code(cid) {
CodeMapping::Text(s) if !s.contains('\u{FFFD}') => {
append(&mut result, &s);
contributing += 1;
}
CodeMapping::ControlDestination => {
result.push('\u{FFFD}');
contributing += 1;
stats.unmapped += 1;
control_destinations += 1;
}
_ => {
if self.cid_passthrough {
if let Some(ch) = char::from_u32(cid as u32) {
if !ch.is_control() || ch == '\t' || ch == '\n' {
result.push(ch);
contributing += 1;
} else {
stats.unmapped += 1;
}
} else {
stats.unmapped += 1;
}
} else if let Some(ch) = self.gap_fill(cid) {
result.push(ch);
contributing += 1;
stats.interpolated += 1;
} else {
result.push('\u{FFFD}');
contributing += 1;
stats.unmapped += 1;
}
}
}
}
}
}
if stats.codes > 0 && stats.unmapped - control_destinations > stats.codes / 2 {
return CidDecoding {
text: String::new(),
contributing: 0,
stats,
};
}
CidDecoding {
text: result,
contributing,
stats,
}
}
pub fn gap_fill(&self, cid: u16) -> Option<char> {
self.gap_fills.get(&cid).copied()
}
pub fn refresh_gap_fills(&mut self) {
self.ranges.sort_unstable_by_key(|&(start, _, _)| start);
self.gap_fills = if self.code_byte_length != 1 && !self.cid_passthrough {
self.compute_gap_fills()
} else {
HashMap::new()
};
}
fn mapped_runs(&self) -> Vec<(u16, u16)> {
let mut intervals: Vec<(u16, u16)> = self
.ranges
.iter()
.filter(|&&(start, end, _)| start <= end)
.map(|&(start, end, _)| (start, end))
.collect();
intervals.extend(self.char_map.keys().map(|&cid| (cid, cid)));
intervals.sort_unstable();
let mut runs: Vec<(u16, u16)> = Vec::with_capacity(intervals.len());
for (start, end) in intervals {
if let Some(last) = runs.last_mut() {
if start <= last.1.saturating_add(1) {
last.1 = last.1.max(end);
continue;
}
}
runs.push((start, end));
}
runs
}
fn single_char(&self, cid: u16) -> Option<char> {
let text = self.lookup(cid)?;
let mut chars = text.chars();
let first = chars.next()?;
(chars.next().is_none() && first != '\u{FFFD}').then_some(first)
}
fn run_ends(&self, run: (u16, u16)) -> Option<(char, char)> {
let first = self.lookup(run.0)?.chars().next()?;
let last = self.lookup(run.1)?.chars().last()?;
Some((first, last))
}
fn run_rises(&self, run: (u16, u16)) -> bool {
let mut previous: Option<char> = None;
for cid in run.0..=run.1 {
let Some(text) = self.lookup(cid) else {
return false;
};
let (Some(first), Some(last)) = (text.chars().next(), text.chars().last()) else {
return false;
};
if previous.is_some_and(|previous| (previous as u32) >= (first as u32)) {
return false;
}
previous = Some(last);
}
previous.is_some()
}
fn runs_rise_across(&self, earlier: (u16, u16), later: (u16, u16)) -> bool {
match (self.run_ends(earlier), self.run_ends(later)) {
(Some((_, last)), Some((first, _))) => (last as u32) < (first as u32),
_ => false,
}
}
fn compute_gap_fills(&self) -> HashMap<u16, char> {
let runs = self.mapped_runs();
let mut fills = HashMap::new();
let mut rises: Vec<Option<bool>> = vec![None; runs.len()];
let mut run_rises = |index: usize| -> bool {
*rises[index].get_or_insert_with(|| self.run_rises(runs[index]))
};
for (index, pair) in runs.windows(2).enumerate() {
let (below, above) = (pair[0], pair[1]);
let (lo, hi) = (below.1, above.0);
let distance = u32::from(hi) - u32::from(lo);
if distance < 2 || distance - 1 > MAX_GAP_FILL_WIDTH {
continue;
}
let (Some(a), Some(b)) = (self.single_char(lo), self.single_char(hi)) else {
continue;
};
let Some(class) = gap_fill_class(a) else {
continue;
};
if gap_fill_class(b) != Some(class) || (b as u32) <= (a as u32) {
continue;
}
if b as u32 - a as u32 != distance {
continue;
}
let between_same_kind = (a as u32 + 1..b as u32)
.all(|cp| char::from_u32(cp).and_then(gap_fill_class) == Some(class));
if !between_same_kind {
continue;
}
if !run_rises(index) || !run_rises(index + 1) {
continue;
}
if let Some(previous) = index.checked_sub(1) {
if !run_rises(previous) || !self.runs_rise_across(runs[previous], below) {
continue;
}
}
if index + 2 < runs.len()
&& (!run_rises(index + 2) || !self.runs_rise_across(above, runs[index + 2]))
{
continue;
}
for offset in 1..distance {
if let Some(ch) = char::from_u32(a as u32 + offset) {
fills.insert(lo + offset as u16, ch);
}
}
}
fills
}
fn min_source_cid(&self) -> Option<u16> {
let char_min = self.char_map.keys().copied().min();
let range_min = self.ranges.iter().map(|&(start, _, _)| start).min();
match (char_min, range_min) {
(Some(a), Some(b)) => Some(a.min(b)),
(a @ Some(_), None) => a,
(None, b @ Some(_)) => b,
(None, None) => None,
}
}
fn max_source_cid(&self) -> Option<u16> {
let char_max = self.char_map.keys().copied().max();
let range_max = self.ranges.iter().map(|&(_, end, _)| end).max();
match (char_max, range_max) {
(Some(a), Some(b)) => Some(a.max(b)),
(a @ Some(_), None) => a,
(None, b @ Some(_)) => b,
(None, None) => None,
}
}
pub fn remap_to_sequential(&self) -> ToUnicodeCMap {
let mut cid_to_unicode: HashMap<u16, String> = HashMap::new();
expand_bfranges_for_remap(&self.ranges, &mut cid_to_unicode, MAX_CID_W_EXPANSION);
for (&cid, unicode) in &self.char_map {
cid_to_unicode.insert(cid, unicode.clone());
}
let mut old_cids: Vec<u16> = cid_to_unicode.keys().copied().collect();
old_cids.sort_unstable();
let mut new_cmap = ToUnicodeCMap::new();
for (i, &old_cid) in old_cids.iter().enumerate() {
let new_cid = (i + 1) as u16; if let Some(unicode) = cid_to_unicode.get(&old_cid) {
new_cmap.char_map.insert(new_cid, unicode.clone());
}
}
new_cmap.code_byte_length = self.code_byte_length;
new_cmap.refresh_gap_fills();
new_cmap
}
}
fn expand_bfranges_for_remap(
ranges: &[(u16, u16, u32)],
cid_to_unicode: &mut HashMap<u16, String>,
max_assignments: usize,
) -> usize {
let mut assigned = 0usize;
'ranges: for &(start, end, base) in ranges {
if start > end {
continue;
}
for cid in start..=end {
if assigned >= max_assignments {
break 'ranges;
}
assigned += 1;
let unicode_cp = base + (cid - start) as u32;
if let Some(ch) = char::from_u32(unicode_cp) {
cid_to_unicode.insert(cid, ch.to_string());
}
}
}
assigned
}
fn parse_hex_u16(hex: &str) -> Option<u16> {
u16::from_str_radix(hex.trim(), 16).ok()
}
fn hex_to_unicode_string(hex: &str) -> Option<String> {
let hex: String = hex.chars().filter(|ch| !ch.is_ascii_whitespace()).collect();
if hex.is_empty() || !hex.len().is_multiple_of(2) {
return None;
}
let bytes: Option<Vec<u8>> = (0..hex.len())
.step_by(2)
.map(|i| u8::from_str_radix(hex.get(i..i + 2)?, 16).ok())
.collect();
let bytes = bytes?;
if bytes.len().is_multiple_of(2) {
let units: Vec<u16> = bytes
.as_chunks::<2>()
.0
.iter()
.map(|chunk| u16::from_be_bytes(*chunk))
.collect();
if let Ok(result) = String::from_utf16(&units) {
if !result.is_empty() {
return Some(normalize_tounicode_destination(result));
}
}
}
if bytes.len() == 1 {
let ch = bytes[0] as char;
if !ch.is_control() || ch == '\t' || ch == '\n' {
return Some(ch.to_string());
}
}
None
}
fn normalize_tounicode_destination(text: String) -> String {
let is_multi_char = text.chars().nth(1).is_some();
if is_multi_char
&& text.chars().all(char::is_whitespace)
&& text.chars().any(|ch| matches!(ch, '\t' | '\n' | '\r'))
{
return if text.contains('\t') {
"\t".to_string()
} else {
" ".to_string()
};
}
if is_multi_char
&& text.contains('\u{00ad}')
&& text.chars().all(|ch| {
matches!(
ch,
'-' | '\u{00ad}' | '\u{2010}' | '\u{2011}' | '\u{2012}' | '\u{2013}' | '\u{2212}'
)
})
{
return "-".to_string();
}
text
}
fn hex_to_unicode_scalar(hex: &str) -> Option<u32> {
let text = hex_to_unicode_string(hex)?;
let mut chars = text.chars();
let ch = chars.next()?;
if chars.next().is_none() {
Some(ch as u32)
} else {
None
}
}
fn find_usecmap_name(text: &str) -> Option<String> {
for line in text.lines() {
if line.contains("usecmap") {
let parts: Vec<&str> = line.split_whitespace().collect();
for i in 0..parts.len() {
if parts[i] == "usecmap" && i > 0 {
let name = parts[i - 1].trim();
if let Some(stripped) = name.strip_prefix('/') {
return Some(stripped.to_string());
}
}
}
}
}
None
}
fn get_descendant_cid_font<'a>(
font_dict: &'a lopdf::Dictionary,
doc: &'a Document,
) -> Option<&'a lopdf::Dictionary> {
let desc_fonts_obj = font_dict.get(b"DescendantFonts").ok()?;
let arr = match desc_fonts_obj {
Object::Array(arr) => arr,
Object::Reference(r) => match doc.get_object(*r) {
Ok(Object::Array(arr)) => arr,
_ => return None,
},
_ => return None,
};
if arr.is_empty() {
return None;
}
match &arr[0] {
Object::Reference(r) => doc.get_dictionary(*r).ok(),
Object::Dictionary(d) => Some(d),
_ => None,
}
}
fn get_w_array_start_cid(cid_font_dict: &lopdf::Dictionary, doc: &Document) -> Option<u16> {
let w_obj = cid_font_dict.get(b"W").ok()?;
let arr = match w_obj {
Object::Array(arr) => arr,
Object::Reference(r) => match doc.get_object(*r) {
Ok(Object::Array(arr)) => arr,
_ => return None,
},
_ => return None,
};
if arr.is_empty() {
return None;
}
match &arr[0] {
Object::Integer(n) => Some(*n as u16),
Object::Reference(r) => match doc.get_object(*r) {
Ok(Object::Integer(n)) => Some(*n as u16),
_ => None,
},
_ => None,
}
}
fn w_array_covers_cid(cid_font_dict: &lopdf::Dictionary, doc: &Document, target: u16) -> bool {
w_array_lookup(cid_font_dict, doc, target).is_some()
}
fn w_array_width(cid_font_dict: &lopdf::Dictionary, doc: &Document, target: u16) -> Option<f64> {
w_array_lookup(cid_font_dict, doc, target).flatten()
}
fn w_array_lookup(
cid_font_dict: &lopdf::Dictionary,
doc: &Document,
target: u16,
) -> Option<Option<f64>> {
let w_obj = cid_font_dict.get(b"W").ok()?;
let arr = match w_obj {
Object::Array(arr) => arr,
Object::Reference(r) => match doc.get_object(*r) {
Ok(Object::Array(arr)) => arr,
_ => return None,
},
_ => return None,
};
let resolve_int = |o: &Object| -> Option<i64> {
match o {
Object::Integer(n) => Some(*n),
Object::Reference(r) => match doc.get_object(*r) {
Ok(Object::Integer(n)) => Some(*n),
_ => None,
},
_ => None,
}
};
let resolve_num = |o: &Object| -> Option<f64> {
match o {
Object::Reference(r) => doc.get_object(*r).ok().and_then(object_number),
other => object_number(other),
}
};
let resolve_arr = |o: &Object| -> Option<Vec<Object>> {
match o {
Object::Array(a) => Some(a.clone()),
Object::Reference(r) => match doc.get_object(*r) {
Ok(Object::Array(a)) => Some(a.clone()),
_ => None,
},
_ => None,
}
};
let target = target as i64;
let mut i = 0usize;
while i < arr.len() {
let Some(first) = resolve_int(&arr[i]) else {
break;
};
i += 1;
if i >= arr.len() {
break;
}
if let Some(widths) = resolve_arr(&arr[i]) {
let last = first + widths.len() as i64 - 1;
if target >= first && target <= last {
return Some(resolve_num(&widths[(target - first) as usize]));
}
i += 1;
} else if let Some(last) = resolve_int(&arr[i]) {
i += 1;
let width = if i < arr.len() {
let width = resolve_num(&arr[i]);
i += 1;
width
} else {
None
};
if target >= first && target <= last {
return Some(width);
}
} else {
break;
}
}
None
}
fn get_cid_to_gid_map(cid_font_dict: &lopdf::Dictionary, doc: &Document) -> Option<Vec<u16>> {
let obj = cid_font_dict.get(b"CIDToGIDMap").ok()?;
match obj {
Object::Name(n) if n.as_slice() == b"Identity" => None,
Object::Reference(r) => match doc.get_object(*r) {
Ok(Object::Stream(s)) => parse_cid_to_gid_stream(&s.decompressed_content().ok()?),
_ => None,
},
Object::Stream(s) => parse_cid_to_gid_stream(&s.decompressed_content().ok()?),
_ => None,
}
}
fn parse_cid_to_gid_stream(data: &[u8]) -> Option<Vec<u16>> {
if data.len() < 2 {
return None;
}
let mut map = Vec::with_capacity(data.len() / 2);
for chunk in data.as_chunks::<2>().0 {
map.push(u16::from_be_bytes(*chunk));
}
Some(map)
}
const MAX_PREMISE_CHECK_CODES: usize = 1024;
fn cmap_keyed_by_glyph_index(cmap: &ToUnicodeCMap, cid_to_gid: &[u16]) -> Option<bool> {
let usable = |code: u16| {
cmap.lookup(code)
.is_some_and(|text| !text.is_empty() && !text.contains('\u{FFFD}'))
};
let (mut at_cid, mut at_glyph) = (0usize, 0usize);
for (cid, &gid) in (0..=u16::MAX)
.zip(cid_to_gid)
.filter(|&(cid, &gid)| gid != 0 && gid != cid)
.take(MAX_PREMISE_CHECK_CODES)
{
at_cid += usize::from(usable(cid));
at_glyph += usize::from(usable(gid));
}
match at_glyph.cmp(&at_cid) {
std::cmp::Ordering::Greater => Some(true),
std::cmp::Ordering::Less => Some(false),
std::cmp::Ordering::Equal => None,
}
}
fn cmap_chars_reading_as(text: &str) -> Vec<char> {
let mut chars = text.chars();
if let (Some(ch), None) = (chars.next(), chars.next()) {
return vec![ch];
}
let spelled = crate::text_utils::expand_ligatures(text);
('\u{FB00}'..='\u{FB06}')
.filter(|ligature| crate::text_utils::expand_ligatures(&ligature.to_string()) == spelled)
.collect()
}
fn glyph_name_reads_as(name: &str, text: &str) -> bool {
glyph_name_to_string(name).is_some_and(|named| {
crate::text_utils::expand_ligatures(&named) == crate::text_utils::expand_ligatures(text)
})
}
fn program_reads_cmap_as_written(
cmap: &ToUnicodeCMap,
renumbered: &ToUnicodeCMap,
program: &[u8],
) -> Option<bool> {
let face = ttf_parser::Face::parse(program, 0).ok()?;
let glyphs = face.number_of_glyphs();
let reads_as = |gid: u16, text: &str| {
let glyph = ttf_parser::GlyphId(gid);
cmap_chars_reading_as(text)
.into_iter()
.any(|ch| face.glyph_index(ch) == Some(glyph))
|| face
.glyph_name(glyph)
.is_some_and(|name| glyph_name_reads_as(name, text))
};
let agreeing = |cmap: &ToUnicodeCMap| {
cmap.mapped_runs()
.into_iter()
.flat_map(|(first, last)| first.max(1)..=last.min(glyphs.saturating_sub(1)))
.take(MAX_PREMISE_CHECK_CODES)
.filter(|&code| {
cmap.lookup(code).is_some_and(|text| {
!text.is_empty() && !text.contains('\u{FFFD}') && reads_as(code, &text)
})
})
.count()
};
match agreeing(cmap).cmp(&agreeing(renumbered)) {
std::cmp::Ordering::Greater => Some(true),
std::cmp::Ordering::Less => Some(false),
std::cmp::Ordering::Equal => None,
}
}
fn build_cmap_with_cid_to_gid_map(
cmap: &ToUnicodeCMap,
cid_to_gid: &[u16],
) -> Option<ToUnicodeCMap> {
let mut new_cmap = ToUnicodeCMap::new();
for (cid, &gid) in cid_to_gid.iter().enumerate() {
if let Some(s) = cmap.destination(gid) {
new_cmap.char_map.insert(cid as u16, s);
}
}
if new_cmap.char_map.is_empty() {
None
} else {
new_cmap.code_byte_length = 2;
new_cmap.refresh_gap_fills();
Some(new_cmap)
}
}
fn try_remap_subset_cmap<'p>(
cmap: ToUnicodeCMap,
font_dict: &lopdf::Dictionary,
doc: &Document,
obj_num: u32,
program: impl FnOnce() -> Option<&'p [u8]>,
) -> (ToUnicodeCMap, Option<ToUnicodeCMap>) {
let encoding = font_dict
.get(b"Encoding")
.ok()
.and_then(|o| o.as_name().ok());
if encoding != Some(b"Identity-H") && encoding != Some(b"Identity-V") {
return (cmap, None);
}
let min_cid = match cmap.min_source_cid() {
Some(c) if c > 2 => c,
_ => return (cmap, None),
};
let cid_font_dict = match get_descendant_cid_font(font_dict, doc) {
Some(d) => d,
None => return (cmap, None),
};
let subtype = cid_font_dict.get(b"Subtype").ok().and_then(|o| match o {
Object::Reference(r) => doc.get_object(*r).ok().and_then(|o| o.as_name().ok()),
other => other.as_name().ok(),
});
if subtype.is_some_and(|name| name != b"CIDFontType2") {
debug!("Subset remap skipped for obj={obj_num}: descendant is not CIDFontType2");
return (cmap, None);
}
let cid_to_gid = get_cid_to_gid_map(cid_font_dict, doc);
if let Some(cid_to_gid) = &cid_to_gid {
if cmap_keyed_by_glyph_index(&cmap, cid_to_gid) == Some(false) {
debug!("CIDToGIDMap repair skipped for obj={obj_num}: the CMap is keyed by CID");
return (cmap, None);
}
if let Some(repaired) = build_cmap_with_cid_to_gid_map(&cmap, cid_to_gid) {
debug!(
"CIDToGIDMap repair applied for obj={}: {} entries",
obj_num,
repaired.char_map.len()
);
return (cmap, Some(repaired));
}
}
let w_start = match get_w_array_start_cid(cid_font_dict, doc) {
Some(c) if c <= 2 => c,
_ => return (cmap, None),
};
if let Some(max_cid) = cmap.max_source_cid() {
if w_array_covers_cid(cid_font_dict, doc, max_cid) {
debug!(
"Subset remap skipped for obj={}: W array covers CMap max CID {}",
obj_num, max_cid
);
return (cmap, None);
}
}
let remapped = cmap.remap_to_sequential();
if cid_to_gid.is_none()
&& program().and_then(|program| program_reads_cmap_as_written(&cmap, &remapped, program))
== Some(true)
{
debug!(
"Subset remap skipped for obj={obj_num}: the embedded program reads the CMap's codes as written"
);
return (cmap, None);
}
debug!(
"Subset GID mismatch detected for obj={}: W starts at CID {}, CMap min CID {}. Remapping to sequential.",
obj_num, w_start, min_cid
);
(cmap, Some(remapped))
}
pub fn build_cmap_from_truetype(font_data: &[u8]) -> Option<ToUnicodeCMap> {
let face = ttf_parser::Face::parse(font_data, 0).ok()?;
let gid_to_unicode = build_gid_to_unicode(&face)?;
debug!(
"TrueType cmap: {} GID→Unicode entries",
gid_to_unicode.len()
);
let mut cmap = ToUnicodeCMap::new();
for (gid, text) in &gid_to_unicode {
cmap.char_map.insert(*gid, text.clone());
}
cmap.code_byte_length = 2; cmap.refresh_gap_fills();
Some(cmap)
}
fn build_simple_cmap_from_truetype(font_data: &[u8]) -> Option<ToUnicodeCMap> {
let face = ttf_parser::Face::parse(font_data, 0).ok()?;
let gid_to_unicode = build_gid_to_unicode(&face)?;
let mut cmap = ToUnicodeCMap::new();
let mut used_encoding_cmap = false;
if let Some(cmap_table) = face.tables().cmap {
for subtable in cmap_table.subtables {
if subtable.platform_id == ttf_parser::PlatformId::Macintosh
&& subtable.encoding_id == 0
{
for code in 0x20..=0xFF_u32 {
if let Some(gid) = subtable.glyph_index(code) {
if let Some(text) = gid_to_unicode.get(&gid.0) {
cmap.char_map
.entry(code as u16)
.or_insert_with(|| strip_pua_text(text));
}
}
}
used_encoding_cmap = true;
break;
}
}
if !used_encoding_cmap {
for subtable in cmap_table.subtables {
if subtable.platform_id == ttf_parser::PlatformId::Windows
&& subtable.encoding_id == 0
{
for code in 0x20..=0xFF_u32 {
if let Some(gid) = subtable.glyph_index(code + 0xF000) {
if let Some(text) = gid_to_unicode.get(&gid.0) {
cmap.char_map
.entry(code as u16)
.or_insert_with(|| strip_pua_text(text));
}
}
}
used_encoding_cmap = true;
break;
}
}
}
if !used_encoding_cmap {
for subtable in cmap_table.subtables {
if subtable.platform_id == ttf_parser::PlatformId::Windows
&& subtable.encoding_id == 1
{
for code in 0x20..=0xFF_u32 {
if let Some(gid) = subtable.glyph_index(code) {
if let Some(text) = gid_to_unicode.get(&gid.0) {
cmap.char_map
.entry(code as u16)
.or_insert_with(|| strip_pua_text(text));
}
}
}
used_encoding_cmap = true;
break;
}
}
}
}
if !used_encoding_cmap {
for (&gid, text) in &gid_to_unicode {
if gid <= 0xFF {
cmap.char_map.insert(gid, text.clone());
}
}
for gid_idx in 0..face.number_of_glyphs() {
let gid = ttf_parser::GlyphId(gid_idx);
let gid_val = gid.0;
if gid_val > 0xFF || cmap.char_map.contains_key(&gid_val) {
continue;
}
if let Some(name) = face.glyph_name(gid) {
if let Some(s) = glyph_name_to_string(name) {
cmap.char_map.insert(gid_val, s);
}
}
}
}
if cmap.char_map.is_empty() {
return None;
}
debug!(
"TrueType simple cmap: {} code→Unicode entries",
cmap.char_map.len()
);
cmap.code_byte_length = 1;
cmap.refresh_gap_fills();
Some(cmap)
}
fn strip_pua_text(text: &str) -> String {
let mut chars = text.chars();
match (chars.next(), chars.next()) {
(Some(ch), None) => strip_pua_char(ch).to_string(),
_ => text.to_string(),
}
}
fn strip_pua_char(ch: char) -> char {
let cp = ch as u32;
if (0xF000..=0xF0FF).contains(&cp) {
char::from_u32(cp - 0xF000).unwrap_or(ch)
} else {
ch
}
}
pub(crate) fn build_gid_to_unicode(face: &ttf_parser::Face<'_>) -> Option<HashMap<u16, String>> {
let cmap_chars = cmap_glyph_chars(face);
let mut gid_to_unicode: HashMap<u16, String> = HashMap::new();
for gid in (0..face.number_of_glyphs()).chain(cmap_chars.keys().copied()) {
if gid_to_unicode.contains_key(&gid) {
continue;
}
if let Some(text) = glyph_text(face, &cmap_chars, gid) {
gid_to_unicode.insert(gid, text);
}
}
if gid_to_unicode.is_empty() {
return None;
}
Some(gid_to_unicode)
}
pub(crate) fn cmap_glyph_chars(face: &ttf_parser::Face<'_>) -> HashMap<u16, char> {
let mut chars: HashMap<u16, char> = HashMap::new();
for subtable in face.tables().cmap.iter().flat_map(|cmap| cmap.subtables) {
let is_symbol =
subtable.platform_id == ttf_parser::PlatformId::Windows && subtable.encoding_id == 0;
if !subtable.is_unicode() && !is_symbol {
continue;
}
subtable.codepoints(|cp| {
if let Some(ch) = char::from_u32(cp) {
if let Some(gid) = subtable.glyph_index(cp) {
chars.entry(gid.0).or_insert(ch);
}
}
});
}
chars
}
pub(crate) fn glyph_text(
face: &ttf_parser::Face<'_>,
cmap_chars: &HashMap<u16, char>,
gid: u16,
) -> Option<String> {
let private_use = |c: char| matches!(c, '\u{E000}'..='\u{F8FF}');
let from_cmap = cmap_chars.get(&gid).copied();
if let Some(ch) = from_cmap.filter(|ch| !private_use(*ch)) {
return Some(ch.to_string());
}
face.glyph_name(ttf_parser::GlyphId(gid))
.and_then(glyph_name_to_string)
.or_else(|| from_cmap.map(|ch| ch.to_string()))
}
fn build_cmap_from_builtin_cmap(ordering: &str) -> Option<ToUnicodeCMap> {
let name = format!("Adobe-{}-UCS2.bcmap", ordering);
let data = read_builtin_cmap_file(&name)?;
let mut cmap = parse_binary_cmap(&data).ok()?;
if cmap.char_map.is_empty() && cmap.ranges.is_empty() {
return None;
}
cmap.code_byte_length = 2;
debug!(
"Built-in CMap {}: char_map={} ranges={}",
name,
cmap.char_map.len(),
cmap.ranges.len()
);
Some(cmap)
}
#[cfg(not(target_arch = "wasm32"))]
fn read_bcmap_override(name: &str) -> Option<Vec<u8>> {
let dir = std::env::var_os("PDF_INSPECTOR_BCMAPS_DIR")?;
std::fs::read(PathBuf::from(dir).join(name)).ok()
}
fn read_builtin_cmap_file(name: &str) -> Option<Cow<'static, [u8]>> {
#[cfg(not(target_arch = "wasm32"))]
if let Some(bytes) = read_bcmap_override(name) {
return Some(Cow::Owned(bytes));
}
BUILTIN_CMAPS
.get_file(name)
.map(|file| Cow::Borrowed(file.contents()))
}
fn parse_binary_cmap(data: &[u8]) -> Result<ToUnicodeCMap, String> {
let mut stream = BinaryCMapStream::new(data);
let _header = stream.read_byte().ok_or("unexpected EOF in bcmap header")?;
let mut cmap = ToUnicodeCMap::new();
let mut use_cmap: Option<String> = None;
while let Some(b) = stream.read_byte() {
let typ = b >> 5;
if typ == 7 {
match b & 0x1f {
0 => {
stream.read_string()?;
}
1 => {
let name = stream.read_string()?;
use_cmap = Some(name);
}
_ => {}
}
continue;
}
let sequence = (b & 0x10) != 0;
let data_size = (b & 0x0f) as usize;
if data_size + 1 > 16 {
return Err("invalid dataSize in bcmap".to_string());
}
let subitems = stream.read_number()? as usize;
match typ {
4 => {
for i in 0..subitems {
let src = stream.read_hex_number(1)?;
let dst = stream.read_hex_bytes(data_size + 1)?;
let src_code = hex_to_u32(&src) as u16;
if let Some(s) = bytes_to_unicode_string(&dst) {
cmap.char_map.insert(src_code, s);
}
if i + 1 < subitems && sequence {
}
}
}
5 => {
for _ in 0..subitems {
let start = stream.read_hex_number(1)?;
let end_delta = stream.read_hex_number(1)?;
let mut end = start.clone();
add_hex(&mut end, &end_delta);
let dst = stream.read_hex_bytes(data_size + 1)?;
let start_code = hex_to_u32(&start) as u16;
let end_code = hex_to_u32(&end) as u16;
if let Some(s) = bytes_to_unicode_string(&dst) {
if s.chars().count() == 1 {
let base = s.chars().next().unwrap() as u32;
cmap.ranges.push((start_code, end_code, base));
} else {
let mut cid = start_code;
for ch in s.chars() {
cmap.char_map.insert(cid, ch.to_string());
if cid == end_code {
break;
}
cid = cid.saturating_add(1);
}
}
}
}
}
_ => {
for _ in 0..subitems {
if typ <= 3 {
let _ = stream.read_hex_number(data_size)?;
let _ = stream.read_hex_number(data_size)?;
if typ >= 1 {
let _ = stream.read_number()?;
}
}
}
}
}
}
cmap.ranges.sort_unstable_by_key(|&(start, _, _)| start);
if let Some(name) = use_cmap {
if let Some(base) = load_builtin_cmap_by_name(&name) {
cmap = merge_cmaps(base, cmap);
} else {
warn!("bcmap usecmap={} could not be loaded", name);
}
}
cmap.refresh_gap_fills();
Ok(cmap)
}
struct BinaryCMapStream<'a> {
data: &'a [u8],
pos: usize,
}
impl<'a> BinaryCMapStream<'a> {
fn new(data: &'a [u8]) -> Self {
Self { data, pos: 0 }
}
fn read_byte(&mut self) -> Option<u8> {
if self.pos >= self.data.len() {
None
} else {
let b = self.data[self.pos];
self.pos += 1;
Some(b)
}
}
fn read_number(&mut self) -> Result<u32, String> {
let mut n = 0u32;
loop {
let b = self.read_byte().ok_or("unexpected EOF in bcmap")?;
let last = (b & 0x80) == 0;
n = (n << 7) | (b & 0x7f) as u32;
if last {
break;
}
}
Ok(n)
}
fn read_hex_number(&mut self, size: usize) -> Result<Vec<u8>, String> {
let mut stack = Vec::new();
loop {
let b = self.read_byte().ok_or("unexpected EOF in bcmap")?;
let last = (b & 0x80) == 0;
stack.push(b & 0x7f);
if last {
break;
}
}
let mut out = vec![0u8; size + 1];
let mut buffer = 0u32;
let mut buffer_size = 0u32;
let mut i: i32 = size as i32;
while i >= 0 {
while buffer_size < 8 && !stack.is_empty() {
buffer |= (stack.pop().unwrap() as u32) << buffer_size;
buffer_size += 7;
}
out[i as usize] = (buffer & 0xff) as u8;
buffer >>= 8;
buffer_size = buffer_size.saturating_sub(8);
i -= 1;
}
Ok(out)
}
fn read_hex_bytes(&mut self, len: usize) -> Result<Vec<u8>, String> {
if self.pos + len > self.data.len() {
return Err("unexpected EOF in bcmap".to_string());
}
let out = self.data[self.pos..self.pos + len].to_vec();
self.pos += len;
Ok(out)
}
fn read_string(&mut self) -> Result<String, String> {
let len = self.read_number()? as usize;
let mut buf = Vec::with_capacity(len);
for _ in 0..len {
let v = self.read_number()? as u8;
buf.push(v);
}
String::from_utf8(buf).map_err(|e| e.to_string())
}
}
fn hex_to_u32(bytes: &[u8]) -> u32 {
let mut n = 0u32;
for &b in bytes {
n = (n << 8) | b as u32;
}
n
}
fn add_hex(a: &mut [u8], b: &[u8]) {
let mut c = 0u16;
for i in (0..a.len()).rev() {
c += a[i] as u16 + b[i] as u16;
a[i] = (c & 0xff) as u8;
c >>= 8;
}
}
fn bytes_to_unicode_string(bytes: &[u8]) -> Option<String> {
if bytes.is_empty() {
return None;
}
if !bytes.len().is_multiple_of(2) {
return Some(bytes.iter().map(|&b| b as char).collect());
}
let mut out = String::new();
for chunk in bytes.as_chunks::<2>().0 {
let cp = u16::from_be_bytes(*chunk) as u32;
if let Some(ch) = char::from_u32(cp) {
out.push(ch);
}
}
if out.is_empty() {
None
} else {
Some(out)
}
}
#[derive(Debug, Clone)]
struct EncodingCMap {
map: HashMap<u16, u16>,
code_byte_length: u8,
is_identity: bool,
}
fn build_fallback_tounicode_from_encoding(
font_dict: &lopdf::Dictionary,
doc: &Document,
) -> Option<ToUnicodeCMap> {
let encoding = build_encoding_cmap_from_font(font_dict, doc)?;
let ordering = get_cid_system_info_ordering(font_dict, doc)?;
let ucs2 = build_cmap_from_builtin_cmap(&ordering)?;
if encoding.is_identity {
return Some(ucs2);
}
let mut cmap = ToUnicodeCMap::new();
for (charcode, cid) in encoding.map {
if let Some(s) = ucs2.lookup(cid) {
cmap.char_map.insert(charcode, s);
}
}
if cmap.char_map.is_empty() {
return None;
}
cmap.code_byte_length = encoding.code_byte_length;
cmap.refresh_gap_fills();
Some(cmap)
}
fn get_cid_system_info_ordering(font_dict: &lopdf::Dictionary, doc: &Document) -> Option<String> {
let cid_font_dict = get_descendant_cid_font(font_dict, doc)?;
let csi_obj = cid_font_dict.get(b"CIDSystemInfo").ok()?;
let csi_dict = match csi_obj {
Object::Reference(r) => doc.get_dictionary(*r).ok()?,
Object::Dictionary(d) => d,
_ => return None,
};
let ordering = csi_dict.get(b"Ordering").ok().and_then(|o| {
if let Object::String(bytes, _) = o {
Some(String::from_utf8_lossy(bytes).to_string())
} else {
None
}
})?;
Some(ordering)
}
fn build_encoding_cmap_from_font(
font_dict: &lopdf::Dictionary,
doc: &Document,
) -> Option<EncodingCMap> {
let encoding_obj = font_dict.get(b"Encoding").ok()?;
match encoding_obj {
Object::Name(name) => {
let enc = name.as_slice();
if enc == b"Identity-H" || enc == b"Identity-V" {
return Some(EncodingCMap {
map: HashMap::new(),
code_byte_length: 2,
is_identity: true,
});
}
let enc_name = String::from_utf8_lossy(enc).to_string();
load_builtin_encoding_cmap(&enc_name)
}
Object::Reference(r) => {
let obj = doc.get_object(*r).ok()?;
parse_encoding_cmap_object(obj, doc)
}
Object::Stream(s) => parse_encoding_cmap_stream(&s.decompressed_content().ok()?),
Object::Dictionary(_) => None,
_ => None,
}
}
fn parse_encoding_cmap_object(obj: &Object, doc: &Document) -> Option<EncodingCMap> {
match obj {
Object::Stream(s) => parse_encoding_cmap_stream(&s.decompressed_content().ok()?),
Object::Reference(r) => {
let obj = doc.get_object(*r).ok()?;
parse_encoding_cmap_object(obj, doc)
}
_ => None,
}
}
fn load_builtin_encoding_cmap(name: &str) -> Option<EncodingCMap> {
let data = read_builtin_cmap_file(&format!("{}.bcmap", name))?;
parse_binary_cmap_encoding(&data).ok()
}
fn parse_encoding_cmap_stream(data: &[u8]) -> Option<EncodingCMap> {
let text = String::from_utf8_lossy(data);
let mut src_hex_lengths: Vec<usize> = Vec::new();
let mut codespace_byte_len: Option<u8> = None;
if let Some(cs_start) = text.find("begincodespacerange") {
let section_start = cs_start + "begincodespacerange".len();
if let Some(cs_end) = text[section_start..].find("endcodespacerange") {
let section = &text[section_start..section_start + cs_end];
let mut in_hex = false;
let mut hex_len = 0;
for c in section.chars() {
if c == '<' {
in_hex = true;
hex_len = 0;
} else if c == '>' {
if in_hex && hex_len > 0 {
let byte_len = (hex_len + 1) / 2;
codespace_byte_len = Some(byte_len as u8);
}
in_hex = false;
} else if in_hex && c.is_ascii_hexdigit() {
hex_len += 1;
}
}
}
}
let mut map = HashMap::new();
let mut assigned = 0usize;
let mut pos = 0;
while let Some(start) = text[pos..].find("begincidchar") {
let section_start = pos + start + "begincidchar".len();
if let Some(end) = text[section_start..].find("endcidchar") {
let section = &text[section_start..section_start + end];
if !parse_cidchar_section(section, &mut map, &mut src_hex_lengths, &mut assigned) {
break;
}
pos = section_start + end;
} else {
break;
}
}
pos = 0;
while assigned < MAX_CID_W_EXPANSION {
let Some(start) = text[pos..].find("begincidrange") else {
break;
};
let section_start = pos + start + "begincidrange".len();
if let Some(end) = text[section_start..].find("endcidrange") {
let section = &text[section_start..section_start + end];
if !parse_cidrange_section(section, &mut map, &mut src_hex_lengths, &mut assigned) {
break;
}
pos = section_start + end;
} else {
break;
}
}
if map.is_empty() {
return None;
}
let code_byte_length = if let Some(cs_len) = codespace_byte_len {
cs_len
} else if !src_hex_lengths.is_empty() {
let max_hex_len = src_hex_lengths.iter().max().copied().unwrap_or(4);
if max_hex_len <= 2 {
1
} else {
2
}
} else {
2
};
Some(EncodingCMap {
map,
code_byte_length,
is_identity: false,
})
}
fn parse_cidchar_section(
section: &str,
map: &mut HashMap<u16, u16>,
src_hex_lengths: &mut Vec<usize>,
assigned: &mut usize,
) -> bool {
let mut chars = section.chars().peekable();
loop {
while chars.peek().is_some_and(|c| c.is_whitespace()) {
chars.next();
}
if chars.peek() != Some(&'<') {
break;
}
chars.next();
let mut src_hex = String::new();
while chars.peek().is_some_and(|&c| c != '>') {
if let Some(c) = chars.next() {
src_hex.push(c);
}
}
chars.next();
let trimmed_src = src_hex.trim();
if !trimmed_src.is_empty() {
src_hex_lengths.push(trimmed_src.len());
}
while chars.peek().is_some_and(|c| c.is_whitespace()) {
chars.next();
}
let mut cid_str = String::new();
while chars.peek().is_some_and(|c| !c.is_whitespace()) {
if let Some(c) = chars.next() {
cid_str.push(c);
}
}
if let (Some(code), Ok(cid)) = (parse_hex_u16(&src_hex), cid_str.parse::<u16>()) {
if !assign_encoding_cid(map, code, cid, assigned) {
return false;
}
}
}
true
}
fn parse_cidrange_section(
section: &str,
map: &mut HashMap<u16, u16>,
src_hex_lengths: &mut Vec<usize>,
assigned: &mut usize,
) -> bool {
let mut chars = section.chars().peekable();
loop {
while chars.peek().is_some_and(|c| c.is_whitespace()) {
chars.next();
}
if chars.peek() != Some(&'<') {
break;
}
chars.next();
let mut start_hex = String::new();
while chars.peek().is_some_and(|&c| c != '>') {
if let Some(c) = chars.next() {
start_hex.push(c);
}
}
chars.next();
let trimmed_start = start_hex.trim();
if !trimmed_start.is_empty() {
src_hex_lengths.push(trimmed_start.len());
}
while chars.peek().is_some_and(|c| c.is_whitespace()) {
chars.next();
}
if chars.peek() != Some(&'<') {
continue;
}
chars.next();
let mut end_hex = String::new();
while chars.peek().is_some_and(|&c| c != '>') {
if let Some(c) = chars.next() {
end_hex.push(c);
}
}
chars.next();
while chars.peek().is_some_and(|c| c.is_whitespace()) {
chars.next();
}
let mut cid_str = String::new();
while chars.peek().is_some_and(|c| !c.is_whitespace()) {
if let Some(c) = chars.next() {
cid_str.push(c);
}
}
let (Some(start), Some(end), Ok(start_cid)) = (
parse_hex_u16(&start_hex),
parse_hex_u16(&end_hex),
cid_str.parse::<u16>(),
) else {
continue;
};
if start > end {
continue;
}
let mut cid = start_cid;
for code in start..=end {
if !assign_encoding_cid(map, code, cid, assigned) {
return false;
}
cid = cid.saturating_add(1);
}
}
true
}
fn assign_encoding_cid(
map: &mut HashMap<u16, u16>,
code: u16,
cid: u16,
assigned: &mut usize,
) -> bool {
if *assigned >= MAX_CID_W_EXPANSION {
return false;
}
map.insert(code, cid);
*assigned += 1;
true
}
fn parse_binary_cmap_encoding(data: &[u8]) -> Result<EncodingCMap, String> {
let mut stream = BinaryCMapStream::new(data);
let _header = stream.read_byte().ok_or("unexpected EOF in bcmap header")?;
let mut map: HashMap<u16, u16> = HashMap::new();
let mut max_code_size: u8 = 1;
let mut use_cmap: Option<String> = None;
while let Some(b) = stream.read_byte() {
let typ = b >> 5;
if typ == 7 {
match b & 0x1f {
0 => {
stream.read_string()?;
}
1 => {
let name = stream.read_string()?;
use_cmap = Some(name);
}
_ => {}
}
continue;
}
let _sequence = (b & 0x10) != 0;
let data_size = (b & 0x0f) as usize;
if data_size + 1 > 16 {
return Err("invalid dataSize in bcmap".to_string());
}
max_code_size = max_code_size.max((data_size + 1) as u8);
let subitems = stream.read_number()? as usize;
match typ {
2 => {
let mut prev_code: u32 = 0;
for i in 0..subitems {
let code_bytes = stream.read_hex_number(data_size)?;
let code = hex_to_u32(&code_bytes);
let cid = stream.read_number()? as u16;
if i == 0 {
prev_code = code;
map.insert(code as u16, cid);
continue;
}
if _sequence {
prev_code = prev_code.saturating_add(1);
map.insert(prev_code as u16, cid);
} else {
map.insert(code as u16, cid);
prev_code = code;
}
}
}
3 => {
for _ in 0..subitems {
let start = stream.read_hex_number(data_size)?;
let end_delta = stream.read_hex_number(data_size)?;
let mut end = start.clone();
add_hex(&mut end, &end_delta);
let cid_start = stream.read_number()? as u16;
let start_code = hex_to_u32(&start) as u16;
let end_code = hex_to_u32(&end) as u16;
let mut cid = cid_start;
for code in start_code..=end_code {
map.insert(code, cid);
cid = cid.saturating_add(1);
}
}
}
_ => {
for _ in 0..subitems {
let _ = stream.read_hex_number(data_size)?;
let _ = stream.read_hex_number(data_size)?;
let _ = stream.read_number()?;
}
}
}
}
if let Some(name) = use_cmap {
if let Some(base) = load_builtin_encoding_cmap(&name) {
let mut merged = base.map;
merged.extend(map);
return Ok(EncodingCMap {
map: merged,
code_byte_length: base.code_byte_length.max(max_code_size),
is_identity: false,
});
}
}
Ok(EncodingCMap {
map,
code_byte_length: max_code_size,
is_identity: false,
})
}
fn load_builtin_cmap_by_name(name: &str) -> Option<ToUnicodeCMap> {
if !name.ends_with("UCS2") {
return None;
}
let data = read_builtin_cmap_file(&format!("{}.bcmap", name))?;
let mut cmap = parse_binary_cmap(&data).ok()?;
if cmap.char_map.is_empty() && cmap.ranges.is_empty() {
return None;
}
cmap.code_byte_length = 2;
Some(cmap)
}
fn merge_cmaps(mut base: ToUnicodeCMap, overlay: ToUnicodeCMap) -> ToUnicodeCMap {
for (cid, s) in overlay.char_map {
base.char_map.insert(cid, s);
}
base.ranges.extend(overlay.ranges);
base.ranges.sort_unstable_by_key(|&(start, _, _)| start);
base.code_byte_length = base.code_byte_length.max(overlay.code_byte_length);
base.refresh_gap_fills();
base
}
pub(crate) const MAX_CID_W_EXPANSION: usize = 65_536;
pub(crate) fn cid_values_look_like_unicode(cid_font_dict: &lopdf::Dictionary) -> bool {
let w_arr = match cid_font_dict.get(b"W").ok() {
Some(Object::Array(arr)) => arr,
_ => return false,
};
let mut seen = HashSet::new();
let mut i = 0;
while i < w_arr.len() && seen.len() < MAX_CID_W_EXPANSION {
if let Ok(cid) = w_arr[i].as_i64() {
let start = cid as u16;
if i + 1 < w_arr.len() {
match &w_arr[i + 1] {
Object::Array(widths) => {
for j in 0..widths.len() {
if seen.len() >= MAX_CID_W_EXPANSION {
break;
}
seen.insert(start.wrapping_add(j as u16));
}
i += 2;
}
_ => {
if i + 2 < w_arr.len() {
if let Ok(cid_end) = w_arr[i + 1].as_i64() {
record_unique_cid_range(start, cid_end as u16, &mut seen);
}
i += 3;
} else {
i += 1;
}
}
}
} else {
seen.insert(start);
i += 1;
}
} else {
i += 1;
}
}
if seen.is_empty() {
return false;
}
let mut cids: Vec<u16> = seen.into_iter().collect();
cids.sort_unstable();
let median = cids[cids.len() / 2];
median >= 0x41
}
fn record_unique_cid_range(start: u16, end: u16, seen: &mut HashSet<u16>) {
if start > end {
return;
}
for cid in start..=end {
if seen.len() >= MAX_CID_W_EXPANSION {
return;
}
seen.insert(cid);
}
}
fn build_cmap_from_cid_system_info(
cid_font_dict: &lopdf::Dictionary,
doc: &Document,
) -> Option<ToUnicodeCMap> {
let csi_obj = cid_font_dict.get(b"CIDSystemInfo").ok()?;
let csi_dict = match csi_obj {
Object::Reference(r) => doc.get_dictionary(*r).ok()?,
Object::Dictionary(d) => d,
_ => return None,
};
let ordering = csi_dict.get(b"Ordering").ok().and_then(|o| {
if let Object::String(bytes, _) = o {
Some(String::from_utf8_lossy(bytes).to_string())
} else {
None
}
})?;
match ordering.as_str() {
"Korea1" => {
use crate::adobe_korea1::ADOBE_KOREA1_CID_TO_UNICODE;
let mut cmap = ToUnicodeCMap::new();
for &(cid, unicode) in ADOBE_KOREA1_CID_TO_UNICODE.iter() {
if let Some(ch) = char::from_u32(unicode as u32) {
cmap.char_map.insert(cid, ch.to_string());
}
}
cmap.code_byte_length = 2;
cmap.refresh_gap_fills();
debug!(
"Adobe-Korea1 predefined CMap: {} entries",
cmap.char_map.len()
);
Some(cmap)
}
"Japan1" | "GB1" | "CNS1" => build_cmap_from_builtin_cmap(&ordering),
_ => None,
}
}
#[derive(Debug, Default, Clone)]
pub struct FontCMaps {
by_obj_num: HashMap<u32, CMapEntry>,
}
#[derive(Debug, Clone)]
pub struct CMapEntry {
pub primary: ToUnicodeCMap,
pub remapped: Option<ToUnicodeCMap>,
pub fallback: Option<ToUnicodeCMap>,
}
impl FontCMaps {
pub fn from_doc(doc: &Document) -> Self {
Self::from_doc_pages(doc, None)
}
pub fn from_doc_pages(doc: &Document, page_filter: Option<&HashSet<u32>>) -> Self {
Self::from_doc_pages_inner(doc, page_filter, false)
}
pub fn from_doc_pages_fast(doc: &Document, page_filter: Option<&HashSet<u32>>) -> Self {
Self::from_doc_pages_inner(doc, page_filter, true)
}
fn from_doc_pages_inner(
doc: &Document,
page_filter: Option<&HashSet<u32>>,
skip_truetype_fallback: bool,
) -> Self {
let mut by_obj_num: HashMap<u32, CMapEntry> = HashMap::new();
for (page_num, &page_id) in doc.get_pages().iter() {
if let Some(filter) = page_filter {
if !filter.contains(page_num) {
continue;
}
}
let fonts = doc.get_page_fonts(page_id).unwrap_or_default();
Self::collect_cmaps_from_fonts_inner(
&fonts,
doc,
&mut by_obj_num,
skip_truetype_fallback,
);
if !skip_truetype_fallback {
Self::collect_cmaps_from_xobjects(doc, page_id, &mut by_obj_num);
}
}
FontCMaps { by_obj_num }
}
fn collect_cmaps_from_fonts(
fonts: &std::collections::BTreeMap<Vec<u8>, &lopdf::Dictionary>,
doc: &Document,
by_obj_num: &mut HashMap<u32, CMapEntry>,
) {
Self::collect_cmaps_from_fonts_inner(fonts, doc, by_obj_num, false);
}
fn collect_cmaps_from_fonts_inner(
fonts: &std::collections::BTreeMap<Vec<u8>, &lopdf::Dictionary>,
doc: &Document,
by_obj_num: &mut HashMap<u32, CMapEntry>,
skip_truetype_fallback: bool,
) {
for font_dict in fonts.values() {
let obj_ref = match font_dict
.get(b"ToUnicode")
.ok()
.and_then(|o| o.as_reference().ok())
{
Some(r) => r,
None => continue,
};
let obj_num = obj_ref.0;
if by_obj_num.contains_key(&obj_num) {
continue;
}
let stream = match doc.get_object(obj_ref).and_then(Object::as_stream) {
Ok(s) => s,
Err(_) => continue,
};
let data = match stream.decompressed_content() {
Ok(d) => d,
Err(_) => stream.content.clone(),
};
if let Some(cmap) = ToUnicodeCMap::parse(&data) {
debug!(
"CMap obj={:<6} code_byte_length={} char_map={} ranges={}",
obj_num,
cmap.code_byte_length,
cmap.char_map.len(),
cmap.ranges.len()
);
let is_type0 = font_subtype(font_dict) == Some(&b"Type0"[..]);
let entry = font_cmap_entry(
cmap,
font_dict,
doc,
obj_num,
EntryBuild {
promote: false,
program_fallback: if skip_truetype_fallback && is_type0 {
ProgramFallback::Never
} else {
ProgramFallback::WhenSparse
},
program_repair: !skip_truetype_fallback,
},
);
by_obj_num.insert(obj_num, entry);
} else {
let fallback = if skip_truetype_fallback {
build_fallback_cmap_for_simple(font_dict, doc)
} else {
build_fallback_cmap_for_type0(font_dict, doc)
.or_else(|| build_fallback_cmap_for_simple(font_dict, doc))
};
if let Some(fb) = fallback {
debug!(
"ToUnicode CMap obj={} parse failed; using fallback (entries={})",
obj_num,
fb.char_map.len()
);
by_obj_num.insert(
obj_num,
CMapEntry {
primary: fb,
remapped: None,
fallback: None,
},
);
}
}
}
if skip_truetype_fallback {
return;
}
for font_dict in fonts.values() {
if font_dict.get(b"ToUnicode").is_ok() {
continue;
}
let encoding = match font_dict
.get(b"Encoding")
.ok()
.and_then(|o| o.as_name().ok())
{
Some(name) => name,
None => continue,
};
if encoding != b"Identity-H" && encoding != b"Identity-V" {
continue;
}
let desc_fonts_obj = match font_dict.get(b"DescendantFonts").ok() {
Some(obj) => obj,
None => continue,
};
let desc_fonts = match desc_fonts_obj {
Object::Array(arr) => arr.clone(),
Object::Reference(r) => match doc.get_object(*r) {
Ok(Object::Array(arr)) => arr.clone(),
_ => continue,
},
_ => continue,
};
if desc_fonts.is_empty() {
continue;
}
let cid_font_dict = match &desc_fonts[0] {
Object::Reference(r) => match doc.get_dictionary(*r) {
Ok(d) => d,
_ => continue,
},
Object::Dictionary(d) => d,
_ => continue,
};
let font_descriptor = cid_font_dict
.get(b"FontDescriptor")
.ok()
.and_then(|o| match o {
Object::Reference(r) => doc.get_dictionary(*r).ok(),
Object::Dictionary(d) => Some(d),
_ => None,
});
let mut resolved = false;
let font_file_ref = font_descriptor.and_then(|fd| {
fd.get(b"FontFile2")
.ok()
.and_then(|o| o.as_reference().ok())
.or_else(|| {
fd.get(b"FontFile3")
.ok()
.and_then(|o| o.as_reference().ok())
})
});
let lookup_key = font_file_ref
.map(|r| r.0)
.unwrap_or_else(|| match &desc_fonts[0] {
Object::Reference(r) => r.0,
_ => 0,
});
if lookup_key == 0 || by_obj_num.contains_key(&lookup_key) {
continue;
}
let font_data = font_file_ref.and_then(|ff_ref| {
let stream = doc.get_object(ff_ref).and_then(Object::as_stream).ok()?;
Some(
stream
.decompressed_content()
.unwrap_or_else(|_| stream.content.clone()),
)
});
if let Some(data) = font_data.as_deref() {
if let Some(cmap) = build_cmap_from_truetype(data) {
debug!(
"TrueType CMap obj={:<6} (embedded font) char_map={}",
lookup_key,
cmap.char_map.len()
);
by_obj_num.insert(
lookup_key,
CMapEntry {
primary: cmap,
remapped: None,
fallback: None,
},
);
resolved = true;
}
}
if !resolved {
if let Some(cmap) = build_cmap_from_cid_system_info(cid_font_dict, doc) {
debug!(
"Predefined CMap obj={:<6} (CIDSystemInfo) char_map={}",
lookup_key,
cmap.char_map.len()
);
by_obj_num.insert(
lookup_key,
CMapEntry {
primary: cmap,
remapped: None,
fallback: None,
},
);
resolved = true;
}
}
if !resolved && crate::mac_glyph_order::cid_to_gid_is_identity(cid_font_dict, doc) {
if let Some(cmap) = font_data
.as_deref()
.and_then(crate::mac_glyph_order::build_cmap_from_mac_glyph_order)
{
debug!(
"Standard Macintosh glyph order obj={:<6} (embedded font without cmap) char_map={}",
lookup_key,
cmap.char_map.len()
);
by_obj_num.insert(
lookup_key,
CMapEntry {
primary: cmap,
remapped: None,
fallback: None,
},
);
resolved = true;
}
}
if !resolved {
if cid_values_look_like_unicode(cid_font_dict) {
debug!(
"Identity-H font obj={}: W array CIDs look like Unicode — using passthrough",
lookup_key
);
let mut cmap = ToUnicodeCMap::new();
cmap.code_byte_length = 2;
cmap.cid_passthrough = true;
by_obj_num.insert(
lookup_key,
CMapEntry {
primary: cmap,
remapped: None,
fallback: None,
},
);
} else {
debug!(
"Identity-H font obj={}: no decoding possible (stripped cmap, GID-based CIDs)",
lookup_key
);
}
}
}
for font_dict in fonts.values() {
if font_dict.get(b"ToUnicode").is_ok() {
continue;
}
if let Ok(enc) = font_dict.get(b"Encoding") {
if enc.as_name().is_ok() || enc.as_dict().is_ok() || enc.as_reference().is_ok() {
continue;
}
}
let subtype = match font_dict
.get(b"Subtype")
.ok()
.and_then(|o| o.as_name().ok())
{
Some(name) => name,
None => continue,
};
if subtype == b"Type0" {
continue;
}
let font_descriptor = font_dict.get(b"FontDescriptor").ok().and_then(|o| match o {
Object::Reference(r) => doc.get_dictionary(*r).ok(),
Object::Dictionary(d) => Some(d),
_ => None,
});
let font_file_ref = font_descriptor.and_then(|fd| {
fd.get(b"FontFile2")
.ok()
.and_then(|o| o.as_reference().ok())
.or_else(|| {
fd.get(b"FontFile3")
.ok()
.and_then(|o| o.as_reference().ok())
})
});
let ff_ref = match font_file_ref {
Some(r) => r,
None => continue,
};
let lookup_key = ff_ref.0;
if by_obj_num.contains_key(&lookup_key) {
continue;
}
if let Ok(stream) = doc.get_object(ff_ref).and_then(Object::as_stream) {
if let Ok(data) = stream.decompressed_content() {
if let Some(cmap) = build_simple_cmap_from_truetype(&data) {
debug!(
"Simple font cmap obj={:<6} (embedded font) char_map={}",
lookup_key,
cmap.char_map.len()
);
by_obj_num.insert(
lookup_key,
CMapEntry {
primary: cmap,
remapped: None,
fallback: None,
},
);
}
}
}
}
}
fn collect_cmaps_from_xobjects(
doc: &Document,
page_id: ObjectId,
by_obj_num: &mut HashMap<u32, CMapEntry>,
) {
let (resource_dict, resource_ids) = match doc.get_page_resources(page_id) {
Ok(r) => r,
Err(_) => return,
};
let mut visited = HashSet::new();
if let Some(resources) = resource_dict {
Self::walk_xobject_fonts(resources, doc, by_obj_num, &mut visited);
}
for resource_id in resource_ids {
if let Ok(resources) = doc.get_dictionary(resource_id) {
Self::walk_xobject_fonts(resources, doc, by_obj_num, &mut visited);
}
}
}
fn walk_xobject_fonts(
resources: &lopdf::Dictionary,
doc: &Document,
by_obj_num: &mut HashMap<u32, CMapEntry>,
visited: &mut HashSet<ObjectId>,
) {
let xobject_dict = match resources.get(b"XObject") {
Ok(Object::Reference(id)) => doc.get_object(*id).and_then(Object::as_dict).ok(),
Ok(Object::Dictionary(dict)) => Some(dict),
_ => None,
};
let xobject_dict = match xobject_dict {
Some(d) => d,
None => return,
};
for (_name, value) in xobject_dict.iter() {
let id = match value {
Object::Reference(id) => *id,
_ => continue,
};
if !visited.insert(id) {
continue;
}
let stream = match doc.get_object(id).and_then(Object::as_stream) {
Ok(s) => s,
Err(_) => continue,
};
let is_form = stream
.dict
.get(b"Subtype")
.and_then(|o| o.as_name())
.is_ok_and(|n| n == b"Form");
if !is_form {
continue;
}
if let Ok(form_resources) = stream.dict.get(b"Resources").and_then(Object::as_dict) {
let font_dict_obj = match form_resources.get(b"Font") {
Ok(Object::Reference(id)) => doc.get_object(*id).and_then(Object::as_dict).ok(),
Ok(Object::Dictionary(dict)) => Some(dict),
_ => None,
};
if let Some(font_dict) = font_dict_obj {
let mut fonts = std::collections::BTreeMap::new();
for (name, value) in font_dict.iter() {
let font = match value {
Object::Reference(id) => doc.get_dictionary(*id).ok(),
Object::Dictionary(dict) => Some(dict),
_ => None,
};
if let Some(font) = font {
fonts.insert(name.clone(), font);
}
}
Self::collect_cmaps_from_fonts(&fonts, doc, by_obj_num);
}
Self::walk_xobject_fonts(form_resources, doc, by_obj_num, visited);
}
}
}
pub fn get_by_obj(&self, obj_num: u32) -> Option<&CMapEntry> {
self.by_obj_num.get(&obj_num)
}
}
fn build_fallback_cmap_for_type0(
font_dict: &lopdf::Dictionary,
doc: &Document,
) -> Option<ToUnicodeCMap> {
if font_subtype(font_dict)? != b"Type0" {
return None;
}
let program = embedded_font_program(font_dict, doc);
program_fallback_cmap(font_dict, doc, program.as_deref())
}
fn build_fallback_cmap_for_simple(
font_dict: &lopdf::Dictionary,
doc: &Document,
) -> Option<ToUnicodeCMap> {
if font_subtype(font_dict)? == b"Type0" {
return None;
}
let program = embedded_font_program(font_dict, doc)?;
program_fallback_cmap(font_dict, doc, Some(&program))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builtin_cmaps_are_embedded_in_the_binary() {
assert!(BUILTIN_CMAPS.files().count() >= 100);
for name in [
"Adobe-Japan1-UCS2.bcmap",
"Adobe-GB1-UCS2.bcmap",
"Adobe-CNS1-UCS2.bcmap",
"Adobe-Korea1-UCS2.bcmap",
"90ms-RKSJ-H.bcmap",
] {
let embedded = BUILTIN_CMAPS
.get_file(name)
.unwrap_or_else(|| panic!("{name} was not embedded"))
.contents();
let disk = std::fs::read(format!(
"{}/external/bcmaps/{name}",
env!("CARGO_MANIFEST_DIR")
))
.unwrap_or_else(|err| panic!("reading {name}: {err}"));
assert_eq!(embedded, disk.as_slice(), "{name}");
}
assert!(BUILTIN_CMAPS.get_file("does-not-exist.bcmap").is_none());
}
#[test]
fn embedded_glyph_names_of_several_letters_map_their_glyphs() {
let doc = lopdf::Document::load("tests/fixtures/ligature_glyph_names.pdf").unwrap();
let program = doc
.objects
.values()
.find_map(|object| {
let stream = object.as_stream().ok()?;
stream.dict.get(b"Length1").ok()?;
stream.decompressed_content().ok()
})
.expect("embedded program");
let face = ttf_parser::Face::parse(&program, 0).unwrap();
let map = build_gid_to_unicode(&face).expect("glyph map");
for (name, text) in [
("f_t", "ft"),
("f_f_i", "ffi"),
("T_h", "Th"),
("a.sc", "a"),
("uni00660069", "fi"),
("f_i.liga", "fi"),
("t_z", "tz"),
] {
let gid = face.glyph_index_by_name(name).expect(name).0;
assert_eq!(map.get(&gid).map(String::as_str), Some(text), "{name}");
}
}
#[test]
fn test_parse_bfchar_2byte() {
let cmap_content = r#"
/CIDInit /ProcSet findresource begin
12 dict begin
begincmap
1 begincodespacerange
<0000><FFFF>
endcodespacerange
3 beginbfchar
<0003> <0020>
<0024> <0041>
<0025> <0042>
endbfchar
endcmap
"#;
let cmap = ToUnicodeCMap::parse(cmap_content.as_bytes()).unwrap();
assert_eq!(cmap.code_byte_length, 2);
assert_eq!(cmap.lookup(0x0003), Some(" ".to_string()));
assert_eq!(cmap.lookup(0x0024), Some("A".to_string()));
assert_eq!(cmap.lookup(0x0025), Some("B".to_string()));
}
#[test]
fn test_hex_to_unicode_non_ascii_no_panic() {
assert_eq!(hex_to_unicode_string("XéY"), None);
assert_eq!(hex_to_unicode_string("\u{fffd}0"), None);
}
#[test]
fn test_parse_bfchar_non_ascii_destination_no_panic() {
let cmap_content = "beginbfchar <0041> <XéY> endbfchar";
let _ = ToUnicodeCMap::parse(cmap_content.as_bytes());
}
#[test]
fn test_parse_bfchar_1byte() {
let cmap_content = r#"
1 begincodespacerange
<0000> <FFFF>
endcodespacerange
3 beginbfchar
<20> <0020>
<41> <0041>
<42> <0042>
endbfchar
"#;
let cmap = ToUnicodeCMap::parse(cmap_content.as_bytes()).unwrap();
assert_eq!(cmap.code_byte_length, 1);
assert_eq!(cmap.lookup(0x0020), Some(" ".to_string()));
assert_eq!(cmap.lookup(0x0041), Some("A".to_string()));
}
#[test]
fn test_decode_cids_2byte() {
let cmap_content = r#"
1 begincodespacerange
<0000><FFFF>
endcodespacerange
3 beginbfchar
<0003> <0020>
<0024> <0041>
<0025> <0042>
endbfchar
"#;
let cmap = ToUnicodeCMap::parse(cmap_content.as_bytes()).unwrap();
let cids = [0x00, 0x24, 0x00, 0x25, 0x00, 0x03];
assert_eq!(cmap.decode_cids(&cids), "AB ");
}
#[test]
fn test_decode_cids_1byte_no_cjk_garbage() {
let cmap_content = r#"
1 begincodespacerange
<0000> <FFFF>
endcodespacerange
5 beginbfchar
<20> <0020>
<42> <0042>
<79> <0079>
<50> <0050>
<52> <0052>
endbfchar
"#;
let cmap = ToUnicodeCMap::parse(cmap_content.as_bytes()).unwrap();
assert_eq!(cmap.code_byte_length, 1);
let bytes = [0x42, 0x79];
let result = cmap.decode_cids(&bytes);
assert_eq!(result, "By");
assert!(!result.contains('䉹'), "Should not produce CJK garbage");
let bytes2 = [0x50, 0x52];
assert_eq!(cmap.decode_cids(&bytes2), "PR");
}
#[test]
fn test_bfrange_array_format() {
let cmap_content = r#"
1 begincodespacerange
<0000> <FFFF>
endcodespacerange
1 beginbfrange
<0003> <0005> [<0041> <0042> <0043>]
endbfrange
"#;
let cmap = ToUnicodeCMap::parse(cmap_content.as_bytes()).unwrap();
assert_eq!(cmap.lookup(0x0003), Some("A".to_string()));
assert_eq!(cmap.lookup(0x0004), Some("B".to_string()));
assert_eq!(cmap.lookup(0x0005), Some("C".to_string()));
}
#[test]
fn test_parse_bfchar_surrogate_pair_emoji() {
let cmap_content = r#"
1 begincodespacerange
<00> <FF>
endcodespacerange
2 beginbfchar
<16> <D83CDF1F>
<9D> <D83CDFAD>
endbfchar
"#;
let cmap = ToUnicodeCMap::parse(cmap_content.as_bytes()).unwrap();
assert_eq!(cmap.code_byte_length, 1);
assert_eq!(cmap.lookup(0x16), Some("🌟".to_string()));
assert_eq!(cmap.lookup(0x9D), Some("🎭".to_string()));
}
#[test]
fn test_parse_bfrange_surrogate_pair_base() {
let cmap_content = r#"
1 begincodespacerange
<00> <FF>
endcodespacerange
1 beginbfrange
<C8> <C9> <D83CDFD8>
endbfrange
"#;
let cmap = ToUnicodeCMap::parse(cmap_content.as_bytes()).unwrap();
assert_eq!(cmap.code_byte_length, 1);
assert_eq!(cmap.lookup(0xC8), Some("🏘".to_string()));
assert_eq!(cmap.lookup(0xC9), Some("🏙".to_string()));
}
#[test]
fn test_parse_bfrange_preserves_single_hyphen_like_base() {
let cmap_content = r#"
1 begincodespacerange
<00> <FF>
endcodespacerange
1 beginbfrange
<21> <22> <2013>
endbfrange
"#;
let cmap = ToUnicodeCMap::parse(cmap_content.as_bytes()).unwrap();
assert_eq!(cmap.lookup(0x21), Some("–".to_string()));
assert_eq!(cmap.lookup(0x22), Some("—".to_string()));
}
#[test]
fn test_parse_spaced_destination_hex_without_control_noise() {
let cmap_content = r#"
1 begincodespacerange
<00> <FF>
endcodespacerange
3 beginbfchar
<21> < 0009 000d 0020 00a0 >
<22> < 002d 00ad 2010 >
<23> <00a0>
endbfchar
"#;
let cmap = ToUnicodeCMap::parse(cmap_content.as_bytes()).unwrap();
assert_eq!(cmap.lookup(0x21), Some("\t".to_string()));
assert_eq!(cmap.lookup(0x22), Some("-".to_string()));
assert_eq!(cmap.lookup(0x23), Some("\u{00a0}".to_string()));
}
#[test]
fn test_parse_preserves_valid_multi_character_destinations() {
let cmap_content = r#"
1 begincodespacerange
<00> <FF>
endcodespacerange
4 beginbfchar
<21> <002d002d>
<22> <20132013>
<23> <002000a0>
<24> <00660069>
endbfchar
"#;
let cmap = ToUnicodeCMap::parse(cmap_content.as_bytes()).unwrap();
assert_eq!(cmap.lookup(0x21), Some("--".to_string()));
assert_eq!(cmap.lookup(0x22), Some("––".to_string()));
assert_eq!(cmap.lookup(0x23), Some(" \u{00a0}".to_string()));
assert_eq!(cmap.lookup(0x24), Some("fi".to_string()));
}
const LIGATURE_INDEX_CMAP: &str = r#"
1 begincodespacerange
<0000> <FFFF>
endcodespacerange
4 beginbfchar
<0001> <0063>
<0002> <006F>
<0003> <0003>
<0004> <0065>
endbfchar
"#;
const LIGATURE_INDEX_CODES: [u8; 10] = [0, 1, 0, 2, 0, 3, 0, 4, 0, 4];
#[test]
fn an_all_nul_destination_is_a_control_destination() {
assert!(destination_is_control("\0"));
assert!(destination_is_control("\0\0"));
assert!(!destination_is_control("\0A"));
assert!(!destination_is_control(""));
let cmap = ToUnicodeCMap::parse(
b"1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n\
3 beginbfchar\n<0001> <0000>\n<0002> <00000041>\n<0003> <0041>\nendbfchar",
)
.unwrap();
assert!(matches!(
cmap.lookup_code(1),
CodeMapping::ControlDestination
));
assert!(matches!(cmap.lookup_code(2), CodeMapping::Text(ref t) if t.contains('A')));
assert!(matches!(cmap.lookup_code(3), CodeMapping::Text(ref t) if t == "A"));
}
#[test]
fn a_control_destination_is_a_miss_not_text() {
let cmap = ToUnicodeCMap::parse(LIGATURE_INDEX_CMAP.as_bytes()).unwrap();
assert_eq!(cmap.lookup(3), None);
assert_eq!(cmap.lookup_code(3), CodeMapping::ControlDestination);
assert_eq!(cmap.lookup_code(1), CodeMapping::Text("c".to_string()));
assert_eq!(cmap.lookup_code(9), CodeMapping::Unmapped);
assert_eq!(cmap.control_destination_codes(), [3]);
assert_eq!(cmap.decode_cids(&LIGATURE_INDEX_CODES), "co\u{FFFD}ee");
}
#[test]
fn a_range_member_that_resolves_to_a_control_is_a_miss() {
let cmap_content = r#"
1 begincodespacerange
<0000> <FFFF>
endcodespacerange
3 beginbfrange
<0010> <0012> <0009>
<0020> <0021> <000C>
<0030> <0031> <007E>
endbfrange
"#;
let cmap = ToUnicodeCMap::parse(cmap_content.as_bytes()).unwrap();
assert_eq!(cmap.lookup(0x10), Some("\t".to_string()));
assert_eq!(cmap.lookup(0x11), Some("\n".to_string()));
assert_eq!(cmap.lookup_code(0x12), CodeMapping::ControlDestination);
assert_eq!(cmap.lookup_code(0x20), CodeMapping::ControlDestination);
assert_eq!(cmap.lookup(0x21), Some("\r".to_string()));
assert_eq!(cmap.lookup(0x30), Some("~".to_string()));
assert_eq!(cmap.lookup_code(0x31), CodeMapping::ControlDestination);
assert_eq!(cmap.lookup_code(0x13), CodeMapping::Unmapped);
assert_eq!(cmap.control_destination_codes(), [0x12, 0x20]);
assert_eq!(
cmap.decode_cids(&[0, 0x30, 0, 0x31, 0, 0x10]),
"~\u{FFFD}\t"
);
}
#[test]
fn a_range_sweeping_through_the_control_block_is_no_defect_the_repair_chases() {
use std::cell::Cell;
let mut identity = ToUnicodeCMap {
code_byte_length: 2,
..Default::default()
};
identity.ranges.push((0, 0xFFFF, 0));
assert_eq!(identity.lookup_code(5), CodeMapping::ControlDestination);
assert_eq!(identity.lookup(9), Some("\t".to_string()));
assert_eq!(identity.lookup(0x41), Some("A".to_string()));
assert!(identity.control_destination_codes().is_empty());
assert!(identity.control_destination_codes().is_empty());
let mut program = ToUnicodeCMap::new();
program.code_byte_length = 2;
program.char_map.insert(5, "x".to_string());
let (reading_asked, bytes_asked) = (Cell::new(false), Cell::new(false));
repair_control_destinations(
&mut identity,
Some(&program),
|| {
reading_asked.set(true);
None
},
|| {
bytes_asked.set(true);
None
},
&lopdf::Dictionary::new(),
&Document::new(),
);
assert_eq!(identity.lookup_code(5), CodeMapping::ControlDestination);
assert!(!reading_asked.get() && !bytes_asked.get());
let mut within = ToUnicodeCMap {
code_byte_length: 2,
..Default::default()
};
within.ranges.push((0x10, 0x13, 0x01)); within.ranges.push((0x20, 0x22, 0x08)); within.ranges.push((0x30, 0x30, 0x7F)); within.ranges.push((0x40, 0x41, 0x7E)); assert_eq!(
within.control_destination_codes(),
[0x10, 0x11, 0x12, 0x13, 0x20, 0x30]
);
assert_eq!(within.lookup_code(0x41), CodeMapping::ControlDestination);
}
#[test]
fn whitespace_controls_and_nul_padding_read_as_before() {
let cmap_content = r#"
1 begincodespacerange
<0000> <FFFF>
endcodespacerange
6 beginbfchar
<0001> <0009>
<0002> <000A>
<0003> <000D>
<0004> <00000041>
<0005> <0000>
<0006> <00000003>
endbfchar
"#;
let cmap = ToUnicodeCMap::parse(cmap_content.as_bytes()).unwrap();
assert_eq!(cmap.decode_cids(&[0, 1, 0, 2, 0, 3]), "\t\n\r");
assert_eq!(cmap.lookup(4), Some("\0A".to_string()));
assert_eq!(cmap.lookup_code(5), CodeMapping::ControlDestination);
assert_eq!(cmap.lookup_code(6), CodeMapping::ControlDestination);
assert_eq!(cmap.control_destination_codes(), [5, 6]);
}
#[test]
fn a_single_byte_control_destination_reads_as_the_marker_too() {
let cmap_content = r#"
1 begincodespacerange
<00> <FF>
endcodespacerange
4 beginbfchar
<21> <0063>
<22> <006F>
<23> <0003>
<24> <0065>
endbfchar
"#;
let cmap = ToUnicodeCMap::parse(cmap_content.as_bytes()).unwrap();
assert_eq!(cmap.code_byte_length, 1);
assert_eq!(
cmap.decode_cids(&[0x21, 0x22, 0x23, 0x24, 0x24]),
"co\u{FFFD}ee"
);
assert_eq!(
cmap.lookup_bytes(&[0x23, 0x25]),
[(0x23, None), (0x25, None)]
);
}
#[test]
fn recovery_fills_the_misses_from_another_reading_and_nothing_else() {
let mut cmap = ToUnicodeCMap::parse(LIGATURE_INDEX_CMAP.as_bytes()).unwrap();
cmap.ranges.push((0x10, 0x11, 0x0B)); let mut program = ToUnicodeCMap::new();
program.code_byte_length = 2;
program.char_map.insert(1, "x".to_string()); program.char_map.insert(3, "ff".to_string());
program.char_map.insert(0x10, "fi".to_string());
program.char_map.insert(0x11, "\u{FFFD}".to_string()); let codes = cmap.control_destination_codes();
cmap.recover_codes(&codes, &program);
assert_eq!(
cmap.lookup(1),
Some("c".to_string()),
"a mapped code keeps its text"
);
assert_eq!(cmap.lookup(3), Some("ff".to_string()));
assert_eq!(cmap.lookup(0x10), Some("fi".to_string()));
assert_eq!(cmap.lookup_code(0x11), CodeMapping::ControlDestination);
assert_eq!(cmap.control_destination_codes(), [0x11]);
assert_eq!(cmap.decode_cids(&LIGATURE_INDEX_CODES), "coffee");
}
#[test]
fn a_cid_to_gid_map_keeps_a_control_destination_under_its_new_key() {
let cmap = ToUnicodeCMap::parse(LIGATURE_INDEX_CMAP.as_bytes()).unwrap();
let repaired = build_cmap_with_cid_to_gid_map(&cmap, &[0, 3, 1]).unwrap();
assert_eq!(repaired.lookup_code(1), CodeMapping::ControlDestination);
assert_eq!(repaired.lookup(2), Some("c".to_string()));
assert_eq!(repaired.lookup_code(0), CodeMapping::Unmapped);
}
#[test]
fn a_cmap_without_control_destinations_reads_as_before() {
let cmap_content = r#"
1 begincodespacerange
<0000> <FFFF>
endcodespacerange
3 beginbfchar
<0001> <0009>
<0002> <0020>
<0003> <00660069>
endbfchar
1 beginbfrange
<0041> <005A> <0041>
endbfrange
"#;
let cmap = ToUnicodeCMap::parse(cmap_content.as_bytes()).unwrap();
assert!(cmap.control_destination_codes().is_empty());
assert_eq!(
cmap.decode_cids(&[0, 0x41, 0, 1, 0, 0x5A, 0, 2, 0, 3]),
"A\tZ fi"
);
}
#[test]
fn a_string_of_mostly_control_destinations_keeps_its_markers() {
let cmap = ToUnicodeCMap::parse(
b"1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n\
4 beginbfchar\n<0001> <0063>\n<0002> <0002>\n<0003> <0003>\n<0004> <0004>\nendbfchar\n",
)
.unwrap();
let (text, stats) = cmap.decode_cids_with_stats(&[0, 1, 0, 2, 0, 3, 0, 4]);
assert_eq!(text, "c\u{FFFD}\u{FFFD}\u{FFFD}");
assert_eq!(
stats,
CidDecodeStats {
codes: 4,
interpolated: 0,
unmapped: 3
}
);
let (text, stats) = cmap.decode_cids_with_stats(&[0, 1, 0, 9, 0, 9, 0, 9]);
assert_eq!(text, "");
assert_eq!(stats.unmapped, 3);
}
#[test]
fn the_promotion_over_a_sequential_remap_follows_the_promote_flag() {
let cmap_of = |codes: std::ops::Range<u16>| {
let mut cmap = ToUnicodeCMap::new();
cmap.code_byte_length = 2;
for code in codes {
cmap.char_map
.insert(code, char::from(b'A' + (code % 26) as u8).to_string());
}
cmap.refresh_gap_fills();
cmap
};
let build = |promote: bool| {
cmap_entry(
cmap_of(40..52),
Some(cmap_of(1..13)),
Some(cmap_of(1..21)),
12,
0,
promote,
)
};
let entry = build(false);
assert_eq!(entry.primary.char_map.len(), 12);
assert_eq!(entry.remapped.as_ref().map(|c| c.char_map.len()), Some(12));
assert_eq!(entry.fallback.as_ref().map(|c| c.char_map.len()), Some(20));
let entry = build(true);
assert_eq!(entry.primary.char_map.len(), 12);
assert_eq!(entry.remapped.as_ref().map(|c| c.char_map.len()), Some(20));
assert_eq!(entry.fallback.as_ref().map(|c| c.char_map.len()), Some(12));
}
#[test]
fn the_program_is_read_only_for_a_code_the_differences_and_the_fallback_leave() {
use lopdf::dictionary;
use std::cell::Cell;
let font_with_differences = |code: i64, name: &[u8]| {
dictionary! {
"Type" => "Font",
"Subtype" => "TrueType",
"Encoding" => dictionary! {
"Type" => "Encoding",
"Differences" => vec![code.into(), Object::Name(name.to_vec())],
},
}
};
let cmap = || {
ToUnicodeCMap::parse(
b"1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n\
4 beginbfchar\n<0001> <0063>\n<0002> <006F>\n<0003> <0003>\n<0004> <0065>\nendbfchar\n",
)
.unwrap()
};
let mut program = ToUnicodeCMap::new();
program.code_byte_length = 2;
program.char_map.insert(3, "x".to_string());
let repair = |target: &mut ToUnicodeCMap, font: &lopdf::Dictionary, reads: bool| {
let (reading_asked, bytes_asked) = (Cell::new(false), Cell::new(false));
repair_control_destinations(
target,
None,
|| {
reading_asked.set(true);
reads.then_some(&program)
},
|| {
bytes_asked.set(true);
None
},
font,
&Document::new(),
);
(reading_asked.get(), bytes_asked.get())
};
let mut target = cmap();
assert_eq!(
repair(&mut target, &font_with_differences(3, b"f_f"), true),
(false, false)
);
assert_eq!(target.lookup_code(3), CodeMapping::ControlDestination);
let mut target = cmap();
assert_eq!(
repair(&mut target, &font_with_differences(3, b"f_zzz"), true),
(false, false)
);
assert_eq!(target.lookup_code(3), CodeMapping::ControlDestination);
let mut target = cmap();
assert_eq!(
repair(&mut target, &font_with_differences(5, b"f_f"), true),
(true, false)
);
assert_eq!(target.lookup(3).as_deref(), Some("x"));
let mut target = cmap();
assert_eq!(
repair(&mut target, &font_with_differences(5, b"f_f"), false),
(true, true)
);
assert_eq!(target.lookup_code(3), CodeMapping::ControlDestination);
}
#[test]
fn a_repair_reads_the_fallback_first_and_the_program_for_what_it_lacks() {
let mut lines = String::from(
"<0001> <0063>\n<0002> <006F>\n<0003> <0003>\n<0004> <0065>\n<0005> <0005>\n",
);
for code in 6..=12u16 {
lines.push_str(&format!("<{code:04X}> <{:04X}>\n", 0x60 + code));
}
let primary = ToUnicodeCMap::parse(
format!("1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n12 beginbfchar\n{lines}endbfchar\n")
.as_bytes(),
)
.unwrap();
let two_byte = |entries: &[(u16, &str)]| {
let mut cmap = ToUnicodeCMap::new();
cmap.code_byte_length = 2;
for &(code, text) in entries {
cmap.char_map.insert(code, text.to_string());
}
cmap.refresh_gap_fills();
cmap
};
let collection = two_byte(&[(1, " "), (2, "!"), (4, "#"), (5, "$")]);
let program = two_byte(&[(3, "ff"), (5, "x")]);
let mut primary = primary;
repair_control_destinations(
&mut primary,
Some(&collection),
|| Some(&program),
|| None,
&lopdf::Dictionary::new(),
&Document::new(),
);
assert_eq!(primary.lookup(3).as_deref(), Some("ff"));
assert_eq!(primary.lookup(5).as_deref(), Some("$"));
assert_eq!(
primary.decode_cids(&[0, 1, 0, 2, 0, 3, 0, 4, 0, 5]),
"coffe$"
);
}
#[test]
fn a_repair_refreshes_the_gaps_the_entries_read() {
let mut cmap = ToUnicodeCMap::parse(
b"1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n\
4 beginbfchar\n<0024> <0041>\n<0025> <0003>\n<0026> <0043>\n<0028> <0045>\nendbfchar\n",
)
.unwrap();
assert_eq!(cmap.lookup_code(37), CodeMapping::ControlDestination);
assert_eq!(cmap.gap_fill(39), None);
let mut program = ToUnicodeCMap::new();
program.code_byte_length = 2;
program.char_map.insert(37, "B".to_string());
program.refresh_gap_fills();
repair_control_destinations(
&mut cmap,
Some(&program),
|| None,
|| None,
&lopdf::Dictionary::new(),
&Document::new(),
);
assert_eq!(cmap.lookup(37).as_deref(), Some("B"));
assert_eq!(cmap.gap_fill(39), Some('D'));
assert_eq!(
cmap.decode_cids(&[0, 36, 0, 37, 0, 38, 0, 39, 0, 40]),
"ABCDE"
);
}
#[test]
fn test_remap_to_sequential() {
let cmap_content = r#"
1 begincodespacerange
<0000><FFFF>
endcodespacerange
3 beginbfchar
<0003> <0020>
<0024> <0041>
<0025> <0042>
endbfchar
"#;
let cmap = ToUnicodeCMap::parse(cmap_content.as_bytes()).unwrap();
assert_eq!(cmap.lookup(0x0003), Some(" ".to_string()));
assert_eq!(cmap.lookup(0x0024), Some("A".to_string()));
assert_eq!(cmap.lookup(0x0025), Some("B".to_string()));
assert_eq!(cmap.lookup(0x0001), None);
assert_eq!(cmap.lookup(0x0002), None);
let remapped = cmap.remap_to_sequential();
assert_eq!(remapped.lookup(0x0001), Some(" ".to_string()));
assert_eq!(remapped.lookup(0x0002), Some("A".to_string()));
assert_eq!(remapped.lookup(0x0003), Some("B".to_string()));
assert_eq!(remapped.lookup(0x0024), None);
assert_eq!(remapped.lookup(0x0025), None);
}
#[test]
fn test_remap_to_sequential_with_ranges() {
let cmap_content = r#"
1 begincodespacerange
<0000><FFFF>
endcodespacerange
1 beginbfchar
<0032> <0020>
endbfchar
1 beginbfrange
<0064> <0066> <0058>
endbfrange
"#;
let cmap = ToUnicodeCMap::parse(cmap_content.as_bytes()).unwrap();
assert_eq!(cmap.lookup(0x0032), Some(" ".to_string())); assert_eq!(cmap.lookup(0x0064), Some("X".to_string())); assert_eq!(cmap.lookup(0x0065), Some("Y".to_string())); assert_eq!(cmap.lookup(0x0066), Some("Z".to_string()));
let remapped = cmap.remap_to_sequential();
assert_eq!(remapped.lookup(0x0001), Some(" ".to_string()));
assert_eq!(remapped.lookup(0x0002), Some("X".to_string()));
assert_eq!(remapped.lookup(0x0003), Some("Y".to_string()));
assert_eq!(remapped.lookup(0x0004), Some("Z".to_string()));
assert!(remapped.ranges.is_empty());
}
#[test]
fn remap_to_sequential_repeated_full_bfranges_stay_bounded() {
let ranges = vec![(3u16, 65535u16, 0x41u32); 5_000];
let mut map = std::collections::HashMap::new();
let assigned = expand_bfranges_for_remap(&ranges, &mut map, MAX_CID_W_EXPANSION);
assert_eq!(assigned, MAX_CID_W_EXPANSION);
assert!(map.len() <= MAX_CID_W_EXPANSION);
let mut body = String::new();
let mut remaining = 5_000usize;
while remaining > 0 {
let n = remaining.min(100);
body.push_str(&format!("{n} beginbfrange\n"));
for _ in 0..n {
body.push_str("<0003> <ffff> <0041>\n");
}
body.push_str("endbfrange\n");
remaining -= n;
}
let data = format!("1 begincodespacerange\n<0000> <ffff>\nendcodespacerange\n{body}");
let cmap = ToUnicodeCMap::parse(data.as_bytes()).unwrap();
let remapped = cmap.remap_to_sequential();
assert_eq!(remapped.lookup(1), Some("A".to_string()));
}
#[test]
fn test_min_source_cid() {
let cmap_content = r#"
1 begincodespacerange
<0000><FFFF>
endcodespacerange
2 beginbfchar
<0003> <0020>
<0024> <0041>
endbfchar
1 beginbfrange
<0030> <0032> <0058>
endbfrange
"#;
let cmap = ToUnicodeCMap::parse(cmap_content.as_bytes()).unwrap();
assert_eq!(cmap.min_source_cid(), Some(3));
}
#[test]
fn test_unmapped_2byte_cids_skipped() {
let cmap_content = r#"
1 begincodespacerange
<0000><FFFF>
endcodespacerange
1 beginbfchar
<0041> <0041>
endbfchar
"#;
let cmap = ToUnicodeCMap::parse(cmap_content.as_bytes()).unwrap();
assert_eq!(cmap.code_byte_length, 2);
let bytes = [0x42, 0x79];
let result = cmap.decode_cids(&bytes);
assert!(
!result.contains('䉹'),
"Unmapped 2-byte CIDs should not produce CJK"
);
let (text, stats) = cmap.decode_cids_with_stats(&[0x00, 0x41, 0x42, 0x79]);
assert_eq!(text, "A\u{FFFD}");
assert_eq!(
stats,
CidDecodeStats {
codes: 2,
interpolated: 0,
unmapped: 1
}
);
}
fn cmap_of_ranges(ranges: &[(u16, u16, u32)]) -> ToUnicodeCMap {
let mut content = String::from("1 begincodespacerange\n<0000><FFFF>\nendcodespacerange\n");
content.push_str(&format!("{} beginbfrange\n", ranges.len()));
for &(first, last, base) in ranges {
content.push_str(&format!("<{first:04X}><{last:04X}><{base:04X}>\n"));
}
content.push_str("endbfrange\n");
let cmap = ToUnicodeCMap::parse(content.as_bytes()).unwrap();
assert_eq!(cmap.code_byte_length, 2);
cmap
}
fn cmap_of_entries(entries: &[(u16, &str)]) -> ToUnicodeCMap {
let mut cmap = ToUnicodeCMap::new();
cmap.code_byte_length = 2;
for &(cid, text) in entries {
cmap.char_map.insert(cid, text.to_string());
}
cmap.refresh_gap_fills();
cmap
}
fn decode_codes(cmap: &ToUnicodeCMap, codes: &[u16]) -> (String, CidDecodeStats) {
let bytes: Vec<u8> = codes.iter().flat_map(|code| code.to_be_bytes()).collect();
cmap.decode_cids_with_stats(&bytes)
}
#[test]
fn gap_inside_a_run_of_letters_reads_as_the_letters_between() {
let cmap = cmap_of_ranges(&[(3, 3, 0x20), (36, 44, 0x41), (46, 49, 0x4B), (52, 61, 0x51)]);
assert_eq!(cmap.gap_fill(45), Some('J'));
assert_eq!(cmap.gap_fill(50), Some('O'));
assert_eq!(cmap.gap_fill(51), Some('P'));
let (text, stats) = decode_codes(&cmap, &[45, 36, 61, 61, 3, 51, 50, 47, 46, 36]);
assert_eq!(text, "JAZZ POLKA");
assert_eq!(
stats,
CidDecodeStats {
codes: 10,
interpolated: 3,
unmapped: 0
}
);
assert!(stats.has_gaps());
}
#[test]
fn gap_inside_a_run_of_digits_reads_as_the_digit() {
let cmap = cmap_of_ranges(&[(3, 3, 0x20), (19, 21, 0x30), (23, 28, 0x34)]);
assert_eq!(cmap.gap_fill(22), Some('3'));
assert_eq!(decode_codes(&cmap, &[20, 22, 26]).0, "137");
}
#[test]
fn gap_across_a_change_of_case_or_of_kind_is_not_read() {
let cmap = cmap_of_ranges(&[(36, 61, 0x41), (68, 93, 0x61)]);
assert_eq!(cmap.gap_fill(62), None);
let (text, stats) = decode_codes(&cmap, &[36, 62, 68]);
assert_eq!(text, "A\u{FFFD}a");
assert_eq!(
stats,
CidDecodeStats {
codes: 3,
interpolated: 0,
unmapped: 1
}
);
let cmap = cmap_of_ranges(&[(19, 28, 0x30), (36, 61, 0x41)]);
assert_eq!(cmap.gap_fill(30), None);
assert_eq!(decode_codes(&cmap, &[28, 30, 36]).0, "9\u{FFFD}A");
}
#[test]
fn gap_at_the_edge_of_the_mapped_codes_is_not_read() {
let cmap = cmap_of_ranges(&[(36, 44, 0x41)]);
assert_eq!(cmap.gap_fill(35), None);
assert_eq!(cmap.gap_fill(45), None);
let (text, stats) = decode_codes(&cmap, &[44, 45]);
assert_eq!(text, "I\u{FFFD}");
assert_eq!(
stats,
CidDecodeStats {
codes: 2,
interpolated: 0,
unmapped: 1
}
);
}
#[test]
fn gap_whose_neighbours_lie_closer_in_code_point_than_in_code_is_not_read() {
let cmap = cmap_of_ranges(&[(36, 36, 0x41), (39, 39, 0x43)]);
assert_eq!(cmap.gap_fill(37), None);
assert_eq!(cmap.gap_fill(38), None);
assert_eq!(
decode_codes(&cmap, &[36, 37, 38, 39]).0,
"A\u{FFFD}\u{FFFD}C"
);
}
#[test]
fn gap_beside_punctuation_or_an_entry_of_several_characters_is_not_read() {
let cmap = cmap_of_ranges(&[(8, 9, 0x28), (11, 11, 0x2B)]);
assert_eq!(cmap.gap_fill(10), None);
let cmap = cmap_of_entries(&[(10, "fi"), (12, "k")]);
assert_eq!(cmap.gap_fill(11), None);
}
#[test]
fn gap_in_a_cmap_whose_entries_do_not_rise_with_their_codes_is_not_read() {
let cmap = cmap_of_entries(&[(1, "t"), (2, "h"), (3, "e"), (5, "g")]);
assert_eq!(cmap.gap_fill(4), None);
let cmap = cmap_of_entries(&[(1, "A"), (2, "B"), (4, "D"), (6, "z"), (7, "y")]);
assert_eq!(cmap.gap_fill(3), None);
let cmap = cmap_of_entries(&[(1, "A"), (2, "B"), (4, "D"), (6, "y"), (7, "z")]);
assert_eq!(cmap.gap_fill(3), Some('C'));
}
#[test]
fn gap_in_another_alphabet_reads_within_its_case() {
let cmap = cmap_of_ranges(&[(100, 100, 0x410), (102, 102, 0x412)]);
assert_eq!(cmap.gap_fill(101), Some('Б'));
let cmap = cmap_of_ranges(&[(50, 50, 0x3FF), (52, 52, 0x401)]);
assert_eq!(cmap.gap_fill(51), None);
}
#[test]
fn gap_wider_than_the_limit_is_not_read() {
let cmap = cmap_of_ranges(&[(100, 100, 0x531), (137, 137, 0x556)]);
assert_eq!(cmap.gap_fill(101), None);
}
#[test]
fn gap_beside_a_run_that_falls_inside_is_not_read() {
let cmap = cmap_of_entries(&[(10, "A"), (11, "C"), (12, "B"), (14, "D")]);
assert_eq!(cmap.gap_fill(13), None);
let cmap = cmap_of_entries(&[(10, "A"), (12, "C"), (13, "E"), (14, "D")]);
assert_eq!(cmap.gap_fill(11), None);
let cmap = cmap_of_entries(&[(10, "A"), (11, "B"), (12, "C"), (14, "E"), (15, "F")]);
assert_eq!(cmap.gap_fill(13), Some('D'));
}
#[test]
fn a_single_byte_cmap_reads_no_gaps() {
for (byte_length, filled) in [(2u8, Some('B')), (0u8, Some('B')), (1u8, None)] {
let mut cmap = ToUnicodeCMap::new();
cmap.code_byte_length = byte_length;
cmap.char_map.insert(0x41, "A".to_string());
cmap.char_map.insert(0x43, "C".to_string());
cmap.refresh_gap_fills();
assert_eq!(cmap.gap_fill(0x42), filled, "{byte_length}-byte CMap");
}
}
#[test]
fn a_passthrough_cmap_has_no_gap_table() {
let mut cmap = ToUnicodeCMap::new();
cmap.code_byte_length = 2;
cmap.cid_passthrough = true;
cmap.char_map.insert(0x41, "A".to_string());
cmap.char_map.insert(0x43, "C".to_string());
cmap.refresh_gap_fills();
assert_eq!(cmap.gap_fill(0x42), None);
let (text, stats) = decode_codes(&cmap, &[0x41, 0x42, 0x43]);
assert_eq!(text, "ABC");
assert_eq!((stats.interpolated, stats.unmapped), (0, 0));
}
#[test]
fn ranges_pushed_out_of_order_are_read_once_refreshed() {
let mut cmap = ToUnicodeCMap::new();
cmap.code_byte_length = 2;
cmap.ranges.push((52, 61, 0x51)); cmap.ranges.push((36, 44, 0x41)); cmap.ranges.push((46, 49, 0x4B)); cmap.refresh_gap_fills();
assert_eq!(cmap.lookup(36).as_deref(), Some("A"));
assert_eq!(cmap.lookup(55).as_deref(), Some("T"));
assert_eq!(cmap.gap_fill(45), Some('J'));
assert_eq!(cmap.gap_fill(50), Some('O'));
assert_eq!(decode_codes(&cmap, &[45, 36, 61, 61, 50]).0, "JAZZO");
}
#[test]
fn gaps_follow_the_entries_once_refreshed() {
let mut cmap = cmap_of_entries(&[(36, "A"), (38, "C")]);
assert_eq!(decode_codes(&cmap, &[37]).0, "B");
cmap.char_map.insert(38, "Z".to_string());
cmap.refresh_gap_fills();
assert_eq!(cmap.gap_fill(37), None);
assert_eq!(decode_codes(&cmap, &[36, 37]).0, "A\u{FFFD}");
cmap.char_map.insert(38, "C".to_string());
cmap.char_map.insert(39, "D".to_string());
cmap.refresh_gap_fills();
assert_eq!(decode_codes(&cmap, &[37, 39]).0, "BD");
}
#[test]
fn a_string_more_than_half_unmapped_still_fails() {
let cmap = cmap_of_ranges(&[(36, 36, 0x41)]);
let (text, stats) = decode_codes(&cmap, &[36, 100, 101]);
assert!(text.is_empty());
assert_eq!(
stats,
CidDecodeStats {
codes: 3,
interpolated: 0,
unmapped: 2
}
);
}
#[test]
fn gaps_are_recomputed_after_merging_cmaps() {
let base = cmap_of_ranges(&[(36, 36, 0x41)]);
assert_eq!(base.gap_fill(37), None);
let merged = merge_cmaps(base, cmap_of_ranges(&[(38, 38, 0x43)]));
assert_eq!(merged.gap_fill(37), Some('B'));
}
#[test]
fn fallback_promotion_when_larger_than_primary() {
let mut primary = ToUnicodeCMap::new();
for i in 0..5u16 {
primary
.char_map
.insert(100 + i, char::from(b'A' + i as u8).to_string());
}
primary.code_byte_length = 2;
let mut sequential_remap = ToUnicodeCMap::new();
for i in 0..5u16 {
sequential_remap
.char_map
.insert(i, char::from(b'A' + i as u8).to_string());
}
sequential_remap.code_byte_length = 2;
let mut truetype_fb = ToUnicodeCMap::new();
for i in 0..20u16 {
truetype_fb
.char_map
.insert(i, format!("U+{:04X}", 0x4E00 + i));
}
truetype_fb.code_byte_length = 2;
let primary_entries = primary.char_map.len() + primary.ranges.len();
let mut remapped: Option<ToUnicodeCMap> = Some(sequential_remap);
let mut fallback: Option<ToUnicodeCMap> = Some(truetype_fb);
if remapped.is_some() {
if let Some(ref fb) = fallback {
let fb_entries = fb.char_map.len() + fb.ranges.len();
if fb_entries > primary_entries {
let old_remap = remapped.take().unwrap();
remapped = fallback.take();
fallback = Some(old_remap);
}
}
}
let r = remapped.unwrap();
assert_eq!(
r.char_map.len(),
20,
"TrueType cmap should be promoted to remapped"
);
let f = fallback.unwrap();
assert_eq!(
f.char_map.len(),
5,
"Sequential remap should be demoted to fallback"
);
}
#[test]
fn no_fallback_promotion_when_smaller() {
let mut primary = ToUnicodeCMap::new();
for i in 0..50u16 {
primary
.char_map
.insert(100 + i, format!("U+{:04X}", 0x0041 + i));
}
primary.code_byte_length = 2;
let mut sequential_remap = ToUnicodeCMap::new();
for i in 0..50u16 {
sequential_remap
.char_map
.insert(i, format!("U+{:04X}", 0x0041 + i));
}
sequential_remap.code_byte_length = 2;
let mut small_fb = ToUnicodeCMap::new();
for i in 0..10u16 {
small_fb.char_map.insert(i, format!("U+{:04X}", 0x4E00 + i));
}
small_fb.code_byte_length = 2;
let primary_entries = primary.char_map.len() + primary.ranges.len();
let mut remapped: Option<ToUnicodeCMap> = Some(sequential_remap);
let mut fallback: Option<ToUnicodeCMap> = Some(small_fb);
if remapped.is_some() {
if let Some(ref fb) = fallback {
let fb_entries = fb.char_map.len() + fb.ranges.len();
if fb_entries > primary_entries {
let old_remap = remapped.take().unwrap();
remapped = fallback.take();
fallback = Some(old_remap);
}
}
}
assert_eq!(remapped.unwrap().char_map.len(), 50);
assert_eq!(fallback.unwrap().char_map.len(), 10);
}
#[test]
fn test_max_source_cid() {
let cmap_content = r#"
1 begincodespacerange
<0000><FFFF>
endcodespacerange
2 beginbfchar
<0003> <0020>
<0031> <004E>
endbfchar
1 beginbfrange
<0208> <0227> <0430>
endbfrange
"#;
let cmap = ToUnicodeCMap::parse(cmap_content.as_bytes()).unwrap();
assert_eq!(cmap.min_source_cid(), Some(0x0003));
assert_eq!(cmap.max_source_cid(), Some(0x0227));
}
fn cid_font_dict_with_w(w_items: Vec<lopdf::Object>) -> lopdf::Dictionary {
let mut d = lopdf::Dictionary::new();
d.set("W", lopdf::Object::Array(w_items));
d
}
#[test]
fn test_w_array_covers_cid_format1() {
let doc = Document::new();
let d = cid_font_dict_with_w(vec![
lopdf::Object::Integer(0),
lopdf::Object::Array(vec![lopdf::Object::Integer(1000)]),
lopdf::Object::Integer(3),
lopdf::Object::Array(vec![lopdf::Object::Integer(313)]),
lopdf::Object::Integer(5),
lopdf::Object::Array(vec![lopdf::Object::Integer(401)]),
lopdf::Object::Integer(11),
lopdf::Object::Array(vec![
lopdf::Object::Integer(383),
lopdf::Object::Integer(383),
]),
lopdf::Object::Integer(16),
lopdf::Object::Array(vec![
lopdf::Object::Integer(363),
lopdf::Object::Integer(303),
lopdf::Object::Integer(382),
]),
lopdf::Object::Integer(570),
lopdf::Object::Array(vec![lopdf::Object::Integer(667); 26]),
]);
assert!(w_array_covers_cid(&d, &doc, 0));
assert!(w_array_covers_cid(&d, &doc, 3));
assert!(w_array_covers_cid(&d, &doc, 5));
assert!(w_array_covers_cid(&d, &doc, 11));
assert!(w_array_covers_cid(&d, &doc, 12));
assert!(w_array_covers_cid(&d, &doc, 16));
assert!(w_array_covers_cid(&d, &doc, 18));
assert!(w_array_covers_cid(&d, &doc, 570));
assert!(w_array_covers_cid(&d, &doc, 595));
assert!(!w_array_covers_cid(&d, &doc, 1));
assert!(!w_array_covers_cid(&d, &doc, 4));
assert!(!w_array_covers_cid(&d, &doc, 19));
assert!(!w_array_covers_cid(&d, &doc, 596));
}
#[test]
fn test_w_array_covers_cid_format2() {
let doc = Document::new();
let d = cid_font_dict_with_w(vec![
lopdf::Object::Integer(100),
lopdf::Object::Integer(120),
lopdf::Object::Integer(500),
]);
assert!(w_array_covers_cid(&d, &doc, 100));
assert!(w_array_covers_cid(&d, &doc, 110));
assert!(w_array_covers_cid(&d, &doc, 120));
assert!(!w_array_covers_cid(&d, &doc, 99));
assert!(!w_array_covers_cid(&d, &doc, 121));
}
#[test]
fn a_w_entry_whose_width_is_no_number_covers_its_cids_without_a_width() {
let doc = Document::new();
let mut d = cid_font_dict_with_w(vec![
lopdf::Object::Integer(1),
lopdf::Object::Integer(5),
lopdf::Object::Name(b"Bogus".to_vec()),
lopdf::Object::Integer(7),
lopdf::Object::Array(vec![
lopdf::Object::Name(b"Bogus".to_vec()),
lopdf::Object::Integer(400),
]),
]);
d.set("DW", lopdf::Object::Integer(750));
assert!(w_array_covers_cid(&d, &doc, 3));
assert_eq!(w_array_width(&d, &doc, 3), None);
assert_eq!(cid_advance(&d, &doc, 3), 750.0);
assert!(w_array_covers_cid(&d, &doc, 7));
assert_eq!(w_array_width(&d, &doc, 7), None);
assert_eq!(w_array_width(&d, &doc, 8), Some(400.0));
assert!(!w_array_covers_cid(&d, &doc, 9));
assert_eq!(cid_advance(&d, &doc, 9), 750.0);
}
#[test]
fn test_w_array_covers_cid_missing_w() {
let doc = Document::new();
let d = lopdf::Dictionary::new();
assert!(!w_array_covers_cid(&d, &doc, 3));
}
#[test]
fn test_try_remap_skipped_when_w_covers_cmap() {
let cmap_content = r#"
1 begincodespacerange
<0000><FFFF>
endcodespacerange
2 beginbfchar
<0003> <0020>
<0031> <004E>
endbfchar
2 beginbfrange
<023A> <0253> <0410>
<0255> <0279> <042B>
endbfrange
"#;
let cmap = ToUnicodeCMap::parse(cmap_content.as_bytes()).unwrap();
let mut doc = Document::new();
let mut cid_font = lopdf::Dictionary::new();
cid_font.set("CIDToGIDMap", lopdf::Object::Name(b"Identity".to_vec()));
cid_font.set(
"W",
lopdf::Object::Array(vec![
lopdf::Object::Integer(0),
lopdf::Object::Array(vec![lopdf::Object::Integer(750)]),
lopdf::Object::Integer(597),
lopdf::Object::Array(vec![lopdf::Object::Integer(500); 37]), ]),
);
let cid_font_id = doc.add_object(cid_font);
let mut font_dict = lopdf::Dictionary::new();
font_dict.set("Encoding", lopdf::Object::Name(b"Identity-H".to_vec()));
font_dict.set(
"DescendantFonts",
lopdf::Object::Array(vec![lopdf::Object::Reference(cid_font_id)]),
);
let (primary, remapped) = try_remap_subset_cmap(cmap, &font_dict, &doc, 123, || None);
assert!(
remapped.is_none(),
"Remap must be skipped when W covers CMap max CID (this is 16.pdf)"
);
assert_eq!(primary.lookup(0x0003), Some(" ".to_string()));
}
#[test]
fn test_try_remap_fires_for_true_subset_mismatch() {
let cmap_content = r#"
1 begincodespacerange
<0000><FFFF>
endcodespacerange
1 beginbfrange
<0200> <0220> <0410>
endbfrange
"#;
let cmap = ToUnicodeCMap::parse(cmap_content.as_bytes()).unwrap();
let mut doc = Document::new();
let mut cid_font = lopdf::Dictionary::new();
cid_font.set("CIDToGIDMap", lopdf::Object::Name(b"Identity".to_vec()));
cid_font.set(
"W",
lopdf::Object::Array(vec![
lopdf::Object::Integer(0),
lopdf::Object::Array(vec![lopdf::Object::Integer(500); 34]), ]),
);
let cid_font_id = doc.add_object(cid_font);
let mut font_dict = lopdf::Dictionary::new();
font_dict.set("Encoding", lopdf::Object::Name(b"Identity-H".to_vec()));
font_dict.set(
"DescendantFonts",
lopdf::Object::Array(vec![lopdf::Object::Reference(cid_font_id)]),
);
let (_primary, remapped) = try_remap_subset_cmap(cmap, &font_dict, &doc, 456, || None);
assert!(
remapped.is_some(),
"Remap must fire when CMap's CIDs are outside W array coverage"
);
}
#[test]
fn test_try_remap_skipped_for_cid_font_type0() {
let cmap_content = r#"
1 begincodespacerange
<0000><FFFF>
endcodespacerange
1 beginbfrange
<0200> <0220> <0410>
endbfrange
"#;
let cmap = ToUnicodeCMap::parse(cmap_content.as_bytes()).unwrap();
let mut doc = Document::new();
let mut cid_to_gid = vec![0u8; 68];
cid_to_gid[2] = 0x02;
cid_to_gid[3] = 0x00;
let cid_to_gid_id =
doc.add_object(lopdf::Stream::new(lopdf::Dictionary::new(), cid_to_gid));
let mut cid_font = lopdf::Dictionary::new();
cid_font.set("Subtype", lopdf::Object::Name(b"CIDFontType0".to_vec()));
cid_font.set("CIDToGIDMap", lopdf::Object::Reference(cid_to_gid_id));
cid_font.set(
"W",
lopdf::Object::Array(vec![
lopdf::Object::Integer(1),
lopdf::Object::Array(vec![lopdf::Object::Integer(500); 34]),
]),
);
let cid_font_id = doc.add_object(cid_font);
let mut font_dict = lopdf::Dictionary::new();
font_dict.set("Encoding", lopdf::Object::Name(b"Identity-H".to_vec()));
font_dict.set(
"DescendantFonts",
lopdf::Object::Array(vec![lopdf::Object::Reference(cid_font_id)]),
);
let (primary, remapped) = try_remap_subset_cmap(cmap, &font_dict, &doc, 789, || None);
assert!(
remapped.is_none(),
"Remap must be skipped for CIDFontType0 (CFF) descendants, including a \
CIDToGIDMap a malformed producer attached to one"
);
assert_eq!(primary.lookup(0x0200), Some("\u{0410}".to_string()));
}
#[test]
fn test_try_remap_resolves_indirect_subtype() {
let cmap_content = r#"
1 begincodespacerange
<0000><FFFF>
endcodespacerange
1 beginbfrange
<0200> <0220> <0410>
endbfrange
"#;
let cmap = ToUnicodeCMap::parse(cmap_content.as_bytes()).unwrap();
let mut doc = Document::new();
let subtype_id = doc.add_object(lopdf::Object::Name(b"CIDFontType2".to_vec()));
let mut cid_font = lopdf::Dictionary::new();
cid_font.set("Subtype", lopdf::Object::Reference(subtype_id));
cid_font.set("CIDToGIDMap", lopdf::Object::Name(b"Identity".to_vec()));
cid_font.set(
"W",
lopdf::Object::Array(vec![
lopdf::Object::Integer(0),
lopdf::Object::Array(vec![lopdf::Object::Integer(500); 34]),
]),
);
let cid_font_id = doc.add_object(cid_font);
let mut font_dict = lopdf::Dictionary::new();
font_dict.set("Encoding", lopdf::Object::Name(b"Identity-H".to_vec()));
font_dict.set(
"DescendantFonts",
lopdf::Object::Array(vec![lopdf::Object::Reference(cid_font_id)]),
);
let (_primary, remapped) = try_remap_subset_cmap(cmap, &font_dict, &doc, 790, || None);
assert!(
remapped.is_some(),
"An indirect /Subtype naming CIDFontType2 must still reach the remap"
);
}
fn letters_at(codes: &[u16]) -> ToUnicodeCMap {
let mut cmap = ToUnicodeCMap::new();
for (&code, letter) in codes.iter().zip('a'..='z') {
cmap.char_map.insert(code, letter.to_string());
}
cmap.code_byte_length = 2;
cmap.refresh_gap_fills();
cmap
}
#[test]
fn cmap_keyed_by_cid_is_told_from_one_keyed_by_glyph_index() {
let cids: Vec<u16> = std::iter::once(4).chain(8..=25).collect();
let mut map = vec![0u16; 26];
for (glyph, &cid) in (1..).zip(&cids) {
map[usize::from(cid)] = glyph;
}
assert_eq!(
cmap_keyed_by_glyph_index(&letters_at(&cids), &map),
Some(false)
);
let glyphs: Vec<u16> = (1..=19).collect();
assert_eq!(
cmap_keyed_by_glyph_index(&letters_at(&glyphs), &map),
Some(true)
);
let identity: Vec<u16> = (0..26).collect();
assert_eq!(
cmap_keyed_by_glyph_index(&letters_at(&cids), &identity),
None
);
let mut unusable = letters_at(&cids);
for (index, &cid) in cids.iter().enumerate() {
let text = if index % 2 == 0 { "\u{0001}" } else { "" };
unusable.char_map.insert(cid, text.to_string());
}
unusable.char_map.insert(1, "x".to_string());
assert_eq!(cmap_keyed_by_glyph_index(&unusable, &map), Some(true));
}
#[test]
fn test_try_remap_skipped_for_a_cmap_keyed_by_cid() {
let cids: Vec<u16> = (0x45..=0x5E).collect();
let cmap = letters_at(&cids);
let mut map = vec![0u8; 2 * 0x5F];
for (glyph, &cid) in (1u16..).zip(&cids) {
map[2 * usize::from(cid)..2 * usize::from(cid) + 2]
.copy_from_slice(&glyph.to_be_bytes());
}
let mut doc = Document::new();
let map_id = doc.add_object(lopdf::Stream::new(lopdf::Dictionary::new(), map));
let mut cid_font = lopdf::Dictionary::new();
cid_font.set("Subtype", lopdf::Object::Name(b"CIDFontType2".to_vec()));
cid_font.set("CIDToGIDMap", lopdf::Object::Reference(map_id));
cid_font.set(
"W",
lopdf::Object::Array(vec![
lopdf::Object::Integer(0),
lopdf::Object::Array(vec![lopdf::Object::Integer(500)]),
]),
);
let cid_font_id = doc.add_object(cid_font);
let mut font_dict = lopdf::Dictionary::new();
font_dict.set("Encoding", lopdf::Object::Name(b"Identity-H".to_vec()));
font_dict.set(
"DescendantFonts",
lopdf::Object::Array(vec![lopdf::Object::Reference(cid_font_id)]),
);
let (primary, remapped) = try_remap_subset_cmap(cmap, &font_dict, &doc, 791, || None);
assert!(remapped.is_none());
assert_eq!(primary.lookup(0x45), Some("a".to_string()));
}
#[test]
fn a_cmap_entry_reads_as_its_ligature_through_a_program() {
assert_eq!(cmap_chars_reading_as("a"), ['a']);
assert_eq!(cmap_chars_reading_as("\u{FB01}"), ['\u{FB01}']);
assert_eq!(cmap_chars_reading_as("fi"), ['\u{FB01}']);
assert_eq!(cmap_chars_reading_as("ffl"), ['\u{FB04}']);
assert_eq!(cmap_chars_reading_as("st"), ['\u{FB05}', '\u{FB06}']);
assert!(cmap_chars_reading_as("ab").is_empty());
assert!(glyph_name_reads_as("fi", "fi"));
assert!(glyph_name_reads_as("fi", "\u{FB01}"));
assert!(glyph_name_reads_as("f_f_i", "ffi"));
assert!(glyph_name_reads_as("parenleft", "("));
assert!(!glyph_name_reads_as("fi", "fl"));
assert!(!glyph_name_reads_as(".notdef", ""));
}
#[test]
fn a_program_over_the_bound_is_left_to_the_readers_without_one() {
let mut doc = Document::new();
let program_id =
doc.add_object(lopdf::Stream::new(lopdf::Dictionary::new(), vec![7u8; 64]));
let mut descriptor = lopdf::Dictionary::new();
descriptor.set("FontFile2", lopdf::Object::Reference(program_id));
let descriptor_id = doc.add_object(descriptor);
let mut cid_font = lopdf::Dictionary::new();
cid_font.set("Subtype", lopdf::Object::Name(b"CIDFontType2".to_vec()));
cid_font.set("FontDescriptor", lopdf::Object::Reference(descriptor_id));
let cid_font_id = doc.add_object(cid_font);
let mut font_dict = lopdf::Dictionary::new();
font_dict.set("Subtype", lopdf::Object::Name(b"Type0".to_vec()));
font_dict.set("Encoding", lopdf::Object::Name(b"Identity-H".to_vec()));
font_dict.set(
"DescendantFonts",
lopdf::Object::Array(vec![lopdf::Object::Reference(cid_font_id)]),
);
let program = LazyProgram::new(&font_dict, &doc);
assert_eq!(program.bytes_within(64).map(<[u8]>::len), Some(64));
assert_eq!(program.bytes().map(<[u8]>::len), Some(64));
let program = LazyProgram::new(&font_dict, &doc);
assert_eq!(program.bytes_within(63), None);
assert_eq!(program.bytes().map(<[u8]>::len), Some(64));
assert_eq!(program.bytes_within(63).map(<[u8]>::len), Some(64));
}
#[test]
fn cid_values_look_like_unicode_letter_range() {
let mut dict = lopdf::Dictionary::new();
dict.set(
"W",
Object::Array(vec![
Object::Integer(0x41),
Object::Integer(0x5A),
Object::Integer(500),
]),
);
assert!(cid_values_look_like_unicode(&dict));
}
#[test]
fn cid_values_look_like_unicode_low_gids() {
let mut dict = lopdf::Dictionary::new();
dict.set(
"W",
Object::Array(vec![
Object::Integer(0),
Object::Array(vec![Object::Integer(500); 10]),
]),
);
assert!(!cid_values_look_like_unicode(&dict));
}
#[test]
fn cid_values_look_like_unicode_repeated_full_ranges_stay_bounded() {
let mut w = Vec::new();
for _ in 0..5_000 {
w.push(Object::Integer(0));
w.push(Object::Integer(65535));
w.push(Object::Integer(500));
}
let mut dict = lopdf::Dictionary::new();
dict.set("W", Object::Array(w));
assert!(cid_values_look_like_unicode(&dict));
}
#[test]
fn encoding_cidrange_maps_a_normal_range() {
let data = b"1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n\
1 begincidrange\n<0041> <0043> 65\nendcidrange\n";
let enc = parse_encoding_cmap_stream(data).unwrap();
assert_eq!(enc.map.get(&0x41), Some(&65));
assert_eq!(enc.map.get(&0x42), Some(&66));
assert_eq!(enc.map.get(&0x43), Some(&67));
assert_eq!(enc.map.len(), 3);
assert_eq!(enc.code_byte_length, 2);
}
#[test]
fn encoding_cidrange_repeated_full_ranges_stay_bounded() {
let mut body = String::new();
let mut remaining = 5_000usize;
while remaining > 0 {
let n = remaining.min(100);
body.push_str(&format!("{n} begincidrange\n"));
for _ in 0..n {
body.push_str("<0000> <ffff> 0\n");
}
body.push_str("endcidrange\n");
remaining -= n;
}
let data = format!("1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n{body}");
let enc = parse_encoding_cmap_stream(data.as_bytes()).unwrap();
assert!(enc.map.len() <= MAX_CID_W_EXPANSION);
assert_eq!(enc.map.get(&0), Some(&0));
assert_eq!(enc.map.get(&65535), Some(&65535));
}
}