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 crate::text::extraction::TextExtractor;
use std::collections::HashMap;
use std::io::{Read, Seek};
#[derive(Debug, Clone)]
#[allow(dead_code)]
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>>,
}
impl Default for FontMetrics {
fn default() -> Self {
Self {
first_char: None,
last_char: None,
widths: None,
missing_width: Some(500.0), kerning: None,
}
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
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_to_gid_map: Option<Vec<u16>>,
pub cid_ordering: Option<String>,
pub metrics: FontMetrics,
pub cid_encoding: Option<crate::text::encoding_cmap::CidEncoding>,
}
#[allow(dead_code)]
pub struct CMapTextExtractor<R: Read + Seek> {
base_extractor: TextExtractor,
font_cache: HashMap<String, FontInfo>,
_phantom: std::marker::PhantomData<R>,
}
impl<R: Read + Seek> CMapTextExtractor<R> {
#[allow(dead_code)]
pub fn new() -> Self {
Self {
base_extractor: TextExtractor::new(),
font_cache: HashMap::new(),
_phantom: std::marker::PhantomData,
}
}
#[allow(dead_code)]
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_to_gid_map: 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") {
match encoding_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) = enc_dict.get("BaseEncoding").and_then(|o| o.as_name()) {
font_info.encoding = Some(base_enc.0.clone());
}
if let Some(PdfObject::Array(differences)) = enc_dict.get("Differences") {
font_info.differences =
Some(self.parse_encoding_differences(&differences.0)?);
}
}
PdfObject::Reference(num, gen) => {
if let Ok(PdfObject::Stream(stream)) = document.get_object(*num, *gen) {
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" {
if let Some(PdfObject::Array(descendant_array)) = font_dict.get("DescendantFonts") {
if let Some(desc_ref) = descendant_array.0.first().and_then(|o| o.as_reference()) {
if let Ok(PdfObject::Dictionary(desc_dict)) =
document.get_object(desc_ref.0, desc_ref.1)
{
let descendant = self.extract_font_info(&desc_dict, document)?;
font_info.descendant_font = Some(Box::new(descendant));
}
}
}
}
Ok(font_info)
}
#[allow(dead_code)]
fn parse_encoding_differences(
&self,
differences: &[PdfObject],
) -> ParseResult<HashMap<u8, String>> {
let mut diff_map = HashMap::new();
let mut current_code = 0u8;
for item in differences {
match item {
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)
}
#[allow(dead_code)]
fn parse_tounicode_stream(
&self,
stream: &PdfStream,
_document: &PdfDocument<R>,
) -> ParseResult<CMap> {
let data = stream.decode(&ParseOptions::default())?;
CMap::parse(&data)
}
#[allow(dead_code)]
fn extract_font_metrics(
&self,
font_dict: &PdfDictionary,
document: &PdfDocument<R>,
) -> ParseResult<FontMetrics> {
let mut metrics = FontMetrics::default();
if let Some(PdfObject::Integer(first)) = font_dict.get("FirstChar") {
metrics.first_char = Some(*first as u32);
}
if let Some(PdfObject::Integer(last)) = font_dict.get("LastChar") {
metrics.last_char = Some(*last as u32);
}
if let Some(widths_obj) = font_dict.get("Widths") {
match widths_obj {
PdfObject::Array(widths_array) => {
let mut widths = Vec::new();
for width_obj in &widths_array.0 {
match width_obj {
PdfObject::Integer(w) => widths.push(*w as f64),
PdfObject::Real(w) => widths.push(*w),
_ => widths.push(0.0),
}
}
metrics.widths = Some(widths);
}
PdfObject::Reference(obj_num, gen_num) => {
if let Ok(PdfObject::Array(widths_array)) =
document.get_object(*obj_num, *gen_num)
{
let mut widths = Vec::new();
for width_obj in &widths_array.0 {
match width_obj {
PdfObject::Integer(w) => widths.push(*w as f64),
PdfObject::Real(w) => widths.push(*w),
_ => widths.push(0.0),
}
}
metrics.widths = Some(widths);
}
}
_ => {}
}
}
if let Some(desc_ref) = font_dict
.get("FontDescriptor")
.and_then(|o| o.as_reference())
{
if let Ok(PdfObject::Dictionary(desc_dict)) =
document.get_object(desc_ref.0, desc_ref.1)
{
if let Some(missing_width_obj) = desc_dict.get("MissingWidth") {
match missing_width_obj {
PdfObject::Integer(w) => metrics.missing_width = Some(*w as f64),
PdfObject::Real(w) => metrics.missing_width = Some(*w),
_ => {}
}
}
}
}
if let Some(desc_ref) = font_dict
.get("FontDescriptor")
.and_then(|o| o.as_reference())
{
if let Ok(PdfObject::Dictionary(desc_dict)) =
document.get_object(desc_ref.0, desc_ref.1)
{
if let Some(font_file_ref) =
desc_dict.get("FontFile2").and_then(|o| o.as_reference())
{
if let Ok(PdfObject::Stream(font_stream)) =
document.get_object(font_file_ref.0, font_file_ref.1)
{
if let Ok(kerning_pairs) = self.extract_truetype_kerning(&font_stream) {
if !kerning_pairs.is_empty() {
metrics.kerning = Some(kerning_pairs);
}
}
}
}
}
}
Ok(metrics)
}
#[allow(dead_code)]
fn extract_truetype_kerning(
&self,
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 self.parse_truetype_kern_table(&font_data) {
Ok(pairs) => Ok(pairs),
Err(_) => Ok(HashMap::new()), }
}
#[allow(dead_code)]
fn parse_truetype_kern_table(&self, 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)
}
#[allow(dead_code)]
pub fn extract_text_from_page(
&mut self,
document: &PdfDocument<R>,
page_index: u32,
) -> ParseResult<String> {
let page = document.get_page(page_index)?;
if let Some(resources) = page.get_resources() {
if let Some(PdfObject::Dictionary(font_dict)) = resources.get("Font") {
for (font_name, font_obj) in font_dict.0.iter() {
if let Some(font_ref) = font_obj.as_reference() {
if let Ok(PdfObject::Dictionary(font_dict)) =
document.get_object(font_ref.0, font_ref.1)
{
if let Ok(font_info) = self.extract_font_info(&font_dict, document) {
self.font_cache.insert(font_name.0.clone(), font_info);
}
}
}
}
}
}
let extracted = self
.base_extractor
.extract_from_page(document, page_index)?;
Ok(extracted.text)
}
}
fn is_preservable_whitespace(c: char) -> bool {
matches!(c, ' ' | '\t' | '\n' | '\r')
}
pub(crate) fn decode_is_usable(decoded: &str) -> bool {
!decoded.is_empty()
&& !decoded
.chars()
.all(|c| c.is_control() && !is_preservable_whitespace(c))
}
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_char) = glyph_name_to_unicode(char_name) {
result.push(unicode_char);
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)
}
#[allow(dead_code)]
fn glyph_name_to_unicode(name: &str) -> Option<char> {
match 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('@'),
"A" => Some('A'),
"B" => Some('B'),
"C" => Some('C'),
_ => None,
}
}
#[allow(dead_code)]
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,
}
}
#[allow(dead_code)]
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,
}
}
#[allow(dead_code)]
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("space"), Some(' '));
assert_eq!(glyph_name_to_unicode("A"), Some('A'));
assert_eq!(glyph_name_to_unicode("zero"), Some('0'));
assert_eq!(glyph_name_to_unicode("unknown"), None);
}
#[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_to_gid_map: 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_to_gid_map: 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_to_gid_map: 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_to_gid_map: None,
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_to_gid_map: 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_to_gid_map: None,
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"
);
}
}
#[allow(dead_code)]
pub fn extract_truetype_kerning(font_data: &[u8]) -> ParseResult<HashMap<(u32, u32), f64>> {
let extractor: CMapTextExtractor<std::fs::File> = CMapTextExtractor::new();
extractor.parse_truetype_kern_table(font_data)
}
#[allow(dead_code)]
pub fn parse_truetype_kern_table(kern_data: &[u8]) -> ParseResult<HashMap<(u32, u32), f64>> {
let extractor: CMapTextExtractor<std::fs::File> = CMapTextExtractor::new();
extractor.parse_truetype_kern_table(kern_data)
}