1use std::collections::HashMap;
13
14use rpdfium_core::{Name, PdfSource};
15use rpdfium_parser::{Object, ObjectStore};
16
17#[derive(Debug, Clone)]
19pub struct BaFontMapEntry {
20 pub font_name: String,
22 pub charset: u8,
24}
25
26#[derive(Debug, Clone)]
31pub struct BaFontMap {
32 entries: Vec<BaFontMapEntry>,
34 default_font: Option<String>,
36 default_size: f32,
38}
39
40impl BaFontMap {
41 pub fn from_resources<S: PdfSource>(
46 dr_dict: Option<&HashMap<Name, Object>>,
47 da_string: Option<&str>,
48 store: &ObjectStore<S>,
49 ) -> Self {
50 let mut entries = Vec::new();
51
52 if let Some(font_dict) = dr_dict {
54 for (name, obj) in font_dict {
55 let font_name = name.as_str().into_owned();
56
57 let charset = if let Ok(resolved) = store.deep_resolve(obj) {
59 extract_charset(resolved)
60 } else {
61 0 };
63
64 entries.push(BaFontMapEntry { font_name, charset });
65 }
66 }
67
68 let (default_font, default_size) = if let Some(da) = da_string {
70 parse_default_appearance_font(da)
71 } else {
72 (None, 0.0)
73 };
74
75 Self {
76 entries,
77 default_font,
78 default_size,
79 }
80 }
81
82 pub fn default_font_name(&self) -> Option<&str> {
84 self.default_font.as_deref()
85 }
86
87 pub fn default_font_size(&self) -> f32 {
89 self.default_size
90 }
91
92 pub fn font_count(&self) -> usize {
94 self.entries.len()
95 }
96
97 pub fn font_name(&self, index: usize) -> Option<&str> {
99 self.entries.get(index).map(|e| e.font_name.as_str())
100 }
101
102 #[inline]
104 pub fn get_font_name(&self, index: usize) -> Option<&str> {
105 self.font_name(index)
106 }
107
108 pub fn charset(&self, index: usize) -> Option<u8> {
110 self.entries.get(index).map(|e| e.charset)
111 }
112
113 #[inline]
115 pub fn get_charset(&self, index: usize) -> Option<u8> {
116 self.charset(index)
117 }
118
119 pub fn find_font(&self, name: &str) -> Option<&BaFontMapEntry> {
121 self.entries.iter().find(|e| e.font_name == name)
122 }
123
124 pub fn entries(&self) -> &[BaFontMapEntry] {
126 &self.entries
127 }
128
129 pub fn find_font_or_fallback(&self, name: &str) -> Option<&BaFontMapEntry> {
137 if let Some(entry) = self.find_font(name) {
139 return Some(entry);
140 }
141
142 let aliases = match name {
144 "Helv" | "Helvetica" => &["Helv", "Helvetica", "Helvetica-Bold", "Arial"][..],
145 "Cour" | "Courier" => &["Cour", "Courier", "Courier-Bold"][..],
146 "TiRo" | "TimesNewRoman" | "Times-Roman" => {
147 &["TiRo", "TimesNewRoman", "Times-Roman", "Times"][..]
148 }
149 "ZaDb" | "ZapfDingbats" => &["ZaDb", "ZapfDingbats"][..],
150 "Symb" | "Symbol" => &["Symb", "Symbol"][..],
151 _ => &[][..],
152 };
153
154 for alias in aliases {
155 if let Some(entry) = self.find_font(alias) {
156 return Some(entry);
157 }
158 }
159
160 None
161 }
162}
163
164pub fn parse_default_appearance_font(da: &str) -> (Option<String>, f32) {
168 let mut font_name: Option<String> = None;
169 let mut font_size = 0.0_f32;
170 let mut last_number: Option<f32> = None;
171
172 for token in da.split_whitespace() {
173 if let Some(stripped) = token.strip_prefix('/') {
174 font_name = Some(stripped.to_string());
175 last_number = None;
176 } else if token == "Tf" {
177 if let Some(size) = last_number {
178 font_size = size;
179 }
180 last_number = None;
181 } else if let Ok(n) = token.parse::<f32>() {
182 last_number = Some(n);
183 } else {
184 last_number = None;
185 }
186 }
187
188 (font_name, font_size)
189}
190
191fn extract_charset(obj: &Object) -> u8 {
193 let dict = match obj {
194 Object::Dictionary(d) => d,
195 Object::Stream { dict, .. } => dict,
196 _ => return 0,
197 };
198
199 if let Some(enc_obj) = dict.get(&Name::encoding()) {
201 if let Some(name) = enc_obj.as_name() {
202 let s = name.as_str();
203 if s.contains("Symbol") {
204 return 2; }
206 }
207 }
208
209 if let Some(bf_obj) = dict.get(&Name::base_font()) {
211 if let Some(name) = bf_obj.as_name() {
212 let s = name.as_str();
213 if s.contains("Symbol") {
214 return 2;
215 }
216 if s.contains("ZapfDingbats") {
217 return 2;
218 }
219 }
220 }
221
222 0 }
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn test_parse_da_font_basic() {
231 let (name, size) = parse_default_appearance_font("0 g /Helv 12 Tf");
232 assert_eq!(name.as_deref(), Some("Helv"));
233 assert_eq!(size, 12.0);
234 }
235
236 #[test]
237 fn test_parse_da_font_courier() {
238 let (name, size) = parse_default_appearance_font("/Cour 10 Tf 0 0 0 rg");
239 assert_eq!(name.as_deref(), Some("Cour"));
240 assert_eq!(size, 10.0);
241 }
242
243 #[test]
244 fn test_parse_da_no_font() {
245 let (name, size) = parse_default_appearance_font("0 g");
246 assert!(name.is_none());
247 assert_eq!(size, 0.0);
248 }
249
250 #[test]
251 fn test_parse_da_empty() {
252 let (name, size) = parse_default_appearance_font("");
253 assert!(name.is_none());
254 assert_eq!(size, 0.0);
255 }
256
257 #[test]
258 fn test_parse_da_zero_size() {
259 let (name, size) = parse_default_appearance_font("/Helv 0 Tf");
260 assert_eq!(name.as_deref(), Some("Helv"));
261 assert_eq!(size, 0.0);
262 }
263
264 fn build_store() -> ObjectStore<Vec<u8>> {
265 let pdf = build_minimal_pdf();
266 ObjectStore::open(pdf, rpdfium_core::ParsingMode::Lenient).unwrap()
267 }
268
269 fn build_minimal_pdf() -> Vec<u8> {
270 let mut pdf = Vec::new();
271 pdf.extend_from_slice(b"%PDF-1.4\n");
272 let obj1_offset = pdf.len();
273 pdf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
274 let obj2_offset = pdf.len();
275 pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
276 let xref_offset = pdf.len();
277 pdf.extend_from_slice(b"xref\n0 3\n");
278 pdf.extend_from_slice(b"0000000000 65535 f \r\n");
279 pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj1_offset).as_bytes());
280 pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj2_offset).as_bytes());
281 pdf.extend_from_slice(b"trailer\n<< /Size 3 /Root 1 0 R >>\n");
282 pdf.extend_from_slice(format!("startxref\n{}\n%%EOF", xref_offset).as_bytes());
283 pdf
284 }
285
286 #[test]
287 fn test_empty_font_map() {
288 let store = build_store();
289 let map = BaFontMap::from_resources(None, None, &store);
290 assert_eq!(map.font_count(), 0);
291 assert!(map.default_font_name().is_none());
292 assert_eq!(map.default_font_size(), 0.0);
293 }
294
295 #[test]
296 fn test_font_map_with_dr() {
297 let store = build_store();
298
299 let mut font_dict = HashMap::new();
300 let mut helv_dict = HashMap::new();
302 helv_dict.insert(Name::base_font(), Object::Name(Name::from("Helvetica")));
303 font_dict.insert(Name::from("Helv"), Object::Dictionary(helv_dict));
304
305 let mut cour_dict = HashMap::new();
306 cour_dict.insert(Name::base_font(), Object::Name(Name::from("Courier")));
307 font_dict.insert(Name::from("Cour"), Object::Dictionary(cour_dict));
308
309 let map = BaFontMap::from_resources(Some(&font_dict), Some("/Helv 12 Tf"), &store);
310 assert_eq!(map.font_count(), 2);
311 assert_eq!(map.default_font_name(), Some("Helv"));
312 assert_eq!(map.default_font_size(), 12.0);
313 }
314
315 #[test]
316 fn test_font_map_find_font() {
317 let store = build_store();
318
319 let mut font_dict = HashMap::new();
320 let helv_dict = HashMap::new();
321 font_dict.insert(Name::from("Helv"), Object::Dictionary(helv_dict));
322
323 let map = BaFontMap::from_resources(Some(&font_dict), None, &store);
324 assert!(map.find_font("Helv").is_some());
325 assert!(map.find_font("Missing").is_none());
326 }
327
328 #[test]
329 fn test_font_map_standard_fonts() {
330 let store = build_store();
331
332 let standard_names = ["Helv", "Cour", "TiRo", "ZaDb"];
333 let mut font_dict = HashMap::new();
334 for name in &standard_names {
335 font_dict.insert(Name::from(*name), Object::Dictionary(HashMap::new()));
336 }
337
338 let map = BaFontMap::from_resources(Some(&font_dict), None, &store);
339 assert_eq!(map.font_count(), 4);
340 for (i, name) in standard_names.iter().enumerate() {
341 assert_eq!(map.font_name(i).is_some(), true);
342 assert!(map.find_font(name).is_some());
344 }
345 }
346
347 #[test]
348 fn test_font_map_symbol_charset() {
349 let store = build_store();
350
351 let mut font_dict = HashMap::new();
352 let mut zadb_dict = HashMap::new();
353 zadb_dict.insert(Name::base_font(), Object::Name(Name::from("ZapfDingbats")));
354 font_dict.insert(Name::from("ZaDb"), Object::Dictionary(zadb_dict));
355
356 let map = BaFontMap::from_resources(Some(&font_dict), None, &store);
357 let entry = map.find_font("ZaDb").unwrap();
358 assert_eq!(entry.charset, 2); }
360
361 #[test]
362 fn test_get_font_name_out_of_bounds() {
363 let store = build_store();
364 let map = BaFontMap::from_resources(None, None, &store);
365 assert!(map.font_name(0).is_none());
366 assert!(map.charset(0).is_none());
367 }
368
369 #[test]
370 fn test_find_font_or_fallback_exact() {
371 let store = build_store();
372 let mut font_dict = HashMap::new();
373 font_dict.insert(Name::from("Helv"), Object::Dictionary(HashMap::new()));
374 let map = BaFontMap::from_resources(Some(&font_dict), None, &store);
375 assert!(map.find_font_or_fallback("Helv").is_some());
376 }
377
378 #[test]
379 fn test_find_font_or_fallback_alias() {
380 let store = build_store();
381 let mut font_dict = HashMap::new();
382 font_dict.insert(Name::from("Helv"), Object::Dictionary(HashMap::new()));
383 let map = BaFontMap::from_resources(Some(&font_dict), None, &store);
384 assert!(map.find_font_or_fallback("Helvetica").is_some());
386 assert_eq!(
387 map.find_font_or_fallback("Helvetica").unwrap().font_name,
388 "Helv"
389 );
390 }
391
392 #[test]
393 fn test_find_font_or_fallback_no_match() {
394 let store = build_store();
395 let mut font_dict = HashMap::new();
396 font_dict.insert(Name::from("Helv"), Object::Dictionary(HashMap::new()));
397 let map = BaFontMap::from_resources(Some(&font_dict), None, &store);
398 assert!(map.find_font_or_fallback("UnknownFont").is_none());
399 }
400
401 #[test]
402 fn test_find_font_or_fallback_zadb() {
403 let store = build_store();
404 let mut font_dict = HashMap::new();
405 font_dict.insert(Name::from("ZaDb"), Object::Dictionary(HashMap::new()));
406 let map = BaFontMap::from_resources(Some(&font_dict), None, &store);
407 assert!(map.find_font_or_fallback("ZapfDingbats").is_some());
408 }
409
410 #[test]
417 fn test_ba_font_map_default_font() {
418 let store = build_store();
419
420 let map = BaFontMap::from_resources(None, Some("0 0 0 rg /F1 12 Tf"), &store);
422
423 assert_eq!(map.font_count(), 0);
425
426 assert_eq!(map.default_font_name(), Some("F1"));
428 assert_eq!(map.default_font_size(), 12.0);
429 }
430
431 #[test]
437 fn test_ba_font_map_bug_853238() {
438 let store = build_store();
439
440 let mut f1_dict = HashMap::new();
442 f1_dict.insert(Name::r#type(), Object::Name(Name::from("Font")));
443 f1_dict.insert(Name::subtype(), Object::Name(Name::from("Type1")));
444 f1_dict.insert(Name::base_font(), Object::Name(Name::from("Times-Roman")));
445
446 let mut font_dict = HashMap::new();
447 font_dict.insert(Name::from("F1"), Object::Dictionary(f1_dict));
448
449 let map = BaFontMap::from_resources(Some(&font_dict), Some("0 0 0 rg /F1 12 Tf"), &store);
450
451 assert_eq!(map.font_count(), 1);
453 assert!(map.find_font("F1").is_some());
454
455 assert_eq!(map.default_font_name(), Some("F1"));
457 assert_eq!(map.default_font_size(), 12.0);
458
459 let entry = map.find_font("F1").unwrap();
461 assert_eq!(entry.charset, 0);
462 }
463}