mod truetype;
mod type1;
use crate::descriptor::{self, FontDescriptor};
use crate::encoding::{FontEncoding, adobe_char_name, load_differences};
use crate::glyphs::{Charmap, Face, GlyphSource};
use crate::ids::GlyphName;
use crate::subst::{
self, CodePage, FontRequest, StandardFont, SubstFont, SubstitutionOptions, strip_subset_prefix,
};
use crate::tounicode::{self, ToUnicode};
use crate::widths::{SimpleWidths, WIDTH_UNSET};
use crate::{CharCode, CharItem, FontCache, FontFlags, FontId, Gid, names, widths};
use pdfrum_common::kurbo::Rect;
use pdfrum_common::{DiagKind, Diagnostics, Limits, Severity};
use pdfrum_object::{Dict, Resolve};
use smallvec::SmallVec;
const SPACE: u8 = 32;
#[cfg(test)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SimpleKind {
Type1 {
base14: Option<StandardFont>,
},
TrueType,
}
#[derive(Debug)]
pub struct SimpleFont {
pub(crate) id: FontId,
pub glyphs: GlyphSource,
pub(crate) encoding_kind: FontEncoding,
pub(crate) unicodes: [u16; 256],
pub(crate) glyph_index: [u16; 256],
pub(crate) widths: SimpleWidths,
pub(crate) to_unicode: Option<ToUnicode>,
pub(crate) descriptor: FontDescriptor,
pub(crate) subst: Option<SubstFont>,
#[cfg(test)]
pub(crate) kind: SimpleKind,
pub(crate) embedded: bool,
pub(crate) base_font_name: Vec<u8>,
char_bbox: [Rect; 256],
}
impl SimpleFont {
#[must_use]
pub(crate) fn glyph_from_charcode(&self, code: CharCode) -> Option<Gid> {
let index = usize::try_from(code.0).ok()?;
match self.glyph_index.get(index) {
Some(&WIDTH_UNSET) | None => None,
Some(&g) => Some(Gid(g)),
}
}
#[must_use]
pub(crate) fn char_width(&self, code: CharCode) -> f32 {
let code = if code.0 > 0xff { 0 } else { code.0 as u8 };
if let Some(w) = self.widths.get(code) {
return w;
}
match self.glyph_from_charcode(CharCode(u32::from(code))) {
Some(gid) => f32::from(self.glyphs.advance_tt(gid) as i16),
None if !self.embedded && code != SPACE => self.space_metric(),
None => 0.0,
}
}
fn space_metric(&self) -> f32 {
if let Some(w) = self.widths.get(SPACE) {
return w;
}
match self.glyph_from_charcode(CharCode(u32::from(SPACE))) {
Some(gid) => f32::from(self.glyphs.advance_tt(gid) as i16),
None => 0.0,
}
}
#[must_use]
pub(crate) fn unicode_from_charcode(&self, code: CharCode) -> SmallVec<[char; 2]> {
if let Some(tu) = &self.to_unicode {
let chars = tu.lookup(code);
if !chars.is_empty() {
return chars;
}
}
let Ok(index) = usize::try_from(code.0) else {
return SmallVec::new();
};
match self.unicodes.get(index) {
Some(&0) | None => SmallVec::new(),
Some(&u) => char::from_u32(u32::from(u))
.map(|c| SmallVec::from_slice(&[c]))
.unwrap_or_default(),
}
}
#[must_use]
pub(crate) fn char_code_from_unicode(&self, unicode: char) -> Option<CharCode> {
if let Some(tu) = &self.to_unicode {
let code = tu.reverse(unicode);
if code.0 != 0 {
return Some(code);
}
}
let target = u16::try_from(u32::from(unicode)).ok()?;
if target == 0 {
return None;
}
self.unicodes
.iter()
.position(|&u| u == target)
.and_then(|i| u32::try_from(i).ok())
.map(CharCode)
}
#[must_use]
pub(crate) fn char_bbox(&self, code: CharCode) -> Rect {
let code = if code.0 > 0xff { 0 } else { code.0 as u8 };
let stored = self
.char_bbox
.get(usize::from(code))
.copied()
.unwrap_or(Rect::ZERO);
if stored != Rect::ZERO
|| self.embedded
|| code == SPACE
|| self
.glyph_from_charcode(CharCode(u32::from(code)))
.is_some()
{
return stored;
}
self.char_bbox
.get(usize::from(SPACE))
.copied()
.unwrap_or(Rect::ZERO)
}
pub(crate) fn char_item(&self, code: CharCode) -> CharItem {
let gid = self.glyph_from_charcode(code);
CharItem {
code,
cid: None,
gid,
unicode: self.unicode_from_charcode(code),
width: self.char_width(code),
vertical_glyph: false,
}
}
#[must_use]
pub(crate) fn has_font_widths(&self) -> bool {
self.widths.has_declared_widths()
}
#[cfg(test)]
#[must_use]
pub(crate) fn is_standard_font(&self) -> bool {
matches!(self.kind, SimpleKind::Type1 { base14: Some(_) }) && !self.embedded
}
}
#[allow(clippy::too_many_lines)]
pub(crate) fn load(
dict: &Dict,
r: &impl Resolve,
cache: &FontCache,
opts: &SubstitutionOptions,
limits: &Limits,
diags: &mut Diagnostics,
is_truetype: bool,
) -> SimpleFont {
let mut base_font_name = dict
.name(names::BASE_FONT)
.map(|n| n.as_bytes().to_vec())
.unwrap_or_default();
let base14 = if is_truetype {
None
} else {
subst::standard_font_index(&base_font_name)
};
let mut flags = FontFlags::DEFAULT;
let mut encoding_kind = FontEncoding::Builtin;
let mut widths_table = SimpleWidths::default();
if let Some(f) = base14 {
base_font_name = subst::canonical_font_name(f).as_bytes().to_vec();
flags = if f.is_symbolic() {
FontFlags::SYMBOLIC
} else {
FontFlags::NON_SYMBOLIC
};
if f.is_fixed() {
widths_table = SimpleWidths {
raw: [600; 256],
use_face_widths: false,
};
}
encoding_kind = match f {
StandardFont::Symbol => FontEncoding::AdobeSymbol,
StandardFont::Dingbats => FontEncoding::ZapfDingbats,
_ if flags.is_non_symbolic() => FontEncoding::Standard,
_ => encoding_kind,
};
}
let desc = dict.dict(names::FONT_DESCRIPTOR, r);
let mut descriptor = FontDescriptor {
flags,
..FontDescriptor::default()
};
if let Some(d) = &desc {
descriptor = descriptor::load(d, r);
}
let (mut glyphs, mut embedded) = load_font_program(desc.as_ref(), r, limits, diags);
let declared = widths::load_simple(dict, desc.as_ref(), r);
if declared.has_declared_widths() || !widths_table.has_declared_widths() {
widths_table = declared;
}
let mut subst_font = None;
if embedded {
base_font_name = strip_subset_prefix(&base_font_name).to_vec();
} else {
let request = FontRequest {
name: base_font_name.clone(),
is_truetype,
flags: descriptor.flags,
weight: descriptor.subst_weight(),
italic_angle: descriptor.italic_angle,
code_page: CodePage::DefAnsi,
vertical: false,
};
let s = substitute(&request, opts, diags);
glyphs = s.glyphs;
subst_font = Some(s.subst);
}
if !descriptor.flags.is_symbolic() {
encoding_kind = FontEncoding::Standard;
}
let mut differences: [Option<GlyphName>; 256] = [const { None }; 256];
let _has_differences = load_pdf_encoding(
dict,
r,
&base_font_name,
descriptor.flags,
embedded,
is_truetype,
&mut encoding_kind,
&mut differences,
);
let to_unicode = load_to_unicode(dict, r, limits, diags);
let mut unicodes = [0u16; 256];
let mut glyph_index = [WIDTH_UNSET; 256];
if glyphs.is_some() {
let ctx = LadderContext {
glyphs: &glyphs,
encoding: encoding_kind,
differences: &differences,
flags: descriptor.flags,
embedded,
base14,
to_unicode: to_unicode.as_ref(),
first_char: dict.int(names::FIRST_CHAR, r).unwrap_or(0),
};
if is_truetype {
truetype::load_glyph_map(&ctx, &mut unicodes, &mut glyph_index);
} else {
type1::load_glyph_map(&ctx, &mut unicodes, &mut glyph_index);
}
}
if descriptor.flags.is_all_cap() {
apply_all_caps(&mut glyph_index, &mut widths_table, embedded);
}
let mut char_bbox = [Rect::ZERO; 256];
for (code, slot) in char_bbox.iter_mut().enumerate() {
let Some(&g) = glyph_index.get(code) else {
continue;
};
if g == WIDTH_UNSET {
continue;
}
if let Some(b) = glyphs.glyph_bbox(Gid(g)) {
*slot = b;
}
}
let metrics = match &glyphs {
GlyphSource::Fontations(f) => f.metrics(),
GlyphSource::Type1(f) => Some(descriptor::FaceMetrics {
upem: f.units_per_em(),
bbox_left: f.bbox().x0 as i64,
bbox_top: f.bbox().y1 as i64,
bbox_right: f.bbox().x1 as i64,
bbox_bottom: f.bbox().y0 as i64,
ascender: f.bbox().y1 as i64,
descender: f.bbox().y0 as i64,
}),
GlyphSource::None => None,
};
descriptor::check_font_metrics(&mut descriptor, metrics, |c| {
char_bbox.get(usize::from(c)).copied().unwrap_or(Rect::ZERO)
});
if !embedded && !glyphs.is_some() {
diags.record(Severity::Suspicious, DiagKind::FontSubstitutionFailed, None);
}
if !glyphs.is_some() {
embedded = false;
}
SimpleFont {
id: cache.next_id(),
glyphs,
encoding_kind,
unicodes,
glyph_index,
widths: widths_table,
to_unicode,
descriptor,
subst: subst_font,
#[cfg(test)]
kind: if is_truetype {
SimpleKind::TrueType
} else {
SimpleKind::Type1 { base14 }
},
embedded,
base_font_name,
char_bbox,
}
}
pub(crate) struct LadderContext<'a> {
pub glyphs: &'a GlyphSource,
pub encoding: FontEncoding,
pub differences: &'a [Option<GlyphName>; 256],
pub flags: FontFlags,
pub embedded: bool,
pub base14: Option<StandardFont>,
pub to_unicode: Option<&'a ToUnicode>,
pub first_char: i64,
}
impl LadderContext<'_> {
pub(crate) fn char_name(&self, code: u8) -> Option<&[u8]> {
adobe_char_name(self.encoding, self.differences, u32::from(code))
}
pub(crate) fn has_differences(&self) -> bool {
self.differences.iter().any(Option::is_some)
}
}
pub(crate) fn load_font_program(
desc: Option<&Dict>,
r: &impl Resolve,
limits: &Limits,
diags: &mut Diagnostics,
) -> (GlyphSource, bool) {
let Some(desc) = desc else {
return (GlyphSource::None, false);
};
let stream = [names::FONT_FILE, names::FONT_FILE2, names::FONT_FILE3]
.into_iter()
.find_map(|k| desc.stream(k, r));
let Some(stream) = stream else {
return (GlyphSource::None, false);
};
let bytes = pdfrum_filters::decode_chain(&stream, 0, r, limits, diags).data;
if bytes.is_empty() {
diags.record(Severity::Suspicious, DiagKind::FontProgramUnreadable, None);
return (GlyphSource::None, false);
}
let shared: std::sync::Arc<[u8]> = std::sync::Arc::from(bytes.as_slice());
if let Some(face) = Face::new(shared.clone(), 0) {
return (GlyphSource::Fontations(face), true);
}
if let Ok(f) = pdfrum_type1::Type1Font::parse(&shared, limits, diags) {
(GlyphSource::Type1(std::sync::Arc::new(f)), true)
} else {
diags.record(Severity::Suspicious, DiagKind::FontProgramUnreadable, None);
(GlyphSource::None, false)
}
}
fn substitute(
request: &FontRequest,
opts: &SubstitutionOptions,
diags: &mut Diagnostics,
) -> subst::Substitution {
subst::resolve_with_options(request, opts, diags)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn load_pdf_encoding(
dict: &Dict,
r: &impl Resolve,
base_font_name: &[u8],
flags: FontFlags,
embedded: bool,
is_truetype: bool,
encoding: &mut FontEncoding,
differences: &mut [Option<GlyphName>; 256],
) -> bool {
let Some(enc) = dict.get(names::ENCODING, r) else {
if base_font_name == b"Symbol" {
*encoding = if is_truetype {
FontEncoding::MsSymbol
} else {
FontEncoding::AdobeSymbol
};
} else if !embedded && *encoding == FontEncoding::Builtin {
*encoding = FontEncoding::WinAnsi;
}
return false;
};
if let Some(name) = enc.as_name() {
if matches!(
*encoding,
FontEncoding::AdobeSymbol | FontEncoding::ZapfDingbats
) {
return false;
}
if flags.is_symbolic() && base_font_name == b"Symbol" {
if !is_truetype {
*encoding = FontEncoding::AdobeSymbol;
}
return false;
}
let mut spelling = name.as_bytes();
if spelling == b"MacExpertEncoding" {
spelling = b"WinAnsiEncoding";
}
if let Some(e) = FontEncoding::from_pdf_name(spelling) {
*encoding = e;
}
return false;
}
let Some(enc_dict) = enc.as_dict() else {
return false;
};
if !matches!(
*encoding,
FontEncoding::AdobeSymbol | FontEncoding::ZapfDingbats
) && let Some(base) = enc_dict.name(names::BASE_ENCODING)
{
let mut spelling = base.as_bytes();
if is_truetype && spelling == b"MacExpertEncoding" {
spelling = b"WinAnsiEncoding";
}
if let Some(e) = FontEncoding::from_pdf_name(spelling) {
*encoding = e;
}
}
if (!embedded || is_truetype) && *encoding == FontEncoding::Builtin {
*encoding = FontEncoding::Standard;
}
match enc_dict.array(names::DIFFERENCES, r) {
Some(diffs) => load_differences(&diffs, r, differences),
None => false,
}
}
pub(crate) fn load_to_unicode(
dict: &Dict,
r: &impl Resolve,
limits: &Limits,
diags: &mut Diagnostics,
) -> Option<ToUnicode> {
let stream = dict.stream(names::TO_UNICODE, r)?;
let bytes = pdfrum_filters::decode_chain(&stream, 0, r, limits, diags).data;
let map = tounicode::parse(&bytes, limits, diags);
if map.is_empty() { None } else { Some(map) }
}
fn apply_all_caps(glyph_index: &mut [u16; 256], widths: &mut SimpleWidths, embedded: bool) {
for (lo, hi) in [(b'a', b'z'), (0xE0u8, 0xF6u8), (0xF8, 0xFD)] {
for i in lo..=hi {
let idx = usize::from(i);
if glyph_index.get(idx) != Some(&WIDTH_UNSET) && embedded {
continue;
}
let Some(j) = idx.checked_sub(32) else {
continue;
};
let (Some(&src_glyph), Some(&src_width)) = (glyph_index.get(j), widths.raw.get(j))
else {
continue;
};
if let Some(slot) = glyph_index.get_mut(idx) {
*slot = src_glyph;
}
if src_width != 0
&& let Some(slot) = widths.raw.get_mut(idx)
{
*slot = src_width;
}
}
}
}
pub(crate) fn name_index(glyphs: &GlyphSource, name: &[u8]) -> u16 {
glyphs.name_index(name)
}
pub(crate) fn char_index(glyphs: &GlyphSource, charmap: Charmap, code: u32) -> u16 {
glyphs.char_index(charmap, code)
}
impl SimpleFont {
#[cfg(test)]
#[must_use]
pub(crate) fn glyph_path(&self, gid: Gid) -> Option<pdfrum_common::kurbo::BezPath> {
self.glyphs
.outline(gid, crate::glyphs::GlyphParams::default())
}
}
#[cfg(test)]
pub(crate) fn resolve_encoding_for_test(
dict: &Dict,
r: &impl Resolve,
base_font_name: &[u8],
flags: FontFlags,
embedded: bool,
is_truetype: bool,
prior: FontEncoding,
) -> (FontEncoding, bool) {
let mut e = prior;
let mut diffs: [Option<GlyphName>; 256] = [const { None }; 256];
let had = load_pdf_encoding(
dict,
r,
base_font_name,
flags,
embedded,
is_truetype,
&mut e,
&mut diffs,
);
(e, had)
}
#[cfg(test)]
use pdfrum_object::Object;
#[cfg(test)]
#[path = "simple_tests.rs"]
mod tests;