use std::sync::Arc;
use pdfboss_core::AsyncObjectSource;
use pdfboss_core::FastMap;
#[cfg(test)]
use pdfboss_core::{block_on, Immediate};
use pdfboss_core::geom::Matrix;
#[cfg(test)]
use pdfboss_core::Document;
use pdfboss_core::{Dict, 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<Arc<Dict>>,
}
impl Type3Font {
#[cfg(test)]
pub(crate) fn load(doc: &Document, font: &Dict) -> Option<Type3Font> {
block_on(Self::load_with(&Immediate(doc), font))
}
pub(crate) async fn load_with<S: AsyncObjectSource>(src: &S, font: &Dict) -> Option<Type3Font> {
let font_matrix = parse_font_matrix(src, font).await?;
let char_procs_obj = src.resolve(font.get("CharProcs")?).await.ok()?;
let char_procs = parse_char_procs(&char_procs_obj)?;
let encoding = parse_encoding(src, font).await;
let widths = parse_widths(src, font).await;
let resources = match font.get("Resources") {
Some(o) => src.resolve(o).await.ok().and_then(|o| o.as_dict().cloned()),
None => None,
}
.map(Arc::new);
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<&Arc<Dict>> {
self.resources.as_ref()
}
}
async fn parse_font_matrix<S: AsyncObjectSource>(src: &S, font: &Dict) -> Option<Matrix> {
let resolved = src.resolve(font.get("FontMatrix")?).await.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 = src.resolve(item).await.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(resolved: &Object) -> Option<FastMap<String, Object>> {
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)
}
async fn parse_encoding<S: AsyncObjectSource>(src: &S, font: &Dict) -> Box<[Option<String>; 256]> {
let diffs = differences(src, font).await;
let fallback = allows_standard_fallback(src, font).await;
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
}
async fn allows_standard_fallback<S: AsyncObjectSource>(src: &S, font: &Dict) -> bool {
let Some(enc) = font.get("Encoding") else {
return true;
};
match src.resolve(enc).await {
Ok(Object::Dict(d)) => match d.get_name("BaseEncoding") {
None => true,
Some(name) => name.0 == "StandardEncoding",
},
_ => true,
}
}
async fn parse_widths<S: AsyncObjectSource>(src: &S, font: &Dict) -> FastMap<u32, f32> {
let first = match font.get("FirstChar") {
Some(o) => src.resolve(o).await.ok().and_then(|o| o.as_int()),
None => None,
}
.unwrap_or(0)
.max(0) as u32;
let mut map = FastMap::default();
let widths = match font.get("Widths") {
Some(o) => src.resolve(o).await.ok(),
None => None,
};
if let Some(Object::Array(items)) = widths {
for (i, item) in items.iter().enumerate() {
let Some(code) = first.checked_add(i as u32) else {
break; };
if let Some(w) = src.resolve(item).await.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");
}
}
}