use pdfboss_core::FastMap;
use pdfboss_core::geom::Matrix;
use pdfboss_core::{Dict, Document, Object};
use crate::glyph::differences;
pub(crate) struct Type3Font {
font_matrix: Matrix,
encoding: Box<[Option<String>; 256]>,
char_procs: FastMap<String, Object>,
widths: FastMap<u32, f32>,
resources: Option<Dict>,
}
impl Type3Font {
pub(crate) fn load(doc: &Document, font: &Dict) -> Option<Type3Font> {
let font_matrix = parse_font_matrix(doc, font)?;
let char_procs = parse_char_procs(doc, font)?;
let encoding = parse_encoding(doc, font);
let widths = parse_widths(doc, font);
let resources = font
.get("Resources")
.and_then(|o| doc.resolve(o).ok())
.and_then(|o| o.as_dict().cloned());
Some(Type3Font {
font_matrix,
encoding,
char_procs,
widths,
resources,
})
}
pub(crate) fn char_proc(&self, code: u32) -> Option<&Object> {
let name = usize::try_from(code)
.ok()
.and_then(|i| self.encoding.get(i))?
.as_ref()?;
self.char_procs.get(name)
}
pub(crate) fn width(&self, code: u32) -> Option<f32> {
self.widths.get(&code).copied()
}
pub(crate) fn font_matrix(&self) -> Matrix {
self.font_matrix
}
pub(crate) fn resources(&self) -> Option<&Dict> {
self.resources.as_ref()
}
}
fn parse_font_matrix(doc: &Document, font: &Dict) -> Option<Matrix> {
let resolved = doc.resolve(font.get("FontMatrix")?).ok()?;
let items = resolved.as_array()?;
if items.len() != 6 {
return None;
}
let mut nums = [0.0f64; 6];
for (slot, item) in nums.iter_mut().zip(items) {
let v = doc.resolve(item).ok()?.as_f64()?;
if !v.is_finite() {
return None;
}
*slot = v;
}
Some(Matrix {
a: nums[0] as f32,
b: nums[1] as f32,
c: nums[2] as f32,
d: nums[3] as f32,
e: nums[4] as f32,
f: nums[5] as f32,
})
}
fn parse_char_procs(doc: &Document, font: &Dict) -> Option<FastMap<String, Object>> {
let resolved = doc.resolve(font.get("CharProcs")?).ok()?;
let dict = resolved.as_dict()?;
if dict.is_empty() {
return None;
}
let map: FastMap<String, Object> = dict
.iter()
.map(|(name, obj)| (name.0.clone(), obj.clone()))
.collect();
Some(map)
}
fn parse_encoding(doc: &Document, font: &Dict) -> Box<[Option<String>; 256]> {
let diffs = differences(doc, font);
let fallback = allows_standard_fallback(doc, font);
let mut table: Box<[Option<String>; 256]> = Box::new(std::array::from_fn(|_| None));
for (code, slot) in table.iter_mut().enumerate() {
let code = code as u8;
if let Some(name) = diffs.get(&code) {
*slot = Some(name.clone());
} else if fallback {
*slot = pdfboss_encoding::standard_encoding_name(code).map(str::to_string);
}
}
table
}
fn allows_standard_fallback(doc: &Document, font: &Dict) -> bool {
match font.get("Encoding").map(|o| doc.resolve(o)) {
None => true,
Some(Ok(Object::Dict(d))) => match d.get_name("BaseEncoding") {
None => true,
Some(name) => name.0 == "StandardEncoding",
},
_ => true,
}
}
fn parse_widths(doc: &Document, font: &Dict) -> FastMap<u32, f32> {
let first = font
.get("FirstChar")
.and_then(|o| doc.resolve(o).ok())
.and_then(|o| o.as_int())
.unwrap_or(0)
.max(0) as u32;
let mut map = FastMap::default();
if let Some(Ok(Object::Array(items))) = font.get("Widths").map(|o| doc.resolve(o)) {
for (i, item) in items.iter().enumerate() {
let Some(code) = first.checked_add(i as u32) else {
break; };
if let Some(w) = doc.resolve(item).ok().and_then(|o| o.as_f64()) {
map.insert(code, w as f32);
}
}
}
map
}
#[cfg(test)]
mod tests {
use pdfboss_core::{Dict, Document, ObjRef};
use pdfboss_testkit::PdfBuilder;
use super::Type3Font;
fn type3_font_doc() -> (Document, Dict) {
let mut b = PdfBuilder::new().version(1, 5);
b.object(1, "<< /Type /Catalog >>");
b.stream(2, "", b"1000 0 d0 100 0 500 700 re f");
b.object(
3,
"<< /Type /Font /Subtype /Type3 /FontBBox [0 0 1000 1000] \
/FontMatrix [0.001 0 0 0.001 0 0] \
/Encoding << /Differences [65 /boxglyph] >> \
/CharProcs << /boxglyph 2 0 R >> \
/FirstChar 65 /Widths [1000] >>",
);
let bytes = b.build(1);
let doc = Document::load(bytes).expect("load");
let font_dict = doc
.get(ObjRef { num: 3, gen: 0 })
.expect("resolve font dict")
.as_dict()
.cloned()
.expect("font dict");
(doc, font_dict)
}
#[test]
fn load_resolves_charproc_width_and_matrix() {
let (doc, font_dict) = type3_font_doc();
let t3 = Type3Font::load(&doc, &font_dict).expect("load");
assert!(t3.char_proc(65).is_some(), "code 65 -> /boxglyph CharProc");
assert_eq!(t3.width(65), Some(1000.0)); let m = t3.font_matrix();
assert!((m.a - 0.001).abs() < 1e-9 && (m.d - 0.001).abs() < 1e-9);
assert!(t3.char_proc(66).is_none(), "unmapped code -> None");
}
#[test]
fn load_without_font_matrix_returns_none() {
let mut b = PdfBuilder::new().version(1, 5);
b.object(1, "<< /Type /Catalog >>");
b.stream(2, "", b"1000 0 d0 100 0 500 700 re f");
b.object(
3,
"<< /Type /Font /Subtype /Type3 /FontBBox [0 0 1000 1000] \
/Encoding << /Differences [65 /boxglyph] >> \
/CharProcs << /boxglyph 2 0 R >> \
/FirstChar 65 /Widths [1000] >>",
);
let bytes = b.build(1);
let doc = Document::load(bytes).expect("load");
let font_dict = doc
.get(ObjRef { num: 3, gen: 0 })
.expect("resolve font dict")
.as_dict()
.cloned()
.expect("font dict");
assert!(Type3Font::load(&doc, &font_dict).is_none());
}
#[test]
fn load_tolerates_malformed_widths_and_charprocs() {
let mut b = PdfBuilder::new().version(1, 5);
b.object(1, "<< /Type /Catalog >>");
b.object(
3,
"<< /Type /Font /Subtype /Type3 /FontBBox [0 0 1000 1000] \
/FontMatrix [0.001 0 0 0.001 0 0] \
/Encoding << /Differences [65 /boxglyph] >> \
/CharProcs << /boxglyph /NotAStream >> \
/FirstChar 65 /Widths /NotAnArray >>",
);
let bytes = b.build(1);
let doc = Document::load(bytes).expect("load");
let font_dict = doc
.get(ObjRef { num: 3, gen: 0 })
.expect("resolve font dict")
.as_dict()
.cloned()
.expect("font dict");
let result = Type3Font::load(&doc, &font_dict);
if let Some(t3) = result {
assert_eq!(t3.width(65), None, "malformed /Widths yields no widths");
}
}
}