use std::collections::HashMap;
use rpdfium_core::{Name, PdfSource};
use rpdfium_parser::{Object, ObjectStore};
#[derive(Debug, Clone)]
pub struct BaFontMapEntry {
pub font_name: String,
pub charset: u8,
}
#[derive(Debug, Clone)]
pub struct BaFontMap {
entries: Vec<BaFontMapEntry>,
default_font: Option<String>,
default_size: f32,
}
impl BaFontMap {
pub fn from_resources<S: PdfSource>(
dr_dict: Option<&HashMap<Name, Object>>,
da_string: Option<&str>,
store: &ObjectStore<S>,
) -> Self {
let mut entries = Vec::new();
if let Some(font_dict) = dr_dict {
for (name, obj) in font_dict {
let font_name = name.as_str().into_owned();
let charset = if let Ok(resolved) = store.deep_resolve(obj) {
extract_charset(resolved)
} else {
0 };
entries.push(BaFontMapEntry { font_name, charset });
}
}
let (default_font, default_size) = if let Some(da) = da_string {
parse_default_appearance_font(da)
} else {
(None, 0.0)
};
Self {
entries,
default_font,
default_size,
}
}
pub fn default_font_name(&self) -> Option<&str> {
self.default_font.as_deref()
}
pub fn default_font_size(&self) -> f32 {
self.default_size
}
pub fn font_count(&self) -> usize {
self.entries.len()
}
pub fn font_name(&self, index: usize) -> Option<&str> {
self.entries.get(index).map(|e| e.font_name.as_str())
}
#[inline]
pub fn get_font_name(&self, index: usize) -> Option<&str> {
self.font_name(index)
}
pub fn charset(&self, index: usize) -> Option<u8> {
self.entries.get(index).map(|e| e.charset)
}
#[inline]
pub fn get_charset(&self, index: usize) -> Option<u8> {
self.charset(index)
}
pub fn find_font(&self, name: &str) -> Option<&BaFontMapEntry> {
self.entries.iter().find(|e| e.font_name == name)
}
pub fn entries(&self) -> &[BaFontMapEntry] {
&self.entries
}
pub fn find_font_or_fallback(&self, name: &str) -> Option<&BaFontMapEntry> {
if let Some(entry) = self.find_font(name) {
return Some(entry);
}
let aliases = match name {
"Helv" | "Helvetica" => &["Helv", "Helvetica", "Helvetica-Bold", "Arial"][..],
"Cour" | "Courier" => &["Cour", "Courier", "Courier-Bold"][..],
"TiRo" | "TimesNewRoman" | "Times-Roman" => {
&["TiRo", "TimesNewRoman", "Times-Roman", "Times"][..]
}
"ZaDb" | "ZapfDingbats" => &["ZaDb", "ZapfDingbats"][..],
"Symb" | "Symbol" => &["Symb", "Symbol"][..],
_ => &[][..],
};
for alias in aliases {
if let Some(entry) = self.find_font(alias) {
return Some(entry);
}
}
None
}
}
pub fn parse_default_appearance_font(da: &str) -> (Option<String>, f32) {
let mut font_name: Option<String> = None;
let mut font_size = 0.0_f32;
let mut last_number: Option<f32> = None;
for token in da.split_whitespace() {
if let Some(stripped) = token.strip_prefix('/') {
font_name = Some(stripped.to_string());
last_number = None;
} else if token == "Tf" {
if let Some(size) = last_number {
font_size = size;
}
last_number = None;
} else if let Ok(n) = token.parse::<f32>() {
last_number = Some(n);
} else {
last_number = None;
}
}
(font_name, font_size)
}
fn extract_charset(obj: &Object) -> u8 {
let dict = match obj {
Object::Dictionary(d) => d,
Object::Stream { dict, .. } => dict,
_ => return 0,
};
if let Some(enc_obj) = dict.get(&Name::encoding()) {
if let Some(name) = enc_obj.as_name() {
let s = name.as_str();
if s.contains("Symbol") {
return 2; }
}
}
if let Some(bf_obj) = dict.get(&Name::base_font()) {
if let Some(name) = bf_obj.as_name() {
let s = name.as_str();
if s.contains("Symbol") {
return 2;
}
if s.contains("ZapfDingbats") {
return 2;
}
}
}
0 }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_da_font_basic() {
let (name, size) = parse_default_appearance_font("0 g /Helv 12 Tf");
assert_eq!(name.as_deref(), Some("Helv"));
assert_eq!(size, 12.0);
}
#[test]
fn test_parse_da_font_courier() {
let (name, size) = parse_default_appearance_font("/Cour 10 Tf 0 0 0 rg");
assert_eq!(name.as_deref(), Some("Cour"));
assert_eq!(size, 10.0);
}
#[test]
fn test_parse_da_no_font() {
let (name, size) = parse_default_appearance_font("0 g");
assert!(name.is_none());
assert_eq!(size, 0.0);
}
#[test]
fn test_parse_da_empty() {
let (name, size) = parse_default_appearance_font("");
assert!(name.is_none());
assert_eq!(size, 0.0);
}
#[test]
fn test_parse_da_zero_size() {
let (name, size) = parse_default_appearance_font("/Helv 0 Tf");
assert_eq!(name.as_deref(), Some("Helv"));
assert_eq!(size, 0.0);
}
fn build_store() -> ObjectStore<Vec<u8>> {
let pdf = build_minimal_pdf();
ObjectStore::open(pdf, rpdfium_core::ParsingMode::Lenient).unwrap()
}
fn build_minimal_pdf() -> Vec<u8> {
let mut pdf = Vec::new();
pdf.extend_from_slice(b"%PDF-1.4\n");
let obj1_offset = pdf.len();
pdf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
let obj2_offset = pdf.len();
pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
let xref_offset = pdf.len();
pdf.extend_from_slice(b"xref\n0 3\n");
pdf.extend_from_slice(b"0000000000 65535 f \r\n");
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj1_offset).as_bytes());
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj2_offset).as_bytes());
pdf.extend_from_slice(b"trailer\n<< /Size 3 /Root 1 0 R >>\n");
pdf.extend_from_slice(format!("startxref\n{}\n%%EOF", xref_offset).as_bytes());
pdf
}
#[test]
fn test_empty_font_map() {
let store = build_store();
let map = BaFontMap::from_resources(None, None, &store);
assert_eq!(map.font_count(), 0);
assert!(map.default_font_name().is_none());
assert_eq!(map.default_font_size(), 0.0);
}
#[test]
fn test_font_map_with_dr() {
let store = build_store();
let mut font_dict = HashMap::new();
let mut helv_dict = HashMap::new();
helv_dict.insert(Name::base_font(), Object::Name(Name::from("Helvetica")));
font_dict.insert(Name::from("Helv"), Object::Dictionary(helv_dict));
let mut cour_dict = HashMap::new();
cour_dict.insert(Name::base_font(), Object::Name(Name::from("Courier")));
font_dict.insert(Name::from("Cour"), Object::Dictionary(cour_dict));
let map = BaFontMap::from_resources(Some(&font_dict), Some("/Helv 12 Tf"), &store);
assert_eq!(map.font_count(), 2);
assert_eq!(map.default_font_name(), Some("Helv"));
assert_eq!(map.default_font_size(), 12.0);
}
#[test]
fn test_font_map_find_font() {
let store = build_store();
let mut font_dict = HashMap::new();
let helv_dict = HashMap::new();
font_dict.insert(Name::from("Helv"), Object::Dictionary(helv_dict));
let map = BaFontMap::from_resources(Some(&font_dict), None, &store);
assert!(map.find_font("Helv").is_some());
assert!(map.find_font("Missing").is_none());
}
#[test]
fn test_font_map_standard_fonts() {
let store = build_store();
let standard_names = ["Helv", "Cour", "TiRo", "ZaDb"];
let mut font_dict = HashMap::new();
for name in &standard_names {
font_dict.insert(Name::from(*name), Object::Dictionary(HashMap::new()));
}
let map = BaFontMap::from_resources(Some(&font_dict), None, &store);
assert_eq!(map.font_count(), 4);
for (i, name) in standard_names.iter().enumerate() {
assert_eq!(map.font_name(i).is_some(), true);
assert!(map.find_font(name).is_some());
}
}
#[test]
fn test_font_map_symbol_charset() {
let store = build_store();
let mut font_dict = HashMap::new();
let mut zadb_dict = HashMap::new();
zadb_dict.insert(Name::base_font(), Object::Name(Name::from("ZapfDingbats")));
font_dict.insert(Name::from("ZaDb"), Object::Dictionary(zadb_dict));
let map = BaFontMap::from_resources(Some(&font_dict), None, &store);
let entry = map.find_font("ZaDb").unwrap();
assert_eq!(entry.charset, 2); }
#[test]
fn test_get_font_name_out_of_bounds() {
let store = build_store();
let map = BaFontMap::from_resources(None, None, &store);
assert!(map.font_name(0).is_none());
assert!(map.charset(0).is_none());
}
#[test]
fn test_find_font_or_fallback_exact() {
let store = build_store();
let mut font_dict = HashMap::new();
font_dict.insert(Name::from("Helv"), Object::Dictionary(HashMap::new()));
let map = BaFontMap::from_resources(Some(&font_dict), None, &store);
assert!(map.find_font_or_fallback("Helv").is_some());
}
#[test]
fn test_find_font_or_fallback_alias() {
let store = build_store();
let mut font_dict = HashMap::new();
font_dict.insert(Name::from("Helv"), Object::Dictionary(HashMap::new()));
let map = BaFontMap::from_resources(Some(&font_dict), None, &store);
assert!(map.find_font_or_fallback("Helvetica").is_some());
assert_eq!(
map.find_font_or_fallback("Helvetica").unwrap().font_name,
"Helv"
);
}
#[test]
fn test_find_font_or_fallback_no_match() {
let store = build_store();
let mut font_dict = HashMap::new();
font_dict.insert(Name::from("Helv"), Object::Dictionary(HashMap::new()));
let map = BaFontMap::from_resources(Some(&font_dict), None, &store);
assert!(map.find_font_or_fallback("UnknownFont").is_none());
}
#[test]
fn test_find_font_or_fallback_zadb() {
let store = build_store();
let mut font_dict = HashMap::new();
font_dict.insert(Name::from("ZaDb"), Object::Dictionary(HashMap::new()));
let map = BaFontMap::from_resources(Some(&font_dict), None, &store);
assert!(map.find_font_or_fallback("ZapfDingbats").is_some());
}
#[test]
fn test_ba_font_map_default_font() {
let store = build_store();
let map = BaFontMap::from_resources(None, Some("0 0 0 rg /F1 12 Tf"), &store);
assert_eq!(map.font_count(), 0);
assert_eq!(map.default_font_name(), Some("F1"));
assert_eq!(map.default_font_size(), 12.0);
}
#[test]
fn test_ba_font_map_bug_853238() {
let store = build_store();
let mut f1_dict = HashMap::new();
f1_dict.insert(Name::r#type(), Object::Name(Name::from("Font")));
f1_dict.insert(Name::subtype(), Object::Name(Name::from("Type1")));
f1_dict.insert(Name::base_font(), Object::Name(Name::from("Times-Roman")));
let mut font_dict = HashMap::new();
font_dict.insert(Name::from("F1"), Object::Dictionary(f1_dict));
let map = BaFontMap::from_resources(Some(&font_dict), Some("0 0 0 rg /F1 12 Tf"), &store);
assert_eq!(map.font_count(), 1);
assert!(map.find_font("F1").is_some());
assert_eq!(map.default_font_name(), Some("F1"));
assert_eq!(map.default_font_size(), 12.0);
let entry = map.find_font("F1").unwrap();
assert_eq!(entry.charset, 0);
}
}