#![doc = include_str!("../README.md")]
#![forbid(unsafe_code)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![warn(clippy::indexing_slicing)]
#![allow(clippy::cast_possible_truncation)]
mod blob;
mod cid2unicode;
mod decode;
mod error;
mod ids;
mod lexer;
mod parser;
mod predefined;
mod static_lookup;
pub use error::Error;
pub use ids::{CharCode, Cid, CidCoding, CidSet, CodingScheme};
pub use lexer::Words;
use decode::Decoder;
use parser::{CidRange, DirectTable};
use pdfrum_common::{DiagKind, Diagnostics, Limits, Severity};
use pdfrum_object::Name;
#[derive(Debug, Clone, PartialEq, Eq)]
enum CidMap {
Identity,
Static { registry: usize, index: usize },
Embedded {
direct: DirectTable,
additional: Vec<CidRange>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CMap {
decoder: Decoder,
map: CidMap,
vertical: bool,
loaded: bool,
charset: CidSet,
coding: CidCoding,
inherited: Option<Box<CMap>>,
}
impl CMap {
fn unrecognized(vertical: bool) -> Self {
Self {
decoder: Decoder::TwoBytes,
map: CidMap::Identity,
vertical,
loaded: false,
charset: CidSet::Unknown,
coding: CidCoding::Unknown,
inherited: None,
}
}
pub fn decode<'a>(&'a self, bytes: &'a [u8]) -> impl Iterator<Item = (CharCode, Cid)> + 'a {
let mut offset = 0usize;
std::iter::from_fn(move || {
if offset >= bytes.len() {
return None;
}
let before = offset;
let code = self.next_char(bytes, &mut offset);
if offset == before {
return None;
}
Some((code, self.cid(code)))
})
}
pub fn next_char(&self, bytes: &[u8], offset: &mut usize) -> CharCode {
decode::next_char(&self.decoder, bytes, offset)
}
#[must_use]
pub fn cid(&self, code: CharCode) -> Cid {
let charcode = code.0;
let own = match &self.map {
CidMap::Identity => charcode as u16,
CidMap::Static { registry, index } => {
static_lookup::cid_from_charcode(*registry, *index, charcode)
}
CidMap::Embedded { direct, additional } => direct
.get(charcode)
.unwrap_or_else(|| lookup_additional(additional, charcode)),
};
if own != 0 {
return Cid(own);
}
match &self.inherited {
Some(parent) => parent.cid(code),
None => Cid(0),
}
}
#[must_use]
pub fn charcode_from_cid(&self, cid: Cid) -> CharCode {
CharCode(match &self.map {
CidMap::Static { registry, index } => {
static_lookup::charcode_from_cid(*registry, *index, cid.0)
}
CidMap::Identity | CidMap::Embedded { .. } => 0,
})
}
#[must_use]
pub fn char_size(&self, code: CharCode) -> u8 {
decode::char_size(&self.decoder, code)
}
#[must_use]
pub fn count_chars(&self, bytes: &[u8]) -> usize {
decode::count_chars(&self.decoder, bytes)
}
pub fn append_char(&self, out: &mut Vec<u8>, code: CharCode) {
decode::append_char(&self.decoder, out, code);
}
#[must_use]
pub fn is_vertical(&self) -> bool {
self.vertical
}
#[must_use]
pub fn is_loaded(&self) -> bool {
self.loaded
}
#[must_use]
pub fn coding(&self) -> CidCoding {
self.coding
}
#[must_use]
pub fn charset(&self) -> CidSet {
self.charset
}
#[must_use]
pub fn coding_scheme(&self) -> CodingScheme {
self.decoder.scheme()
}
#[must_use]
pub fn has_no_direct_table(&self) -> bool {
!matches!(self.map, CidMap::Embedded { .. })
}
#[must_use]
pub fn has_static_map(&self) -> bool {
matches!(self.map, CidMap::Static { .. })
}
}
fn lookup_additional(ranges: &[CidRange], charcode: u32) -> u16 {
let at = ranges.partition_point(|r| r.end_code < charcode);
match ranges.get(at) {
Some(r) if r.start_code <= charcode => {
(u32::from(r.start_cid) + charcode - r.start_code) as u16
}
_ => 0,
}
}
#[must_use]
pub fn predefined(name: &Name) -> Option<CMap> {
let raw = predefined::strip_slash(name.as_bytes());
let vertical = predefined::is_vertical(raw);
if predefined::is_identity(raw) {
return Some(CMap {
decoder: Decoder::TwoBytes,
map: CidMap::Identity,
vertical,
loaded: true,
charset: CidSet::Unknown,
coding: CidCoding::Cid,
inherited: None,
});
}
let row = predefined::resolve(raw)?;
let decoder = match (row.scheme, row.leading) {
(CodingScheme::MixedTwoBytes, Some(leading)) => Decoder::MixedTwoBytes { leading },
(CodingScheme::OneByte, _) => Decoder::OneByte,
(CodingScheme::MixedFourBytes, _) => Decoder::MixedFourBytes { ranges: Vec::new() },
_ => Decoder::TwoBytes,
};
let table = row
.charset
.registry_index()
.and_then(|reg| static_lookup::find(reg, raw).map(|index| (reg, index)));
let (map, loaded) = match table {
Some((registry, index)) => (CidMap::Static { registry, index }, true),
None => (CidMap::Identity, false),
};
Some(CMap {
decoder,
map,
vertical,
loaded,
charset: row.charset,
coding: row.coding,
inherited: None,
})
}
#[must_use]
pub fn from_encoding_name(name: &Name, diags: &mut Diagnostics) -> CMap {
let raw = predefined::strip_slash(name.as_bytes());
let Some(cmap) = predefined(name) else {
diags.record(Severity::Suspicious, DiagKind::CMapNameUnknown, None);
return CMap::unrecognized(predefined::is_vertical(raw));
};
if !cmap.is_loaded() {
diags.record(Severity::Suspicious, DiagKind::CMapTableMissing, None);
}
cmap
}
#[must_use]
pub fn parse_embedded(bytes: &[u8], limits: &Limits, diags: &mut Diagnostics) -> CMap {
let parsed = parser::parse(bytes, limits, diags);
let inherited = parsed.use_cmap.as_deref().and_then(|name| {
let cmap = predefined(&Name::from(predefined::strip_slash(name)))?;
Some(Box::new(cmap))
});
if parsed.use_cmap.is_some() && inherited.is_none() {
diags.record(Severity::Suspicious, DiagKind::CMapUsecmapUnknown, None);
}
let decoder = match &inherited {
Some(parent) if !parsed.declared_codespace => parent.decoder.clone(),
_ => parsed.decoder,
};
CMap {
decoder,
map: CidMap::Embedded {
direct: parsed.direct,
additional: parsed.additional,
},
vertical: parsed.vertical,
loaded: true,
charset: parsed.charset,
coding: CidCoding::Unknown,
inherited,
}
}
#[must_use]
pub fn inherit_from(
mut cmap: CMap,
parent: CMap,
depth: u32,
limits: &Limits,
diags: &mut Diagnostics,
) -> CMap {
if depth >= limits.max_name_tree_depth {
diags.record(Severity::Suspicious, DiagKind::CMapUsecmapDepth, None);
return cmap;
}
cmap.inherited = Some(Box::new(parent));
cmap
}
#[must_use]
pub fn unicode_from_cid(set: CidSet, cid: Cid) -> Option<char> {
cid2unicode::unicode_from_cid(set, cid)
}
#[must_use]
pub fn has_cid2unicode(set: CidSet) -> bool {
cid2unicode::has_table(set)
}
#[must_use]
pub fn charcode_from_unicode(cmap: &CMap, unicode: char) -> CharCode {
let Some(reg) = cmap.charset.registry_index() else {
return CharCode(0);
};
let want = u32::from(unicode);
let len = blob::cid2unicode_len(reg);
for cid in 0..len {
let Ok(cid) = u16::try_from(cid) else { break };
if blob::cid2unicode(reg, cid).map(u32::from) == Some(want) {
let code = cmap.charcode_from_cid(Cid(cid));
if code.0 != 0 {
return code;
}
}
}
CharCode(0)
}
#[must_use]
pub fn charset_from_ordering(ordering: &[u8]) -> CidSet {
match ordering {
b"GB1" => CidSet::Gb1,
b"CNS1" => CidSet::Cns1,
b"Japan1" => CidSet::Japan1,
b"Korea1" => CidSet::Korea1,
b"UCS" => CidSet::Unicode,
_ => CidSet::Unknown,
}
}
#[cfg(test)]
mod tests;