use std::collections::HashMap;
use skrifa::instance::{LocationRef, Size};
use skrifa::metrics::Metrics;
use skrifa::outline::OutlinePen;
use skrifa::raw::FontRef;
use skrifa::{GlyphId, MetadataProvider};
use tiny_skia::PathBuilder;
use crate::core::objects::{DictExt, Dictionary, Object};
use crate::fonts::encoding::Encoding;
use super::colors::obj_f64;
struct SkiaPen {
builder: PathBuilder,
}
impl SkiaPen {
fn new() -> Self {
Self {
builder: PathBuilder::new(),
}
}
fn finish(self) -> Option<tiny_skia::Path> {
self.builder.finish()
}
}
impl OutlinePen for SkiaPen {
fn move_to(&mut self, x: f32, y: f32) {
self.builder.move_to(x, y);
}
fn line_to(&mut self, x: f32, y: f32) {
self.builder.line_to(x, y);
}
fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
self.builder.quad_to(cx0, cy0, x, y);
}
fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
self.builder.cubic_to(cx0, cy0, cx1, cy1, x, y);
}
fn close(&mut self) {
self.builder.close();
}
}
pub(crate) struct FontProgram {
data: Vec<u8>,
code_to_gid: Vec<Option<GlyphId>>,
advance_widths: Vec<Option<f32>>,
units_per_em: f32,
}
impl FontProgram {
pub fn from_data_with_encoding(data: Vec<u8>, encoding: &Encoding) -> Option<Self> {
let font_ref = FontRef::new(&data).ok()?;
let charmap = font_ref.charmap();
let metrics = Metrics::new(&font_ref, Size::unscaled(), LocationRef::default());
let upem = metrics.units_per_em as f32;
let code_to_gid: Vec<Option<GlyphId>> = (0u8..=255)
.map(|code| encoding.decode_byte(code).and_then(|c| charmap.map(c)))
.collect();
let glyph_metrics =
skrifa::metrics::GlyphMetrics::new(&font_ref, Size::unscaled(), LocationRef::default());
let advance_widths: Vec<Option<f32>> = code_to_gid
.iter()
.map(|gid| gid.map(|g| glyph_metrics.advance_width(g).unwrap_or(0.0)))
.collect();
Some(FontProgram {
data,
code_to_gid,
advance_widths,
units_per_em: upem,
})
}
pub fn glyph_path(&self, code: u8) -> Option<tiny_skia::Path> {
let gid = self.code_to_gid.get(code as usize).copied().flatten()?;
let font_ref = FontRef::new(&self.data).ok()?;
let outlines = font_ref.outline_glyphs();
let glyph = outlines.get(gid)?;
let mut pen = SkiaPen::new();
let settings =
skrifa::outline::DrawSettings::unhinted(Size::unscaled(), LocationRef::default());
glyph.draw(settings, &mut pen).ok()?;
pen.finish()
}
pub fn units_per_em(&self) -> f32 {
self.units_per_em
}
pub fn advance_width(&self, code: u8) -> Option<f32> {
self.advance_widths.get(code as usize).copied().flatten()
}
}
pub(crate) struct Type3FontProgram {
char_procs: HashMap<String, Vec<u8>>,
glyph_names: Vec<Option<String>>,
widths: Vec<f64>,
first_char: u8,
font_matrix: [f64; 6],
}
impl Type3FontProgram {
pub fn from_dict(font_dict: &Dictionary, doc: &crate::document::Document) -> Option<Self> {
let matrix_arr = font_dict.get_str("FontMatrix")?.as_array()?;
if matrix_arr.len() != 6 {
return None;
}
let font_matrix = [
obj_f64(&matrix_arr[0])?,
obj_f64(&matrix_arr[1])?,
obj_f64(&matrix_arr[2])?,
obj_f64(&matrix_arr[3])?,
obj_f64(&matrix_arr[4])?,
obj_f64(&matrix_arr[5])?,
];
let first_char = match font_dict.get_str("FirstChar") {
Some(Object::Integer(n)) => *n as u8,
_ => 0,
};
let widths: Vec<f64> = font_dict
.get_str("Widths")
.and_then(|o| o.as_array())
.map(|arr| arr.iter().filter_map(obj_f64).collect())
.unwrap_or_default();
let mut glyph_names: Vec<Option<String>> = vec![None; 256];
if let Some(Object::Dictionary(enc_dict)) =
font_dict.get_str("Encoding").and_then(|o| doc.resolve(o))
{
if let Some(Object::Array(diff)) = enc_dict.get_str("Differences") {
let mut code: Option<u8> = None;
for obj in diff {
match obj {
Object::Integer(n) => code = Some(*n as u8),
Object::Name(name) => {
if let Some(c) = code {
glyph_names[c as usize] = Some(name.as_str().to_string());
code = Some(c.wrapping_add(1));
}
}
_ => {}
}
}
}
}
let cp_dict = font_dict
.get_str("CharProcs")
.and_then(|o| doc.resolve(o))
.and_then(|o| match o {
Object::Dictionary(d) => Some(d),
_ => None,
})?;
let mut char_procs = HashMap::new();
for (name, val) in cp_dict.iter() {
if let Some(Object::Stream(s)) = doc.resolve(val) {
if let Ok(data) = s.decode_data() {
char_procs.insert(name.as_str().to_string(), data);
}
}
}
Some(Type3FontProgram {
char_procs,
glyph_names,
widths,
first_char,
font_matrix,
})
}
pub fn glyph_width(&self, code: u8) -> Option<f64> {
let idx = (code as usize).checked_sub(self.first_char as usize)?;
self.widths.get(idx).copied()
}
pub fn font_matrix(&self) -> [f64; 6] {
self.font_matrix
}
pub fn glyph_stream(&self, code: u8) -> Option<&[u8]> {
let name = self.glyph_names.get(code as usize)?.as_ref()?;
self.char_procs.get(name).map(|v| v.as_slice())
}
}
pub(crate) fn extract_font_program(
font_dict: &Dictionary,
doc: &crate::document::Document,
) -> Option<FontProgram> {
let descriptor = font_dict
.get_str("FontDescriptor")
.and_then(|o| doc.resolve(o))
.and_then(|o| match o {
Object::Dictionary(d) => Some(d),
_ => None,
})?;
let (font_stream, is_type1) = if let Some(s) = descriptor
.get_str("FontFile2")
.or_else(|| descriptor.get_str("FontFile3"))
.and_then(|o| doc.resolve(o))
.and_then(|o| match o {
Object::Stream(s) => Some(s),
_ => None,
}) {
(s, false)
} else if let Some(s) = descriptor
.get_str("FontFile")
.and_then(|o| doc.resolve(o))
.and_then(|o| match o {
Object::Stream(s) => Some(s),
_ => None,
})
{
(s, true)
} else {
return None;
};
let raw_data = font_stream.decode_data().ok()?;
let font_data = if is_type1 {
strip_pfb_wrapper(raw_data)
} else {
raw_data
};
let encoding = font_dict
.get_str("Encoding")
.and_then(|obj| Encoding::from_object(obj).ok())
.unwrap_or_else(Encoding::win_ansi);
FontProgram::from_data_with_encoding(font_data, &encoding)
}
pub(crate) struct CidFontProgram {
data: Vec<u8>,
default_width: f32,
widths: HashMap<u16, f32>,
units_per_em: f32,
}
impl CidFontProgram {
pub fn glyph_path_by_gid(&self, gid: u16) -> Option<tiny_skia::Path> {
let font_ref = FontRef::new(&self.data).ok()?;
let outlines = font_ref.outline_glyphs();
let glyph = outlines.get(GlyphId::new(gid as u32))?;
let mut pen = SkiaPen::new();
let settings =
skrifa::outline::DrawSettings::unhinted(Size::unscaled(), LocationRef::default());
glyph.draw(settings, &mut pen).ok()?;
pen.finish()
}
pub fn advance_width(&self, cid: u16) -> f32 {
self.widths.get(&cid).copied().unwrap_or(self.default_width)
}
pub fn units_per_em(&self) -> f32 {
self.units_per_em
}
}
pub(crate) fn extract_cid_font_program(
font_dict: &Dictionary,
doc: &crate::document::Document,
) -> Option<CidFontProgram> {
let descendants = font_dict
.get_str("DescendantFonts")
.and_then(|o| doc.resolve(o))
.and_then(|o| o.as_array())?;
let cid_font_dict = descendants
.first()
.and_then(|o| doc.resolve(o))
.and_then(|o| match o {
Object::Dictionary(d) => Some(d),
_ => None,
})?;
let descriptor = cid_font_dict
.get_str("FontDescriptor")
.and_then(|o| doc.resolve(o))
.and_then(|o| match o {
Object::Dictionary(d) => Some(d),
_ => None,
})?;
let font_stream = descriptor
.get_str("FontFile2")
.or_else(|| descriptor.get_str("FontFile3"))
.and_then(|o| doc.resolve(o))
.and_then(|o| match o {
Object::Stream(s) => Some(s),
_ => None,
})?;
let font_data = font_stream.decode_data().ok()?;
let font_ref = FontRef::new(&font_data).ok()?;
let metrics = Metrics::new(&font_ref, Size::unscaled(), LocationRef::default());
let units_per_em = metrics.units_per_em as f32;
let default_width = cid_font_dict.get_i64("DW").unwrap_or(1000) as f32;
let widths = parse_w_array(cid_font_dict.get_str("W").and_then(|o| doc.resolve(o)));
Some(CidFontProgram {
data: font_data,
default_width,
widths,
units_per_em,
})
}
fn parse_w_array(obj: Option<&Object>) -> HashMap<u16, f32> {
let mut widths = HashMap::new();
let arr = match obj.and_then(|o| o.as_array()) {
Some(a) => a,
None => return widths,
};
let mut i = 0;
while i < arr.len() {
let cid = match &arr[i] {
Object::Integer(n) => *n as u16,
_ => break,
};
i += 1;
if i >= arr.len() {
break;
}
match &arr[i] {
Object::Array(sub) => {
for (j, w_obj) in sub.iter().enumerate() {
if let Some(w) = obj_f64(w_obj) {
widths.insert(cid + j as u16, w as f32);
}
}
i += 1;
}
Object::Integer(_) | Object::Real(_) => {
if i + 1 < arr.len() {
let cid_last = match &arr[i] {
Object::Integer(n) => *n as u16,
_ => break,
};
i += 1;
if let Some(w) = obj_f64(&arr[i]) {
for c in cid..=cid_last {
widths.insert(c, w as f32);
}
}
i += 1;
} else {
break;
}
}
_ => break,
}
}
widths
}
fn strip_pfb_wrapper(data: Vec<u8>) -> Vec<u8> {
if data.first() != Some(&0x80) {
return data;
}
let mut result = Vec::with_capacity(data.len());
let mut offset = 0;
while offset + 6 <= data.len() && data[offset] == 0x80 {
let segment_type = data[offset + 1];
if segment_type == 3 {
break;
}
let len = u32::from_le_bytes([
data[offset + 2],
data[offset + 3],
data[offset + 4],
data[offset + 5],
]) as usize;
offset += 6;
let end = (offset + len).min(data.len());
result.extend_from_slice(&data[offset..end]);
offset = end;
}
if result.is_empty() {
data
} else {
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn skia_pen_builds_path() {
let mut pen = SkiaPen::new();
pen.move_to(0.0, 0.0);
pen.line_to(100.0, 0.0);
pen.line_to(100.0, 100.0);
pen.close();
let path = pen.finish();
assert!(path.is_some(), "SkiaPen should produce a valid path");
}
#[test]
fn font_program_with_encoding() {
let data = match std::fs::read("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf") {
Ok(d) => d,
Err(_) => return,
};
let win =
FontProgram::from_data_with_encoding(data.clone(), &Encoding::win_ansi()).unwrap();
let mac = FontProgram::from_data_with_encoding(data, &Encoding::mac_roman()).unwrap();
let win_path = win.glyph_path(0xC0);
let mac_path = mac.glyph_path(0xC0);
assert!(win_path.is_some(), "WinAnsi should have glyph for 0xC0");
assert!(mac_path.is_some(), "MacRoman should have glyph for 0xC0");
let win_bounds = win_path.unwrap().bounds();
let mac_bounds = mac_path.unwrap().bounds();
assert_ne!(
(win_bounds.width() as i32, win_bounds.height() as i32),
(mac_bounds.width() as i32, mac_bounds.height() as i32),
"WinAnsi and MacRoman should map 0xC0 to different glyphs"
);
}
#[test]
fn extract_font_program_uses_encoding() {
use crate::core::objects::{IndirectRef, PdfName, PdfStream};
use crate::document::Document;
let ttf_data = match std::fs::read("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf") {
Ok(d) => d,
Err(_) => return,
};
let mut doc = Document::new();
let font_stream = PdfStream::new(Dictionary::new(), ttf_data.clone());
let fs_id = doc.add_object(Object::Stream(font_stream));
let mut descriptor = Dictionary::new();
descriptor.insert(
PdfName::new("FontFile2"),
Object::Reference(IndirectRef::new(fs_id.0, fs_id.1)),
);
let desc_id = doc.add_object(Object::Dictionary(descriptor));
let mut font_dict = Dictionary::new();
font_dict.insert(
PdfName::new("FontDescriptor"),
Object::Reference(IndirectRef::new(desc_id.0, desc_id.1)),
);
font_dict.insert(
PdfName::new("Encoding"),
Object::Name(PdfName::new("MacRomanEncoding")),
);
let fp = extract_font_program(&font_dict, &doc);
assert!(
fp.is_some(),
"should extract font program with MacRoman encoding"
);
let fp = fp.unwrap();
let mac_ref =
FontProgram::from_data_with_encoding(ttf_data, &Encoding::mac_roman()).unwrap();
let extracted_path = fp.glyph_path(0xCA);
let mac_path = mac_ref.glyph_path(0xCA);
assert_eq!(
extracted_path.is_some(),
mac_path.is_some(),
"Extracted font program should use MacRoman encoding"
);
}
#[test]
fn font_program_from_real_ttf() {
let data = match std::fs::read("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf") {
Ok(d) => d,
Err(_) => return, };
let fp = FontProgram::from_data_with_encoding(data, &Encoding::win_ansi())
.expect("should parse DejaVu Sans");
assert!(fp.units_per_em() > 0.0);
let path = fp.glyph_path(b'A');
assert!(path.is_some(), "glyph 'A' should produce a path");
let aw = fp.advance_width(b'A');
assert!(aw.is_some());
assert!(aw.unwrap() > 0.0);
}
#[test]
fn cidfont_program_from_dict() {
use crate::core::objects::{Dictionary, IndirectRef, Object, PdfName, PdfStream};
use crate::document::Document;
let ttf_data = match std::fs::read("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf") {
Ok(d) => d,
Err(_) => return,
};
let mut doc = Document::new();
let font_stream = PdfStream::new(Dictionary::new(), ttf_data);
let fs_id = doc.add_object(Object::Stream(font_stream));
let mut descriptor = Dictionary::new();
descriptor.insert(
PdfName::new("FontFile2"),
Object::Reference(IndirectRef::new(fs_id.0, fs_id.1)),
);
let desc_id = doc.add_object(Object::Dictionary(descriptor));
let mut cid_font = Dictionary::new();
cid_font.insert(
PdfName::new("Subtype"),
Object::Name(PdfName::new("CIDFontType2")),
);
cid_font.insert(PdfName::new("DW"), Object::Integer(500));
cid_font.insert(
PdfName::new("W"),
Object::Array(vec![
Object::Integer(36),
Object::Array(vec![Object::Integer(600)]),
]),
);
cid_font.insert(
PdfName::new("FontDescriptor"),
Object::Reference(IndirectRef::new(desc_id.0, desc_id.1)),
);
let cid_id = doc.add_object(Object::Dictionary(cid_font));
let mut font_dict = Dictionary::new();
font_dict.insert(PdfName::new("Subtype"), Object::Name(PdfName::new("Type0")));
font_dict.insert(
PdfName::new("Encoding"),
Object::Name(PdfName::new("Identity-H")),
);
font_dict.insert(
PdfName::new("DescendantFonts"),
Object::Array(vec![Object::Reference(IndirectRef::new(
cid_id.0, cid_id.1,
))]),
);
let cid_prog = extract_cid_font_program(&font_dict, &doc);
assert!(cid_prog.is_some(), "should parse CIDFont from Type 0 dict");
let cid_prog = cid_prog.unwrap();
assert!(cid_prog.units_per_em() > 0.0);
assert_eq!(cid_prog.advance_width(36), 600.0);
assert_eq!(cid_prog.advance_width(99), 500.0); }
#[test]
fn cidfont_glyph_path() {
let ttf_data = match std::fs::read("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf") {
Ok(d) => d,
Err(_) => return,
};
let cid_prog = CidFontProgram {
data: ttf_data,
default_width: 1000.0,
widths: HashMap::new(),
units_per_em: 2048.0,
};
let path = cid_prog.glyph_path_by_gid(36);
assert!(path.is_some(), "GID 36 should produce a glyph path");
}
#[test]
fn parse_w_array_formats() {
let arr = Object::Array(vec![
Object::Integer(10),
Object::Array(vec![Object::Integer(300), Object::Integer(400)]),
]);
let widths = parse_w_array(Some(&arr));
assert_eq!(widths.get(&10), Some(&300.0));
assert_eq!(widths.get(&11), Some(&400.0));
assert_eq!(widths.get(&12), None);
let arr = Object::Array(vec![
Object::Integer(20),
Object::Integer(22),
Object::Integer(500),
]);
let widths = parse_w_array(Some(&arr));
assert_eq!(widths.get(&20), Some(&500.0));
assert_eq!(widths.get(&21), Some(&500.0));
assert_eq!(widths.get(&22), Some(&500.0));
assert_eq!(widths.get(&23), None);
}
#[test]
fn strip_pfb_wrapper_basic() {
let mut pfb = Vec::new();
pfb.push(0x80);
pfb.push(1); pfb.extend_from_slice(&5u32.to_le_bytes());
pfb.extend_from_slice(b"Hello");
pfb.push(0x80);
pfb.push(2); pfb.extend_from_slice(&3u32.to_le_bytes());
pfb.extend_from_slice(&[0xDE, 0xAD, 0xBE]);
pfb.push(0x80);
pfb.push(3);
let result = strip_pfb_wrapper(pfb);
assert_eq!(&result[..5], b"Hello");
assert_eq!(&result[5..], &[0xDE, 0xAD, 0xBE]);
}
#[test]
fn strip_pfb_wrapper_no_header() {
let data = b"not a pfb file".to_vec();
let expected = data.clone();
let result = strip_pfb_wrapper(data);
assert_eq!(result, expected);
}
#[test]
fn extract_font_program_tries_fontfile() {
use crate::core::objects::{IndirectRef, PdfName, PdfStream};
use crate::document::Document;
let mut doc = Document::new();
let type1_data = b"%!PS-AdobeFont-1.0: TestFont";
let font_stream = PdfStream::new(Dictionary::new(), type1_data.to_vec());
let fs_id = doc.add_object(Object::Stream(font_stream));
let mut descriptor = Dictionary::new();
descriptor.insert(
PdfName::new("FontFile"),
Object::Reference(IndirectRef::new(fs_id.0, fs_id.1)),
);
let desc_id = doc.add_object(Object::Dictionary(descriptor));
let mut font_dict = Dictionary::new();
font_dict.insert(
PdfName::new("FontDescriptor"),
Object::Reference(IndirectRef::new(desc_id.0, desc_id.1)),
);
let _fp = extract_font_program(&font_dict, &doc);
}
#[test]
fn extract_font_program_prefers_fontfile2() {
use crate::core::objects::{IndirectRef, PdfName, PdfStream};
use crate::document::Document;
let ttf_data = match std::fs::read("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf") {
Ok(d) => d,
Err(_) => return,
};
let mut doc = Document::new();
let dummy = PdfStream::new(Dictionary::new(), b"dummy".to_vec());
let dummy_id = doc.add_object(Object::Stream(dummy));
let ttf_stream = PdfStream::new(Dictionary::new(), ttf_data);
let ttf_id = doc.add_object(Object::Stream(ttf_stream));
let mut descriptor = Dictionary::new();
descriptor.insert(
PdfName::new("FontFile"),
Object::Reference(IndirectRef::new(dummy_id.0, dummy_id.1)),
);
descriptor.insert(
PdfName::new("FontFile2"),
Object::Reference(IndirectRef::new(ttf_id.0, ttf_id.1)),
);
let desc_id = doc.add_object(Object::Dictionary(descriptor));
let mut font_dict = Dictionary::new();
font_dict.insert(
PdfName::new("FontDescriptor"),
Object::Reference(IndirectRef::new(desc_id.0, desc_id.1)),
);
let fp = extract_font_program(&font_dict, &doc);
assert!(
fp.is_some(),
"Should extract from FontFile2 when both present"
);
}
#[test]
fn type3_font_program_from_dict() {
use crate::core::objects::{Dictionary, IndirectRef, Object, PdfName, PdfStream};
use crate::document::Document;
let mut doc = Document::new();
let glyph_stream = PdfStream::new(Dictionary::new(), b"0 0 100 100 re f".to_vec());
let glyph_id = doc.add_object(Object::Stream(glyph_stream));
let mut char_procs = Dictionary::new();
char_procs.insert(
PdfName::new("A"),
Object::Reference(IndirectRef::new(glyph_id.0, glyph_id.1)),
);
let cp_id = doc.add_object(Object::Dictionary(char_procs));
let mut enc = Dictionary::new();
enc.insert(PdfName::new("Type"), Object::Name(PdfName::new("Encoding")));
enc.insert(
PdfName::new("Differences"),
Object::Array(vec![Object::Integer(65), Object::Name(PdfName::new("A"))]),
);
let enc_id = doc.add_object(Object::Dictionary(enc));
let mut font_dict = Dictionary::new();
font_dict.insert(PdfName::new("Type"), Object::Name(PdfName::new("Font")));
font_dict.insert(PdfName::new("Subtype"), Object::Name(PdfName::new("Type3")));
font_dict.insert(
PdfName::new("FontMatrix"),
Object::Array(vec![
Object::Real(0.001),
Object::Integer(0),
Object::Integer(0),
Object::Real(0.001),
Object::Integer(0),
Object::Integer(0),
]),
);
font_dict.insert(PdfName::new("FirstChar"), Object::Integer(65));
font_dict.insert(PdfName::new("LastChar"), Object::Integer(65));
font_dict.insert(
PdfName::new("Widths"),
Object::Array(vec![Object::Integer(500)]),
);
font_dict.insert(
PdfName::new("CharProcs"),
Object::Reference(IndirectRef::new(cp_id.0, cp_id.1)),
);
font_dict.insert(
PdfName::new("Encoding"),
Object::Reference(IndirectRef::new(enc_id.0, enc_id.1)),
);
let t3 = Type3FontProgram::from_dict(&font_dict, &doc);
assert!(t3.is_some(), "should parse Type 3 font");
let t3 = t3.unwrap();
assert_eq!(t3.glyph_width(65), Some(500.0));
assert!(t3.glyph_stream(65).is_some());
assert_eq!(t3.glyph_stream(65).unwrap(), b"0 0 100 100 re f");
assert!(t3.glyph_stream(66).is_none());
}
}