use crate::parser::document::PdfDocument;
use crate::parser::objects::{PdfDictionary, PdfName, PdfObject, PdfStream};
use crate::parser::{ParseError, ParseOptions, ParseResult};
use crate::text::cid_to_unicode::CidCollection;
use crate::text::cmap::CMap;
use std::borrow::Cow;
use std::collections::HashMap;
use std::io::{Read, Seek};
use std::sync::OnceLock;
#[derive(Debug, Clone, Default)]
pub struct CidWidths {
pub widths: HashMap<u32, f64>,
pub ranges: Vec<(u32, u32, f64)>,
pub default_width: f64,
}
impl CidWidths {
pub fn width_for(&self, cid: u32) -> f64 {
self.widths
.get(&cid)
.copied()
.or_else(|| {
self.ranges
.iter()
.rev()
.find(|(first, last, _)| cid >= *first && cid <= *last)
.map(|(_, _, width)| *width)
})
.unwrap_or(self.default_width)
}
pub(crate) fn narrowest_positive_width(&self) -> Option<f64> {
self.widths
.values()
.copied()
.chain(self.ranges.iter().map(|(_, _, width)| *width))
.filter(|width| width.is_finite() && *width > 0.0)
.min_by(f64::total_cmp)
.or_else(|| {
(self.default_width.is_finite() && self.default_width > 0.0)
.then_some(self.default_width)
})
}
}
#[derive(Debug, Clone)]
pub struct FontMetrics {
pub first_char: Option<u32>,
pub last_char: Option<u32>,
pub widths: Option<Vec<f64>>,
pub missing_width: Option<f64>,
pub kerning: Option<HashMap<(u32, u32), f64>>,
pub cid_widths: Option<CidWidths>,
}
impl Default for FontMetrics {
fn default() -> Self {
Self {
first_char: None,
last_char: None,
widths: None,
missing_width: Some(500.0), kerning: None,
cid_widths: None,
}
}
}
#[derive(Debug, Clone)]
pub struct FontInfo {
pub name: String,
pub font_type: String,
pub encoding: Option<String>,
pub to_unicode: Option<CMap>,
pub differences: Option<HashMap<u8, String>>,
pub descendant_font: Option<Box<FontInfo>>,
pub cid_ordering: Option<String>,
pub metrics: FontMetrics,
pub cid_encoding: Option<crate::text::encoding_cmap::CidEncoding>,
}
pub struct CMapTextExtractor<R: Read + Seek> {
_phantom: std::marker::PhantomData<R>,
}
impl<R: Read + Seek> CMapTextExtractor<R> {
pub fn new() -> Self {
Self {
_phantom: std::marker::PhantomData,
}
}
pub fn extract_font_info(
&mut self,
font_dict: &PdfDictionary,
document: &PdfDocument<R>,
) -> ParseResult<FontInfo> {
let font_type = font_dict
.get("Subtype")
.and_then(|obj| obj.as_name())
.ok_or_else(|| ParseError::MissingKey("Font Subtype".to_string()))?;
let default_name = PdfName("Unknown".to_string());
let name = font_dict
.get("BaseFont")
.and_then(|obj| obj.as_name())
.unwrap_or(&default_name);
let mut font_info = FontInfo {
name: name.0.clone(),
font_type: font_type.0.clone(),
encoding: None,
to_unicode: None,
differences: None,
descendant_font: None,
cid_ordering: None,
metrics: FontMetrics::default(),
cid_encoding: None,
};
if let Some(cid_sys_info) = font_dict.get("CIDSystemInfo") {
if let PdfObject::Dictionary(cid_dict) = cid_sys_info {
if let Some(ordering) = cid_dict.get("Ordering") {
if let PdfObject::String(s) = ordering {
if let Ok(ordering_str) = String::from_utf8(s.0.clone()) {
font_info.cid_ordering = Some(ordering_str);
}
} else if let PdfObject::Name(n) = ordering {
font_info.cid_ordering = Some(n.0.clone());
}
}
}
}
if let Some(encoding_obj) = font_dict.get("Encoding") {
let resolved_encoding = document.resolve(encoding_obj).ok();
let target_obj = resolved_encoding.as_ref().unwrap_or(encoding_obj);
match target_obj {
PdfObject::Name(enc_name) => {
font_info.encoding = Some(enc_name.0.clone());
if enc_name.0 != "Identity-H" && enc_name.0 != "Identity-V" {
font_info.cid_encoding =
crate::text::encoding_cmap::resolve_predefined(&enc_name.0);
}
}
PdfObject::Dictionary(enc_dict) => {
if let Some(base_enc_obj) = enc_dict.get("BaseEncoding") {
let base_enc_resolved = document.resolve(base_enc_obj).ok();
let target_base = base_enc_resolved.as_ref().unwrap_or(base_enc_obj);
if let Some(base_enc) = target_base.as_name() {
font_info.encoding = Some(base_enc.0.clone());
}
}
if let Some(diff_obj) = enc_dict.get("Differences") {
let diff_resolved = document.resolve(diff_obj).ok();
let diff_target = diff_resolved.as_ref().unwrap_or(diff_obj);
if let Some(differences) = diff_target.as_array() {
font_info.differences =
Some(self.parse_encoding_differences(&differences.0, document)?);
}
}
}
PdfObject::Stream(stream) => {
if let Ok(data) = stream.decode(&ParseOptions::default()) {
if let Ok(enc) = crate::text::encoding_cmap::EncodingCMap::parse(&data) {
font_info.cid_encoding =
Some(crate::text::encoding_cmap::CidEncoding::Cmap(enc));
}
}
}
_ => {}
}
}
if let Some(to_unicode_obj) = font_dict.get("ToUnicode") {
if let Some(stream_ref) = to_unicode_obj.as_reference() {
if let Ok(PdfObject::Stream(stream)) =
document.get_object(stream_ref.0, stream_ref.1)
{
font_info.to_unicode = Some(self.parse_tounicode_stream(&stream, document)?);
}
}
}
font_info.metrics = self.extract_font_metrics(font_dict, document)?;
if font_type.as_str() == "Type0" {
let resolved_array;
let descendant_fonts = match font_dict.get("DescendantFonts") {
Some(PdfObject::Array(array)) => Some(array),
Some(PdfObject::Reference(num, gen)) => match document.get_object(*num, *gen) {
Ok(PdfObject::Array(array)) => {
resolved_array = array;
Some(&resolved_array)
}
_ => None,
},
_ => None,
};
let descendant = match descendant_fonts.and_then(|array| array.0.first()) {
Some(PdfObject::Dictionary(dict)) => Some(self.extract_font_info(dict, document)?),
Some(PdfObject::Reference(num, gen)) => match document.get_object(*num, *gen) {
Ok(PdfObject::Dictionary(dict)) => {
Some(self.extract_font_info(&dict, document)?)
}
_ => None,
},
_ => None,
};
if let Some(descendant) = descendant {
font_info.descendant_font = Some(Box::new(descendant));
}
}
Ok(font_info)
}
fn parse_encoding_differences(
&self,
differences: &[PdfObject],
document: &PdfDocument<R>,
) -> ParseResult<HashMap<u8, String>> {
let mut diff_map = HashMap::new();
let mut current_code = 0u8;
for item in differences {
let resolved = document.resolve(item).ok();
let target = resolved.as_ref().unwrap_or(item);
match target {
PdfObject::Integer(code) => {
current_code = *code as u8;
}
PdfObject::Name(name) => {
diff_map.insert(current_code, name.0.clone());
current_code = current_code.wrapping_add(1);
}
_ => {}
}
}
Ok(diff_map)
}
fn parse_tounicode_stream(
&self,
stream: &PdfStream,
_document: &PdfDocument<R>,
) -> ParseResult<CMap> {
let data = stream.decode(&ParseOptions::default())?;
CMap::parse(&data)
}
fn extract_font_metrics(
&self,
font_dict: &PdfDictionary,
document: &PdfDocument<R>,
) -> ParseResult<FontMetrics> {
let mut metrics = FontMetrics::default();
metrics.first_char = font_dict
.get("FirstChar")
.and_then(|obj| document.resolve(obj).ok())
.and_then(|obj| obj.as_integer())
.map(|first| first as u32);
metrics.last_char = font_dict
.get("LastChar")
.and_then(|obj| document.resolve(obj).ok())
.and_then(|obj| obj.as_integer())
.map(|last| last as u32);
let font_matrix = font_dict
.get("FontMatrix")
.and_then(|obj| document.resolve(obj).ok())
.and_then(|obj| {
if let PdfObject::Array(arr) = obj {
let mut matrix = Vec::new();
for elem in &arr.0 {
if let Ok(elem_resolved) = document.resolve(elem) {
if let Some(val) = elem_resolved.as_real() {
matrix.push(val);
}
}
}
if matrix.len() == 6 {
Some(matrix)
} else {
None
}
} else {
None
}
});
let width_scale = font_matrix.as_ref().map_or(1.0, |m| m[0] * 1000.0);
metrics.widths = font_dict
.get("Widths")
.and_then(|obj| document.resolve(obj).ok())
.and_then(|obj| match obj {
PdfObject::Array(widths_array) => {
let widths = widths_array
.0
.iter()
.map(|width_obj| {
let width_val = match document.resolve(width_obj) {
Ok(PdfObject::Integer(w)) => w as f64,
Ok(PdfObject::Real(w)) => w,
_ => 0.0,
};
width_val * width_scale
})
.collect();
Some(widths)
}
_ => None,
});
metrics.missing_width = font_dict
.get("FontDescriptor")
.and_then(|o| document.resolve(o).ok())
.and_then(|o| match o {
PdfObject::Dictionary(d) => d.get("MissingWidth").cloned(),
_ => None,
})
.and_then(|o| document.resolve(&o).ok())
.and_then(|o| match o {
PdfObject::Integer(w) => Some(w as f64),
PdfObject::Real(w) => Some(w),
_ => None,
});
let is_cid_font = font_dict
.get("Subtype")
.and_then(|o| document.resolve(o).ok())
.is_some_and(|o| match o {
PdfObject::Name(n) => matches!(n.0.as_str(), "CIDFontType0" | "CIDFontType2"),
_ => false,
});
let dw = font_dict
.get("DW")
.and_then(|o| document.resolve(o).ok())
.and_then(|o| o.as_real());
let w_array: Option<Cow<[PdfObject]>> = match font_dict.get("W") {
Some(obj) => match document.resolve(obj) {
Ok(PdfObject::Array(array)) => Some(Cow::Owned(array.0)),
_ => None,
},
_ => None,
};
if let Some(entries) = w_array {
let mut widths = HashMap::new();
let mut ranges = Vec::new();
let mut i = 0;
while i < entries.len() {
let first_cid = document
.resolve(&entries[i])
.ok()
.and_then(|o| o.as_integer());
let Some(first_cid) = first_cid else {
break;
};
match entries.get(i + 1) {
Some(w_list_obj) => {
let resolved_w_list = document.resolve(w_list_obj).ok();
if let Some(PdfObject::Array(w_list)) = resolved_w_list {
if let Ok(first_cid) = u32::try_from(first_cid) {
for (offset, w_obj) in w_list.0.iter().enumerate() {
let w = document.resolve(w_obj).ok().and_then(|o| o.as_real());
if let (Ok(offset), Some(w)) = (u32::try_from(offset), w) {
if let Some(cid) = first_cid
.checked_add(offset)
.filter(|cid| *cid <= u16::MAX as u32)
{
widths.insert(cid, w);
}
}
}
}
i += 2;
} else {
let last_cid = resolved_w_list.as_ref().and_then(|o| o.as_integer());
let w = entries
.get(i + 2)
.and_then(|o| document.resolve(o).ok())
.and_then(|o| o.as_real());
let (Some(last_cid), Some(w)) = (last_cid, w) else {
break;
};
let first_cid = first_cid.max(0).min(u16::MAX as i64) as u32;
let last_cid = last_cid.max(0).min(u16::MAX as i64) as u32;
if last_cid >= first_cid {
ranges.push((first_cid, last_cid, w));
}
i += 3;
}
}
None => break,
}
}
metrics.cid_widths = Some(CidWidths {
widths,
ranges,
default_width: dw.unwrap_or(1000.0),
});
} else if is_cid_font {
metrics.cid_widths = Some(CidWidths {
widths: HashMap::new(),
ranges: Vec::new(),
default_width: dw.unwrap_or(1000.0),
});
}
if let Some(desc_obj) = font_dict.get("FontDescriptor") {
if let Ok(PdfObject::Dictionary(desc_dict)) = document.resolve(desc_obj) {
if let Some(font_file_obj) = desc_dict.get("FontFile2") {
if let Ok(PdfObject::Stream(font_stream)) = document.resolve(font_file_obj) {
if let Ok(kerning_pairs) = extract_truetype_kerning(&font_stream) {
if !kerning_pairs.is_empty() {
metrics.kerning = Some(kerning_pairs);
}
}
}
}
}
}
Ok(metrics)
}
}
pub(crate) fn extract_truetype_kerning(
font_stream: &PdfStream,
) -> ParseResult<HashMap<(u32, u32), f64>> {
let font_data = match font_stream.decode(&ParseOptions::default()) {
Ok(data) => data,
Err(_) => return Ok(HashMap::new()), };
match parse_truetype_kern_table(&font_data) {
Ok(pairs) => Ok(pairs),
Err(_) => Ok(HashMap::new()), }
}
pub(crate) fn parse_truetype_kern_table(font_data: &[u8]) -> ParseResult<HashMap<(u32, u32), f64>> {
if font_data.len() < 12 {
return Err(ParseError::SyntaxError {
position: 0,
message: "Font data too short for TrueType header".to_string(),
});
}
let num_tables = u16::from_be_bytes([font_data[4], font_data[5]]) as usize;
let mut kern_offset = None;
let mut kern_length = None;
for i in 0..num_tables {
let table_offset = 12 + i * 16;
if table_offset + 16 > font_data.len() {
break;
}
let tag = &font_data[table_offset..table_offset + 4];
if tag == b"kern" {
kern_offset = Some(u32::from_be_bytes([
font_data[table_offset + 8],
font_data[table_offset + 9],
font_data[table_offset + 10],
font_data[table_offset + 11],
]) as usize);
kern_length = Some(u32::from_be_bytes([
font_data[table_offset + 12],
font_data[table_offset + 13],
font_data[table_offset + 14],
font_data[table_offset + 15],
]) as usize);
break;
}
}
let (offset, length) = match (kern_offset, kern_length) {
(Some(o), Some(l)) => (o, l),
_ => return Ok(HashMap::new()),
};
if offset + length > font_data.len() {
return Err(ParseError::SyntaxError {
position: offset,
message: "Invalid kern table offset".to_string(),
});
}
let kern_data = &font_data[offset..offset + length];
if kern_data.len() < 4 {
return Ok(HashMap::new());
}
let n_tables = u16::from_be_bytes([kern_data[2], kern_data[3]]) as usize;
let mut kerning_pairs = HashMap::new();
let mut table_offset = 4;
for _ in 0..n_tables {
if table_offset + 6 > kern_data.len() {
break;
}
let subtable_length = u32::from_be_bytes([
0,
0,
kern_data[table_offset + 2],
kern_data[table_offset + 3],
]) as usize;
let coverage =
u16::from_be_bytes([kern_data[table_offset + 4], kern_data[table_offset + 5]]);
let format = coverage & 0xFF;
if format == 0 && table_offset + subtable_length <= kern_data.len() {
let subtable_data = &kern_data[table_offset + 6..table_offset + subtable_length];
if subtable_data.len() >= 8 {
let n_pairs = u16::from_be_bytes([subtable_data[0], subtable_data[1]]) as usize;
let mut pair_offset = 8;
for _ in 0..n_pairs {
if pair_offset + 6 > subtable_data.len() {
break;
}
let left_glyph = u16::from_be_bytes([
subtable_data[pair_offset],
subtable_data[pair_offset + 1],
]) as u32;
let right_glyph = u16::from_be_bytes([
subtable_data[pair_offset + 2],
subtable_data[pair_offset + 3],
]) as u32;
let value = i16::from_be_bytes([
subtable_data[pair_offset + 4],
subtable_data[pair_offset + 5],
]) as f64;
kerning_pairs.insert((left_glyph, right_glyph), value);
pair_offset += 6;
}
}
}
table_offset += subtable_length;
}
Ok(kerning_pairs)
}
fn is_preservable_whitespace(c: char) -> bool {
matches!(c, ' ' | '\t' | '\n')
}
pub(crate) fn decode_is_usable(decoded: &str) -> bool {
!decoded.is_empty()
&& !decoded
.chars()
.all(|c| c.is_control() && !is_preservable_whitespace(c))
}
pub(crate) enum FontEntry {
Indirect(u32, u16),
Inline(PdfDictionary),
}
pub(crate) fn resolve_font_entries<R: Read + Seek>(
resources: &PdfDictionary,
document: &PdfDocument<R>,
) -> Vec<(String, FontEntry)> {
let font_dict = match resources.get("Font") {
Some(PdfObject::Dictionary(dict)) => Some(dict.clone()),
Some(PdfObject::Reference(num, gen)) => match document.get_object(*num, *gen) {
Ok(PdfObject::Dictionary(dict)) => Some(dict),
_ => None,
},
_ => None,
};
let Some(font_dict) = font_dict else {
return Vec::new();
};
font_dict
.0
.iter()
.filter_map(|(name, obj)| match obj {
PdfObject::Reference(num, gen) => {
Some((name.0.clone(), FontEntry::Indirect(*num, *gen)))
}
PdfObject::Dictionary(dict) => Some((name.0.clone(), FontEntry::Inline(dict.clone()))),
_ => None,
})
.collect()
}
pub fn decode_text_with_font(text_bytes: &[u8], font_info: &FontInfo) -> ParseResult<String> {
if let Some(ref to_unicode) = font_info.to_unicode {
return decode_with_cmap(text_bytes, to_unicode);
}
if font_info.font_type == "Type0" {
if let Some(ref descendant) = font_info.descendant_font {
if descendant.to_unicode.is_some() {
return decode_text_with_font(text_bytes, descendant);
}
let ordering = descendant
.cid_ordering
.as_deref()
.or(font_info.cid_ordering.as_deref());
match &font_info.cid_encoding {
Some(crate::text::encoding_cmap::CidEncoding::Utf16Be) => {
return Ok(crate::text::encoding_cmap::decode_utf16be(text_bytes));
}
Some(crate::text::encoding_cmap::CidEncoding::Cmap(enc)) => {
if let Some(coll) =
ordering.and_then(crate::text::cid_to_unicode::CidCollection::from_ordering)
{
return Ok(decode_via_encoding_cmap(text_bytes, enc, &coll));
}
}
None => {}
}
if let Some(ordering) = ordering {
if let Some(collection) =
crate::text::cid_to_unicode::CidCollection::from_ordering(ordering)
{
let result = decode_with_cid_table(text_bytes, &collection);
if decode_is_usable(&result) {
return Ok(result);
}
}
}
return decode_text_with_font(text_bytes, descendant);
}
}
decode_with_encoding(text_bytes, font_info)
}
fn decode_via_encoding_cmap(
text_bytes: &[u8],
enc: &crate::text::encoding_cmap::EncodingCMap,
collection: &crate::text::cid_to_unicode::CidCollection,
) -> String {
let mut result = String::new();
let mut i = 0;
while i < text_bytes.len() {
let len = enc
.code_len_at(text_bytes, i)
.max(1)
.min(text_bytes.len() - i);
let code = &text_bytes[i..i + len];
match enc.map_code_to_cid(code).or_else(|| enc.map_notdef(code)) {
Some(cid) => match collection.cid_to_unicode(cid) {
Some(ch) => result.push(ch),
None if cid > 0 => result.push('\u{FFFD}'),
None => {}
},
None => result.push('\u{FFFD}'),
}
i += len;
}
result
}
fn decode_with_cid_table(
text_bytes: &[u8],
collection: &crate::text::cid_to_unicode::CidCollection,
) -> String {
let mut result = String::new();
let mut i = 0;
while i + 1 < text_bytes.len() {
let cid = u16::from_be_bytes([text_bytes[i], text_bytes[i + 1]]);
if let Some(ch) = collection.cid_to_unicode(cid) {
result.push(ch);
} else if cid > 0 {
result.push('\u{FFFD}');
}
i += 2;
}
result
}
fn decode_with_cmap(text_bytes: &[u8], cmap: &CMap) -> ParseResult<String> {
let inherited = cmap
.inherited_ordering()
.and_then(CidCollection::from_ordering);
let mut result = String::new();
let mut i = 0;
while i < text_bytes.len() {
let mut decoded = false;
for len in 1..=4.min(text_bytes.len() - i) {
let code = &text_bytes[i..i + len];
if let Some(mapped) = cmap.map(code) {
if let Some(unicode_str) = cmap.to_unicode(&mapped) {
result.push_str(&unicode_str);
i += len;
decoded = true;
break;
}
}
}
if !decoded {
if let Some(coll) = inherited {
if text_bytes.len() - i >= 2 {
let cid = u16::from_be_bytes([text_bytes[i], text_bytes[i + 1]]);
match coll.cid_to_unicode(cid) {
Some(ch) => result.push(ch),
None if cid > 0 => result.push('\u{FFFD}'),
None => {}
}
i += 2;
continue;
}
}
i += 1;
}
}
Ok(result)
}
fn decode_with_encoding(text_bytes: &[u8], font_info: &FontInfo) -> ParseResult<String> {
let mut result = String::new();
for &byte in text_bytes {
if let Some(ref differences) = font_info.differences {
if let Some(char_name) = differences.get(&byte) {
if let Some(unicode) = glyph_name_to_unicode_sequence(char_name) {
result.push_str(&unicode);
continue;
}
}
}
let ch = match font_info.encoding.as_deref() {
Some("WinAnsiEncoding") => decode_winansi(byte),
Some("MacRomanEncoding") => decode_macroman(byte),
Some("StandardEncoding") => decode_standard(byte),
_ => byte as char,
};
result.push(ch);
}
Ok(result)
}
pub fn glyph_name_to_unicode(name: &str) -> Option<char> {
let value = glyph_name_to_unicode_sequence(name)?;
let mut chars = value.chars();
match (chars.next(), chars.next()) {
(Some(character), None) => Some(character),
_ => None,
}
}
pub fn glyph_name_to_unicode_sequence(name: &str) -> Option<String> {
if name.chars().count() == 1 {
return Some(name.to_owned());
}
let lookup_name = name.split_once('.').map_or(name, |(base, _)| base);
if lookup_name.is_empty() {
return None;
}
if let Some(hex) = lookup_name.strip_prefix("uni") {
if !hex.is_empty() && hex.len() % 4 == 0 && hex.bytes().all(|byte| byte.is_ascii_hexdigit())
{
return decode_unicode_scalars(
hex.as_bytes()
.chunks_exact(4)
.map(|chunk| std::str::from_utf8(chunk).expect("ASCII hexadecimal glyph name")),
);
}
}
if let Some(hex) = lookup_name.strip_prefix('u') {
if (4..=6).contains(&hex.len()) && hex.bytes().all(|byte| byte.is_ascii_hexdigit()) {
return decode_unicode_scalars(std::iter::once(hex));
}
}
adobe_glyph_list()
.get(lookup_name)
.and_then(|scalars| decode_unicode_scalars(scalars.split_whitespace()))
.or_else(|| {
legacy_glyph_name_to_unicode(lookup_name).map(|character| character.to_string())
})
}
fn decode_unicode_scalars<'a>(scalars: impl IntoIterator<Item = &'a str>) -> Option<String> {
let mut result = String::new();
for scalar in scalars {
let value = u32::from_str_radix(scalar, 16).ok()?;
result.push(char::from_u32(value)?);
}
(!result.is_empty()).then_some(result)
}
fn adobe_glyph_list() -> &'static HashMap<&'static str, &'static str> {
static GLYPHS: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
GLYPHS.get_or_init(|| {
include_str!("adobe_glyph_list.txt")
.lines()
.filter_map(|line| line.split_once(';'))
.collect()
})
}
fn legacy_glyph_name_to_unicode(name: &str) -> Option<char> {
let mut chars = name.chars();
if let (Some(c), None) = (chars.next(), chars.next()) {
return Some(c);
}
if name.len() == 7
&& name.starts_with("uni")
&& name[3..].chars().all(|c| c.is_ascii_hexdigit())
{
if let Ok(val) = u32::from_str_radix(&name[3..], 16) {
if let Some(c) = char::from_u32(val) {
return Some(c);
}
}
}
if (5..=7).contains(&name.len())
&& name.starts_with('u')
&& name[1..].chars().all(|c| c.is_ascii_hexdigit())
{
if let Ok(val) = u32::from_str_radix(&name[1..], 16) {
if let Some(c) = char::from_u32(val) {
return Some(c);
}
}
}
let lookup_name = if let Some((base, _)) = name.split_once('.') {
if !base.is_empty() {
if let Some(c) = glyph_name_to_unicode(base) {
return Some(c);
}
}
base
} else {
name
};
match lookup_name {
"space" => Some(' '),
"exclam" => Some('!'),
"quotedbl" => Some('"'),
"numbersign" => Some('#'),
"dollar" => Some('$'),
"percent" => Some('%'),
"ampersand" => Some('&'),
"quotesingle" => Some('\''),
"parenleft" => Some('('),
"parenright" => Some(')'),
"asterisk" => Some('*'),
"plus" => Some('+'),
"comma" => Some(','),
"hyphen" => Some('-'),
"period" => Some('.'),
"slash" => Some('/'),
"zero" => Some('0'),
"one" => Some('1'),
"two" => Some('2'),
"three" => Some('3'),
"four" => Some('4'),
"five" => Some('5'),
"six" => Some('6'),
"seven" => Some('7'),
"eight" => Some('8'),
"nine" => Some('9'),
"colon" => Some(':'),
"semicolon" => Some(';'),
"less" => Some('<'),
"equal" => Some('='),
"greater" => Some('>'),
"question" => Some('?'),
"at" => Some('@'),
"bracketleft" => Some('['),
"backslash" => Some('\\'),
"bracketright" => Some(']'),
"asciicircum" => Some('^'),
"underscore" => Some('_'),
"grave" => Some('`'),
"braceleft" => Some('{'),
"bar" => Some('|'),
"braceright" => Some('}'),
"asciitilde" => Some('~'),
"exclamdown" => Some('¡'),
"cent" => Some('¢'),
"sterling" => Some('£'),
"currency" => Some('¤'),
"yen" => Some('¥'),
"brokenbar" => Some('¦'),
"section" => Some('§'),
"dieresis" => Some('¨'),
"copyright" => Some('©'),
"ordfeminine" => Some('ª'),
"guillemotleft" | "guillemetleft" => Some('«'),
"logicalnot" => Some('¬'),
"softhyphen" | "hyphensoft" => Some('\u{00AD}'),
"registered" => Some('®'),
"macron" => Some('¯'),
"degree" => Some('°'),
"plusminus" => Some('±'),
"twosuperior" => Some('²'),
"threesuperior" => Some('³'),
"acute" => Some('´'),
"mu" | "micro" => Some('µ'),
"paragraph" => Some('¶'),
"periodcentered" | "bulletcentered" => Some('·'),
"cedilla" => Some('¸'),
"onesuperior" => Some('¹'),
"ordmasculine" => Some('º'),
"guillemotright" | "guillemetright" => Some('»'),
"onequarter" => Some('¼'),
"onehalf" => Some('½'),
"threequarters" => Some('¾'),
"questiondown" => Some('¿'),
"Agrave" => Some('À'),
"Aacute" => Some('Á'),
"Acircumflex" => Some('Â'),
"Atilde" => Some('Ã'),
"Adieresis" => Some('Ä'),
"Aring" => Some('Å'),
"AE" => Some('Æ'),
"Ccedilla" => Some('Ç'),
"Egrave" => Some('È'),
"Eacute" => Some('É'),
"Ecircumflex" => Some('Ê'),
"Edieresis" => Some('Ë'),
"Igrave" => Some('Ì'),
"Iacute" => Some('Í'),
"Icircumflex" => Some('Î'),
"Idieresis" => Some('Ï'),
"Eth" => Some('Ð'),
"Ntilde" => Some('Ñ'),
"Ograve" => Some('Ò'),
"Oacute" => Some('Ó'),
"Ocircumflex" => Some('Ô'),
"Otilde" => Some('Õ'),
"Odieresis" => Some('Ö'),
"multiply" => Some('×'),
"Oslash" => Some('Ø'),
"Ugrave" => Some('Ù'),
"Uacute" => Some('Ú'),
"Ucircumflex" => Some('Û'),
"Udieresis" => Some('Ü'),
"Yacute" => Some('Ý'),
"Thorn" => Some('Þ'),
"germandbls" => Some('ß'),
"agrave" => Some('à'),
"aacute" => Some('á'),
"acircumflex" => Some('â'),
"atilde" => Some('ã'),
"adieresis" => Some('ä'),
"aring" => Some('å'),
"ae" => Some('æ'),
"ccedilla" => Some('ç'),
"egrave" => Some('è'),
"eacute" => Some('é'),
"ecircumflex" => Some('ê'),
"edieresis" => Some('ë'),
"igrave" => Some('ì'),
"iacute" => Some('í'),
"icircumflex" => Some('î'),
"idieresis" => Some('ï'),
"eth" => Some('ð'),
"ntilde" => Some('ñ'),
"ograve" => Some('ò'),
"oacute" => Some('ó'),
"ocircumflex" => Some('ô'),
"otilde" => Some('õ'),
"odieresis" => Some('ö'),
"divide" => Some('÷'),
"oslash" => Some('ø'),
"ugrave" => Some('ù'),
"uacute" => Some('ú'),
"ucircumflex" => Some('û'),
"udieresis" => Some('ü'),
"yacute" => Some('ý'),
"thorn" => Some('þ'),
"ydieresis" => Some('ÿ'),
"Amacron" => Some('Ā'),
"amacron" => Some('ā'),
"Abreve" => Some('Ă'),
"abreve" => Some('ă'),
"Aogonek" => Some('Ą'),
"aogonek" => Some('ą'),
"Cacute" => Some('Ć'),
"cacute" => Some('ć'),
"Ccircumflex" => Some('Ĉ'),
"ccircumflex" => Some('ĉ'),
"Cdotaccent" => Some('Ċ'),
"cdotaccent" => Some('ċ'),
"Ccaron" => Some('Č'),
"ccaron" => Some('č'),
"Dcaron" => Some('Ď'),
"dcaron" => Some('ď'),
"Dcroat" => Some('Đ'),
"dcroat" => Some('đ'),
"Emacron" => Some('Ē'),
"emacron" => Some('ē'),
"Ebreve" => Some('Ĕ'),
"ebreve" => Some('ĕ'),
"Edotaccent" => Some('Ė'),
"edotaccent" => Some('ė'),
"Eogonek" => Some('Ę'),
"eogonek" => Some('ę'),
"Ecaron" => Some('Ě'),
"ecaron" => Some('ě'),
"Gcircumflex" => Some('Ĝ'),
"gcircumflex" => Some('ĝ'),
"Gbreve" => Some('Ğ'),
"gbreve" => Some('ğ'),
"Gdotaccent" => Some('Ġ'),
"gdotaccent" => Some('ġ'),
"Gcommaaccent" => Some('Ģ'),
"gcommaaccent" => Some('ģ'),
"Hcircumflex" => Some('Ĥ'),
"hcircumflex" => Some('ĥ'),
"Hbar" => Some('Ħ'),
"hbar" => Some('ħ'),
"Itilde" => Some('Ĩ'),
"itilde" => Some('ĩ'),
"Imacron" => Some('Ī'),
"imacron" => Some('ī'),
"Ibreve" => Some('Ĭ'),
"ibreve" => Some('ĭ'),
"Iogonek" => Some('Į'),
"iogonek" => Some('į'),
"Idotaccent" | "Idot" => Some('İ'),
"dotlessi" => Some('ı'),
"IJ" => Some('IJ'),
"ij" => Some('ij'),
"Jcircumflex" => Some('Ĵ'),
"jcircumflex" => Some('ĵ'),
"Kcommaaccent" => Some('Ķ'),
"kcommaaccent" => Some('ķ'),
"kgreenlandic" => Some('ĸ'),
"Lacute" => Some('Ĺ'),
"lacute" => Some('ĺ'),
"Lcommaaccent" => Some('Ļ'),
"lcommaaccent" => Some('ļ'),
"Lcaron" => Some('Ľ'),
"lcaron" => Some('ľ'),
"Ldot" => Some('Ŀ'),
"ldot" => Some('ŀ'),
"Lslash" => Some('Ł'),
"lslash" => Some('ł'),
"Nacute" => Some('Ń'),
"nacute" => Some('ń'),
"Ncommaaccent" => Some('Ņ'),
"ncommaaccent" => Some('ņ'),
"Ncaron" => Some('Ň'),
"ncaron" => Some('ň'),
"napostrophe" => Some('ʼn'),
"Eng" => Some('Ŋ'),
"eng" => Some('ŋ'),
"Omacron" => Some('Ō'),
"omacron" => Some('ō'),
"Obreve" => Some('Ŏ'),
"obreve" => Some('ŏ'),
"Ohungarumlaut" => Some('Ő'),
"ohungarumlaut" => Some('ő'),
"OE" => Some('Œ'),
"oe" => Some('œ'),
"Racute" => Some('Ŕ'),
"racute" => Some('ŕ'),
"Rcommaaccent" => Some('Ŗ'),
"rcommaaccent" => Some('ŗ'),
"Rcaron" => Some('Ř'),
"rcaron" => Some('ř'),
"Sacute" => Some('Ś'),
"sacute" => Some('ś'),
"Scircumflex" => Some('Ŝ'),
"scircumflex" => Some('ŝ'),
"Scedilla" => Some('Ş'),
"scedilla" => Some('ş'),
"Scaron" => Some('Š'),
"scaron" => Some('š'),
"Scommaaccent" => Some('Ș'),
"scommaaccent" => Some('ș'),
"Tcommaaccent" => Some('Ț'),
"tcommaaccent" => Some('ț'),
"Tcedilla" => Some('Ţ'),
"tcedilla" => Some('ţ'),
"Tcaron" => Some('Ť'),
"tcaron" => Some('ť'),
"Tbar" => Some('Ŧ'),
"tbar" => Some('ŧ'),
"Utilde" => Some('Ũ'),
"utilde" => Some('ũ'),
"Umacron" => Some('Ū'),
"umacron" => Some('ū'),
"Ubreve" => Some('Ŭ'),
"ubreve" => Some('ŭ'),
"Uring" => Some('Ů'),
"uring" => Some('ů'),
"Uhungarumlaut" => Some('Ű'),
"uhungarumlaut" => Some('ű'),
"Uogonek" => Some('Ų'),
"uogonek" => Some('ų'),
"Wcircumflex" => Some('Ŵ'),
"wcircumflex" => Some('ŵ'),
"Ycircumflex" => Some('Ŷ'),
"ycircumflex" => Some('ŷ'),
"Ydieresis" => Some('Ÿ'),
"Zacute" => Some('Ź'),
"zacute" => Some('ź'),
"Zdotaccent" => Some('Ż'),
"zdotaccent" => Some('ż'),
"Zcaron" => Some('Ž'),
"zcaron" => Some('ž'),
"longs" => Some('ſ'),
"florin" => Some('ƒ'),
"dotlessj" => Some('ȷ'),
"circumflex" => Some('ˆ'),
"caron" => Some('ˇ'),
"breve" => Some('˘'),
"dotaccent" => Some('˙'),
"ring" => Some('˚'),
"ogonek" => Some('˛'),
"tilde" => Some('˜'),
"hungarumlaut" => Some('˝'),
"quoteleft" | "leftsinglequote" => Some('‘'),
"quoteright" | "rightsinglequote" => Some('’'),
"quotesinglbase" | "singlelow9quote" => Some('‚'),
"quotedblleft" | "leftdoublequote" => Some('“'),
"quotedblright" | "rightdoublequote" => Some('”'),
"quotedblbase" | "doublelow9quote" => Some('„'),
"dagger" => Some('†'),
"daggerdbl" => Some('‡'),
"bullet" => Some('•'),
"ellipsis" => Some('…'),
"perthousand" => Some('‰'),
"guilsinglleft" | "singleleftguillemet" => Some('‹'),
"guilsinglright" | "singlerightguillemet" => Some('›'),
"fraction" => Some('⁄'),
"endash" | "figuredash" => Some('–'),
"emdash" => Some('—'),
"Euro" | "euro" => Some('€'),
"trademark" => Some('™'),
"minus" => Some('−'),
"checkmark" => Some('✓'),
"partialdiff" => Some('∂'),
"summation" => Some('∑'),
"radical" => Some('√'),
"infinity" => Some('∞'),
"integral" => Some('∫'),
"approxequal" => Some('≈'),
"notequal" => Some('≠'),
"lessequal" => Some('≤'),
"greaterequal" => Some('≥'),
"lozenge" => Some('◊'),
"apple" => Some('\u{F8FF}'),
"ff" => Some('ff'),
"fi" => Some('fi'),
"fl" => Some('fl'),
"ffi" => Some('ffi'),
"ffl" => Some('ffl'),
"ft" => Some('ſt'),
"st" => Some('st'),
"Alpha" => Some('Α'),
"Beta" => Some('Β'),
"Gamma" => Some('Γ'),
"Delta" => Some('Δ'),
"Epsilon" => Some('Ε'),
"Zeta" => Some('Ζ'),
"Eta" => Some('Η'),
"Theta" => Some('Θ'),
"Iota" => Some('Ι'),
"Kappa" => Some('Κ'),
"Lambda" => Some('Λ'),
"Mu" => Some('Μ'),
"Nu" => Some('Ν'),
"Xi" => Some('Ξ'),
"Omicron" => Some('Ο'),
"Pi" => Some('Π'),
"Rho" => Some('Ρ'),
"Sigma" => Some('Σ'),
"Tau" => Some('Τ'),
"Upsilon" => Some('Υ'),
"Phi" => Some('Φ'),
"Chi" => Some('Χ'),
"Psi" => Some('Ψ'),
"Omega" => Some('Ω'),
"alpha" => Some('α'),
"beta" => Some('β'),
"gamma" => Some('γ'),
"delta" => Some('δ'),
"epsilon" => Some('ε'),
"zeta" => Some('ζ'),
"eta" => Some('η'),
"theta" => Some('θ'),
"iota" => Some('ι'),
"kappa" => Some('κ'),
"lambda" => Some('λ'),
"nu" => Some('ν'),
"xi" => Some('ξ'),
"omicron" => Some('ο'),
"pi" => Some('π'),
"rho" => Some('ρ'),
"sigma" => Some('σ'),
"sigma1" => Some('ς'),
"tau" => Some('τ'),
"upsilon" => Some('υ'),
"phi" => Some('φ'),
"chi" => Some('χ'),
"psi" => Some('ψ'),
"omega" => Some('ω'),
_ => None,
}
}
fn decode_winansi(byte: u8) -> char {
match byte {
0x80 => '€',
0x82 => '‚',
0x83 => 'ƒ',
0x84 => '„',
0x85 => '…',
0x86 => '†',
0x87 => '‡',
0x88 => 'ˆ',
0x89 => '‰',
0x8A => 'Š',
0x8B => '‹',
0x8C => 'Œ',
0x8E => 'Ž',
0x91 => '\u{2018}', 0x92 => '\u{2019}', 0x93 => '"',
0x94 => '"',
0x95 => '•',
0x96 => '–',
0x97 => '—',
0x98 => '˜',
0x99 => '™',
0x9A => 'š',
0x9B => '›',
0x9C => 'œ',
0x9E => 'ž',
0x9F => 'Ÿ',
_ => byte as char,
}
}
fn decode_macroman(byte: u8) -> char {
match byte {
0x80 => 'Ä',
0x81 => 'Å',
0x82 => 'Ç',
0x83 => 'É',
0x84 => 'Ñ',
0x85 => 'Ö',
0x86 => 'Ü',
0x87 => 'á',
0x88 => 'à',
0x89 => 'â',
0x8A => 'ä',
0x8B => 'ã',
0x8C => 'å',
0x8D => 'ç',
0x8E => 'é',
0x8F => 'è',
0x90 => 'ê',
0x91 => 'ë',
0x92 => 'í',
0x93 => 'ì',
0x94 => 'î',
0x95 => 'ï',
0x96 => 'ñ',
0x97 => 'ó',
0x98 => 'ò',
0x99 => 'ô',
0x9A => 'ö',
0x9B => 'õ',
0x9C => 'ú',
0x9D => 'ù',
0x9E => 'û',
0x9F => 'ü',
_ => byte as char,
}
}
fn decode_standard(byte: u8) -> char {
byte as char
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_glyph_name_to_unicode() {
assert_eq!(glyph_name_to_unicode("A"), Some('A'));
assert_eq!(glyph_name_to_unicode("a"), Some('a'));
assert_eq!(glyph_name_to_unicode("0"), Some('0'));
assert_eq!(glyph_name_to_unicode("+"), Some('+'));
assert_eq!(glyph_name_to_unicode("?"), Some('?'));
assert_eq!(glyph_name_to_unicode("uni0041"), Some('A'));
assert_eq!(glyph_name_to_unicode("uni00E9"), Some('é'));
assert_eq!(glyph_name_to_unicode("uni20AC"), Some('€'));
assert_eq!(glyph_name_to_unicode("uni00DF"), Some('ß'));
assert_eq!(glyph_name_to_unicode("u0041"), Some('A'));
assert_eq!(glyph_name_to_unicode("u00E9"), Some('é'));
assert_eq!(glyph_name_to_unicode("u1F600"), Some('😀'));
assert_eq!(glyph_name_to_unicode("u000041"), Some('A'));
assert_eq!(glyph_name_to_unicode("space"), Some(' '));
assert_eq!(glyph_name_to_unicode("zero"), Some('0'));
assert_eq!(glyph_name_to_unicode("nine"), Some('9'));
assert_eq!(glyph_name_to_unicode("exclam"), Some('!'));
assert_eq!(glyph_name_to_unicode("aacute"), Some('á'));
assert_eq!(glyph_name_to_unicode("eacute"), Some('é'));
assert_eq!(glyph_name_to_unicode("atilde"), Some('ã'));
assert_eq!(glyph_name_to_unicode("ccedilla"), Some('ç'));
assert_eq!(glyph_name_to_unicode("Adieresis"), Some('Ä'));
assert_eq!(glyph_name_to_unicode("Eacute"), Some('É'));
assert_eq!(glyph_name_to_unicode("ntilde"), Some('ñ'));
assert_eq!(glyph_name_to_unicode("Oslash"), Some('Ø'));
assert_eq!(glyph_name_to_unicode("oslash"), Some('ø'));
assert_eq!(glyph_name_to_unicode("Scaron"), Some('Š'));
assert_eq!(glyph_name_to_unicode("scaron"), Some('š'));
assert_eq!(glyph_name_to_unicode("Zcaron"), Some('Ž'));
assert_eq!(glyph_name_to_unicode("zcaron"), Some('ž'));
assert_eq!(glyph_name_to_unicode("germandbls"), Some('ß'));
assert_eq!(glyph_name_to_unicode("periodcentered"), Some('·'));
assert_eq!(glyph_name_to_unicode("bullet"), Some('•'));
assert_eq!(glyph_name_to_unicode("hyphen"), Some('-'));
assert_eq!(glyph_name_to_unicode("endash"), Some('–'));
assert_eq!(glyph_name_to_unicode("emdash"), Some('—'));
assert_eq!(glyph_name_to_unicode("quoteleft"), Some('‘'));
assert_eq!(glyph_name_to_unicode("quoteright"), Some('’'));
assert_eq!(glyph_name_to_unicode("quotedblleft"), Some('“'));
assert_eq!(glyph_name_to_unicode("quotedblright"), Some('”'));
assert_eq!(glyph_name_to_unicode("ellipsis"), Some('…'));
assert_eq!(glyph_name_to_unicode("fi"), Some('fi'));
assert_eq!(glyph_name_to_unicode("fl"), Some('fl'));
assert_eq!(glyph_name_to_unicode("ffi"), Some('ffi'));
assert_eq!(glyph_name_to_unicode("ffl"), Some('ffl'));
assert_eq!(glyph_name_to_unicode("ff"), Some('ff'));
assert_eq!(glyph_name_to_unicode("oe"), Some('œ'));
assert_eq!(glyph_name_to_unicode("OE"), Some('Œ'));
assert_eq!(glyph_name_to_unicode("ae"), Some('æ'));
assert_eq!(glyph_name_to_unicode("AE"), Some('Æ'));
assert_eq!(glyph_name_to_unicode("plus"), Some('+'));
assert_eq!(glyph_name_to_unicode("minus"), Some('−'));
assert_eq!(glyph_name_to_unicode("slash"), Some('/'));
assert_eq!(glyph_name_to_unicode("backslash"), Some('\\'));
assert_eq!(glyph_name_to_unicode("Euro"), Some('€'));
assert_eq!(glyph_name_to_unicode("trademark"), Some('™'));
assert_eq!(glyph_name_to_unicode("copyright"), Some('©'));
assert_eq!(glyph_name_to_unicode("registered"), Some('®'));
assert_eq!(glyph_name_to_unicode("checkmark"), Some('✓'));
assert_eq!(glyph_name_to_unicode("A.swash"), Some('A'));
assert_eq!(glyph_name_to_unicode("aacute.alt"), Some('á'));
assert_eq!(glyph_name_to_unicode("nonexistent_glyph_xyz"), None);
}
#[test]
fn test_decode_with_encoding_differences() {
let mut diffs = HashMap::new();
diffs.insert(1, "aacute".to_string());
diffs.insert(2, "germandbls".to_string());
diffs.insert(3, "bullet".to_string());
diffs.insert(4, "fi".to_string());
diffs.insert(5, "uni0041".to_string());
diffs.insert(6, "u00E9".to_string());
diffs.insert(7, "endash".to_string());
diffs.insert(8, "minus".to_string());
let font_info = FontInfo {
name: "CustomFont".to_string(),
font_type: "Type1".to_string(),
encoding: Some("WinAnsiEncoding".to_string()),
to_unicode: None,
differences: Some(diffs),
descendant_font: None,
cid_ordering: None,
metrics: FontMetrics::default(),
cid_encoding: None,
};
let decoded = decode_text_with_font(&[1, 2, 3, 4, 5, 6, 7, 8], &font_info).unwrap();
assert_eq!(decoded, "áß•fiAé–−");
}
#[test]
fn test_decode_winansi() {
assert_eq!(decode_winansi(0x20), ' ');
assert_eq!(decode_winansi(0x41), 'A');
assert_eq!(decode_winansi(0x80), '€');
assert_eq!(decode_winansi(0x99), '™');
}
#[test]
fn test_decode_macroman() {
assert_eq!(decode_macroman(0x20), ' ');
assert_eq!(decode_macroman(0x41), 'A');
assert_eq!(decode_macroman(0x80), 'Ä');
assert_eq!(decode_macroman(0x87), 'á');
}
#[test]
fn test_font_info_creation() {
let font_info = FontInfo {
name: "Helvetica".to_string(),
font_type: "Type1".to_string(),
encoding: Some("WinAnsiEncoding".to_string()),
to_unicode: None,
differences: None,
descendant_font: None,
cid_ordering: None,
metrics: FontMetrics::default(),
cid_encoding: None,
};
assert_eq!(font_info.name, "Helvetica");
assert_eq!(font_info.font_type, "Type1");
assert_eq!(font_info.encoding, Some("WinAnsiEncoding".to_string()));
}
#[test]
fn simple_font_one_byte_tounicode_under_two_byte_codespace_decodes() {
let cmap_src = br#"begincmap
/CMapName /Adobe-Identity-UCS def
/CMapType 2 def
1 begincodespacerange
<0000> <FFFF>
endcodespacerange
2 beginbfchar
<92> <2019>
<2C> <002C>
endbfchar
endcmap
"#;
let cmap = crate::text::cmap::CMap::parse(cmap_src).unwrap();
let font_info = FontInfo {
name: "VUNXGH+ArialMT".to_string(),
font_type: "TrueType".to_string(),
encoding: Some("WinAnsiEncoding".to_string()),
to_unicode: Some(cmap),
differences: None,
descendant_font: None,
cid_ordering: None,
metrics: FontMetrics::default(),
cid_encoding: None,
};
let out = decode_text_with_font(&[0x92, 0x2C], &font_info).unwrap();
assert_eq!(out, "\u{2019},");
}
#[test]
fn embedded_encoding_cmap_decodes_via_cid_table() {
use crate::text::cid_to_unicode::CidCollection;
use crate::text::encoding_cmap::{CidEncoding, EncodingCMap};
let coll = CidCollection::from_ordering("GB1").unwrap();
let (cid, expected) = (1u16..2000)
.find_map(|c| coll.cid_to_unicode(c).map(|ch| (c, ch)))
.expect("GB1 has at least one mapped CID");
let enc = EncodingCMap::parse(
format!(
"begincmap\n1 begincodespacerange <0000> <FFFF> endcodespacerange\n\
1 begincidchar <0041> {cid} endcidchar\nendcmap"
)
.as_bytes(),
)
.unwrap();
let descendant = FontInfo {
name: "Desc".into(),
font_type: "CIDFontType0".into(),
encoding: None,
to_unicode: None,
differences: None,
descendant_font: None,
cid_ordering: Some("GB1".into()),
metrics: FontMetrics::default(),
cid_encoding: None,
};
let parent = FontInfo {
name: "Type0".into(),
font_type: "Type0".into(),
encoding: None,
to_unicode: None,
differences: None,
descendant_font: Some(Box::new(descendant)),
cid_ordering: None,
metrics: FontMetrics::default(),
cid_encoding: Some(CidEncoding::Cmap(enc)),
};
let out = decode_text_with_font(&[0x00, 0x41], &parent).unwrap();
assert_eq!(out, expected.to_string());
}
#[test]
fn type0_utf16be_encoding_decodes_through_dispatch() {
use crate::text::encoding_cmap::CidEncoding;
let descendant = FontInfo {
name: "Desc".into(),
font_type: "CIDFontType2".into(),
encoding: None,
to_unicode: None,
differences: None,
descendant_font: None,
cid_ordering: Some("GB1".into()),
metrics: FontMetrics::default(),
cid_encoding: None,
};
let parent = FontInfo {
name: "Type0".into(),
font_type: "Type0".into(),
encoding: None,
to_unicode: None,
differences: None,
descendant_font: Some(Box::new(descendant)),
cid_ordering: None,
metrics: FontMetrics::default(),
cid_encoding: Some(CidEncoding::Utf16Be),
};
let out = decode_text_with_font(&[0x4E, 0x2D], &parent).unwrap();
assert_eq!(out, "中");
}
#[test]
fn explicit_bfchar_overrides_usecmap_cid_fallback() {
use crate::text::cmap::CMap;
let with_override = CMap::parse(
b"begincmap\n/Adobe-Korea1-UCS2 usecmap\n\
1 begincodespacerange <0000> <FFFF> endcodespacerange\n\
1 beginbfchar <0041> <AC00> endbfchar\nendcmap",
)
.expect("parse with_override");
let without = CMap::parse(
b"begincmap\n/Adobe-Korea1-UCS2 usecmap\n\
1 begincodespacerange <0000> <FFFF> endcodespacerange\nendcmap",
)
.expect("parse without");
let got = decode_with_cmap(&[0x00, 0x41], &with_override).unwrap();
let fallback = decode_with_cmap(&[0x00, 0x41], &without).unwrap();
assert_eq!(got, "\u{AC00}", "explicit bfchar must win");
assert_ne!(
got, fallback,
"explicit mapping must override the CID-table fallback"
);
}
#[test]
fn extract_font_metrics_type3_font_matrix_scaling() {
use crate::parser::objects::PdfArray;
use crate::parser::PdfReader;
use std::io::Cursor;
let pdf = b"%PDF-1.4\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\nxref\n0 3\n0000000000 65535 f \n0000000009 00000 n \n0000000058 00000 n \ntrailer\n<< /Size 3 /Root 1 0 R >>\nstartxref\n115\n%%EOF\n";
let document = PdfDocument::new(PdfReader::new(Cursor::new(&pdf[..])).unwrap());
let extractor = CMapTextExtractor::new();
let mut font_dict = PdfDictionary::new();
font_dict.insert("Subtype".into(), PdfObject::Name(PdfName("Type3".into())));
font_dict.insert("FirstChar".into(), PdfObject::Integer(65));
font_dict.insert("LastChar".into(), PdfObject::Integer(66));
font_dict.insert(
"FontMatrix".into(),
PdfObject::Array(PdfArray(vec![
PdfObject::Real(1.0),
PdfObject::Real(0.0),
PdfObject::Real(0.0),
PdfObject::Real(1.0),
PdfObject::Real(0.0),
PdfObject::Real(0.0),
])),
);
font_dict.insert(
"Widths".into(),
PdfObject::Array(PdfArray(vec![PdfObject::Real(0.5), PdfObject::Real(0.75)])),
);
let metrics = extractor
.extract_font_metrics(&font_dict, &document)
.unwrap();
assert_eq!(metrics.first_char, Some(65));
assert_eq!(metrics.last_char, Some(66));
assert_eq!(metrics.widths, Some(vec![500.0, 750.0]));
let mut font_dict2 = PdfDictionary::new();
font_dict2.insert("Subtype".into(), PdfObject::Name(PdfName("Type3".into())));
font_dict2.insert("FirstChar".into(), PdfObject::Integer(65));
font_dict2.insert("LastChar".into(), PdfObject::Integer(66));
font_dict2.insert(
"FontMatrix".into(),
PdfObject::Array(PdfArray(vec![
PdfObject::Real(0.001),
PdfObject::Real(0.0),
PdfObject::Real(0.0),
PdfObject::Real(0.001),
PdfObject::Real(0.0),
PdfObject::Real(0.0),
])),
);
font_dict2.insert(
"Widths".into(),
PdfObject::Array(PdfArray(vec![
PdfObject::Integer(500),
PdfObject::Integer(750),
])),
);
let metrics2 = extractor
.extract_font_metrics(&font_dict2, &document)
.unwrap();
assert_eq!(metrics2.widths, Some(vec![500.0, 750.0]));
let mut font_dict3 = PdfDictionary::new();
font_dict3.insert(
"Subtype".into(),
PdfObject::Name(PdfName("TrueType".into())),
);
font_dict3.insert("FirstChar".into(), PdfObject::Integer(32));
font_dict3.insert("LastChar".into(), PdfObject::Integer(33));
font_dict3.insert(
"Widths".into(),
PdfObject::Array(PdfArray(vec![
PdfObject::Integer(250),
PdfObject::Integer(333),
])),
);
let metrics3 = extractor
.extract_font_metrics(&font_dict3, &document)
.unwrap();
assert_eq!(metrics3.widths, Some(vec![250.0, 333.0]));
}
}