use crate::fonts::{Type3Font, MAX_FONT_STREAM_SIZE};
use crate::parser::document::PdfDocument;
use crate::parser::objects::{PdfDictionary, PdfObject};
use crate::parser::{ParseError, ParseResult};
use crate::text::cid_to_unicode::AdobeCidCollection;
use crate::text::cmap::CMap;
use crate::text::encoding::TextEncoding;
use crate::text::encoding_cmap::{decode_utf16be, resolve_predefined, CidEncoding};
use crate::text::extraction_cmap::{resolve_cid_ordering, CMapTextExtractor, CidVerticalWidths};
use std::collections::{BTreeMap, HashMap};
use std::io::{Read, Seek};
const MAX_CID_TO_GID_MAP_SIZE: usize = (u16::MAX as usize + 1) * 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FontSubtype {
Type1,
TrueType,
CidFontType0,
CidFontType2,
Type3,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WritingMode {
Horizontal,
Vertical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EmbeddedFontFormat {
Type1,
TrueType,
Type1C,
CidFontType0C,
OpenType,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EmbeddedFont {
pub format: EmbeddedFontFormat,
pub data: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct DecodedGlyph {
pub source_code: Vec<u8>,
pub cid: Option<u32>,
pub gid: Option<u16>,
pub unicode: Option<String>,
pub advance: f64,
}
#[derive(Debug, Clone)]
enum CodeEncoding {
Simple,
Identity,
Composite(CidEncoding),
}
#[derive(Debug, Clone)]
enum CidToGid {
Identity,
Table(Vec<u16>),
Cff(BTreeMap<u16, u16>),
}
#[derive(Debug, Clone)]
struct CidWidths {
explicit: HashMap<u32, f64>,
ranges: Vec<(u32, u32, f64)>,
default: f64,
}
impl CidWidths {
fn width(&self, cid: u32) -> f64 {
self.explicit
.get(&cid)
.copied()
.or_else(|| {
self.ranges
.iter()
.rev()
.find(|(first, last, _)| cid >= *first && cid <= *last)
.map(|(_, _, width)| *width)
})
.unwrap_or(self.default)
}
}
#[derive(Debug, Clone)]
pub struct ResolvedFontResource {
pub resource_name: String,
pub base_font: Option<String>,
pub subtype: FontSubtype,
pub encoding: Option<String>,
pub writing_mode: WritingMode,
pub differences: BTreeMap<u8, String>,
pub embedded_font: Option<EmbeddedFont>,
pub type3: Option<Type3Font>,
code_encoding: CodeEncoding,
to_unicode: Option<CMap>,
cid_to_gid: Option<CidToGid>,
cid_widths: Option<CidWidths>,
cid_collection: Option<AdobeCidCollection>,
vertical_widths: CidVerticalWidths,
intrinsic: Option<HashMap<u8, String>>,
first_char: u32,
simple_widths: Vec<f64>,
missing_width: f64,
symbolic: bool,
}
impl ResolvedFontResource {
pub fn from_page<R: Read + Seek>(
document: &PdfDocument<R>,
page_index: u32,
resource_name: &str,
) -> ParseResult<Self> {
let page = document.get_page(page_index)?;
let resources = document
.get_page_resources(&page)?
.ok_or_else(|| missing("page Resources"))?;
let fonts = resolve_required(document, resources, "Font")?;
let fonts = expect_dict(&fonts, "page Font resources")?;
let font_object = fonts
.get(resource_name)
.ok_or_else(|| missing(&format!("font resource /{resource_name}")))?;
Self::resolve(document, resource_name, font_object)
}
pub fn resolve<R: Read + Seek>(
document: &PdfDocument<R>,
resource_name: &str,
font_object: &PdfObject,
) -> ParseResult<Self> {
let resolved = document.resolve(font_object)?;
let normalized_font = crate::text::extraction_cmap::without_null_metrics(
expect_dict(&resolved, "font resource")?,
document,
);
let font = normalized_font.as_ref();
let declared_subtype = required_name(font, "Subtype")?;
if declared_subtype == "Type3" {
let type3 = Type3Font::resolve(font_object, document)?;
let to_unicode = resolve_cmap(document, font.get("ToUnicode"))?;
let mut differences = BTreeMap::new();
let mut widths = vec![0.0; 256];
for (&code, name) in &type3.names {
differences.insert(code, name.clone());
}
for (&code, &width) in &type3.normalized_widths {
widths[code as usize] = width;
}
return Ok(Self {
resource_name: resource_name.into(),
base_font: type3.name.clone(),
subtype: FontSubtype::Type3,
encoding: None,
writing_mode: WritingMode::Horizontal,
differences,
embedded_font: None,
type3: Some(type3),
code_encoding: CodeEncoding::Simple,
to_unicode,
cid_to_gid: None,
cid_widths: None,
cid_collection: None,
vertical_widths: CidVerticalWidths::default(),
intrinsic: None,
first_char: 0,
simple_widths: widths,
missing_width: 0.0,
symbolic: false,
});
}
let base_font = font
.get("BaseFont")
.and_then(PdfObject::as_name)
.map(|name| name.0.clone());
let to_unicode = resolve_cmap(document, font.get("ToUnicode"))?;
let (mut encoding, differences, code_encoding, writing_mode) =
resolve_encoding(document, font.get("Encoding"), declared_subtype == "Type0")?;
let descendant_storage;
let concrete = if declared_subtype == "Type0" {
let descendants = resolve_required(document, font, "DescendantFonts")?;
let descendants = descendants
.as_array()
.ok_or_else(|| syntax("DescendantFonts must be an array"))?;
let descendant = descendants
.0
.first()
.ok_or_else(|| syntax("DescendantFonts must not be empty"))?;
descendant_storage = document.resolve(descendant)?;
expect_dict(&descendant_storage, "CID descendant font")?
} else {
font
};
let normalized_concrete =
crate::text::extraction_cmap::without_null_metrics(concrete, document);
let concrete = normalized_concrete.as_ref();
let concrete_name = required_name(concrete, "Subtype")?;
let subtype = match concrete_name {
"Type1" | "MMType1" => FontSubtype::Type1,
"TrueType" => FontSubtype::TrueType,
"CIDFontType0" => FontSubtype::CidFontType0,
"CIDFontType2" => FontSubtype::CidFontType2,
other => return Err(syntax(&format!("unsupported font subtype /{other}"))),
};
let composite = declared_subtype == "Type0";
let cid_subtype = matches!(
subtype,
FontSubtype::CidFontType0 | FontSubtype::CidFontType2
);
if composite != cid_subtype {
return Err(syntax(if composite {
"Type0 descendant must be CIDFontType0 or CIDFontType2"
} else {
"CID font subtype must be a descendant of Type0"
}));
}
let cid_collection = if composite {
let ordering = resolve_cid_ordering(document, concrete)?;
let encoding = match &code_encoding {
CodeEncoding::Composite(encoding) => Some(encoding),
_ => None,
};
crate::text::extraction_cmap::compatible_cid_ordering(ordering.as_deref(), encoding)
.and_then(AdobeCidCollection::from_ordering)
} else {
None
};
let descriptor = resolve_optional_dict(document, concrete.get("FontDescriptor"))?;
let embedded_font = descriptor
.as_ref()
.map(|dict| resolve_embedded_font(document, dict))
.transpose()?
.flatten();
let intrinsic = if subtype == FontSubtype::Type1 && encoding.is_none() {
CMapTextExtractor::embedded_intrinsic_encoding(font, document)
} else {
None
};
let vertical_widths = if composite {
CMapTextExtractor::new()
.extract_font_metrics(concrete, document)?
.cid_widths
.map(|w| w.vertical)
.unwrap_or_default()
} else {
CidVerticalWidths::default()
};
let missing_width = descriptor
.as_ref()
.and_then(|dict| dict.get("MissingWidth"))
.map(|value| document.resolve(value))
.transpose()?
.as_ref()
.map(metric_number)
.transpose()?
.unwrap_or(0.0);
let symbolic = descriptor
.as_ref()
.and_then(|dict| dict.get("Flags"))
.map(|object| document.resolve(object))
.transpose()?
.and_then(|object| object.as_integer())
.is_some_and(|flags| flags & 4 != 0)
|| base_font.as_deref() == Some("Symbol");
if encoding.is_none() && declared_subtype == "Type1" {
let has_embedded_program = descriptor.as_ref().is_some_and(|dict| {
["FontFile", "FontFile2", "FontFile3"]
.iter()
.any(|key| dict.get(key).is_some())
});
if !has_embedded_program {
encoding = base_font
.as_deref()
.and_then(|name| {
crate::text::extraction_cmap::builtin_type1_encoding(name, symbolic)
})
.map(str::to_owned);
}
}
let cid_widths = matches!(
subtype,
FontSubtype::CidFontType0 | FontSubtype::CidFontType2
)
.then(|| parse_cid_widths(document, concrete))
.transpose()?;
let cid_to_gid = if subtype == FontSubtype::CidFontType2 {
resolve_cid_to_gid(document, concrete.get("CIDToGIDMap"))?
} else if subtype == FontSubtype::CidFontType0 {
embedded_font
.as_ref()
.filter(|font| {
matches!(
font.format,
EmbeddedFontFormat::CidFontType0C | EmbeddedFontFormat::OpenType
)
})
.map(|font| {
crate::text::fonts::cff::cid::CidFont::parse(&font.data)
.map(|cff| CidToGid::Cff(cff.mappings().collect()))
})
.transpose()?
} else {
None
};
let (first, last) = crate::text::extraction_cmap::simple_font_bounds(document, font)?;
let mut first_char = first.unwrap_or(0);
let mut simple_widths = resolve_number_array(document, font.get("Widths"))?;
if let Some(last) = last {
simple_widths.truncate((last - first_char + 1) as usize);
}
if !composite && font.get("Widths").is_none() {
if let Some(metrics) = base_font
.as_deref()
.and_then(crate::text::fonts::standard::get_standard_font_metrics_by_name)
{
first_char = 0;
simple_widths = (0..=255u8)
.map(|code| {
let width = if let Some(name) = differences.get(&code) {
metrics.get_glyph_width(name)
} else {
metrics.encoded_char_width(encoding.as_deref(), None, code)
};
f64::from(width.unwrap_or(500))
})
.collect();
}
}
Ok(Self {
resource_name: resource_name.into(),
base_font,
subtype,
encoding,
writing_mode,
differences,
embedded_font,
type3: None,
code_encoding,
to_unicode,
cid_to_gid,
cid_widths,
cid_collection,
vertical_widths,
intrinsic,
first_char,
simple_widths,
missing_width,
symbolic,
})
}
pub fn decode_glyphs(&self, bytes: &[u8]) -> ParseResult<Vec<DecodedGlyph>> {
let mut glyphs = Vec::new();
let mut position = 0;
while position < bytes.len() {
let code_len = match &self.code_encoding {
CodeEncoding::Simple => 1,
CodeEncoding::Identity | CodeEncoding::Composite(CidEncoding::Utf16Be) => 2,
CodeEncoding::Composite(CidEncoding::Cmap(cmap)) => {
cmap.code_len_at(bytes, position)
}
};
if code_len > 4 {
return Err(syntax("character code exceeds four bytes"));
}
if position + code_len > bytes.len() {
return Err(syntax("truncated character code"));
}
let code = bytes[position..position + code_len].to_vec();
position += code_len;
let cid = match &self.code_encoding {
CodeEncoding::Simple => None,
CodeEncoding::Identity | CodeEncoding::Composite(CidEncoding::Utf16Be) => {
Some(be_code(&code)?)
}
CodeEncoding::Composite(CidEncoding::Cmap(cmap)) => cmap
.map_code_to_cid(&code)
.or_else(|| cmap.map_notdef(&code))
.map(u32::from),
};
let gid = cid.and_then(|cid| self.gid_for(cid));
let unicode = self.unicode_for(&code, cid);
let advance = if self.writing_mode == WritingMode::Vertical {
cid.map_or(self.vertical_widths.default_advance, |cid| {
self.vertical_widths.advance_for(cid)
})
} else {
match &self.cid_widths {
Some(widths) => cid.map_or(widths.default, |cid| widths.width(cid)),
None => self.simple_width(be_code(&code)?),
}
};
glyphs.push(DecodedGlyph {
source_code: code,
cid,
gid,
unicode,
advance,
});
}
if self
.to_unicode
.as_ref()
.is_some_and(CMap::is_unicode_identity)
{
for index in 1..glyphs.len() {
let (previous, current) = glyphs.split_at_mut(index);
let high = &mut previous[index - 1];
let low = &mut current[0];
if let ([hi0, hi1], [lo0, lo1]) =
(high.source_code.as_slice(), low.source_code.as_slice())
{
let high_unit = u16::from_be_bytes([*hi0, *hi1]);
let low_unit = u16::from_be_bytes([*lo0, *lo1]);
if (0xd800..=0xdbff).contains(&high_unit)
&& (0xdc00..=0xdfff).contains(&low_unit)
{
high.unicode = String::from_utf16(&[high_unit, low_unit]).ok();
low.unicode = Some(String::new());
}
}
}
}
Ok(glyphs)
}
fn gid_for(&self, cid: u32) -> Option<u16> {
match self.cid_to_gid.as_ref()? {
CidToGid::Identity => u16::try_from(cid).ok(),
CidToGid::Table(table) => table.get(cid as usize).copied(),
CidToGid::Cff(map) => u16::try_from(cid)
.ok()
.and_then(|cid| map.get(&cid).copied()),
}
}
fn unicode_for(&self, code: &[u8], cid: Option<u32>) -> Option<String> {
if let Some(cmap) = &self.to_unicode {
return cmap.map(code).and_then(|mapped| cmap.to_unicode(&mapped));
}
if matches!(
self.code_encoding,
CodeEncoding::Composite(CidEncoding::Utf16Be)
) {
return Some(decode_utf16be(code));
}
if let (Some(collection), Some(cid)) = (self.cid_collection, cid) {
return u16::try_from(cid)
.ok()
.and_then(|cid| collection.cid_to_unicode_sequence(cid))
.map(str::to_owned);
}
if matches!(self.code_encoding, CodeEncoding::Simple) {
let value = code[0];
if let Some(name) = self.differences.get(&value) {
return crate::text::extraction_cmap::glyph_name_to_unicode_sequence_in_font(
name,
self.base_font.as_deref(),
);
}
if let Some(names) = &self.intrinsic {
return Some(
names
.get(&value)
.and_then(|name| glyph_name_to_unicode(name))
.unwrap_or_else(|| "\u{fffd}".into()),
);
}
if let Some(character) = self.encoding.as_deref().and_then(|encoding| {
crate::text::extraction_cmap::decode_named_simple_encoding(encoding, value)
}) {
return Some(character.to_string());
}
if self.symbolic {
if self.subtype == FontSubtype::TrueType {
if self.encoding.is_none() {
return None;
}
} else {
return symbol_code_to_unicode(value).map(str::to_owned);
}
}
return Some(TextEncoding::StandardEncoding.decode(&[value]));
}
None
}
fn simple_width(&self, code: u32) -> f64 {
code.checked_sub(self.first_char)
.and_then(|offset| self.simple_widths.get(offset as usize))
.copied()
.unwrap_or(self.missing_width)
}
}
fn resolve_encoding<R: Read + Seek>(
document: &PdfDocument<R>,
object: Option<&PdfObject>,
composite: bool,
) -> ParseResult<(
Option<String>,
BTreeMap<u8, String>,
CodeEncoding,
WritingMode,
)> {
let Some(object) = object else {
if composite {
return Err(syntax("Type0 font is missing its Encoding"));
}
return Ok((
None,
BTreeMap::new(),
CodeEncoding::Simple,
WritingMode::Horizontal,
));
};
let resolved = document.resolve(object)?;
match resolved {
PdfObject::Null => resolve_encoding(document, None, composite),
PdfObject::Name(name) if composite => {
let vertical = name.0.ends_with("-V");
let code_encoding =
if matches!(name.0.as_str(), "Identity-H" | "Identity-V") {
CodeEncoding::Identity
} else {
CodeEncoding::Composite(resolve_predefined(&name.0).ok_or_else(|| {
syntax(&format!("unsupported predefined CMap /{}", name.0))
})?)
};
Ok((
Some(name.0),
BTreeMap::new(),
code_encoding,
if vertical {
WritingMode::Vertical
} else {
WritingMode::Horizontal
},
))
}
PdfObject::Name(name) => Ok((
Some(name.0),
BTreeMap::new(),
CodeEncoding::Simple,
WritingMode::Horizontal,
)),
PdfObject::Dictionary(dict) if !composite => {
let base = dict
.get("BaseEncoding")
.and_then(|obj| document.resolve(obj).ok())
.and_then(|obj| match obj {
PdfObject::Name(name) => Some(name.0),
_ => None,
});
Ok((
base,
parse_differences(document, &dict)?,
CodeEncoding::Simple,
WritingMode::Horizontal,
))
}
PdfObject::Stream(stream) if composite => {
let cmap = CMapTextExtractor::parse_encoding_stream(&stream, document, 0)?;
let mode = if cmap.wmode == 1 {
WritingMode::Vertical
} else {
WritingMode::Horizontal
};
Ok((
None,
BTreeMap::new(),
CodeEncoding::Composite(CidEncoding::Cmap(cmap)),
mode,
))
}
_ => Err(syntax("invalid font Encoding")),
}
}
fn resolve_cmap<R: Read + Seek>(
document: &PdfDocument<R>,
object: Option<&PdfObject>,
) -> ParseResult<Option<CMap>> {
let Some(object) = object else {
return Ok(None);
};
CMapTextExtractor::resolve_tounicode(object, document)
}
fn resolve_cid_to_gid<R: Read + Seek>(
document: &PdfDocument<R>,
object: Option<&PdfObject>,
) -> ParseResult<Option<CidToGid>> {
let Some(object) = object else {
return Ok(Some(CidToGid::Identity));
};
let resolved = match document.resolve(object) {
Err(crate::parser::ParseError::InvalidReference(_, _)) => PdfObject::Null,
other => other?,
};
match resolved {
PdfObject::Null => Ok(Some(CidToGid::Identity)),
PdfObject::Name(name) if name.0 == "Identity" => Ok(Some(CidToGid::Identity)),
PdfObject::Stream(stream) => {
let data = document.decode_stream_with_limit(&stream, MAX_CID_TO_GID_MAP_SIZE)?;
if data.len() % 2 != 0 {
return Err(syntax("CIDToGIDMap stream has odd length"));
}
Ok(Some(CidToGid::Table(
data.chunks_exact(2)
.map(|pair| u16::from_be_bytes([pair[0], pair[1]]))
.collect(),
)))
}
_ => Err(syntax("CIDToGIDMap must be /Identity or a stream")),
}
}
fn parse_cid_widths<R: Read + Seek>(
document: &PdfDocument<R>,
dict: &PdfDictionary,
) -> ParseResult<CidWidths> {
let widths = crate::text::extraction_cmap::parse_cid_widths(document, dict)?;
Ok(CidWidths {
explicit: widths.widths,
ranges: widths.ranges,
default: widths.default_width,
})
}
fn resolve_embedded_font<R: Read + Seek>(
document: &PdfDocument<R>,
descriptor: &PdfDictionary,
) -> ParseResult<Option<EmbeddedFont>> {
for (key, default_format) in [
("FontFile", EmbeddedFontFormat::Type1),
("FontFile2", EmbeddedFontFormat::TrueType),
("FontFile3", EmbeddedFontFormat::Type1C),
] {
let Some(object) = descriptor.get(key) else {
continue;
};
let resolved = match document.resolve(object) {
Err(crate::parser::ParseError::InvalidReference(_, _)) => PdfObject::Null,
other => other?,
};
if matches!(resolved, PdfObject::Null) {
continue;
}
let stream = resolved
.as_stream()
.ok_or_else(|| syntax(&format!("{key} must be a stream")))?;
let format = if key == "FontFile3" {
match stream
.dict
.get("Subtype")
.and_then(PdfObject::as_name)
.map(|n| n.0.as_str())
{
Some("Type1C") => EmbeddedFontFormat::Type1C,
Some("CIDFontType0C") => EmbeddedFontFormat::CidFontType0C,
Some("OpenType") => EmbeddedFontFormat::OpenType,
_ => return Err(syntax("FontFile3 has unsupported or missing Subtype")),
}
} else {
default_format
};
let data = document.decode_stream_with_limit(stream, MAX_FONT_STREAM_SIZE)?;
return Ok(Some(EmbeddedFont { format, data }));
}
Ok(None)
}
fn parse_differences<R: Read + Seek>(
document: &PdfDocument<R>,
dict: &PdfDictionary,
) -> ParseResult<BTreeMap<u8, String>> {
let mut result = BTreeMap::new();
let Some(differences_obj) = dict.get("Differences") else {
return Ok(result);
};
let resolved_differences = document.resolve(differences_obj)?;
let Some(array) = resolved_differences.as_array() else {
return Ok(result);
};
let mut code = None;
for object in &array.0 {
let resolved_obj = document.resolve(object)?;
if let Some(value) = resolved_obj.as_integer() {
code =
Some(u8::try_from(value).map_err(|_| syntax("Differences code outside 0..=255"))?);
} else if let Some(name) = resolved_obj.as_name() {
let current = code.ok_or_else(|| syntax("Differences name without a code"))?;
result.insert(current, name.0.clone());
code = current.checked_add(1);
} else {
return Err(syntax("invalid Differences entry"));
}
}
Ok(result)
}
fn resolve_number_array<R: Read + Seek>(
document: &PdfDocument<R>,
object: Option<&PdfObject>,
) -> ParseResult<Vec<f64>> {
let Some(object) = resolve_number_or_array_object(document, object)? else {
return Ok(Vec::new());
};
let array = object
.as_array()
.ok_or_else(|| syntax("Widths must be an array"))?;
array
.0
.iter()
.map(|value| metric_number(&document.resolve(value)?))
.collect()
}
fn metric_number(value: &PdfObject) -> ParseResult<f64> {
value
.as_real()
.filter(|value| value.is_finite())
.ok_or_else(|| syntax("font metric must be a finite number"))
}
fn resolve_number_or_array_object<R: Read + Seek>(
document: &PdfDocument<R>,
object: Option<&PdfObject>,
) -> ParseResult<Option<PdfObject>> {
object.map(|value| document.resolve(value)).transpose()
}
fn resolve_optional_dict<R: Read + Seek>(
document: &PdfDocument<R>,
object: Option<&PdfObject>,
) -> ParseResult<Option<PdfDictionary>> {
let Some(object) = object else {
return Ok(None);
};
let resolved = document.resolve(object)?;
resolved
.as_dict()
.map(|dict| crate::text::extraction_cmap::without_null_metrics(dict, document).into_owned())
.ok_or_else(|| syntax("FontDescriptor must be a dictionary"))
.map(Some)
}
fn resolve_required<R: Read + Seek>(
document: &PdfDocument<R>,
dict: &PdfDictionary,
key: &str,
) -> ParseResult<PdfObject> {
document.resolve(dict.get(key).ok_or_else(|| missing(key))?)
}
fn expect_dict<'a>(object: &'a PdfObject, description: &str) -> ParseResult<&'a PdfDictionary> {
object
.as_dict()
.ok_or_else(|| syntax(&format!("{description} must be a dictionary")))
}
fn required_name<'a>(dict: &'a PdfDictionary, key: &str) -> ParseResult<&'a str> {
dict.get(key)
.and_then(PdfObject::as_name)
.map(|name| name.0.as_str())
.ok_or_else(|| missing(key))
}
fn be_code(code: &[u8]) -> ParseResult<u32> {
if code.len() > 4 {
return Err(syntax("character code exceeds four bytes"));
}
Ok(code
.iter()
.fold(0u32, |value, byte| (value << 8) | u32::from(*byte)))
}
fn glyph_name_to_unicode(name: &str) -> Option<String> {
crate::text::extraction_cmap::glyph_name_to_unicode_sequence(name)
}
fn symbol_code_to_unicode(code: u8) -> Option<&'static str> {
match code {
0xB7 => Some("•"),
b'x' => Some("ξ"),
_ => None,
}
}
fn missing(key: &str) -> ParseError {
ParseError::MissingKey(key.into())
}
fn syntax(message: &str) -> ParseError {
ParseError::SyntaxError {
position: 0,
message: message.into(),
}
}