Files
pdfcraft/vendor/hayro-cmap/src/lib.rs
T
aiongg 69c87d2f3e Integration: #3195 keep CMap entries read before a stream ends inside a block
A begin… block cut off at the end of the stream returned None through ? and discarded the whole map; it now ends the scan like any other unparseable tail. Test: stream_cut_off_inside_a_block_keeps_the_entries_before_it.
2026-10-22 01:48:30 +00:00

1700 lines
54 KiB
Rust

/*!
A parser for cmap files, as they are found in PDFs.
This crate provides a parser for cmap files and allows you to
- Map character codes from text-showing operators to CID identifiers.
- Map CIDs to Unicode characters or strings.
## Safety
This crate forbids unsafe code via a crate-level attribute.
*/
#![cfg_attr(not(feature = "std"), no_std)]
#![forbid(unsafe_code)]
#![deny(missing_docs)]
extern crate alloc;
#[cfg(feature = "embed-cmaps")]
mod bcmap;
mod parse;
#[cfg(feature = "embed-cmaps")]
pub use bcmap::load_embedded;
/// Look up an embedded binary cmap by name.
///
/// Returns `None` when the `embed-cmaps` feature is not enabled.
#[cfg(not(feature = "embed-cmaps"))]
pub fn load_embedded(_name: CMapName<'_>) -> Option<&'static [u8]> {
None
}
use alloc::boxed::Box;
use alloc::string::String;
use alloc::vec::Vec;
/// A CID (Character Identifier).
pub type Cid = u32;
/// The name of the cmap.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CMapName<'a> {
// ── Adobe-Japan1 (Japanese) ── Shift-JIS (RKSJ) encodings ──
/// `83pv-RKSJ-H` — Adobe-Japan1, Mac Shift-JIS (`KanjiTalk` 6), horizontal.
N83pvRksjH,
/// `90ms-RKSJ-H` — Adobe-Japan1, Windows Shift-JIS (code page 932), horizontal.
N90msRksjH,
/// `90ms-RKSJ-V` — Adobe-Japan1, Windows Shift-JIS (code page 932), vertical.
N90msRksjV,
/// `90msp-RKSJ-H` — Adobe-Japan1, Windows Shift-JIS with proportional Roman, horizontal.
N90mspRksjH,
/// `90msp-RKSJ-V` — Adobe-Japan1, Windows Shift-JIS with proportional Roman, vertical.
N90mspRksjV,
/// `90pv-RKSJ-H` — Adobe-Japan1, Mac Shift-JIS (`KanjiTalk` 7), horizontal.
N90pvRksjH,
/// `Add-RKSJ-H` — Adobe-Japan1, Fujitsu FMR Shift-JIS, horizontal.
AddRksjH,
/// `Add-RKSJ-V` — Adobe-Japan1, Fujitsu FMR Shift-JIS, vertical.
AddRksjV,
// ── Adobe UCS2 (CID → Unicode) ──
// Note that these are not part of the predefined CMaps, but we use them
// so we can more easily map CIDs back to Unicode.
/// `Adobe-CNS1-UCS2` — Adobe-CNS1, CID-to-Unicode mapping.
AdobeCns1Ucs2,
/// `Adobe-GB1-UCS2` — Adobe-GB1, CID-to-Unicode mapping.
AdobeGb1Ucs2,
/// `Adobe-Japan1-UCS2` — Adobe-Japan1, CID-to-Unicode mapping.
AdobeJapan1Ucs2,
/// `Adobe-Korea1-UCS2` — Adobe-Korea1, CID-to-Unicode mapping.
AdobeKorea1Ucs2,
// ── Adobe-CNS1 (Traditional Chinese) ── Big Five encodings ──
/// `B5pc-H` — Adobe-CNS1, Mac Big Five (`ETen` extensions), horizontal.
B5pcH,
/// `B5pc-V` — Adobe-CNS1, Mac Big Five (`ETen` extensions), vertical.
B5pcV,
/// `CNS-EUC-H` — Adobe-CNS1, CNS 11643 EUC encoding, horizontal.
CnsEucH,
/// `CNS-EUC-V` — Adobe-CNS1, CNS 11643 EUC encoding, vertical.
CnsEucV,
/// `ETen-B5-H` — Adobe-CNS1, `ETen` Big Five extensions, horizontal.
ETenB5H,
/// `ETen-B5-V` — Adobe-CNS1, `ETen` Big Five extensions, vertical.
ETenB5V,
/// `ETenms-B5-H` — Adobe-CNS1, `ETen` Big Five with Microsoft symbol extensions, horizontal.
ETenmsB5H,
/// `ETenms-B5-V` — Adobe-CNS1, `ETen` Big Five with Microsoft symbol extensions, vertical.
ETenmsB5V,
// ── Adobe-Japan1 (Japanese) ── EUC and extended Shift-JIS encodings ──
/// `EUC-H` — Adobe-Japan1, JIS X 0208 EUC-JP encoding, horizontal.
EucH,
/// `EUC-V` — Adobe-Japan1, JIS X 0208 EUC-JP encoding, vertical.
EucV,
/// `Ext-RKSJ-H` — Adobe-Japan1, Shift-JIS with NEC/IBM extensions, horizontal.
ExtRksjH,
/// `Ext-RKSJ-V` — Adobe-Japan1, Shift-JIS with NEC/IBM extensions, vertical.
ExtRksjV,
// ── Adobe-GB1 (Simplified Chinese) ──
/// `GB-EUC-H` — Adobe-GB1, GB 2312-80 EUC encoding, horizontal.
GbEucH,
/// `GB-EUC-V` — Adobe-GB1, GB 2312-80 EUC encoding, vertical.
GbEucV,
/// `GBK-EUC-H` — Adobe-GB1, GBK encoding (Microsoft code page 936), horizontal.
GbkEucH,
/// `GBK-EUC-V` — Adobe-GB1, GBK encoding (Microsoft code page 936), vertical.
GbkEucV,
/// `GBK2K-H` — Adobe-GB1, GB 18030-2000 encoding, horizontal.
Gbk2kH,
/// `GBK2K-V` — Adobe-GB1, GB 18030-2000 encoding, vertical.
Gbk2kV,
/// `GBKp-EUC-H` — Adobe-GB1, GBK with proportional Roman, horizontal.
GbkpEucH,
/// `GBKp-EUC-V` — Adobe-GB1, GBK with proportional Roman, vertical.
GbkpEucV,
/// `GBpc-EUC-H` — Adobe-GB1, Mac GB 2312 (simplified) EUC encoding, horizontal.
GbpcEucH,
/// `GBpc-EUC-V` — Adobe-GB1, Mac GB 2312 (simplified) EUC encoding, vertical.
GbpcEucV,
// ── Adobe-Japan1 (Japanese) ── JIS encoding ──
/// `H` — Adobe-Japan1, JIS X 0208 row-cell encoding, horizontal.
H,
// ── Adobe-CNS1 (Traditional Chinese) ── Hong Kong SCS ──
/// `HKscs-B5-H` — Adobe-CNS1, Hong Kong SCS (Big Five with HKSCS extensions), horizontal.
HKscsB5H,
/// `HKscs-B5-V` — Adobe-CNS1, Hong Kong SCS (Big Five with HKSCS extensions), vertical.
HKscsB5V,
// ── Adobe-Identity ──
/// `Identity-H` — Adobe-Identity, two-byte identity mapping, horizontal.
/// Character codes map directly to CIDs (i.e. CID = character code).
IdentityH,
/// `Identity-V` — Adobe-Identity, two-byte identity mapping, vertical.
/// Character codes map directly to CIDs (i.e. CID = character code).
IdentityV,
// ── Adobe-Korea1 (Korean) ──
/// `KSC-EUC-H` — Adobe-Korea1, KS X 1001:1992 EUC-KR encoding, horizontal.
KscEucH,
/// `KSC-EUC-V` — Adobe-Korea1, KS X 1001:1992 EUC-KR encoding, vertical.
KscEucV,
/// `KSCms-UHC-H` — Adobe-Korea1, Microsoft UHC (Unified Hangul Code, code page 949), horizontal.
KscmsUhcH,
/// `KSCms-UHC-HW-H` — Adobe-Korea1, Microsoft UHC with half-width Roman, horizontal.
KscmsUhcHwH,
/// `KSCms-UHC-HW-V` — Adobe-Korea1, Microsoft UHC with half-width Roman, vertical.
KscmsUhcHwV,
/// `KSCms-UHC-V` — Adobe-Korea1, Microsoft UHC (Unified Hangul Code, code page 949), vertical.
KscmsUhcV,
/// `KSCpc-EUC-H` — Adobe-Korea1, Mac KS X 1001:1992 EUC-KR encoding, horizontal.
KscpcEucH,
// ── Adobe-CNS1 (Traditional Chinese) ── Unicode encodings ──
/// `UniCNS-UCS2-H` — Adobe-CNS1, Unicode UCS-2 encoding, horizontal.
UniCnsUcs2H,
/// `UniCNS-UCS2-V` — Adobe-CNS1, Unicode UCS-2 encoding, vertical.
UniCnsUcs2V,
/// `UniCNS-UTF16-H` — Adobe-CNS1, Unicode UTF-16 encoding, horizontal.
UniCnsUtf16H,
/// `UniCNS-UTF16-V` — Adobe-CNS1, Unicode UTF-16 encoding, vertical.
UniCnsUtf16V,
// ── Adobe-GB1 (Simplified Chinese) ── Unicode encodings ──
/// `UniGB-UCS2-H` — Adobe-GB1, Unicode UCS-2 encoding, horizontal.
UniGbUcs2H,
/// `UniGB-UCS2-V` — Adobe-GB1, Unicode UCS-2 encoding, vertical.
UniGbUcs2V,
/// `UniGB-UTF16-H` — Adobe-GB1, Unicode UTF-16 encoding, horizontal.
UniGbUtf16H,
/// `UniGB-UTF16-V` — Adobe-GB1, Unicode UTF-16 encoding, vertical.
UniGbUtf16V,
// ── Adobe-Japan1 (Japanese) ── Unicode encodings ──
/// `UniJIS-UCS2-H` — Adobe-Japan1, Unicode UCS-2 encoding, horizontal.
UniJisUcs2H,
/// `UniJIS-UCS2-HW-H` — Adobe-Japan1, Unicode UCS-2 with half-width Roman, horizontal.
UniJisUcs2HwH,
/// `UniJIS-UCS2-HW-V` — Adobe-Japan1, Unicode UCS-2 with half-width Roman, vertical.
UniJisUcs2HwV,
/// `UniJIS-UCS2-V` — Adobe-Japan1, Unicode UCS-2 encoding, vertical.
UniJisUcs2V,
/// `UniJIS-UTF16-H` — Adobe-Japan1, Unicode UTF-16 encoding, horizontal.
UniJisUtf16H,
/// `UniJIS-UTF16-V` — Adobe-Japan1, Unicode UTF-16 encoding, vertical.
UniJisUtf16V,
// ── Adobe-Korea1 (Korean) ── Unicode encodings ──
/// `UniKS-UCS2-H` — Adobe-Korea1, Unicode UCS-2 encoding, horizontal.
UniKsUcs2H,
/// `UniKS-UCS2-V` — Adobe-Korea1, Unicode UCS-2 encoding, vertical.
UniKsUcs2V,
/// `UniKS-UTF16-H` — Adobe-Korea1, Unicode UTF-16 encoding, horizontal.
UniKsUtf16H,
/// `UniKS-UTF16-V` — Adobe-Korea1, Unicode UTF-16 encoding, vertical.
UniKsUtf16V,
// ── Adobe-Japan1 (Japanese) ── JIS encoding ──
/// `V` — Adobe-Japan1, JIS X 0208 row-cell encoding, vertical.
V,
/// A custom (non-predefined) `CMap` name.
Custom(&'a [u8]),
}
impl<'a> CMapName<'a> {
/// Create a `CMapType` from raw bytes.
pub fn from_bytes(name: &'a [u8]) -> Self {
match name {
b"83pv-RKSJ-H" => Self::N83pvRksjH,
b"90ms-RKSJ-H" => Self::N90msRksjH,
b"90ms-RKSJ-V" => Self::N90msRksjV,
b"90msp-RKSJ-H" => Self::N90mspRksjH,
b"90msp-RKSJ-V" => Self::N90mspRksjV,
b"90pv-RKSJ-H" => Self::N90pvRksjH,
b"Add-RKSJ-H" => Self::AddRksjH,
b"Add-RKSJ-V" => Self::AddRksjV,
b"Adobe-CNS1-UCS2" => Self::AdobeCns1Ucs2,
b"Adobe-GB1-UCS2" => Self::AdobeGb1Ucs2,
b"Adobe-Japan1-UCS2" => Self::AdobeJapan1Ucs2,
b"Adobe-Korea1-UCS2" => Self::AdobeKorea1Ucs2,
b"B5pc-H" => Self::B5pcH,
b"B5pc-V" => Self::B5pcV,
b"CNS-EUC-H" => Self::CnsEucH,
b"CNS-EUC-V" => Self::CnsEucV,
b"ETen-B5-H" => Self::ETenB5H,
b"ETen-B5-V" => Self::ETenB5V,
b"ETenms-B5-H" => Self::ETenmsB5H,
b"ETenms-B5-V" => Self::ETenmsB5V,
b"EUC-H" => Self::EucH,
b"EUC-V" => Self::EucV,
b"Ext-RKSJ-H" => Self::ExtRksjH,
b"Ext-RKSJ-V" => Self::ExtRksjV,
b"GB-EUC-H" => Self::GbEucH,
b"GB-EUC-V" => Self::GbEucV,
b"GBK-EUC-H" => Self::GbkEucH,
b"GBK-EUC-V" => Self::GbkEucV,
b"GBK2K-H" => Self::Gbk2kH,
b"GBK2K-V" => Self::Gbk2kV,
b"GBKp-EUC-H" => Self::GbkpEucH,
b"GBKp-EUC-V" => Self::GbkpEucV,
b"GBpc-EUC-H" => Self::GbpcEucH,
b"GBpc-EUC-V" => Self::GbpcEucV,
b"H" => Self::H,
b"HKscs-B5-H" => Self::HKscsB5H,
b"HKscs-B5-V" => Self::HKscsB5V,
b"Identity-H" => Self::IdentityH,
b"Identity-V" => Self::IdentityV,
b"KSC-EUC-H" => Self::KscEucH,
b"KSC-EUC-V" => Self::KscEucV,
b"KSCms-UHC-H" => Self::KscmsUhcH,
b"KSCms-UHC-HW-H" => Self::KscmsUhcHwH,
b"KSCms-UHC-HW-V" => Self::KscmsUhcHwV,
b"KSCms-UHC-V" => Self::KscmsUhcV,
b"KSCpc-EUC-H" => Self::KscpcEucH,
b"UniCNS-UCS2-H" => Self::UniCnsUcs2H,
b"UniCNS-UCS2-V" => Self::UniCnsUcs2V,
b"UniCNS-UTF16-H" => Self::UniCnsUtf16H,
b"UniCNS-UTF16-V" => Self::UniCnsUtf16V,
b"UniGB-UCS2-H" => Self::UniGbUcs2H,
b"UniGB-UCS2-V" => Self::UniGbUcs2V,
b"UniGB-UTF16-H" => Self::UniGbUtf16H,
b"UniGB-UTF16-V" => Self::UniGbUtf16V,
b"UniJIS-UCS2-H" => Self::UniJisUcs2H,
b"UniJIS-UCS2-HW-H" => Self::UniJisUcs2HwH,
b"UniJIS-UCS2-HW-V" => Self::UniJisUcs2HwV,
b"UniJIS-UCS2-V" => Self::UniJisUcs2V,
b"UniJIS-UTF16-H" => Self::UniJisUtf16H,
b"UniJIS-UTF16-V" => Self::UniJisUtf16V,
b"UniKS-UCS2-H" => Self::UniKsUcs2H,
b"UniKS-UCS2-V" => Self::UniKsUcs2V,
b"UniKS-UTF16-H" => Self::UniKsUtf16H,
b"UniKS-UTF16-V" => Self::UniKsUtf16V,
b"V" => Self::V,
_ => Self::Custom(name),
}
}
/// Convert the `CMapType` back to its raw byte representation.
pub fn to_bytes(&self) -> &[u8] {
match self {
Self::N83pvRksjH => b"83pv-RKSJ-H",
Self::N90msRksjH => b"90ms-RKSJ-H",
Self::N90msRksjV => b"90ms-RKSJ-V",
Self::N90mspRksjH => b"90msp-RKSJ-H",
Self::N90mspRksjV => b"90msp-RKSJ-V",
Self::N90pvRksjH => b"90pv-RKSJ-H",
Self::AddRksjH => b"Add-RKSJ-H",
Self::AddRksjV => b"Add-RKSJ-V",
Self::AdobeCns1Ucs2 => b"Adobe-CNS1-UCS2",
Self::AdobeGb1Ucs2 => b"Adobe-GB1-UCS2",
Self::AdobeJapan1Ucs2 => b"Adobe-Japan1-UCS2",
Self::AdobeKorea1Ucs2 => b"Adobe-Korea1-UCS2",
Self::B5pcH => b"B5pc-H",
Self::B5pcV => b"B5pc-V",
Self::CnsEucH => b"CNS-EUC-H",
Self::CnsEucV => b"CNS-EUC-V",
Self::ETenB5H => b"ETen-B5-H",
Self::ETenB5V => b"ETen-B5-V",
Self::ETenmsB5H => b"ETenms-B5-H",
Self::ETenmsB5V => b"ETenms-B5-V",
Self::EucH => b"EUC-H",
Self::EucV => b"EUC-V",
Self::ExtRksjH => b"Ext-RKSJ-H",
Self::ExtRksjV => b"Ext-RKSJ-V",
Self::GbEucH => b"GB-EUC-H",
Self::GbEucV => b"GB-EUC-V",
Self::GbkEucH => b"GBK-EUC-H",
Self::GbkEucV => b"GBK-EUC-V",
Self::Gbk2kH => b"GBK2K-H",
Self::Gbk2kV => b"GBK2K-V",
Self::GbkpEucH => b"GBKp-EUC-H",
Self::GbkpEucV => b"GBKp-EUC-V",
Self::GbpcEucH => b"GBpc-EUC-H",
Self::GbpcEucV => b"GBpc-EUC-V",
Self::H => b"H",
Self::HKscsB5H => b"HKscs-B5-H",
Self::HKscsB5V => b"HKscs-B5-V",
Self::IdentityH => b"Identity-H",
Self::IdentityV => b"Identity-V",
Self::KscEucH => b"KSC-EUC-H",
Self::KscEucV => b"KSC-EUC-V",
Self::KscmsUhcH => b"KSCms-UHC-H",
Self::KscmsUhcHwH => b"KSCms-UHC-HW-H",
Self::KscmsUhcHwV => b"KSCms-UHC-HW-V",
Self::KscmsUhcV => b"KSCms-UHC-V",
Self::KscpcEucH => b"KSCpc-EUC-H",
Self::UniCnsUcs2H => b"UniCNS-UCS2-H",
Self::UniCnsUcs2V => b"UniCNS-UCS2-V",
Self::UniCnsUtf16H => b"UniCNS-UTF16-H",
Self::UniCnsUtf16V => b"UniCNS-UTF16-V",
Self::UniGbUcs2H => b"UniGB-UCS2-H",
Self::UniGbUcs2V => b"UniGB-UCS2-V",
Self::UniGbUtf16H => b"UniGB-UTF16-H",
Self::UniGbUtf16V => b"UniGB-UTF16-V",
Self::UniJisUcs2H => b"UniJIS-UCS2-H",
Self::UniJisUcs2HwH => b"UniJIS-UCS2-HW-H",
Self::UniJisUcs2HwV => b"UniJIS-UCS2-HW-V",
Self::UniJisUcs2V => b"UniJIS-UCS2-V",
Self::UniJisUtf16H => b"UniJIS-UTF16-H",
Self::UniJisUtf16V => b"UniJIS-UTF16-V",
Self::UniKsUcs2H => b"UniKS-UCS2-H",
Self::UniKsUcs2V => b"UniKS-UCS2-V",
Self::UniKsUtf16H => b"UniKS-UTF16-H",
Self::UniKsUtf16V => b"UniKS-UTF16-V",
Self::V => b"V",
Self::Custom(name) => name,
}
}
}
/// Let's limit the number of nested `usecmap` references to 16.
const MAX_NESTING_DEPTH: u32 = 16;
/// A parsed cmap.
#[derive(Debug, Clone)]
pub struct CMap {
metadata: Metadata,
// Note that we don't actually use this, because Acrobat _seems_ to ignore
// it, too.
_codespace_ranges: Vec<CodespaceRange>,
cid_ranges: PartitionedRanges,
notdef_ranges: PartitionedRanges,
bf_entries: Vec<BfRange>,
base: Option<Box<Self>>,
}
impl CMap {
/// Parse a cmap from raw bytes.
///
/// The `get_cmap` callback is used to recursively resolve cmaps that
/// are referenced via `usecmap`.
pub fn parse<'a>(
data: &[u8],
get_cmap: impl Fn(CMapName<'_>) -> Option<&'a [u8]> + Clone + 'a,
) -> Option<Self> {
parse::parse_inner(data, get_cmap, 0)
}
/// Create an Identity-H cmap.
pub fn identity_h() -> Self {
Self::identity(WritingMode::Horizontal, b"Identity-H")
}
/// Create an Identity-V cmap.
pub fn identity_v() -> Self {
Self::identity(WritingMode::Vertical, b"Identity-V")
}
fn identity(writing_mode: WritingMode, name: &[u8]) -> Self {
Self {
metadata: Metadata {
character_collection: Some(CharacterCollection {
family: CidFamily::AdobeIdentity,
supplement: 0,
}),
name: Some(Vec::from(name)),
writing_mode: Some(writing_mode),
},
_codespace_ranges: Vec::new(),
cid_ranges: {
let mut r = PartitionedRanges::new();
r.push(
2,
CidRange {
range: Range {
start: 0,
end: 0xFFFF,
},
cid_start: 0,
},
);
r
},
notdef_ranges: PartitionedRanges::new(),
bf_entries: Vec::new(),
base: None,
}
}
/// Return the metadata of this cmap.
pub fn metadata(&self) -> &Metadata {
&self.metadata
}
/// Look up the CID code of a character code.
///
/// Returns `None` if the code does not match any range for the given byte length.
pub fn lookup_cid_code(&self, code: u32, byte_len: u8) -> Option<Cid> {
// Note that, in theory, we are supposed to first check the code space range
// whether the entry exists in the first place. However, from my experiments
// Acrobat mostly seems to ignore this, so we do that as well.
let cid_ranges = self.cid_ranges.get(byte_len)?;
let notdef_ranges = self.notdef_ranges.get(byte_len)?;
if let Some(entry) = find_in_ranges(cid_ranges, code) {
let offset = code.checked_sub(entry.range.start)?;
return entry.cid_start.checked_add(offset);
} else if let Some(entry) = find_in_ranges(notdef_ranges, code) {
// For `.notdef` ranges, all codes map to the same `.notdef` CID, so
// no adding of the offset here.
return Some(entry.cid_start);
}
// See pdfjs_bug920426, it uses bf chars for encoding the characters
// of a text-showing operator, so try that as well. We don't want to
// recurse though, only check the bf chars of this level.
if let Some(BfString::Char(lookup)) = self.lookup_bf_string_inner(code, false) {
return Some(lookup as u32);
}
// If we still haven't found anything, check the base cmap.
self.base
.as_ref()
.and_then(|b| b.lookup_cid_code(code, byte_len))
}
/// Look up a bf string in the cmap. This is usually
/// used for mapping character codes to Unicode codepoints in a
/// `ToUnicode` cmap.
///
/// Returns `None` if no mapping is available.
pub fn lookup_bf_string(&self, code: u32) -> Option<BfString> {
self.lookup_bf_string_inner(code, true)
}
fn lookup_bf_string_inner(&self, code: u32, recurse: bool) -> Option<BfString> {
if let Some(entry) = find_in_ranges(&self.bf_entries, code) {
let offset = u16::try_from(code - entry.range.start).ok()?;
fn decode_utf16(units: &[u16]) -> Option<BfString> {
let mut iter = core::char::decode_utf16(units.iter().copied());
let first = iter.next()?.ok()?;
if iter.next().is_none() {
Some(BfString::Char(first))
} else {
let s = String::from_utf16(units).ok()?;
Some(BfString::String(s))
}
}
return if offset == 0 {
Some(decode_utf16(&entry.dst_base)?)
} else {
let mut units = entry.dst_base.clone();
*units.last_mut()? = units.last()?.checked_add(offset)?;
Some(decode_utf16(&units)?)
};
}
if recurse {
self.base.as_ref()?.lookup_bf_string(code)
} else {
None
}
}
}
trait HasRange {
fn range(&self) -> &Range;
}
fn find_in_ranges<T: HasRange>(entries: &[T], code: u32) -> Option<&T> {
let idx = entries
.binary_search_by(|entry| {
let r = entry.range();
if code < r.start {
core::cmp::Ordering::Greater
} else if code > r.end {
core::cmp::Ordering::Less
} else {
core::cmp::Ordering::Equal
}
})
.ok()?;
Some(&entries[idx])
}
/// A range with a start and end code.
#[derive(Debug, Clone)]
pub(crate) struct Range {
pub(crate) start: u32,
pub(crate) end: u32,
}
/// A range of character codes mapped to CIDs.
#[derive(Debug, Clone)]
pub struct CidRange {
pub(crate) range: Range,
pub(crate) cid_start: Cid,
}
impl HasRange for CidRange {
fn range(&self) -> &Range {
&self.range
}
}
#[derive(Debug, Clone)]
pub(crate) struct PartitionedRanges {
buckets: [Vec<CidRange>; 4],
}
impl PartitionedRanges {
pub(crate) fn new() -> Self {
Self {
buckets: [Vec::new(), Vec::new(), Vec::new(), Vec::new()],
}
}
// PdfCraft patch: needed by the truncated-stream guard in parse.rs.
pub(crate) fn is_empty(&self) -> bool {
self.buckets.iter().all(|b| b.is_empty())
}
pub(crate) fn push(&mut self, byte_len: usize, range: CidRange) {
if let Some(bucket) = byte_len
.checked_sub(1)
.and_then(|i| self.buckets.get_mut(i))
{
bucket.push(range);
}
}
pub(crate) fn get(&self, byte_len: u8) -> Option<&[CidRange]> {
let idx = (byte_len as usize).checked_sub(1)?;
self.buckets.get(idx).map(|v| v.as_slice())
}
pub(crate) fn sort(&mut self) {
for bucket in &mut self.buckets {
bucket.sort_by(|a, b| a.range.start.cmp(&b.range.start));
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct BfRange {
pub(crate) range: Range,
pub(crate) dst_base: Vec<u16>,
}
impl HasRange for BfRange {
fn range(&self) -> &Range {
&self.range
}
}
/// A codespace range defining valid character code byte sequences.
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub(crate) struct CodespaceRange {
pub(crate) number_bytes: u8,
pub(crate) low: u32,
pub(crate) high: u32,
}
/// A Unicode value decoded from a cmap.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BfString {
/// A single character.
Char(char),
/// A UTF-8 string.
String(String),
}
/// Metadata extracted from a cmap file.
#[derive(Debug, Clone)]
pub struct Metadata {
/// The referenced character collection.
pub character_collection: Option<CharacterCollection>,
/// The cmap name.
pub name: Option<Vec<u8>>,
/// The writing mode.
pub writing_mode: Option<WritingMode>,
}
/// The registry+ordering family of a CID character collection.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CidFamily {
/// Adobe-Japan1
AdobeJapan1,
/// Adobe-GB1
AdobeGB1,
/// Adobe-CNS1
AdobeCNS1,
/// Adobe-Korea1
AdobeKorea1,
/// Adobe-Identity
AdobeIdentity,
/// A non-predefined registry/ordering pair.
Custom {
/// The registry name.
registry: Vec<u8>,
/// The ordering name.
ordering: Vec<u8>,
},
}
impl CidFamily {
/// Create a `CidFamily` from raw registry and ordering byte strings.
pub fn from_registry_ordering(registry: &[u8], ordering: &[u8]) -> Self {
if registry == b"Adobe" {
match ordering {
b"Japan1" => Self::AdobeJapan1,
b"GB1" => Self::AdobeGB1,
b"CNS1" => Self::AdobeCNS1,
b"Korea1" => Self::AdobeKorea1,
b"Identity" => Self::AdobeIdentity,
_ => Self::Custom {
registry: registry.to_vec(),
ordering: ordering.to_vec(),
},
}
} else {
Self::Custom {
registry: registry.to_vec(),
ordering: ordering.to_vec(),
}
}
}
/// Return the predefined UTF-16 `CMap` that maps Unicode code points to CIDs
/// for this family, if one exists.
pub fn unicode_cmap(&self) -> Option<CMapName<'static>> {
match self {
Self::AdobeJapan1 => Some(CMapName::UniJisUtf16H),
Self::AdobeGB1 => Some(CMapName::UniGbUtf16H),
Self::AdobeCNS1 => Some(CMapName::UniCnsUtf16H),
Self::AdobeKorea1 => Some(CMapName::UniKsUtf16H),
Self::AdobeIdentity | Self::Custom { .. } => None,
}
}
/// Return the `UCS2` cmap corresponding to the given character collection.
pub fn ucs2_cmap(&self) -> Option<CMapName<'static>> {
match self {
Self::AdobeJapan1 => Some(CMapName::AdobeJapan1Ucs2),
Self::AdobeGB1 => Some(CMapName::AdobeGb1Ucs2),
Self::AdobeCNS1 => Some(CMapName::AdobeCns1Ucs2),
Self::AdobeKorea1 => Some(CMapName::AdobeKorea1Ucs2),
Self::AdobeIdentity | Self::Custom { .. } => None,
}
}
}
/// A CID character collection identifying the character set and ordering.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CharacterCollection {
/// The registry+ordering family.
pub family: CidFamily,
/// The supplement number.
pub supplement: i32,
}
/// The writing mode of a cmap.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WritingMode {
/// Horizontal writing mode.
#[default]
Horizontal,
/// Vertical writing mode.
Vertical,
}
#[cfg(test)]
mod tests {
use super::*;
// Note that those cmaps might not be completely valid according to the rules
// of cmap/Postscript, but since our parser is very lenient and doesn't run a real
// interpreter we can shorten them by a lot.
const PREAMBLE: &[u8] = br#"/CIDSystemInfo 3 dict dup begin
/Registry (Adobe) def
/Ordering (Japan1) def
/Supplement 0 def
end def
/CMapName /Test def
/WMode 0 def
2 begincodespacerange
<00> <FF>
<0000> <FFFF>
endcodespacerange
"#;
fn parse_with_preamble(body: &[u8]) -> CMap {
let mut data = Vec::new();
data.extend_from_slice(PREAMBLE);
data.extend_from_slice(body);
CMap::parse(&data, |_| None).unwrap()
}
#[test]
fn metadata_parsing() {
let data = br#"
/CIDInit /ProcSet findresource begin
12 dict begin
begincmap
/CIDSystemInfo 3 dict dup begin
/Registry (Adobe) def
/Ordering (Japan1) def
/Supplement 6 def
end def
/CMapName /Adobe-Japan1-H def
/CMapType 1 def
/WMode 0 def
endcmap"#;
let cmap = CMap::parse(data, |_| None).unwrap();
let cc = cmap.metadata().character_collection.as_ref().unwrap();
assert_eq!(cc.family, CidFamily::AdobeJapan1);
assert_eq!(cc.supplement, 6);
assert_eq!(
cmap.metadata().name.as_deref(),
Some(b"Adobe-Japan1-H".as_slice())
);
assert_eq!(cmap.metadata().writing_mode, Some(WritingMode::Horizontal));
}
#[test]
fn vertical_writing_mode() {
let data = br#"
/CIDSystemInfo 3 dict dup begin
/Registry (Adobe) def
/Ordering (Japan1) def
/Supplement 6 def
end def
/CMapName /Adobe-Japan1-V def
/WMode 1 def
"#;
let cmap = CMap::parse(data, |_| None).unwrap();
assert_eq!(cmap.metadata().writing_mode, Some(WritingMode::Vertical));
assert_eq!(
cmap.metadata().name.as_deref(),
Some(b"Adobe-Japan1-V".as_slice())
);
}
#[test]
fn cid_range_lookup() {
let cmap = parse_with_preamble(
br#"
3 begincidrange
<0000> <00FF> 0
<0100> <01FF> 256
<8140> <817E> 633
endcidrange
"#,
);
assert_eq!(cmap.lookup_cid_code(0x0000, 2), Some(0));
assert_eq!(cmap.lookup_cid_code(0x0042, 2), Some(0x42));
assert_eq!(cmap.lookup_cid_code(0x00FF, 2), Some(0xFF));
assert_eq!(cmap.lookup_cid_code(0x0100, 2), Some(256));
assert_eq!(cmap.lookup_cid_code(0x01FF, 2), Some(511));
assert_eq!(cmap.lookup_cid_code(0x8140, 2), Some(633));
assert_eq!(cmap.lookup_cid_code(0x817E, 2), Some(633 + 62));
}
#[test]
fn cid_char_lookup() {
let cmap = parse_with_preamble(
br#"
3 begincidchar
<03> 1
<04> 2
<20> 50
endcidchar
"#,
);
assert_eq!(cmap.lookup_cid_code(0x03, 1), Some(1));
assert_eq!(cmap.lookup_cid_code(0x04, 1), Some(2));
assert_eq!(cmap.lookup_cid_code(0x20, 1), Some(50));
}
#[test]
fn lookup_miss() {
let cmap = parse_with_preamble(
br#"
1 begincidrange
<0100> <01FF> 0
endcidrange
"#,
);
assert_eq!(cmap.lookup_cid_code(0x00FF, 2), None);
assert_eq!(cmap.lookup_cid_code(0x0200, 2), None);
assert_eq!(cmap.lookup_cid_code(0xFFFF, 2), None);
}
#[test]
fn multiple_sections() {
let cmap = parse_with_preamble(
br#"
2 begincidrange
<0000> <00FF> 0
<0100> <01FF> 256
endcidrange
1 begincidchar
<0200> 600
endcidchar
1 begincidrange
<8140> <817E> 633
endcidrange
"#,
);
assert_eq!(cmap.lookup_cid_code(0x0000, 2), Some(0));
assert_eq!(cmap.lookup_cid_code(0x0100, 2), Some(256));
assert_eq!(cmap.lookup_cid_code(0x0200, 2), Some(600));
assert_eq!(cmap.lookup_cid_code(0x8140, 2), Some(633));
}
#[test]
fn single_byte_codes() {
let cmap = parse_with_preamble(
br#"
2 begincidrange
<00> <7F> 0
<80> <FF> 200
endcidrange
"#,
);
assert_eq!(cmap.lookup_cid_code(0x00, 1), Some(0));
assert_eq!(cmap.lookup_cid_code(0x41, 1), Some(0x41));
assert_eq!(cmap.lookup_cid_code(0x80, 1), Some(200));
assert_eq!(cmap.lookup_cid_code(0xFF, 1), Some(200 + 127));
}
#[test]
fn dict_style_cidsysteminfo() {
let data = br#"
/CIDInit /ProcSet findresource begin
10 dict begin
begincmap
/CIDSystemInfo
<< /Registry (Adobe)
/Ordering (UCS)
/Supplement 0
>> def
/CMapName /Adobe-Identity-UCS def
/CMapType 2 def
1 begincodespacerange
<0000> <FFFF>
endcodespacerange
2 beginbfchar
<001F> <F049>
<002A> <F055>
endbfchar
endcmap
CMapName currentdict /CMap defineresource pop
end
end
"#;
let cmap = CMap::parse(data, |_| None).unwrap();
let cc = cmap.metadata().character_collection.as_ref().unwrap();
assert_eq!(
cc.family,
CidFamily::Custom {
registry: b"Adobe".to_vec(),
ordering: b"UCS".to_vec(),
}
);
assert_eq!(cc.supplement, 0);
assert_eq!(
cmap.metadata().name.as_deref(),
Some(b"Adobe-Identity-UCS".as_slice())
);
assert_eq!(
cmap.lookup_bf_string(0x001F),
Some(BfString::Char('\u{F049}'))
);
assert_eq!(
cmap.lookup_bf_string(0x002A),
Some(BfString::Char('\u{F055}'))
);
}
#[test]
fn usecmap_chaining() {
let base_data = br#"
/CIDSystemInfo 3 dict dup begin
/Registry (Adobe) def
/Ordering (Japan1) def
/Supplement 0 def
end def
/CMapName /Base def
/WMode 0 def
1 begincodespacerange
<0000> <FFFF>
endcodespacerange
1 begincidrange
<0000> <00FF> 0
endcidrange
"#;
let child_data = br#"
/Base usecmap
/CIDSystemInfo 3 dict dup begin
/Registry (Adobe) def
/Ordering (Japan1) def
/Supplement 0 def
end def
/CMapName /Child def
/WMode 0 def
1 begincodespacerange
<0000> <FFFF>
endcodespacerange
1 begincidrange
<0100> <01FF> 256
endcidrange
"#;
let cmap = CMap::parse(child_data, |name| {
if name.to_bytes() == b"Base" {
Some(base_data.as_slice())
} else {
None
}
})
.unwrap();
assert_eq!(cmap.lookup_cid_code(0x0100, 2), Some(256));
assert_eq!(cmap.lookup_cid_code(0x01FF, 2), Some(511));
assert_eq!(cmap.lookup_cid_code(0x0000, 2), Some(0));
assert_eq!(cmap.lookup_cid_code(0x00FF, 2), Some(0xFF));
assert_eq!(cmap.lookup_cid_code(0x0200, 2), None);
}
#[test]
fn usecmap_partial_override() {
let base_data = br#"
/CIDSystemInfo 3 dict dup begin
/Registry (Adobe) def
/Ordering (Japan1) def
/Supplement 0 def
end def
/CMapName /Base def
/WMode 0 def
1 begincodespacerange
<0000> <FFFF>
endcodespacerange
1 begincidrange
<0000> <00FF> 0
endcidrange
"#;
let child_data = br#"
/Base usecmap
/CIDSystemInfo 3 dict dup begin
/Registry (Adobe) def
/Ordering (Japan1) def
/Supplement 0 def
end def
/CMapName /Child def
/WMode 0 def
1 begincodespacerange
<0000> <FFFF>
endcodespacerange
1 begincidrange
<0040> <007F> 500
endcidrange
"#;
let cmap = CMap::parse(child_data, |name| {
if name.to_bytes() == b"Base" {
Some(base_data.as_slice())
} else {
None
}
})
.unwrap();
assert_eq!(cmap.lookup_cid_code(0x0000, 2), Some(0));
assert_eq!(cmap.lookup_cid_code(0x003F, 2), Some(0x3F));
assert_eq!(cmap.lookup_cid_code(0x0040, 2), Some(500));
assert_eq!(cmap.lookup_cid_code(0x007F, 2), Some(563));
assert_eq!(cmap.lookup_cid_code(0x0080, 2), Some(0x80));
assert_eq!(cmap.lookup_cid_code(0x00FF, 2), Some(0xFF));
}
#[test]
fn notdef_char_lookup() {
let cmap = parse_with_preamble(
br#"
2 beginnotdefchar
<03> 10
<20> 20
endnotdefchar
"#,
);
assert_eq!(cmap.lookup_cid_code(0x03, 1), Some(10));
assert_eq!(cmap.lookup_cid_code(0x20, 1), Some(20));
assert_eq!(cmap.lookup_cid_code(0x04, 1), None);
}
#[test]
fn notdef_range_lookup() {
let cmap = parse_with_preamble(
br#"
1 beginnotdefrange
<0000> <001F> 100
endnotdefrange
"#,
);
assert_eq!(cmap.lookup_cid_code(0x0000, 2), Some(100));
assert_eq!(cmap.lookup_cid_code(0x0001, 2), Some(100));
assert_eq!(cmap.lookup_cid_code(0x001F, 2), Some(100));
assert_eq!(cmap.lookup_cid_code(0x0020, 2), None);
}
#[test]
fn bfchar_lookup() {
let cmap = parse_with_preamble(
br#"
2 beginbfchar
<0041> <0048>
<0042> <0065>
endbfchar
"#,
);
assert_eq!(cmap.lookup_bf_string(0x0041), Some(BfString::Char('H')));
assert_eq!(cmap.lookup_bf_string(0x0042), Some(BfString::Char('e')));
assert_eq!(cmap.lookup_bf_string(0x0043), None);
}
#[test]
fn bfchar_ligature() {
let cmap = parse_with_preamble(
br#"
1 beginbfchar
<005F> <00660066>
endbfchar
"#,
);
assert_eq!(
cmap.lookup_bf_string(0x005F),
Some(BfString::String(String::from("ff")))
);
}
// PdfCraft patch: malformed pieces of a CMap are skipped, not fatal - the reading
// path used to discard the whole CMap and fall back to the font encoding for every
// code, so search and search-redaction missed text the edit layer could see (#3177).
#[test]
fn bad_bfchar_entry_is_skipped() {
let cmap = parse_with_preamble(
br#"
2 beginbfchar
<41> <ZZZZ>
<42> <0059>
endbfchar
"#,
);
assert_eq!(cmap.lookup_bf_string(0x41), None);
assert_eq!(
cmap.lookup_bf_string(0x42),
Some(BfString::Char('Y'))
);
}
#[test]
fn stream_cut_off_inside_a_block_keeps_the_entries_before_it() {
let cmap = parse_with_preamble(b"1 beginbfchar <41> <0058>\n<42> <00");
assert_eq!(cmap.lookup_bf_string(0x41), Some(BfString::Char('X')));
assert_eq!(cmap.lookup_bf_string(0x42), None);
}
#[test]
fn truncated_stream_with_no_entries_reports_failure() {
// The tolerant parser keeps collected entries, but a stream that
// breaks before any entry must still report failure so callers
// fall back to the font's own encoding.
assert!(CMap::parse(b"<00", |_| None).is_none());
}
#[test]
fn truncated_stream_with_only_metadata_reports_failure() {
// Character-collection metadata alone is not a usable CMap: an empty
// metadata-only map would block the caller's fallback paths, so a
// truncated stream with no mappings must report failure here too.
assert!(CMap::parse(
b"/Registry (Adobe) /Ordering (Japan1) /Supplement 0 <00",
|_| None
)
.is_none());
}
#[test]
fn unresolved_usecmap_with_no_mappings_reports_failure() {
// A stream whose only content is a `usecmap` reference that cannot be
// resolved yields no mappings at all; returning an empty map here
// would block the caller's encoding fallback paths, so it must
// report failure exactly as a full parse failure did before.
assert!(CMap::parse(b"/Bogus usecmap", |_| None).is_none());
}
#[test]
fn unresolved_usecmap_keeps_local_mappings() {
// The same unresolved reference must not discard mappings the stream
// carries itself: only the missing base is dropped.
let cmap = CMap::parse(
b"/Bogus usecmap 1 beginbfchar <41> <0041> endbfchar",
|_| None,
)
.expect("local mappings survive an unresolved usecmap");
assert!(cmap.lookup_bf_string(0x41).is_some());
}
#[test]
fn bad_bfchar_code_keeps_later_entries_aligned() {
// The malformed 5-byte code still has a destination object; both
// must be skipped or every later pair shifts by one object.
let cmap = parse_with_preamble(
br#"
2 beginbfchar
<0102030405> <0041>
<42> <0059>
endbfchar
"#,
);
assert_eq!(cmap.lookup_bf_string(0x41), None);
assert_eq!(cmap.lookup_bf_string(0x42), Some(BfString::Char('Y')));
}
#[test]
fn bfrange_array_stops_at_exhaustion() {
// One member for 256 codes: the array cannot be resynced, so the
// loop must stop rather than spin over the remaining codes.
let cmap = parse_with_preamble(
br#"
1 beginbfrange
<0000> <00FF> [<0041>]
endbfrange
"#,
);
assert_eq!(cmap.lookup_bf_string(0x00), Some(BfString::Char('A')));
assert_eq!(cmap.lookup_bf_string(0x01), None);
}
#[test]
fn unresolvable_usecmap_is_skipped() {
let cmap = parse_with_preamble(
br#"
/Bogus usecmap
1 beginbfchar
<41> <0058>
endbfchar
"#,
);
assert_eq!(
cmap.lookup_bf_string(0x41),
Some(BfString::Char('X'))
);
}
#[test]
fn bfchar_surrogate_pair() {
let cmap = parse_with_preamble(
br#"
1 beginbfchar
<3A51> <D840DC3E>
endbfchar
"#,
);
assert_eq!(
cmap.lookup_bf_string(0x3A51),
Some(BfString::Char('\u{2003E}'))
);
}
#[test]
fn bfrange_incrementing() {
let cmap = parse_with_preamble(
br#"
1 beginbfrange
<0000> <0004> <0041>
endbfrange
"#,
);
assert_eq!(cmap.lookup_bf_string(0x0000), Some(BfString::Char('A')));
assert_eq!(cmap.lookup_bf_string(0x0001), Some(BfString::Char('B')));
assert_eq!(cmap.lookup_bf_string(0x0004), Some(BfString::Char('E')));
assert_eq!(cmap.lookup_bf_string(0x0005), None);
}
#[test]
fn bfrange_array() {
let cmap = parse_with_preamble(
br#"
1 beginbfrange
<005F> <0061> [<00660066> <00660069> <0066006C>]
endbfrange
"#,
);
// ff, fi, fl ligatures
assert_eq!(
cmap.lookup_bf_string(0x005F),
Some(BfString::String(String::from("ff")))
);
assert_eq!(
cmap.lookup_bf_string(0x0060),
Some(BfString::String(String::from("fi")))
);
assert_eq!(
cmap.lookup_bf_string(0x0061),
Some(BfString::String(String::from("fl")))
);
}
#[test]
fn unicode_lookup_miss() {
let cmap = parse_with_preamble(
br#"
1 beginbfchar
<0041> <0048>
endbfchar
"#,
);
assert_eq!(cmap.lookup_bf_string(0x0000), None);
assert_eq!(cmap.lookup_bf_string(0x0042), None);
}
#[test]
fn identity_h() {
let cmap = CMap::identity_h();
assert_eq!(
cmap.metadata().name.as_deref(),
Some(b"Identity-H".as_slice())
);
assert_eq!(cmap.metadata().writing_mode, Some(WritingMode::Horizontal));
assert_eq!(cmap.lookup_cid_code(0x0041, 2), Some(0x0041));
assert_eq!(cmap.lookup_cid_code(0x1234, 2), Some(0x1234));
assert_eq!(cmap.lookup_cid_code(0xFFFF, 2), Some(0xFFFF));
assert_eq!(cmap.lookup_cid_code(0x0041, 1), None);
assert_eq!(cmap.lookup_cid_code(0x0041, 3), None);
}
#[test]
fn identity_v() {
let cmap = CMap::identity_v();
assert_eq!(
cmap.metadata().name.as_deref(),
Some(b"Identity-V".as_slice())
);
assert_eq!(cmap.metadata().writing_mode, Some(WritingMode::Vertical));
assert_eq!(cmap.lookup_cid_code(0x0041, 2), Some(0x0041));
assert_eq!(cmap.lookup_cid_code(0xFFFF, 2), Some(0xFFFF));
}
#[test]
fn minimal_cmap_no_name_no_wmode() {
// Extracted from corpus PDF 0500013.
let data = br#"
/CIDInit /ProcSet findresource begin
12 dict begin
begincmap
/CMapType 2 def
1 begincodespacerange
<00><ff>
endcodespacerange
1 beginbfrange
<01><01><0020>
endbfrange
endcmap
"#;
let cmap = CMap::parse(data, |_| None).unwrap();
assert_eq!(cmap.metadata().name, None);
assert_eq!(cmap.metadata().character_collection, None);
assert_eq!(cmap.metadata().writing_mode, None);
assert_eq!(cmap.lookup_bf_string(0x01), Some(BfString::Char(' ')));
}
#[test]
fn registry_as_name() {
// Extracted from corpus PDF 0875241, uses names instead of strings for
// strings for metadata.
let data = br#"
/CIDSystemInfo
<< /Registry /ABCDEF+SimSun
/Ordering (pdfbeaninc)
/Supplement 0
>> def
/CMapName /ABCDEF+SimSun def
/CMapType 2 def
1 begincodespacerange
<0000> <FFFF>
endcodespacerange
1 beginbfrange
<0000> <0000> <6881>
endbfrange
"#;
let cmap = CMap::parse(data, |_| None).unwrap();
let cc = cmap.metadata().character_collection.as_ref().unwrap();
assert_eq!(
cc.family,
CidFamily::Custom {
registry: b"ABCDEF+SimSun".to_vec(),
ordering: b"pdfbeaninc".to_vec(),
}
);
assert_eq!(cc.supplement, 0);
assert_eq!(
cmap.lookup_bf_string(0x0000),
Some(BfString::Char('\u{6881}'))
);
}
#[test]
fn single_byte_bfrange_destination() {
// See for example corpus test case 0625248.
let cmap = parse_with_preamble(
br#"
1 beginbfrange
<00> <7f> <00>
endbfrange
"#,
);
assert_eq!(
cmap.lookup_bf_string(0x00),
Some(BfString::Char('\u{0000}'))
);
assert_eq!(cmap.lookup_bf_string(0x41), Some(BfString::Char('A')));
assert_eq!(
cmap.lookup_bf_string(0x7f),
Some(BfString::Char('\u{007F}'))
);
}
}
#[cfg(all(test, feature = "embed-cmaps"))]
mod bcmap_tests {
use super::*;
fn get_embedded_cmap(name: CMapName<'_>) -> Option<&'static [u8]> {
load_embedded(name)
}
#[test]
fn embedded_h_cmap() {
let data = load_embedded(CMapName::H).expect("embedded H cmap not found");
let cmap = CMap::parse(data, get_embedded_cmap).expect("failed to parse H cmap");
assert_eq!(cmap.metadata().writing_mode, None);
assert_eq!(
cmap.metadata().character_collection,
Some(CharacterCollection {
family: CidFamily::AdobeJapan1,
supplement: 1,
})
);
assert_eq!(cmap.lookup_cid_code(0x2121, 2), Some(633));
assert_eq!(cmap.lookup_cid_code(0x2221, 2), Some(727));
}
#[test]
fn embedded_90ms_rksj_h() {
let data =
load_embedded(CMapName::N90msRksjH).expect("embedded 90ms-RKSJ-H cmap not found");
let cmap = CMap::parse(data, get_embedded_cmap).expect("failed to parse 90ms-RKSJ-H cmap");
assert_eq!(
cmap.metadata().character_collection,
Some(CharacterCollection {
family: CidFamily::AdobeJapan1,
supplement: 2,
})
);
assert_eq!(cmap.lookup_cid_code(0x20, 1), Some(231));
assert_eq!(cmap.lookup_cid_code(0x8140, 2), Some(633));
}
#[test]
fn embedded_v_cmap() {
let data = load_embedded(CMapName::V).expect("embedded V cmap not found");
let cmap = CMap::parse(data, get_embedded_cmap).expect("failed to parse V cmap");
assert_eq!(cmap.metadata().writing_mode, Some(WritingMode::Vertical));
assert_eq!(
cmap.metadata().character_collection,
Some(CharacterCollection {
family: CidFamily::AdobeJapan1,
supplement: 1,
})
);
// Inherited from base "H" via usecmap.
assert_eq!(cmap.lookup_cid_code(0x2121, 2), Some(633));
}
#[test]
fn embedded_gbk_euc_h() {
let data = load_embedded(CMapName::GbkEucH).expect("embedded GBK-EUC-H not found");
let cmap = CMap::parse(data, get_embedded_cmap).expect("failed to parse GBK-EUC-H");
assert_eq!(
cmap.metadata().character_collection,
Some(CharacterCollection {
family: CidFamily::AdobeGB1,
supplement: 2,
})
);
assert_eq!(cmap.lookup_cid_code(0x20, 1), Some(7716));
assert_eq!(cmap.lookup_cid_code(0x21, 1), Some(814));
assert_eq!(cmap.lookup_cid_code(0x7E, 1), Some(814 + 0x7E - 0x21));
assert_eq!(cmap.lookup_cid_code(0xFE80, 2), Some(22094));
assert_eq!(cmap.lookup_cid_code(0xFEA0, 2), Some(22094 + 0xA0 - 0x80));
}
#[test]
fn embedded_ksc_euc_h() {
let data = load_embedded(CMapName::KscEucH).unwrap();
let cmap = CMap::parse(data, get_embedded_cmap).unwrap();
assert_eq!(
cmap.metadata().character_collection,
Some(CharacterCollection {
family: CidFamily::AdobeKorea1,
supplement: 0,
})
);
assert_eq!(cmap.lookup_cid_code(0x20, 1), Some(8094));
assert_eq!(cmap.lookup_cid_code(0x41, 1), Some(8094 + 0x41 - 0x20));
assert_eq!(cmap.lookup_cid_code(0xA1A1, 2), Some(101));
assert_eq!(cmap.lookup_cid_code(0xA1FE, 2), Some(101 + 0xFE - 0xA1));
assert_eq!(cmap.lookup_cid_code(0xFDA1, 2), Some(7962));
}
#[test]
fn embedded_b5pc_h() {
let data = load_embedded(CMapName::B5pcH).unwrap();
let cmap = CMap::parse(data, get_embedded_cmap).unwrap();
assert_eq!(
cmap.metadata().character_collection,
Some(CharacterCollection {
family: CidFamily::AdobeCNS1,
supplement: 0,
})
);
assert_eq!(cmap.lookup_cid_code(0x20, 1), Some(1));
assert_eq!(cmap.lookup_cid_code(0x41, 1), Some(1 + 0x41 - 0x20));
assert_eq!(cmap.lookup_cid_code(0x80, 1), Some(61));
assert_eq!(cmap.lookup_cid_code(0xD140, 2), Some(7251));
assert_eq!(cmap.lookup_cid_code(0xD17E, 2), Some(7251 + 0x7E - 0x40));
assert_eq!(cmap.lookup_cid_code(0xF9D5, 2), Some(13642 + 3));
}
#[test]
fn embedded_unijis_utf16_h() {
let data = load_embedded(CMapName::UniJisUtf16H).unwrap();
let cmap = CMap::parse(data, get_embedded_cmap).unwrap();
assert_eq!(
cmap.metadata().character_collection,
Some(CharacterCollection {
family: CidFamily::AdobeJapan1,
supplement: 7,
})
);
assert_eq!(cmap.lookup_cid_code(0x20, 2), Some(1));
assert_eq!(cmap.lookup_cid_code(0x41, 2), Some(1 + 0x41 - 0x20));
assert_eq!(cmap.lookup_cid_code(0x5C, 2), Some(97));
assert_eq!(cmap.lookup_cid_code(0x5BCC, 2), Some(3531));
assert_eq!(cmap.lookup_cid_code(0xD884DF50, 4), Some(19130));
}
#[test]
fn embedded_identity_h() {
let data = load_embedded(CMapName::IdentityH).expect("embedded Identity-H not found");
let cmap = CMap::parse(data, get_embedded_cmap).expect("failed to parse Identity-H cmap");
assert_eq!(cmap.metadata().writing_mode, None);
assert_eq!(
cmap.metadata().character_collection,
Some(CharacterCollection {
family: CidFamily::AdobeIdentity,
supplement: 0,
})
);
assert_eq!(cmap.lookup_cid_code(0x0000, 2), Some(0));
assert_eq!(cmap.lookup_cid_code(0x0041, 2), Some(0x41));
assert_eq!(cmap.lookup_cid_code(0xFFFF, 2), Some(0xFFFF));
}
#[test]
fn embedded_identity_v() {
let data = load_embedded(CMapName::IdentityV).expect("embedded Identity-V not found");
let cmap = CMap::parse(data, get_embedded_cmap).expect("failed to parse Identity-V cmap");
assert_eq!(cmap.metadata().writing_mode, Some(WritingMode::Vertical));
assert_eq!(
cmap.metadata().character_collection,
Some(CharacterCollection {
family: CidFamily::AdobeIdentity,
supplement: 0,
})
);
assert_eq!(cmap.lookup_cid_code(0x0000, 2), Some(0));
assert_eq!(cmap.lookup_cid_code(0x0041, 2), Some(0x41));
assert_eq!(cmap.lookup_cid_code(0xFFFF, 2), Some(0xFFFF));
}
// Note: Some tests AI-generated, haven't double-checked them.
#[test]
fn embedded_adobe_gb1_ucs2() {
let data = load_embedded(CMapName::AdobeGb1Ucs2).unwrap();
let cmap = CMap::parse(data, get_embedded_cmap).unwrap();
assert_eq!(
cmap.metadata().character_collection,
Some(CharacterCollection {
family: CidFamily::Custom {
registry: b"Adobe".to_vec(),
ordering: b"Adobe_GB1_UCS2".to_vec(),
},
supplement: 5,
})
);
assert_eq!(
cmap.lookup_bf_string(0x0063),
Some(BfString::Char('\u{00B7}'))
);
assert_eq!(cmap.lookup_bf_string(0x0001), Some(BfString::Char(' ')));
assert_eq!(cmap.lookup_bf_string(0x0022), Some(BfString::Char('A')));
assert_eq!(cmap.lookup_bf_string(0x005F), Some(BfString::Char('~')));
assert_eq!(
cmap.lookup_bf_string(0x560F),
Some(BfString::String(String::from("\u{90CE}\u{FE00}")))
);
}
#[test]
fn embedded_adobe_japan1_ucs2() {
let data = load_embedded(CMapName::AdobeJapan1Ucs2).unwrap();
let cmap = CMap::parse(data, get_embedded_cmap).unwrap();
assert_eq!(
cmap.metadata().character_collection,
Some(CharacterCollection {
family: CidFamily::Custom {
registry: b"Adobe".to_vec(),
ordering: b"Adobe_Japan1_UCS2".to_vec(),
},
supplement: 7,
})
);
assert_eq!(
cmap.lookup_bf_string(0x003D),
Some(BfString::Char('\u{00A5}'))
);
assert_eq!(cmap.lookup_bf_string(0x0001), Some(BfString::Char(' ')));
assert_eq!(cmap.lookup_bf_string(0x003C), Some(BfString::Char('[')));
assert_eq!(
cmap.lookup_bf_string(0x0476),
Some(BfString::String(String::from("\u{82A6}\u{E0100}")))
);
assert_eq!(
cmap.lookup_bf_string(0x265B),
Some(BfString::String(String::from("10/11")))
);
assert_eq!(
cmap.lookup_bf_string(0x2E6B),
Some(BfString::String(String::from("オングストローム")))
);
}
#[test]
fn embedded_adobe_korea1_ucs2() {
let data = load_embedded(CMapName::AdobeKorea1Ucs2).unwrap();
let cmap = CMap::parse(data, get_embedded_cmap).unwrap();
assert_eq!(
cmap.metadata().character_collection,
Some(CharacterCollection {
family: CidFamily::Custom {
registry: b"Adobe".to_vec(),
ordering: b"Adobe_Korea1_UCS2".to_vec(),
},
supplement: 2,
})
);
assert_eq!(
cmap.lookup_bf_string(0x0060),
Some(BfString::Char('\u{20A9}'))
);
assert_eq!(
cmap.lookup_bf_string(0x2072),
Some(BfString::String(String::from("[10]")))
);
assert_eq!(
cmap.lookup_bf_string(0x2073),
Some(BfString::String(String::from("[11]")))
);
}
#[test]
fn embedded_adobe_cns1_ucs2() {
let data = load_embedded(CMapName::AdobeCns1Ucs2).unwrap();
let cmap = CMap::parse(data, get_embedded_cmap).unwrap();
assert_eq!(
cmap.metadata().character_collection,
Some(CharacterCollection {
family: CidFamily::Custom {
registry: b"Adobe".to_vec(),
ordering: b"Adobe_CNS1_UCS2".to_vec(),
},
supplement: 7,
})
);
assert_eq!(
cmap.lookup_bf_string(0x0060),
Some(BfString::Char('\u{00A9}'))
);
assert_eq!(cmap.lookup_bf_string(0x0001), Some(BfString::Char(' ')));
assert_eq!(cmap.lookup_bf_string(0x005F), Some(BfString::Char('~')));
assert_eq!(
cmap.lookup_bf_string(0x36B0),
Some(BfString::Char('\u{200CC}'))
);
}
#[test]
fn embedded_eten_b5_h() {
let data = load_embedded(CMapName::ETenB5H).unwrap();
let cmap = CMap::parse(data, get_embedded_cmap).unwrap();
assert_eq!(
cmap.metadata().character_collection,
Some(CharacterCollection {
family: CidFamily::AdobeCNS1,
supplement: 0,
})
);
assert_eq!(cmap.lookup_cid_code(0x20, 1), Some(13648));
assert_eq!(cmap.lookup_cid_code(0x7E, 1), Some(13648 + 0x7E - 0x20));
assert_eq!(cmap.lookup_cid_code(0xA140, 2), Some(99));
assert_eq!(cmap.lookup_cid_code(0xA158, 2), Some(99 + 0x58 - 0x40));
assert_eq!(cmap.lookup_cid_code(0xD040, 2), Some(7094));
assert_eq!(cmap.lookup_cid_code(0xF9FE, 2), Some(14056 + 0xFE - 0xD6));
}
}