1use std::collections::HashMap;
4use std::sync::LazyLock;
5
6static AGL_DATA: &str = include_str!("data/glyphlist.txt");
8
9static AGL_MAP: LazyLock<HashMap<&'static str, String>> = LazyLock::new(|| {
11 let mut map = HashMap::with_capacity(4300);
12 for line in AGL_DATA.lines() {
13 let line = line.trim();
14 if line.is_empty() || line.starts_with('#') {
15 continue;
16 }
17 if let Some((name, hex_part)) = line.split_once(';') {
18 let mut s = String::new();
20 for hex_str in hex_part.split_whitespace() {
21 if let Ok(cp) = u32::from_str_radix(hex_str, 16) {
22 if let Some(c) = char::from_u32(cp) {
23 s.push(c);
24 }
25 }
26 }
27 if !s.is_empty() {
28 map.insert(name, s);
29 }
30 }
31 }
32 map
33});
34
35static TEX_GLYPH_MAP: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(|| {
38 let mut m = HashMap::new();
39 m.insert("asteriskmath", "\u{2217}"); m.insert("diamondmath", "\u{22C4}"); m.insert("minusplus", "\u{2213}"); m.insert("circleminus", "\u{2296}"); m.insert("circledivide", "\u{2298}"); m.insert("circledot", "\u{2299}"); m.insert("circlecopyrt", "\u{00A9}"); m.insert("equivasymptotic", "\u{224D}"); m.insert("precedesequal", "\u{227C}"); m.insert("followsequal", "\u{227D}"); m.insert("similarequal", "\u{2243}"); m.insert("lessmuch", "\u{226A}"); m.insert("greatermuch", "\u{226B}"); m.insert("follows", "\u{227B}"); m.insert("arrownortheast", "\u{2197}"); m.insert("arrowsoutheast", "\u{2198}"); m.insert("arrownorthwest", "\u{2196}"); m.insert("arrowsouthwest", "\u{2199}"); m.insert("negationslash", "\u{0338}"); m.insert("owner", "\u{220B}"); m.insert("triangleinv", "\u{25BD}"); m.insert("latticetop", "\u{22A4}"); m.insert("tie", "\u{2040}"); m.insert("dotlessj", "\u{0237}"); m.insert("vector", "\u{20D7}"); m.insert("bardbl", "\u{2016}"); m.insert("mapsto", "\u{21A6}"); m.insert("lscript", "\u{2113}"); m.insert("weierstrass", "\u{2118}"); m.insert("visiblespace", "\u{2423}"); m
73});
74
75#[derive(Debug, Clone)]
86pub struct PdfFont {
87 pub name: String,
89 pub base_font: String,
91 pub subtype: String,
93 pub widths: HashMap<u32, f64>,
95 pub default_width: f64,
97 pub to_unicode: HashMap<u32, String>,
99 pub encoding: String,
101 pub is_standard: bool,
103 pub flags: u32,
105 pub italic_angle: f64,
107 pub weight: f64,
109 pub bytes_per_code: u8,
111 pub ascent: f64,
113 pub descent: f64,
115 pub font_bbox: [f64; 4],
117}
118
119impl PdfFont {
120 pub fn default_font(name: &str) -> Self {
122 Self {
123 name: name.to_string(),
124 base_font: "Unknown".to_string(),
125 subtype: "Type1".to_string(),
126 widths: HashMap::new(),
127 default_width: 600.0, to_unicode: HashMap::new(),
129 encoding: "WinAnsiEncoding".to_string(),
130 is_standard: false,
131 flags: 0,
132 italic_angle: 0.0,
133 weight: 400.0,
134 bytes_per_code: 1,
135 ascent: 800.0,
136 descent: -200.0,
137 font_bbox: [0.0, -200.0, 1000.0, 800.0],
138 }
139 }
140
141 pub fn glyph_width(&self, char_code: u32) -> f64 {
143 *self.widths.get(&char_code).unwrap_or(&self.default_width)
144 }
145
146 pub fn decode_char(&self, char_code: u32) -> String {
153 if let Some(unicode) = self.to_unicode.get(&char_code) {
154 return unicode.clone();
155 }
156 if self.bytes_per_code == 2 {
157 if (0x20..=0x7E).contains(&char_code) {
160 return char::from_u32(char_code).unwrap_or('.').to_string();
161 }
162 return "\u{FFFD}".to_string();
165 }
166 match self.encoding.as_str() {
167 "MacRomanEncoding" => decode_macroman(char_code),
168 _ => decode_winansi(char_code),
169 }
170 }
171
172 pub fn is_bold(&self) -> bool {
174 let name_lower = self.base_font.to_lowercase();
175 name_lower.contains("bold")
176 || name_lower.contains("black")
177 || name_lower.contains("heavy")
178 || (self.flags & 0x40000) != 0
179 || self.weight >= 700.0
180 }
181
182 pub fn is_italic(&self) -> bool {
184 let name_lower = self.base_font.to_lowercase();
185 name_lower.contains("italic")
186 || name_lower.contains("oblique")
187 || (self.flags & 0x40) != 0
188 || self.italic_angle.abs() > 0.5
189 }
190}
191
192#[derive(Debug, Default, Clone)]
194pub struct FontCache {
195 fonts: HashMap<String, PdfFont>,
196}
197
198impl FontCache {
199 pub fn get_or_default(&mut self, name: &str) -> &PdfFont {
201 if !self.fonts.contains_key(name) {
202 self.fonts
203 .insert(name.to_string(), PdfFont::default_font(name));
204 }
205 &self.fonts[name]
206 }
207
208 pub fn insert(&mut self, name: String, font: PdfFont) {
210 self.fonts.insert(name, font);
211 }
212
213 pub fn get(&self, name: &str) -> Option<&PdfFont> {
215 self.fonts.get(name)
216 }
217
218 pub fn iter(&self) -> impl Iterator<Item = (&String, &PdfFont)> {
220 self.fonts.iter()
221 }
222}
223
224pub fn resolve_page_fonts(doc: &lopdf::Document, page_id: lopdf::ObjectId) -> FontCache {
226 let mut cache = FontCache::default();
227
228 let page_dict = match doc.get_object(page_id).and_then(|o| o.as_dict()) {
230 Ok(d) => d,
231 Err(_) => return cache,
232 };
233
234 let resources = match page_dict.get(b"Resources") {
236 Ok(r) => resolve_object(doc, r),
237 Err(_) => return cache,
238 };
239
240 let resources_dict = match resources.as_dict() {
241 Ok(d) => d,
242 Err(_) => return cache,
243 };
244
245 let font_dict = match resources_dict.get(b"Font") {
247 Ok(f) => resolve_object(doc, f),
248 Err(_) => return cache,
249 };
250
251 let font_dict = match font_dict.as_dict() {
252 Ok(d) => d,
253 Err(_) => return cache,
254 };
255
256 for (name_bytes, font_ref) in font_dict.iter() {
258 let name = String::from_utf8_lossy(name_bytes).to_string();
259 let font_obj = resolve_object(doc, font_ref);
260
261 if let Ok(fd) = font_obj.as_dict() {
262 let pdf_font = resolve_font_dict(doc, &name, fd);
263 cache.insert(name, pdf_font);
264 }
265 }
266
267 cache
268}
269
270pub(crate) fn resolve_font_dict(
272 doc: &lopdf::Document,
273 name: &str,
274 dict: &lopdf::Dictionary,
275) -> PdfFont {
276 let subtype = dict
277 .get(b"Subtype")
278 .ok()
279 .and_then(|o| {
280 if let lopdf::Object::Name(n) = o {
281 Some(String::from_utf8_lossy(n).to_string())
282 } else {
283 None
284 }
285 })
286 .unwrap_or_else(|| "Type1".to_string());
287
288 let base_font_raw = dict.get(b"BaseFont").ok().and_then(|o| {
289 if let lopdf::Object::Name(n) = o {
290 Some(String::from_utf8_lossy(n).to_string())
291 } else {
292 None
293 }
294 });
295
296 let encoding = dict
297 .get(b"Encoding")
298 .ok()
299 .and_then(|o| {
300 let resolved = resolve_object(doc, o);
301 match resolved {
302 lopdf::Object::Name(n) => Some(String::from_utf8_lossy(&n).to_string()),
303 lopdf::Object::Dictionary(ref d) => d.get(b"BaseEncoding").ok().and_then(|be| {
304 if let lopdf::Object::Name(n) = be {
305 Some(String::from_utf8_lossy(n).to_string())
306 } else {
307 None
308 }
309 }),
310 _ => None,
311 }
312 })
313 .unwrap_or_else(|| "WinAnsiEncoding".to_string());
314
315 let is_type0 = subtype == "Type0";
316 let is_type3 = subtype == "Type3";
317 let bytes_per_code: u8 = if is_type0 { 2 } else { 1 };
318
319 let mut widths = resolve_widths(doc, dict);
320 let mut to_unicode = resolve_tounicode(doc, dict);
321
322 let mut flags = 0u32;
323 let mut italic_angle = 0.0f64;
324 let mut stem_v = 0.0f64;
325 let mut descriptor_font_weight: Option<f64> = None;
326 let mut descriptor_font_name: Option<String> = None;
327 let mut ascent = 800.0f64;
328 let mut descent = -200.0f64;
329 let mut font_bbox = [0.0f64, -200.0, 1000.0, 800.0];
330 let mut default_width = 1000.0f64;
331
332 if is_type0 {
333 if let Ok(desc_ref) = dict.get(b"DescendantFonts") {
334 let desc_obj = resolve_object(doc, desc_ref);
335 if let Ok(desc_arr) = desc_obj.as_array() {
336 if let Some(first) = desc_arr.first() {
337 let desc_font_obj = resolve_object(doc, first);
338 if let Ok(desc_dict) = desc_font_obj.as_dict() {
339 if let Ok(dw) = desc_dict.get(b"DW") {
340 if let Some(dw_val) = obj_to_f64(resolve_object(doc, dw)) {
341 default_width = dw_val;
342 }
343 }
344 resolve_cid_widths(doc, desc_dict, &mut widths);
345 let desc = resolve_font_descriptor(doc, desc_dict);
346 flags = desc.flags;
347 italic_angle = desc.italic_angle;
348 stem_v = desc.stem_v;
349 descriptor_font_weight = desc.font_weight;
350 descriptor_font_name = desc.font_name;
351 ascent = desc.ascent;
352 descent = desc.descent;
353 font_bbox = desc.font_bbox;
354
355 resolve_encoding_differences(doc, dict, &mut to_unicode);
358 resolve_cid_font_cmap_fallback(doc, desc_dict, &mut to_unicode);
359 }
360 }
361 }
362 }
363 } else {
364 resolve_encoding_differences(doc, dict, &mut to_unicode);
365 if subtype == "Type1" {
366 resolve_type1_font_program_encoding(doc, dict, &mut to_unicode);
367 }
368 let desc = resolve_font_descriptor(doc, dict);
369 flags = desc.flags;
370 italic_angle = desc.italic_angle;
371 stem_v = desc.stem_v;
372 descriptor_font_weight = desc.font_weight;
373 descriptor_font_name = desc.font_name;
374 ascent = desc.ascent;
375 descent = desc.descent;
376 font_bbox = desc.font_bbox;
377
378 if is_type3 {
380 if let Some(bbox) = read_number_array(doc, dict, b"FontBBox") {
381 if bbox.len() >= 4 {
382 font_bbox = [bbox[0], bbox[1], bbox[2], bbox[3]];
383 }
384 }
385 }
386 }
387
388 let base_font = crate::pdf::font_style::resolve_base_font_name(
389 base_font_raw.as_deref(),
390 descriptor_font_name.as_deref(),
391 );
392
393 let is_standard = is_standard_font(&base_font);
394 if is_standard {
395 default_width = standard_font_default_width(&base_font);
396 }
397
398 if is_type3 {
401 let matrix_vals = read_number_array(doc, dict, b"FontMatrix").unwrap_or_default();
402 let matrix = crate::pdf::font_type3::font_matrix_from_array(&matrix_vals);
403 let scale = matrix.em_scale();
404 crate::pdf::font_type3::normalize_widths(&mut widths, scale);
405 default_width *= scale;
406 if desc_missing_ascent_descent(doc, dict) {
407 let (a, d) = crate::pdf::font_type3::vertical_extent(font_bbox, matrix);
408 ascent = a;
409 descent = d;
410 }
411 font_bbox = crate::pdf::font_type3::normalize_font_bbox(font_bbox, matrix);
412 }
413
414 let weight =
415 crate::pdf::font_style::resolve_font_weight(&base_font, descriptor_font_weight, stem_v);
416
417 PdfFont {
418 name: name.to_string(),
419 base_font,
420 subtype,
421 widths,
422 default_width,
423 to_unicode,
424 encoding,
425 is_standard,
426 flags,
427 italic_angle,
428 weight,
429 bytes_per_code,
430 ascent,
431 descent,
432 font_bbox,
433 }
434}
435
436fn desc_missing_ascent_descent(doc: &lopdf::Document, dict: &lopdf::Dictionary) -> bool {
438 let Ok(fd_ref) = dict.get(b"FontDescriptor") else {
439 return true;
440 };
441 let fd_obj = resolve_object(doc, fd_ref);
442 let Ok(fd) = fd_obj.as_dict() else {
443 return true;
444 };
445 fd.get(b"Ascent").is_err() && fd.get(b"Descent").is_err()
446}
447
448fn read_number_array(
450 doc: &lopdf::Document,
451 dict: &lopdf::Dictionary,
452 key: &[u8],
453) -> Option<Vec<f64>> {
454 let obj = resolve_object(doc, dict.get(key).ok()?);
455 let arr = obj.as_array().ok()?;
456 let vals: Vec<f64> = arr
457 .iter()
458 .filter_map(|o| obj_to_f64(resolve_object(doc, o)))
459 .collect();
460 if vals.is_empty() {
461 None
462 } else {
463 Some(vals)
464 }
465}
466
467fn resolve_widths(doc: &lopdf::Document, dict: &lopdf::Dictionary) -> HashMap<u32, f64> {
469 let mut widths = HashMap::new();
470
471 let first_char = dict
472 .get(b"FirstChar")
473 .ok()
474 .and_then(|o| obj_to_i64(resolve_object(doc, o)))
475 .unwrap_or(0) as u32;
476
477 if let Ok(widths_ref) = dict.get(b"Widths") {
478 let widths_obj = resolve_object(doc, widths_ref);
479 if let Ok(arr) = widths_obj.as_array() {
480 for (i, w) in arr.iter().enumerate() {
481 if let Some(width) = obj_to_f64(resolve_object(doc, w)) {
482 widths.insert(first_char + i as u32, width);
483 }
484 }
485 }
486 }
487
488 widths
489}
490
491fn resolve_cid_widths(
498 doc: &lopdf::Document,
499 dict: &lopdf::Dictionary,
500 widths: &mut HashMap<u32, f64>,
501) {
502 let w_obj = match dict.get(b"W") {
503 Ok(o) => resolve_object(doc, o),
504 Err(_) => return,
505 };
506 let w_arr = match w_obj.as_array() {
507 Ok(a) => a,
508 Err(_) => return,
509 };
510
511 let mut i = 0;
512 while i < w_arr.len() {
513 let first = resolve_object(doc, &w_arr[i]);
514 if let Some(cid_start) = obj_to_i64(first) {
515 let cid_start = cid_start as u32;
516 i += 1;
517 if i >= w_arr.len() {
518 break;
519 }
520 let next = resolve_object(doc, &w_arr[i]);
521 if let Ok(arr) = next.as_array() {
522 for (j, w) in arr.iter().enumerate() {
524 if let Some(width) = obj_to_f64(resolve_object(doc, w)) {
525 widths.insert(cid_start + j as u32, width);
526 }
527 }
528 i += 1;
529 } else if let Some(cid_end) = obj_to_i64(next) {
530 let cid_end = cid_end as u32;
532 i += 1;
533 if i >= w_arr.len() {
534 break;
535 }
536 let w_val = resolve_object(doc, &w_arr[i]);
537 if let Some(width) = obj_to_f64(w_val) {
538 for cid in cid_start..=cid_end {
539 widths.insert(cid, width);
540 }
541 }
542 i += 1;
543 } else {
544 i += 1;
545 }
546 } else {
547 i += 1;
548 }
549 }
550}
551
552fn resolve_tounicode(doc: &lopdf::Document, dict: &lopdf::Dictionary) -> HashMap<u32, String> {
554 let mut mapping = HashMap::new();
555
556 if let Ok(tounicode_ref) = dict.get(b"ToUnicode") {
557 let tounicode_ref = match tounicode_ref {
558 lopdf::Object::Reference(r) => *r,
559 _ => return mapping,
560 };
561
562 if let Ok(stream) = doc.get_object(tounicode_ref) {
563 if let Ok(stream) = stream.as_stream() {
564 if let Ok(data) = stream.decompressed_content() {
565 parse_cmap(&data, &mut mapping);
566 }
567 }
568 }
569 }
570
571 mapping
572}
573
574fn resolve_encoding_differences(
579 doc: &lopdf::Document,
580 dict: &lopdf::Dictionary,
581 to_unicode: &mut HashMap<u32, String>,
582) {
583 let enc_obj = match dict.get(b"Encoding") {
584 Ok(o) => resolve_object(doc, o),
585 Err(_) => return,
586 };
587
588 let enc_dict = match enc_obj.as_dict() {
589 Ok(d) => d,
590 Err(_) => return, };
592
593 let diffs_obj = match enc_dict.get(b"Differences") {
594 Ok(o) => resolve_object(doc, o),
595 Err(_) => return,
596 };
597
598 let diffs = match diffs_obj.as_array() {
599 Ok(a) => a,
600 Err(_) => return,
601 };
602
603 let mut current_code: u32 = 0;
604 for item in diffs {
605 let resolved = resolve_object(doc, item);
606 match resolved {
607 lopdf::Object::Integer(i) => {
608 current_code = i as u32;
609 }
610 lopdf::Object::Name(ref name_bytes) => {
611 let glyph_name = String::from_utf8_lossy(name_bytes).to_string();
612 if let std::collections::hash_map::Entry::Vacant(e) = to_unicode.entry(current_code)
614 {
615 if let Some(unicode) = glyph_name_to_unicode(&glyph_name) {
616 e.insert(unicode);
617 }
618 }
619 current_code += 1;
620 }
621 _ => {}
622 }
623 }
624}
625
626fn resolve_type1_font_program_encoding(
632 doc: &lopdf::Document,
633 dict: &lopdf::Dictionary,
634 to_unicode: &mut HashMap<u32, String>,
635) {
636 let fd_obj = match dict.get(b"FontDescriptor") {
638 Ok(o) => resolve_object(doc, o),
639 Err(_) => return,
640 };
641 let fd = match fd_obj.as_dict() {
642 Ok(d) => d,
643 Err(_) => return,
644 };
645
646 let stream_data = None
648 .or_else(|| get_font_file_data(doc, fd, b"FontFile"))
649 .or_else(|| get_font_file_data(doc, fd, b"FontFile3"));
650
651 let data = match stream_data {
652 Some(d) => d,
653 None => return,
654 };
655
656 let text = String::from_utf8_lossy(&data);
659
660 if text.contains("/Encoding StandardEncoding def") {
662 return; }
664
665 for line in text.lines() {
666 let trimmed = line.trim();
667 if !trimmed.starts_with("dup ") || !trimmed.ends_with(" put") {
669 continue;
670 }
671 let inner = &trimmed[4..trimmed.len() - 4].trim();
672 let parts: Vec<&str> = inner.splitn(2, ' ').collect();
674 if parts.len() != 2 {
675 continue;
676 }
677 let code: u32 = match parts[0].trim().parse() {
678 Ok(c) => c,
679 Err(_) => continue,
680 };
681 let glyph = parts[1].trim().trim_start_matches('/');
682 if glyph.is_empty() || glyph == ".notdef" {
683 continue;
684 }
685 if let std::collections::hash_map::Entry::Vacant(e) = to_unicode.entry(code) {
687 if let Some(unicode) = glyph_name_to_unicode(glyph) {
688 e.insert(unicode);
689 }
690 }
691 }
692}
693
694fn get_font_file_data(
696 doc: &lopdf::Document,
697 fd: &lopdf::Dictionary,
698 key: &[u8],
699) -> Option<Vec<u8>> {
700 let ff_ref = fd.get(key).ok()?;
701 let ff_id = match ff_ref {
702 lopdf::Object::Reference(r) => *r,
703 _ => return None,
704 };
705 let ff_obj = doc.get_object(ff_id).ok()?;
706 let stream = ff_obj.as_stream().ok()?;
707 stream.decompressed_content().ok()
708}
709
710fn resolve_cid_font_cmap_fallback(
715 doc: &lopdf::Document,
716 cid_font_dict: &lopdf::Dictionary,
717 to_unicode: &mut HashMap<u32, String>,
718) {
719 let fd_obj = match cid_font_dict.get(b"FontDescriptor") {
721 Ok(o) => resolve_object(doc, o),
722 Err(_) => return,
723 };
724 let fd = match fd_obj.as_dict() {
725 Ok(d) => d,
726 Err(_) => return,
727 };
728
729 let data = None
730 .or_else(|| get_font_file_data(doc, fd, b"FontFile2"))
731 .or_else(|| get_font_file_data(doc, fd, b"FontFile3"));
732 let Some(font_bytes) = data else {
733 return;
734 };
735
736 let cid_to_gid = resolve_cid_to_gid_map(doc, cid_font_dict);
738
739 let Ok(face) = ttf_parser::Face::parse(&font_bytes, 0) else {
740 return;
741 };
742
743 let mut gid_to_unicode: HashMap<u16, String> = HashMap::new();
745 if let Some(subtable) = face
746 .tables()
747 .cmap
748 .and_then(|c| c.subtables.into_iter().find(|s| s.is_unicode()))
749 {
750 subtable.codepoints(|cp| {
751 if let Some(gid) = subtable.glyph_index(cp) {
752 if let Some(ch) = char::from_u32(cp) {
753 gid_to_unicode
754 .entry(gid.0)
755 .or_insert_with(|| ch.to_string());
756 }
757 }
758 });
759 }
760
761 if gid_to_unicode.is_empty() {
762 return;
763 }
764
765 let max_cid = cid_to_gid
766 .as_ref()
767 .map(|m| m.len() as u32)
768 .unwrap_or_else(|| face.number_of_glyphs() as u32);
769
770 for cid in 0..max_cid {
771 if to_unicode.contains_key(&cid) {
772 continue;
773 }
774 let gid = match &cid_to_gid {
775 Some(map) => map.get(cid as usize).copied().unwrap_or(cid as u16),
776 None => cid as u16,
777 };
778 if let Some(uni) = gid_to_unicode.get(&gid) {
779 to_unicode.insert(cid, uni.clone());
780 }
781 }
782}
783
784fn resolve_cid_to_gid_map(doc: &lopdf::Document, cid_font_dict: &lopdf::Dictionary) -> Option<Vec<u16>> {
786 let obj = cid_font_dict.get(b"CIDToGIDMap").ok()?;
787 let resolved = resolve_object(doc, obj);
788 match resolved {
789 lopdf::Object::Name(n) if n.as_slice() == b"Identity" => None,
790 lopdf::Object::Stream(stream) => {
791 let data = stream.decompressed_content().ok()?;
792 if data.len() < 2 {
793 return None;
794 }
795 let mut map = Vec::with_capacity(data.len() / 2);
796 for chunk in data.chunks_exact(2) {
797 map.push(u16::from_be_bytes([chunk[0], chunk[1]]));
798 }
799 Some(map)
800 }
801 _ => None,
802 }
803}
804
805fn glyph_name_to_unicode(name: &str) -> Option<String> {
811 match name {
813 "fi" => return Some("fi".to_string()),
814 "fl" => return Some("fl".to_string()),
815 "ff" => return Some("ff".to_string()),
816 "ffi" => return Some("ffi".to_string()),
817 "ffl" => return Some("ffl".to_string()),
818 "IJ" => return Some("IJ".to_string()),
819 "ij" => return Some("ij".to_string()),
820 _ => {}
821 }
822
823 if let Some(s) = resolve_glyph_component(name) {
825 return Some(s);
826 }
827
828 let base = if let Some(dot_pos) = name.find('.') {
831 &name[..dot_pos]
832 } else {
833 name
834 };
835
836 if base != name {
837 if let Some(s) = resolve_glyph_component(base) {
838 return Some(s);
839 }
840 }
841
842 if base.contains('_') {
845 let mut result = String::new();
846 for component in base.split('_') {
847 if let Some(s) = resolve_glyph_component(component) {
848 result.push_str(&s);
849 } else {
850 return None; }
852 }
853 if !result.is_empty() {
854 return Some(result);
855 }
856 }
857
858 None
859}
860
861fn resolve_glyph_component(name: &str) -> Option<String> {
863 if name.is_empty() {
864 return None;
865 }
866
867 if let Some(s) = AGL_MAP.get(name) {
869 return Some(s.clone());
870 }
871
872 if let Some(s) = TEX_GLYPH_MAP.get(name) {
874 return Some((*s).to_string());
875 }
876
877 if name.len() == 1 {
879 return Some(name.to_string());
880 }
881
882 if let Some(hex) = name.strip_prefix("uni") {
884 if hex.len() == 4 {
885 if let Ok(cp) = u32::from_str_radix(hex, 16) {
886 if let Some(c) = char::from_u32(cp) {
887 return Some(c.to_string());
888 }
889 }
890 }
891 if hex.len() > 4 && hex.len() % 4 == 0 {
893 let mut s = String::new();
894 for chunk in hex.as_bytes().chunks(4) {
895 if let Ok(h) = std::str::from_utf8(chunk) {
896 if let Ok(cp) = u32::from_str_radix(h, 16) {
897 if let Some(c) = char::from_u32(cp) {
898 s.push(c);
899 }
900 }
901 }
902 }
903 if !s.is_empty() {
904 return Some(s);
905 }
906 }
907 }
908
909 if let Some(hex) = name.strip_prefix('u') {
911 if (4..=6).contains(&hex.len()) && hex.chars().all(|c| c.is_ascii_hexdigit()) {
912 if let Ok(cp) = u32::from_str_radix(hex, 16) {
913 if let Some(c) = char::from_u32(cp) {
914 return Some(c.to_string());
915 }
916 }
917 }
918 }
919
920 None
921}
922
923fn parse_cmap(data: &[u8], mapping: &mut HashMap<u32, String>) {
925 let text = String::from_utf8_lossy(data);
926
927 let mut in_bfchar = false;
929 for line in text.lines() {
930 let trimmed = line.trim();
931 if trimmed.contains("beginbfchar") {
932 in_bfchar = true;
933 continue;
934 }
935 if trimmed.contains("endbfchar") {
936 in_bfchar = false;
937 continue;
938 }
939 if in_bfchar {
940 let parts: Vec<&str> = trimmed.split('>').collect();
942 if parts.len() >= 2 {
943 if let (Some(src), Some(dst)) =
944 (parse_hex_value(parts[0]), parse_hex_unicode(parts[1]))
945 {
946 mapping.insert(src, dst);
947 }
948 }
949 }
950 }
951
952 let mut in_bfrange = false;
957 for line in text.lines() {
958 let trimmed = line.trim();
959 if trimmed.contains("beginbfrange") {
960 in_bfrange = true;
961 continue;
962 }
963 if trimmed.contains("endbfrange") {
964 in_bfrange = false;
965 continue;
966 }
967 if in_bfrange {
968 if let Some(bracket_start) = trimmed.find('[') {
970 let before_bracket = &trimmed[..bracket_start];
972 let parts: Vec<&str> = before_bracket.split('>').collect();
973 if parts.len() >= 2 {
974 if let (Some(start), Some(end)) =
975 (parse_hex_value(parts[0]), parse_hex_value(parts[1]))
976 {
977 let bracket_end = trimmed.rfind(']').unwrap_or(trimmed.len());
979 let inside = &trimmed[bracket_start + 1..bracket_end];
980 let values: Vec<String> = inside
981 .split('>')
982 .filter_map(|s| {
983 let s = s.trim().trim_start_matches('<');
984 if s.is_empty() {
985 None
986 } else {
987 parse_hex_unicode_str(s)
988 }
989 })
990 .collect();
991 for (i, val) in values.iter().enumerate() {
992 let code = start + i as u32;
993 if code > end {
994 break;
995 }
996 mapping.insert(code, val.clone());
997 }
998 }
999 }
1000 } else {
1001 let parts: Vec<&str> = trimmed.split('>').collect();
1003 if parts.len() >= 3 {
1004 if let (Some(start), Some(end), Some(dst_start)) = (
1005 parse_hex_value(parts[0]),
1006 parse_hex_value(parts[1]),
1007 parse_hex_value(parts[2]),
1008 ) {
1009 for code in start..=end {
1010 let unicode_point = dst_start + (code - start);
1011 if let Some(c) = char::from_u32(unicode_point) {
1012 mapping.insert(code, c.to_string());
1013 }
1014 }
1015 }
1016 }
1017 }
1018 }
1019 }
1020}
1021
1022fn parse_hex_value(s: &str) -> Option<u32> {
1024 let cleaned = s.trim().trim_start_matches('<').trim();
1025 if cleaned.is_empty() {
1026 return None;
1027 }
1028 u32::from_str_radix(cleaned, 16).ok()
1029}
1030
1031fn parse_hex_unicode(s: &str) -> Option<String> {
1033 let cleaned = s
1034 .trim()
1035 .trim_start_matches('<')
1036 .trim_end_matches('>')
1037 .trim();
1038 parse_hex_unicode_str(cleaned)
1039}
1040
1041fn parse_hex_unicode_str(cleaned: &str) -> Option<String> {
1043 if cleaned.is_empty() {
1044 return None;
1045 }
1046
1047 let mut result = String::new();
1049 let bytes: Vec<&str> = cleaned
1050 .as_bytes()
1051 .chunks(4)
1052 .map(|c| std::str::from_utf8(c).unwrap_or(""))
1053 .collect();
1054
1055 for hex_str in bytes {
1056 if let Ok(code_point) = u32::from_str_radix(hex_str, 16) {
1057 if let Some(c) = char::from_u32(code_point) {
1058 result.push(c);
1059 }
1060 }
1061 }
1062
1063 if result.is_empty() {
1064 None
1065 } else {
1066 Some(result)
1067 }
1068}
1069
1070struct FontDescriptorFields {
1072 flags: u32,
1073 italic_angle: f64,
1074 stem_v: f64,
1075 font_weight: Option<f64>,
1076 font_name: Option<String>,
1077 ascent: f64,
1078 descent: f64,
1079 font_bbox: [f64; 4],
1080}
1081
1082fn resolve_font_descriptor(
1084 doc: &lopdf::Document,
1085 dict: &lopdf::Dictionary,
1086) -> FontDescriptorFields {
1087 let mut fields = FontDescriptorFields {
1088 flags: 0,
1089 italic_angle: 0.0,
1090 stem_v: 0.0,
1091 font_weight: None,
1092 font_name: None,
1093 ascent: 800.0,
1094 descent: -200.0,
1095 font_bbox: [0.0, -200.0, 1000.0, 800.0],
1096 };
1097
1098 if let Ok(fd_ref) = dict.get(b"FontDescriptor") {
1099 let fd_obj = resolve_object(doc, fd_ref);
1100 if let Ok(fd) = fd_obj.as_dict() {
1101 fields.flags = fd
1102 .get(b"Flags")
1103 .ok()
1104 .and_then(|o| obj_to_i64(resolve_object(doc, o)))
1105 .unwrap_or(0) as u32;
1106
1107 fields.italic_angle = fd
1108 .get(b"ItalicAngle")
1109 .ok()
1110 .and_then(|o| obj_to_f64(resolve_object(doc, o)))
1111 .unwrap_or(0.0);
1112
1113 fields.stem_v = fd
1114 .get(b"StemV")
1115 .ok()
1116 .and_then(|o| obj_to_f64(resolve_object(doc, o)))
1117 .unwrap_or(0.0);
1118
1119 fields.font_weight = fd
1120 .get(b"FontWeight")
1121 .ok()
1122 .and_then(|o| obj_to_f64(resolve_object(doc, o)));
1123
1124 fields.font_name = fd.get(b"FontName").ok().and_then(|o| {
1125 let resolved = resolve_object(doc, o);
1126 if let lopdf::Object::Name(n) = resolved {
1127 Some(String::from_utf8_lossy(&n).to_string())
1128 } else {
1129 None
1130 }
1131 });
1132
1133 if let Some(vals) = read_number_array(doc, fd, b"FontBBox") {
1134 if vals.len() >= 4 {
1135 fields.font_bbox = [vals[0], vals[1], vals[2], vals[3]];
1136 }
1137 }
1138
1139 if let Ok(a_ref) = fd.get(b"Ascent") {
1140 if let Some(a) = obj_to_f64(resolve_object(doc, a_ref)) {
1141 fields.ascent = a;
1142 }
1143 } else {
1144 fields.ascent = fields.font_bbox[3];
1145 }
1146
1147 if let Ok(d_ref) = fd.get(b"Descent") {
1148 if let Some(d) = obj_to_f64(resolve_object(doc, d_ref)) {
1149 fields.descent = d;
1150 }
1151 } else {
1152 fields.descent = fields.font_bbox[1];
1153 }
1154 }
1155 }
1156
1157 fields
1158}
1159
1160fn resolve_object<'a>(doc: &'a lopdf::Document, obj: &'a lopdf::Object) -> lopdf::Object {
1162 match obj {
1163 lopdf::Object::Reference(id) => doc.get_object(*id).cloned().unwrap_or(lopdf::Object::Null),
1164 other => other.clone(),
1165 }
1166}
1167
1168fn obj_to_f64(obj: lopdf::Object) -> Option<f64> {
1170 match obj {
1171 lopdf::Object::Integer(i) => Some(i as f64),
1172 lopdf::Object::Real(f) => Some(f),
1173 _ => None,
1174 }
1175}
1176
1177fn obj_to_i64(obj: lopdf::Object) -> Option<i64> {
1179 match obj {
1180 lopdf::Object::Integer(i) => Some(i),
1181 lopdf::Object::Real(f) => Some(f as i64),
1182 _ => None,
1183 }
1184}
1185
1186fn is_standard_font(name: &str) -> bool {
1188 matches!(
1189 name,
1190 "Courier"
1191 | "Courier-Bold"
1192 | "Courier-Oblique"
1193 | "Courier-BoldOblique"
1194 | "Helvetica"
1195 | "Helvetica-Bold"
1196 | "Helvetica-Oblique"
1197 | "Helvetica-BoldOblique"
1198 | "Times-Roman"
1199 | "Times-Bold"
1200 | "Times-Italic"
1201 | "Times-BoldItalic"
1202 | "Symbol"
1203 | "ZapfDingbats"
1204 )
1205}
1206
1207fn standard_font_default_width(name: &str) -> f64 {
1209 if name.starts_with("Courier") {
1210 600.0
1211 } else {
1212 500.0
1214 }
1215}
1216
1217fn decode_macroman(code: u32) -> String {
1222 if code < 128 {
1223 if let Some(c) = char::from_u32(code) {
1224 return c.to_string();
1225 }
1226 }
1227 let mapped = match code {
1228 0x80 => '\u{00C4}', 0x81 => '\u{00C5}', 0x82 => '\u{00C7}', 0x83 => '\u{00C9}', 0x84 => '\u{00D1}', 0x85 => '\u{00D6}', 0x86 => '\u{00DC}', 0x87 => '\u{00E1}', 0x88 => '\u{00E0}', 0x89 => '\u{00E2}', 0x8A => '\u{00E4}', 0x8B => '\u{00E3}', 0x8C => '\u{00E5}', 0x8D => '\u{00E7}', 0x8E => '\u{00E9}', 0x8F => '\u{00E8}', 0x90 => '\u{00EA}', 0x91 => '\u{00EB}', 0x92 => '\u{00ED}', 0x93 => '\u{00EC}', 0x94 => '\u{00EE}', 0x95 => '\u{00EF}', 0x96 => '\u{00F1}', 0x97 => '\u{00F3}', 0x98 => '\u{00F2}', 0x99 => '\u{00F4}', 0x9A => '\u{00F6}', 0x9B => '\u{00F5}', 0x9C => '\u{00FA}', 0x9D => '\u{00F9}', 0x9E => '\u{00FB}', 0x9F => '\u{00FC}', 0xA0 => '\u{2020}', 0xA1 => '\u{00B0}', 0xA2 => '\u{00A2}', 0xA3 => '\u{00A3}', 0xA4 => '\u{00A7}', 0xA5 => '\u{2022}', 0xA6 => '\u{00B6}', 0xA7 => '\u{00DF}', 0xA8 => '\u{00AE}', 0xA9 => '\u{00A9}', 0xAA => '\u{2122}', 0xAB => '\u{00B4}', 0xAC => '\u{00A8}', 0xAD => '\u{2260}', 0xAE => '\u{00C6}', 0xAF => '\u{00D8}', 0xB0 => '\u{221E}', 0xB1 => '\u{00B1}', 0xB2 => '\u{2264}', 0xB3 => '\u{2265}', 0xB4 => '\u{00A5}', 0xB5 => '\u{00B5}', 0xB6 => '\u{2202}', 0xB7 => '\u{2211}', 0xB8 => '\u{220F}', 0xB9 => '\u{03C0}', 0xBA => '\u{222B}', 0xBB => '\u{00AA}', 0xBC => '\u{00BA}', 0xBD => '\u{03A9}', 0xBE => '\u{00E6}', 0xBF => '\u{00F8}', 0xC0 => '\u{00BF}', 0xC1 => '\u{00A1}', 0xC2 => '\u{00AC}', 0xC3 => '\u{221A}', 0xC4 => '\u{0192}', 0xC5 => '\u{2248}', 0xC6 => '\u{2206}', 0xC7 => '\u{00AB}', 0xC8 => '\u{00BB}', 0xC9 => '\u{2026}', 0xCA => '\u{00A0}', 0xCB => '\u{00C0}', 0xCC => '\u{00C3}', 0xCD => '\u{00D5}', 0xCE => '\u{0152}', 0xCF => '\u{0153}', 0xD0 => '\u{2013}', 0xD1 => '\u{2014}', 0xD2 => '\u{201C}', 0xD3 => '\u{201D}', 0xD4 => '\u{2018}', 0xD5 => '\u{2019}', 0xD6 => '\u{00F7}', 0xD7 => '\u{25CA}', 0xD8 => '\u{00FF}', 0xD9 => '\u{0178}', 0xDA => '\u{2044}', 0xDB => '\u{20AC}', 0xDC => '\u{2039}', 0xDD => '\u{203A}', 0xDE => '\u{FB01}', 0xDF => '\u{FB02}', 0xE0 => '\u{2021}', 0xE1 => '\u{00B7}', 0xE2 => '\u{201A}', 0xE3 => '\u{201E}', 0xE4 => '\u{2030}', 0xE5 => '\u{00C2}', 0xE6 => '\u{00CA}', 0xE7 => '\u{00C1}', 0xE8 => '\u{00CB}', 0xE9 => '\u{00C8}', 0xEA => '\u{00CD}', 0xEB => '\u{00CE}', 0xEC => '\u{00CF}', 0xED => '\u{00CC}', 0xEE => '\u{00D3}', 0xEF => '\u{00D4}', 0xF0 => '\u{F8FF}', 0xF1 => '\u{00D2}', 0xF2 => '\u{00DA}', 0xF3 => '\u{00DB}', 0xF4 => '\u{00D9}', 0xF5 => '\u{0131}', 0xF6 => '\u{02C6}', 0xF7 => '\u{02DC}', 0xF8 => '\u{00AF}', 0xF9 => '\u{02D8}', 0xFA => '\u{02D9}', 0xFB => '\u{02DA}', 0xFC => '\u{00B8}', 0xFD => '\u{02DD}', 0xFE => '\u{02DB}', 0xFF => '\u{02C7}', _ => {
1357 return char::from_u32(code)
1358 .map(|c| c.to_string())
1359 .unwrap_or_default();
1360 }
1361 };
1362 mapped.to_string()
1363}
1364
1365fn decode_winansi(code: u32) -> String {
1367 if code < 128 {
1368 if let Some(c) = char::from_u32(code) {
1370 return c.to_string();
1371 }
1372 }
1373 let mapped = match code {
1375 0x80 => '\u{20AC}', 0x82 => '\u{201A}', 0x83 => '\u{0192}', 0x84 => '\u{201E}', 0x85 => '\u{2026}', 0x86 => '\u{2020}', 0x87 => '\u{2021}', 0x88 => '\u{02C6}', 0x89 => '\u{2030}', 0x8A => '\u{0160}', 0x8B => '\u{2039}', 0x8C => '\u{0152}', 0x8E => '\u{017D}', 0x91 => '\u{2018}', 0x92 => '\u{2019}', 0x93 => '\u{201C}', 0x94 => '\u{201D}', 0x95 => '\u{2022}', 0x96 => '\u{2013}', 0x97 => '\u{2014}', 0x98 => '\u{02DC}', 0x99 => '\u{2122}', 0x9A => '\u{0161}', 0x9B => '\u{203A}', 0x9C => '\u{0153}', 0x9E => '\u{017E}', 0x9F => '\u{0178}', c @ 0xA0..=0xFF => {
1404 return char::from_u32(c)
1405 .map(|ch| ch.to_string())
1406 .unwrap_or_default();
1407 }
1408 _ => {
1409 return char::from_u32(code)
1410 .map(|c| c.to_string())
1411 .unwrap_or_default();
1412 }
1413 };
1414 mapped.to_string()
1415}
1416
1417#[cfg(test)]
1418mod tests {
1419 use super::*;
1420
1421 #[test]
1422 fn test_default_font() {
1423 let font = PdfFont::default_font("F1");
1424 assert_eq!(font.name, "F1");
1425 assert!((font.default_width - 600.0).abs() < 1e-10);
1426 }
1427
1428 #[test]
1429 fn test_bold_detection() {
1430 let mut font = PdfFont::default_font("F1");
1431 font.base_font = "Helvetica-Bold".to_string();
1432 assert!(font.is_bold());
1433
1434 font.base_font = "Helvetica".to_string();
1435 assert!(!font.is_bold());
1436 }
1437
1438 #[test]
1439 fn test_decode_winansi() {
1440 assert_eq!(decode_winansi(65), "A");
1441 assert_eq!(decode_winansi(0x93), "\u{201C}");
1442 }
1443
1444 #[test]
1445 fn test_parse_hex_value() {
1446 assert_eq!(parse_hex_value("<0041"), Some(0x41));
1447 assert_eq!(parse_hex_value("<00FF"), Some(0xFF));
1448 }
1449
1450 #[test]
1451 fn test_standard_font() {
1452 assert!(is_standard_font("Helvetica"));
1453 assert!(is_standard_font("Courier-Bold"));
1454 assert!(!is_standard_font("ArialMT"));
1455 }
1456
1457 #[test]
1458 fn type3_skia_widths_normalized_to_per_mille() {
1459 use lopdf::{dictionary, Document, Object, Stream};
1460
1461 let mut doc = Document::with_version("1.5");
1462 let proc_id = doc.add_object(Stream::new(dictionary! {}, Vec::new()));
1464 let fd_id = doc.add_object(dictionary! {
1465 "Type" => "FontDescriptor",
1466 "FontName" => "AAAA+.SFNS-Regular_wght2580000",
1467 "FontWeight" => 600,
1468 "StemV" => 262,
1469 "Flags" => 4,
1470 "ItalicAngle" => 0,
1471 });
1472 let font_id = doc.add_object(dictionary! {
1473 "Type" => "Font",
1474 "Subtype" => "Type3",
1475 "FontMatrix" => vec![
1476 Object::Real(1.0 / 2048.0),
1477 Object::Integer(0),
1478 Object::Integer(0),
1479 Object::Real(-1.0 / 2048.0),
1480 Object::Integer(0),
1481 Object::Integer(0),
1482 ],
1483 "FirstChar" => 0,
1484 "LastChar" => 3,
1485 "Widths" => vec![
1486 Object::Integer(2048), Object::Real(544.00195), Object::Integer(1334), Object::Integer(0),
1490 ],
1491 "FontBBox" => vec![91.into(), 495.into(), 1949.into(), (-1951).into()],
1492 "FontDescriptor" => fd_id,
1493 "CharProcs" => dictionary! { "g0" => proc_id },
1494 "Encoding" => dictionary! {
1495 "Type" => "Encoding",
1496 "Differences" => vec![Object::Integer(0), Object::Name(b"g0".to_vec())],
1497 },
1498 });
1499
1500 let font_obj = doc.get_object(font_id).unwrap().as_dict().unwrap();
1501 let font = resolve_font_dict(&doc, "F4", font_obj);
1503
1504 assert_eq!(font.subtype, "Type3");
1505 assert!(
1506 font.base_font.contains("SFNS"),
1507 "expected descriptor FontName fallback, got {}",
1508 font.base_font
1509 );
1510 assert!(
1511 (font.weight - 600.0).abs() < 1e-6,
1512 "expected FontWeight=600 over StemV, got {}",
1513 font.weight
1514 );
1515 let w0 = font.glyph_width(0);
1516 assert!(
1517 (w0 - 1000.0).abs() < 1.0,
1518 "2048 glyph-space units → 1000 per-mille, got {w0}"
1519 );
1520 let w1 = font.glyph_width(1);
1521 let expected_space = 544.00195 * 1000.0 / 2048.0;
1522 assert!(
1523 (w1 - expected_space).abs() < 0.1,
1524 "space width got {w1}, expected ~{expected_space}"
1525 );
1526 }
1527
1528 #[test]
1530 fn type3_skia_pdf_preserves_word_space() {
1531 use crate::api::config::ProcessingConfig;
1532 use crate::convert_bytes;
1533 use crate::output::markdown::to_markdown;
1534 use lopdf::content::{Content, Operation};
1535 use lopdf::{dictionary, Document, Object, Stream};
1536
1537 let mut doc = Document::with_version("1.5");
1538 let pages_id = doc.new_object_id();
1539 let proc_id = doc.add_object(Stream::new(dictionary! {}, Vec::new()));
1540
1541 let fd_id = doc.add_object(dictionary! {
1542 "Type" => "FontDescriptor",
1543 "FontName" => "AAAA+.SFNS-Regular",
1544 "FontWeight" => 400,
1545 "StemV" => 80,
1546 "Flags" => 32,
1547 });
1548
1549 let widths: Vec<Object> = [1200, 500, 544, 600, 700, 650, 550, 650]
1552 .into_iter()
1553 .map(Object::Integer)
1554 .collect();
1555
1556 let font_id = doc.add_object(dictionary! {
1557 "Type" => "Font",
1558 "Subtype" => "Type3",
1559 "FontMatrix" => vec![
1560 Object::Real(1.0 / 2048.0),
1561 0.into(),
1562 0.into(),
1563 Object::Real(-1.0 / 2048.0),
1564 0.into(),
1565 0.into(),
1566 ],
1567 "FirstChar" => 0,
1568 "LastChar" => 7,
1569 "Widths" => widths,
1570 "FontBBox" => vec![0.into(), 0.into(), 2048.into(), (-2048).into()],
1571 "FontDescriptor" => fd_id,
1572 "CharProcs" => dictionary! { "g0" => proc_id },
1573 "Encoding" => dictionary! {
1575 "Type" => "Encoding",
1576 "Differences" => vec![
1577 Object::Integer(0),
1578 Object::Name(b"H".to_vec()),
1579 Object::Name(b"i".to_vec()),
1580 Object::Name(b"space".to_vec()),
1581 Object::Name(b"t".to_vec()),
1582 Object::Name(b"h".to_vec()),
1583 Object::Name(b"e".to_vec()),
1584 Object::Name(b"r".to_vec()),
1585 Object::Name(b"e".to_vec()),
1586 ],
1587 },
1588 });
1589
1590 let resources_id = doc.add_object(dictionary! {
1591 "Font" => dictionary! { "F1" => font_id },
1592 });
1593
1594 let mut ops = vec![
1598 Operation::new("BT", vec![]),
1599 Operation::new("Tf", vec!["F1".into(), Object::Real(24.0)]),
1600 Operation::new("Td", vec![72.into(), 700.into()]),
1601 ];
1602 for code in [0u8, 1, 2, 3, 4, 5, 6, 7] {
1603 ops.push(Operation::new(
1604 "Tj",
1605 vec![Object::String(vec![code], lopdf::StringFormat::Hexadecimal)],
1606 ));
1607 }
1608 ops.push(Operation::new("ET", vec![]));
1609
1610 let content_id = doc.add_object(Stream::new(
1611 dictionary! {},
1612 Content { operations: ops }.encode().unwrap(),
1613 ));
1614 let page_id = doc.add_object(dictionary! {
1615 "Type" => "Page",
1616 "Parent" => pages_id,
1617 "Contents" => content_id,
1618 "Resources" => resources_id,
1619 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
1620 });
1621 doc.objects.insert(
1622 pages_id,
1623 Object::Dictionary(dictionary! {
1624 "Type" => "Pages",
1625 "Kids" => vec![page_id.into()],
1626 "Count" => 1,
1627 }),
1628 );
1629 let catalog_id = doc.add_object(dictionary! {
1630 "Type" => "Catalog",
1631 "Pages" => pages_id,
1632 });
1633 doc.trailer.set("Root", catalog_id);
1634
1635 let mut bytes = Vec::new();
1636 doc.save_to(&mut bytes).unwrap();
1637
1638 let mut config = ProcessingConfig::default();
1639 config.raster_table_ocr = false;
1640 config.image_output = crate::api::config::ImageOutput::Off;
1641 let parsed = convert_bytes(&bytes, "skia-type3.pdf", &config).unwrap();
1642 let md = to_markdown(&parsed).unwrap();
1643 assert!(
1644 md.contains("Hi there"),
1645 "Type3 width normalization must preserve word space; got: {md:?}"
1646 );
1647 }
1648
1649 #[test]
1650 fn identity_h_ascii_cid_fallback_without_tounicode() {
1651 use lopdf::{dictionary, Document, Object};
1653
1654 let mut doc = Document::with_version("1.5");
1655 let desc_id = doc.add_object(dictionary! {
1656 "Type" => "Font",
1657 "Subtype" => "CIDFontType2",
1658 "BaseFont" => "Dummy",
1659 "CIDSystemInfo" => dictionary! {
1660 "Registry" => Object::string_literal("Adobe"),
1661 "Ordering" => Object::string_literal("Identity"),
1662 "Supplement" => 0,
1663 },
1664 "DW" => 1000,
1665 });
1666 let font_id = doc.add_object(dictionary! {
1667 "Type" => "Font",
1668 "Subtype" => "Type0",
1669 "BaseFont" => "Dummy",
1670 "Encoding" => "Identity-H",
1671 "DescendantFonts" => vec![desc_id.into()],
1672 });
1673 let font_obj = doc.get_object(font_id).unwrap().as_dict().unwrap().clone();
1674 let font = resolve_font_dict(&doc, "F1", &font_obj);
1675 assert_eq!(font.bytes_per_code, 2);
1676 assert_eq!(font.decode_char(0x0041), "A");
1677 assert_eq!(font.decode_char(0x0020), " ");
1678 assert_eq!(font.decode_char(0x1234), "\u{FFFD}");
1680 }
1681
1682 #[test]
1684 fn type3_real_skia_pdf_word_ratio_env_gated() {
1685 let path = match std::env::var("EDGEPARSE_SKIA_PDF") {
1686 Ok(p) => std::path::PathBuf::from(p),
1687 Err(_) => return, };
1689 if !path.exists() {
1690 return;
1691 }
1692 use crate::api::config::ProcessingConfig;
1693 use crate::convert;
1694 use crate::output::markdown::to_markdown;
1695
1696 let mut config = ProcessingConfig::default();
1697 config.raster_table_ocr = false;
1698 config.image_output = crate::api::config::ImageOutput::Off;
1699 let doc = convert(&path, &config).unwrap();
1700 let md = to_markdown(&doc).unwrap();
1701 let words = md.split_whitespace().count();
1702 assert!(
1703 words > 2000,
1704 "expected dense word-spaced markdown, got {words} words"
1705 );
1706 assert!(
1707 !md.contains("W e ") && !md.contains("W ikipedia"),
1708 "stray W fragments still present"
1709 );
1710 }
1711
1712 #[test]
1713 fn test_glyph_name_to_unicode_ligatures() {
1714 assert_eq!(glyph_name_to_unicode("fi"), Some("fi".to_string()));
1715 assert_eq!(glyph_name_to_unicode("fl"), Some("fl".to_string()));
1716 assert_eq!(glyph_name_to_unicode("ff"), Some("ff".to_string()));
1717 assert_eq!(glyph_name_to_unicode("ffi"), Some("ffi".to_string()));
1718 assert_eq!(glyph_name_to_unicode("ffl"), Some("ffl".to_string()));
1719 }
1720
1721 #[test]
1722 fn test_glyph_name_to_unicode_common() {
1723 assert_eq!(glyph_name_to_unicode("percent"), Some("%".to_string()));
1724 assert_eq!(glyph_name_to_unicode("ampersand"), Some("&".to_string()));
1725 assert_eq!(glyph_name_to_unicode("parenleft"), Some("(".to_string()));
1726 assert_eq!(
1727 glyph_name_to_unicode("endash"),
1728 Some("\u{2013}".to_string())
1729 );
1730 assert_eq!(glyph_name_to_unicode("A"), Some("A".to_string()));
1731 assert_eq!(glyph_name_to_unicode("uni0041"), Some("A".to_string()));
1732 }
1733
1734 #[test]
1735 fn test_glyph_name_to_unicode_unknown() {
1736 assert_eq!(glyph_name_to_unicode("nonexistent_glyph_xyz"), None);
1737 }
1738
1739 #[test]
1740 fn test_glyph_name_to_unicode_agl_extended() {
1741 assert_eq!(
1743 glyph_name_to_unicode("Dcroat"),
1744 Some("\u{0110}".to_string())
1745 );
1746 assert_eq!(
1747 glyph_name_to_unicode("dcroat"),
1748 Some("\u{0111}".to_string())
1749 );
1750 assert_eq!(
1751 glyph_name_to_unicode("Emacron"),
1752 Some("\u{0112}".to_string())
1753 );
1754 assert_eq!(
1755 glyph_name_to_unicode("afii10017"),
1756 Some("\u{0410}".to_string())
1757 ); assert_eq!(
1759 glyph_name_to_unicode("afii57636"),
1760 Some("\u{20AA}".to_string())
1761 ); assert_eq!(
1764 glyph_name_to_unicode("dalethatafpatah"),
1765 Some("\u{05D3}\u{05B2}".to_string())
1766 );
1767 }
1768
1769 #[test]
1770 fn test_glyph_name_to_unicode_uni_formats() {
1771 assert_eq!(glyph_name_to_unicode("uni0041"), Some("A".to_string()));
1773 assert_eq!(glyph_name_to_unicode("uni00E9"), Some("é".to_string()));
1774 assert_eq!(glyph_name_to_unicode("uni00410042"), Some("AB".to_string()));
1776 assert_eq!(
1778 glyph_name_to_unicode("u1F600"),
1779 Some("\u{1F600}".to_string())
1780 );
1781 }
1782
1783 #[test]
1784 fn test_parse_cmap_bfrange_array() {
1785 let cmap_data = b"beginbfrange\n<0001> <0003> [<0041> <0042> <0043>]\nendbfrange\n";
1787 let mut mapping = HashMap::new();
1788 parse_cmap(cmap_data, &mut mapping);
1789 assert_eq!(mapping.get(&1), Some(&"A".to_string()));
1790 assert_eq!(mapping.get(&2), Some(&"B".to_string()));
1791 assert_eq!(mapping.get(&3), Some(&"C".to_string()));
1792 }
1793
1794 #[test]
1795 fn test_parse_cmap_bfrange_incremented() {
1796 let cmap_data = b"beginbfrange\n<0041> <0043> <0061>\nendbfrange\n";
1798 let mut mapping = HashMap::new();
1799 parse_cmap(cmap_data, &mut mapping);
1800 assert_eq!(mapping.get(&0x41), Some(&"a".to_string()));
1801 assert_eq!(mapping.get(&0x42), Some(&"b".to_string()));
1802 assert_eq!(mapping.get(&0x43), Some(&"c".to_string()));
1803 }
1804
1805 #[test]
1806 fn test_decode_winansi_extended() {
1807 assert_eq!(decode_winansi(0x82), "\u{201A}"); assert_eq!(decode_winansi(0x83), "\u{0192}"); assert_eq!(decode_winansi(0x8A), "\u{0160}"); assert_eq!(decode_winansi(0x8C), "\u{0152}"); assert_eq!(decode_winansi(0x99), "\u{2122}"); assert_eq!(decode_winansi(0x9C), "\u{0153}"); assert_eq!(decode_winansi(0xA0), "\u{00A0}"); assert_eq!(decode_winansi(0xE9), "\u{00E9}"); }
1816
1817 #[test]
1818 fn test_tex_glyph_names() {
1819 assert_eq!(
1821 glyph_name_to_unicode("asteriskmath"),
1822 Some("\u{2217}".to_string())
1823 ); assert_eq!(
1825 glyph_name_to_unicode("diamondmath"),
1826 Some("\u{22C4}".to_string())
1827 ); assert_eq!(
1829 glyph_name_to_unicode("minusplus"),
1830 Some("\u{2213}".to_string())
1831 ); assert_eq!(
1833 glyph_name_to_unicode("circleminus"),
1834 Some("\u{2296}".to_string())
1835 ); assert_eq!(
1837 glyph_name_to_unicode("circledot"),
1838 Some("\u{2299}".to_string())
1839 ); assert_eq!(
1841 glyph_name_to_unicode("follows"),
1842 Some("\u{227B}".to_string())
1843 ); assert_eq!(
1845 glyph_name_to_unicode("lessmuch"),
1846 Some("\u{226A}".to_string())
1847 ); assert_eq!(
1849 glyph_name_to_unicode("greatermuch"),
1850 Some("\u{226B}".to_string())
1851 ); assert_eq!(
1853 glyph_name_to_unicode("latticetop"),
1854 Some("\u{22A4}".to_string())
1855 ); assert_eq!(
1857 glyph_name_to_unicode("mapsto"),
1858 Some("\u{21A6}".to_string())
1859 ); assert_eq!(
1862 glyph_name_to_unicode("dagger"),
1863 Some("\u{2020}".to_string())
1864 ); assert_eq!(
1866 glyph_name_to_unicode("daggerdbl"),
1867 Some("\u{2021}".to_string())
1868 ); assert_eq!(
1870 glyph_name_to_unicode("braceleft"),
1871 Some("\u{007B}".to_string())
1872 ); assert_eq!(
1874 glyph_name_to_unicode("braceright"),
1875 Some("\u{007D}".to_string())
1876 ); }
1878}