use crate::encoding::{FontEncoding, adobe_char_name};
use crate::ids::GlyphName;
use crate::tounicode::{self, ToUnicode};
use crate::{CharCode, CharItem, FontCache, FontId, names, simple};
use pdfrum_common::kurbo::{Affine, Rect};
use pdfrum_common::{Diagnostics, Limits};
use pdfrum_object::{Dict, Resolve, Stream};
use smallvec::SmallVec;
pub const MAX_TYPE3_DEPTH: u32 = 4;
#[derive(Debug)]
pub struct Type3Font {
pub(crate) id: FontId,
pub font_matrix: Affine,
pub(crate) char_procs: Dict,
pub resources: Option<Dict>,
pub(crate) encoding: [Option<GlyphName>; 256],
pub(crate) encoding_kind: FontEncoding,
pub(crate) widths: [i32; 256],
pub(crate) font_bbox: Rect,
pub(crate) to_unicode: Option<ToUnicode>,
}
impl Type3Font {
#[must_use]
pub fn char_proc(&self, code: CharCode, r: &impl Resolve) -> Option<Stream> {
let name = self.char_proc_name(code)?;
let key = pdfrum_object::Name::new(name.to_vec());
self.char_procs.stream(&key, r)
}
#[must_use]
pub(crate) fn char_proc_name(&self, code: CharCode) -> Option<&[u8]> {
adobe_char_name(self.encoding_kind, &self.encoding, code.0)
}
#[must_use]
pub(crate) fn char_width(&self, code: CharCode) -> f32 {
let code = if code.0 >= 256 { 0 } else { code.0 as usize };
self.widths.get(code).copied().unwrap_or(0) as f32
}
#[must_use]
pub(crate) fn unicode_from_charcode(&self, code: CharCode) -> SmallVec<[char; 2]> {
self.to_unicode
.as_ref()
.map(|tu| tu.lookup(code))
.unwrap_or_default()
}
#[must_use]
pub(crate) fn char_code_from_unicode(&self, unicode: char) -> Option<CharCode> {
let code = self.to_unicode.as_ref()?.reverse(unicode);
(code.0 != 0).then_some(code)
}
pub(crate) fn char_item(&self, code: CharCode) -> CharItem {
CharItem {
code,
cid: None,
gid: None,
unicode: self.unicode_from_charcode(code),
width: self.char_width(code),
vertical_glyph: false,
}
}
}
pub(crate) fn load(
dict: &Dict,
r: &impl Resolve,
cache: &FontCache,
limits: &Limits,
diags: &mut Diagnostics,
) -> Type3Font {
let (font_matrix, xscale, yscale) = match dict.raw(names::FONT_MATRIX) {
Some(_) => {
let m = dict.matrix(names::FONT_MATRIX, r);
let c = m.as_coeffs();
(m, c[0], c[3])
}
None => (Affine::IDENTITY, 1.0, 1.0),
};
let font_bbox = match dict.array(names::FONT_BBOX, r) {
Some(b) => {
Rect::new(
f64::from(b.number_at_or_zero(0)) * xscale * 1000.0,
f64::from(b.number_at_or_zero(1)) * yscale * 1000.0,
f64::from(b.number_at_or_zero(2)) * xscale * 1000.0,
f64::from(b.number_at_or_zero(3)) * yscale * 1000.0,
)
}
None => Rect::ZERO,
};
let mut widths = [0i32; 256];
let start = dict.int(names::FIRST_CHAR, r).unwrap_or(0);
if let (Ok(start), Some(w)) = (usize::try_from(start), dict.array(names::WIDTHS, r))
&& start < 256
{
let count = w.len().min(256).min(256 - start);
for i in 0..count {
let value = f64::from(w.number_at_or_zero(i)) * xscale * 1000.0;
if let Some(slot) = widths.get_mut(start + i) {
*slot = round_f32(value);
}
}
}
let mut encoding: [Option<GlyphName>; 256] = [const { None }; 256];
let mut encoding_kind = FontEncoding::Builtin;
if dict.raw(names::ENCODING).is_some() {
simple::load_pdf_encoding(
dict,
r,
b"",
crate::FontFlags::DEFAULT,
false,
false,
&mut encoding_kind,
&mut encoding,
);
}
let to_unicode = dict
.stream(names::TO_UNICODE, r)
.map(|s| {
let bytes = pdfrum_filters::decode_chain(&s, 0, r, limits, diags).data;
tounicode::parse(&bytes, limits, diags)
})
.filter(|m| !m.is_empty());
Type3Font {
id: cache.next_id(),
font_matrix,
char_procs: dict.dict(names::CHAR_PROCS, r).unwrap_or_default(),
resources: dict.dict(names::RESOURCES, r),
encoding,
encoding_kind,
widths,
font_bbox,
to_unicode,
}
}
fn round_f32(v: f64) -> i32 {
let r = v.round();
if r.is_nan() {
0
} else if r >= f64::from(i32::MAX) {
i32::MAX
} else if r <= f64::from(i32::MIN) {
i32::MIN
} else {
r as i32
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::float_cmp)]
use super::*;
use pdfrum_object::{Array, Name, NoResolve, Object};
fn font_dict(pairs: Vec<(&Name, Object)>) -> Dict {
Dict::from_pairs(pairs.into_iter().map(|(k, v)| (k.clone(), v)))
}
fn load_it(dict: &Dict) -> Type3Font {
load(
dict,
&NoResolve,
&FontCache::new(),
&Limits::default(),
&mut Diagnostics::default(),
)
}
#[test]
fn without_an_encoding_no_code_names_anything() {
let f = load_it(&Dict::new());
assert_eq!(f.encoding_kind, FontEncoding::Builtin);
for code in 0..256u32 {
assert!(f.char_proc_name(CharCode(code)).is_none(), "code {code}");
}
}
#[test]
fn differences_name_the_procedures() {
let enc = font_dict(vec![(
names::DIFFERENCES,
Object::Array(Array::of([
Object::Int(97),
Object::Name(Name::from("square")),
Object::Name(Name::from("triangle")),
])),
)]);
let f = load_it(&font_dict(vec![(names::ENCODING, Object::Dict(enc))]));
assert_eq!(f.char_proc_name(CharCode(97)), Some(&b"square"[..]));
assert_eq!(f.char_proc_name(CharCode(98)), Some(&b"triangle"[..]));
assert_eq!(f.encoding_kind, FontEncoding::Standard);
assert_eq!(f.char_proc_name(CharCode(99)), Some(&b"c"[..]));
}
#[test]
fn the_font_matrix_defaults_to_identity() {
let f = load_it(&Dict::new());
assert_eq!(f.font_matrix, Affine::IDENTITY);
}
#[test]
fn widths_are_scaled_by_the_matrix_and_by_a_thousand() {
let f = load_it(&font_dict(vec![
(
names::FONT_MATRIX,
Object::Array(Array::of([
Object::Real(0.01),
Object::Int(0),
Object::Int(0),
Object::Real(0.01),
Object::Int(0),
Object::Int(0),
])),
),
(names::FIRST_CHAR, Object::Int(97)),
(
names::WIDTHS,
Object::Array(Array::of([Object::Int(50), Object::Int(75)])),
),
]));
assert_eq!(f.char_width(CharCode(97)), 500.0);
assert_eq!(f.char_width(CharCode(98)), 750.0);
assert_eq!(f.char_width(CharCode(99)), 0.0);
}
#[test]
fn a_code_at_or_above_256_reads_code_zero() {
let f = load_it(&font_dict(vec![
(names::FIRST_CHAR, Object::Int(0)),
(names::WIDTHS, Object::Array(Array::of([Object::Int(1)]))),
]));
assert_eq!(f.char_width(CharCode(0)), 1000.0);
assert_eq!(f.char_width(CharCode(256)), 1000.0);
assert_eq!(f.char_width(CharCode(u32::MAX)), 1000.0);
}
#[test]
fn widths_beyond_the_table_are_dropped_without_wrapping() {
let f = load_it(&font_dict(vec![
(names::FIRST_CHAR, Object::Int(254)),
(
names::WIDTHS,
Object::Array(Array::of([
Object::Int(1),
Object::Int(2),
Object::Int(3),
Object::Int(4),
])),
),
]));
assert_eq!(f.char_width(CharCode(254)), 1000.0);
assert_eq!(f.char_width(CharCode(255)), 2000.0);
assert_eq!(f.char_width(CharCode(0)), 0.0);
}
#[test]
fn a_first_char_past_the_table_drops_every_width() {
let f = load_it(&font_dict(vec![
(names::FIRST_CHAR, Object::Int(300)),
(names::WIDTHS, Object::Array(Array::of([Object::Int(9)]))),
]));
assert!(f.widths.iter().all(|&w| w == 0));
}
#[test]
fn the_bbox_is_scaled_into_glyph_units() {
let f = load_it(&font_dict(vec![
(
names::FONT_MATRIX,
Object::Array(Array::of([
Object::Real(0.001),
Object::Int(0),
Object::Int(0),
Object::Real(0.001),
Object::Int(0),
Object::Int(0),
])),
),
(
names::FONT_BBOX,
Object::Array(Array::of([
Object::Int(0),
Object::Int(0),
Object::Int(1000),
Object::Int(1000),
])),
),
]));
assert!((f.font_bbox.x1 - 1000.0).abs() < 1.0, "{:?}", f.font_bbox);
assert!((f.font_bbox.y1 - 1000.0).abs() < 1.0, "{:?}", f.font_bbox);
assert_eq!((f.font_bbox.x0, f.font_bbox.y0), (0.0, 0.0));
}
#[test]
fn a_type3_font_has_no_glyphs_at_all() {
let f = load_it(&Dict::new());
let item = f.char_item(CharCode(65));
assert_eq!(item.gid, None);
}
#[test]
fn the_depth_cap_is_four() {
assert_eq!(MAX_TYPE3_DEPTH, 4);
}
}