1use std::collections::HashMap;
17use std::sync::Arc;
18
19use lopdf::{Dictionary, Document, Object};
20
21use crate::pdfium_backend::Glyph;
22
23#[derive(Default)]
32struct DocCaches {
33 fonts: HashMap<(lopdf::ObjectId, Vec<u8>), Arc<Font>>,
34 forms: HashMap<lopdf::ObjectId, Arc<lopdf::content::Content>>,
35}
36
37#[derive(Clone, Copy)]
39struct Mat {
40 a: f64,
41 b: f64,
42 c: f64,
43 d: f64,
44 e: f64,
45 f: f64,
46}
47
48impl Mat {
49 const ID: Mat = Mat {
50 a: 1.0,
51 b: 0.0,
52 c: 0.0,
53 d: 1.0,
54 e: 0.0,
55 f: 0.0,
56 };
57
58 fn then(self, m: Mat) -> Mat {
60 Mat {
61 a: self.a * m.a + self.b * m.c,
62 b: self.a * m.b + self.b * m.d,
63 c: self.c * m.a + self.d * m.c,
64 d: self.c * m.b + self.d * m.d,
65 e: self.e * m.a + self.f * m.c + m.e,
66 f: self.e * m.b + self.f * m.d + m.f,
67 }
68 }
69
70 fn apply(self, x: f64, y: f64) -> (f64, f64) {
71 (
72 self.a * x + self.c * y + self.e,
73 self.b * x + self.d * y + self.f,
74 )
75 }
76}
77
78struct Font {
80 two_byte: bool,
82 to_unicode: HashMap<u32, String>,
84 widths: HashMap<u32, f64>,
86 default_width: f64,
87 simple_encoding: Option<HashMap<u8, char>>,
89 fallback_names: HashMap<u8, String>,
93 program_encoding: HashMap<u8, char>,
97 ascent: f64,
98 descent: f64,
99 hash: u64,
100 style: crate::font_style::FontStyle,
103}
104
105impl Font {
106 fn decode_code(&self, code: u32) -> (Option<String>, f64) {
107 let w = self
108 .widths
109 .get(&code)
110 .copied()
111 .unwrap_or(self.default_width);
112 if let Some(s) = self.to_unicode.get(&code) {
113 return (Some(decompose_ligatures(s)), w);
114 }
115 if !self.two_byte {
116 if let Some(name) = self.fallback_names.get(&(code as u8)) {
119 return (Some(format!("/{name}")), w);
120 }
121 if let Some(enc) = &self.simple_encoding {
122 if let Some(&ch) = enc.get(&(code as u8)) {
123 return (Some(decompose_ligatures(&ch.to_string())), w);
124 }
125 }
126 if let Some(&ch) = self.program_encoding.get(&(code as u8)) {
134 return (Some(decompose_ligatures(&ch.to_string())), w);
135 }
136 }
137 (None, w)
138 }
139}
140
141fn decompose_ligatures(s: &str) -> String {
145 if !s.chars().any(|c| ('\u{FB00}'..='\u{FB06}').contains(&c)) {
146 return s.to_string();
147 }
148 s.chars()
149 .map(|c| {
150 match c {
151 '\u{FB00}' => "ff",
152 '\u{FB01}' => "fi",
153 '\u{FB02}' => "fl",
154 '\u{FB03}' => "ffi",
155 '\u{FB04}' => "ffl",
156 '\u{FB05}' => "ft",
157 '\u{FB06}' => "st",
158 _ => return c.to_string(),
159 }
160 .to_string()
161 })
162 .collect()
163}
164
165fn hash_name(name: &[u8]) -> u64 {
166 use std::hash::{Hash, Hasher};
167 let mut h = std::collections::hash_map::DefaultHasher::new();
168 name.hash(&mut h);
169 h.finish()
170}
171
172fn as_dict<'a>(doc: &'a Document, obj: &'a Object) -> Option<&'a Dictionary> {
174 match obj {
175 Object::Dictionary(d) => Some(d),
176 Object::Reference(id) => doc.get_object(*id).ok().and_then(|o| o.as_dict().ok()),
177 _ => None,
178 }
179}
180
181fn deref<'a>(doc: &'a Document, obj: &'a Object) -> Option<&'a Object> {
182 match obj {
183 Object::Reference(id) => doc.get_object(*id).ok(),
184 other => Some(other),
185 }
186}
187
188fn sanitizer_font_name(doc: &Document, fdict: &Dictionary, two_byte: bool) -> Vec<u8> {
201 let descriptor_font_name = |owner: &Dictionary| {
202 owner
203 .get(b"FontDescriptor")
204 .ok()
205 .and_then(|o| as_dict(doc, o))
206 .and_then(|fd| fd.get(b"FontName").ok())
207 .and_then(|o| deref(doc, o))
208 .and_then(|o| o.as_name().ok().map(<[u8]>::to_vec))
209 };
210 if let Some(n) = descriptor_font_name(fdict) {
211 return n;
212 }
213 if two_byte {
214 let descendant = fdict
215 .get(b"DescendantFonts")
216 .ok()
217 .and_then(|o| deref(doc, o))
218 .and_then(|o| match o {
219 Object::Array(a) => a.first(),
220 _ => None,
221 })
222 .and_then(|o| as_dict(doc, o));
223 if let Some(n) = descendant.and_then(descriptor_font_name) {
224 return n;
225 }
226 }
227 if let Some(n) = fdict
228 .get(b"Name")
229 .ok()
230 .and_then(|o| deref(doc, o))
231 .and_then(|o| o.as_name().ok())
232 {
233 return n.to_vec();
234 }
235 if let Some(n) = fdict.get(b"BaseFont").ok().and_then(|o| o.as_name().ok()) {
238 return n.to_vec();
239 }
240 b"null".to_vec()
241}
242
243fn parse_font(doc: &Document, name: &[u8], fdict: &Dictionary) -> Font {
245 let subtype: &[u8] = fdict
246 .get(b"Subtype")
247 .ok()
248 .and_then(|o| o.as_name().ok())
249 .unwrap_or(&[]);
250 let two_byte = subtype == b"Type0".as_slice();
251
252 let mut to_unicode = fdict
253 .get(b"ToUnicode")
254 .ok()
255 .and_then(|o| deref(doc, o))
256 .and_then(|o| o.as_stream().ok())
257 .and_then(|s| s.decompressed_content().ok())
258 .map(|data| parse_tounicode(&data))
259 .unwrap_or_default();
260 if !two_byte {
261 if let Some(diffs) = differences_array(doc, fdict) {
262 reconcile_stale_tounicode(&mut to_unicode, &diffs);
263 }
264 }
265
266 let (mut widths, mut default_width) = if two_byte {
267 cid_widths(doc, fdict)
268 } else {
269 simple_widths(doc, fdict)
270 };
271
272 let simple_encoding = if two_byte {
273 None
274 } else {
275 Some(simple_encoding_table(doc, fdict))
276 };
277
278 if !two_byte && widths.is_empty() && default_width == 0.0 {
286 if let Some(std14) = base_font_name(fdict).and_then(|n| crate::std14::widths_for(&n)) {
287 if let Some(enc) = &simple_encoding {
288 for (&code, &ch) in enc {
289 if let Some(w) = std14.width(ch) {
290 widths.insert(u32::from(code), w);
291 }
292 }
293 }
294 default_width = 500.0;
297 }
298 }
299 let fallback_names = if two_byte {
300 HashMap::new()
301 } else {
302 differences_gid_names(doc, fdict)
303 };
304 let program_encoding = if two_byte {
305 HashMap::new()
306 } else {
307 type1_program_encoding(doc, fdict)
308 };
309
310 let (ascent, descent) = font_ascent_descent(doc, fdict, two_byte);
311
312 Font {
313 two_byte,
314 to_unicode,
315 widths,
316 default_width,
317 simple_encoding,
318 fallback_names,
319 program_encoding,
320 ascent,
321 descent,
322 hash: hash_name(&sanitizer_font_name(doc, fdict, two_byte)),
323 style: crate::font_style::parse_font_style(&String::from_utf8_lossy(
324 &base_font_name(fdict).unwrap_or_else(|| name.to_vec()),
325 )),
326 }
327}
328
329fn differences_array(doc: &Document, fdict: &Dictionary) -> Option<Vec<Object>> {
331 let enc = fdict.get(b"Encoding").ok().and_then(|o| deref(doc, o))?;
332 let diffs = enc.as_dict().ok()?.get(b"Differences").ok()?;
333 match deref(doc, diffs)? {
334 Object::Array(a) => Some(a.clone()),
335 _ => None,
336 }
337}
338
339fn reconcile_stale_tounicode(to_unicode: &mut HashMap<u32, String>, diffs: &[Object]) {
350 let mut code = 0u8;
351 for el in diffs {
352 match el {
353 Object::Integer(i) => code = *i as u8,
354 Object::Name(name) => {
355 if let Some(ch) = glyph_name_to_char(name) {
356 let identity = (code as char).to_string();
357 if ch.to_string() != identity
358 && to_unicode.get(&u32::from(code)) == Some(&identity)
359 {
360 to_unicode.insert(u32::from(code), ch.to_string());
361 }
362 }
363 code = code.wrapping_add(1);
364 }
365 _ => {}
366 }
367 }
368}
369
370fn differences_gid_names(doc: &Document, fdict: &Dictionary) -> HashMap<u8, String> {
377 let mut map = HashMap::new();
378 let Some(Object::Dictionary(enc)) = fdict.get(b"Encoding").ok().and_then(|o| deref(doc, o))
379 else {
380 return map;
381 };
382 let Some(Object::Array(diffs)) = enc.get(b"Differences").ok().and_then(|o| deref(doc, o))
383 else {
384 return map;
385 };
386 let mut code = 0u8;
387 for el in diffs {
388 match el {
389 Object::Integer(i) => code = *i as u8,
390 Object::Name(name) => {
391 if glyph_name_to_char(name).is_none() && is_gid_name(name) {
392 map.insert(code, String::from_utf8_lossy(name).into_owned());
393 }
394 code = code.wrapping_add(1);
395 }
396 _ => {}
397 }
398 }
399 map
400}
401
402fn type1_program_encoding(doc: &Document, fdict: &Dictionary) -> HashMap<u8, char> {
410 let mut map = HashMap::new();
411 let Some(desc) = fdict
412 .get(b"FontDescriptor")
413 .ok()
414 .and_then(|o| deref(doc, o))
415 .and_then(|o| o.as_dict().ok())
416 else {
417 return map;
418 };
419 let Some(data) = desc
420 .get(b"FontFile")
421 .ok()
422 .and_then(|o| deref(doc, o))
423 .and_then(|o| o.as_stream().ok())
424 .and_then(|s| s.decompressed_content().ok())
425 else {
426 return map;
427 };
428 let head_end = data
430 .windows(5)
431 .position(|w| w == b"eexec")
432 .unwrap_or(data.len());
433 let head = String::from_utf8_lossy(&data[..head_end]);
434 let toks: Vec<&str> = head.split_whitespace().collect();
436 for w in toks.windows(4) {
437 if w[0] == "dup" && w[3] == "put" {
438 if let (Ok(code), Some(name)) = (w[1].parse::<u32>(), w[2].strip_prefix('/')) {
439 if code <= 255 {
440 if let Some(ch) = glyph_name_to_char(name.as_bytes()) {
441 map.insert(code as u8, ch);
442 }
443 }
444 }
445 }
446 }
447 map
448}
449
450fn is_gid_name(name: &[u8]) -> bool {
456 let Ok(s) = std::str::from_utf8(name) else {
457 return false;
458 };
459 if s.starts_with("afii") || s.starts_with("uni") {
460 return false;
461 }
462 for prefix in ["g", "G", "cid", "CID", "glyph", "index"] {
463 if let Some(rest) = s.strip_prefix(prefix) {
464 if !rest.is_empty() && rest.bytes().all(|b| b.is_ascii_digit()) {
465 return true;
466 }
467 }
468 }
469 let alpha = s.bytes().take_while(|b| b.is_ascii_alphabetic()).count();
473 let digits = s.len() - alpha;
474 (1..=3).contains(&alpha)
475 && digits >= 3
476 && s.as_bytes()[alpha..].iter().all(|b| b.is_ascii_digit())
477}
478
479fn font_ascent_descent(doc: &Document, fdict: &Dictionary, two_byte: bool) -> (f64, f64) {
480 let descr_owner = if two_byte {
482 fdict
483 .get(b"DescendantFonts")
484 .ok()
485 .and_then(|o| deref(doc, o))
486 .and_then(|o| match o {
487 Object::Array(a) => a.first(),
488 _ => None,
489 })
490 .and_then(|o| as_dict(doc, o))
491 } else {
492 Some(fdict)
493 };
494 let fd = descr_owner
495 .and_then(|d| d.get(b"FontDescriptor").ok())
496 .and_then(|o| as_dict(doc, o));
497 let asc = fd
498 .and_then(|d| d.get(b"Ascent").ok())
499 .and_then(|o| {
500 o.as_float()
501 .ok()
502 .or_else(|| o.as_i64().ok().map(|i| i as f32))
503 })
504 .unwrap_or(750.0) as f64;
505 let desc = fd
506 .and_then(|d| d.get(b"Descent").ok())
507 .and_then(|o| {
508 o.as_float()
509 .ok()
510 .or_else(|| o.as_i64().ok().map(|i| i as f32))
511 })
512 .unwrap_or(-250.0) as f64;
513 if asc - desc <= 1.0 {
520 return (750.0, -250.0);
521 }
522 (asc, desc)
523}
524
525fn base_font_name(fdict: &Dictionary) -> Option<Vec<u8>> {
527 let name = fdict.get(b"BaseFont").ok()?.as_name().ok()?;
528 let stripped = match name.iter().position(|&b| b == b'+') {
529 Some(i) if i == 6 => &name[i + 1..],
530 _ => name,
531 };
532 Some(stripped.to_vec())
533}
534
535fn simple_widths(doc: &Document, fdict: &Dictionary) -> (HashMap<u32, f64>, f64) {
537 let mut map = HashMap::new();
538 let first = fdict
539 .get(b"FirstChar")
540 .ok()
541 .and_then(|o| o.as_i64().ok())
542 .unwrap_or(0) as u32;
543 if let Some(Object::Array(arr)) = fdict.get(b"Widths").ok().and_then(|o| deref(doc, o)) {
544 for (i, w) in arr.iter().enumerate() {
545 if let Some(w) = num(w) {
546 map.insert(first + i as u32, w);
547 }
548 }
549 }
550 let dw = fdict
551 .get(b"FontDescriptor")
552 .ok()
553 .and_then(|o| as_dict(doc, o))
554 .and_then(|d| d.get(b"MissingWidth").ok())
555 .and_then(num)
556 .unwrap_or(0.0);
557 (map, dw)
558}
559
560fn cid_widths(doc: &Document, fdict: &Dictionary) -> (HashMap<u32, f64>, f64) {
562 let mut map = HashMap::new();
563 let Some(desc) = fdict
564 .get(b"DescendantFonts")
565 .ok()
566 .and_then(|o| deref(doc, o))
567 .and_then(|o| match o {
568 Object::Array(a) => a.first(),
569 _ => None,
570 })
571 .and_then(|o| as_dict(doc, o))
572 else {
573 return (map, 1000.0);
574 };
575 let dw = desc.get(b"DW").ok().and_then(num).unwrap_or(1000.0);
576 if let Some(Object::Array(w)) = desc.get(b"W").ok().and_then(|o| deref(doc, o)) {
577 let mut i = 0;
578 while i < w.len() {
579 let c = w.get(i).and_then(num);
580 match (c, w.get(i + 1)) {
581 (Some(c), Some(Object::Array(list))) => {
583 for (k, wv) in list.iter().enumerate() {
584 if let Some(wv) = num(wv) {
585 map.insert(c as u32 + k as u32, wv);
586 }
587 }
588 i += 2;
589 }
590 (Some(c1), Some(o2)) => {
592 if let (Some(c2), Some(wv)) = (num(o2), w.get(i + 2).and_then(num)) {
593 for cid in c1 as u32..=c2 as u32 {
594 map.insert(cid, wv);
595 }
596 }
597 i += 3;
598 }
599 _ => break,
600 }
601 }
602 }
603 (map, dw)
604}
605
606fn num(o: &Object) -> Option<f64> {
607 match o {
608 Object::Integer(i) => Some(*i as f64),
609 Object::Real(r) => Some(*r as f64),
610 _ => None,
611 }
612}
613
614pub(crate) fn parse_tounicode(data: &[u8]) -> HashMap<u32, String> {
616 let text = String::from_utf8_lossy(data);
617 let mut map = HashMap::new();
618 let hex = |s: &str| -> Option<Vec<u16>> {
619 let s = s.trim();
620 if !s.starts_with('<') || !s.ends_with('>') {
621 return None;
622 }
623 let h = &s[1..s.len() - 1];
624 let bytes: Vec<u8> = (0..h.len())
625 .step_by(2)
626 .filter_map(|i| u8::from_str_radix(h.get(i..i + 2)?, 16).ok())
627 .collect();
628 Some(
629 bytes
630 .chunks(2)
631 .map(|c| {
632 if c.len() == 2 {
633 u16::from_be_bytes([c[0], c[1]])
634 } else {
635 c[0] as u16
636 }
637 })
638 .collect(),
639 )
640 };
641 let u16s_to_string = |u: &[u16]| String::from_utf16_lossy(u);
642 let code_of = |u: &[u16]| u.iter().fold(0u32, |acc, &x| (acc << 16) | x as u32);
643
644 let tokens: Vec<String> = {
648 let bytes = text.as_bytes();
649 let mut toks = Vec::new();
650 let mut i = 0;
651 while i < bytes.len() {
652 let c = bytes[i];
653 if c.is_ascii_whitespace() {
654 i += 1;
655 } else if c == b'<' {
656 let start = i;
657 while i < bytes.len() && bytes[i] != b'>' {
658 i += 1;
659 }
660 i += 1; toks.push(String::from_utf8_lossy(&bytes[start..i.min(bytes.len())]).into_owned());
662 } else if c == b'[' || c == b']' {
663 toks.push((c as char).to_string());
664 i += 1;
665 } else {
666 let start = i;
667 while i < bytes.len()
668 && !bytes[i].is_ascii_whitespace()
669 && bytes[i] != b'<'
670 && bytes[i] != b'['
671 && bytes[i] != b']'
672 {
673 i += 1;
674 }
675 toks.push(String::from_utf8_lossy(&bytes[start..i]).into_owned());
676 }
677 }
678 toks
679 };
680 let tokens: Vec<&str> = tokens.iter().map(|s| s.as_str()).collect();
681 let mut i = 0;
682 while i < tokens.len() {
683 match tokens[i] {
684 "beginbfchar" => {
685 i += 1;
686 while i + 1 < tokens.len() && tokens[i] != "endbfchar" {
687 if let (Some(src), Some(dst)) = (hex(tokens[i]), hex(tokens[i + 1])) {
688 map.insert(code_of(&src), u16s_to_string(&dst));
689 }
690 i += 2;
691 }
692 }
693 "beginbfrange" => {
694 i += 1;
695 while i + 2 < tokens.len() && tokens[i] != "endbfrange" {
696 let (Some(lo), Some(hi)) = (hex(tokens[i]), hex(tokens[i + 1])) else {
697 i += 1;
698 continue;
699 };
700 let lo = code_of(&lo);
701 let hi = code_of(&hi);
702 if tokens[i + 2] == "[" {
703 let mut j = i + 3;
705 let mut code = lo;
706 while j < tokens.len() && tokens[j] != "]" {
707 if let Some(dst) = hex(tokens[j]) {
708 map.insert(code, u16s_to_string(&dst));
709 }
710 code += 1;
711 j += 1;
712 }
713 i = j + 1;
714 } else if let Some(dst) = hex(tokens[i + 2]) {
715 let base = code_of(&dst);
717 for (k, code) in (lo..=hi).enumerate() {
718 if let Some(ch) = char::from_u32(base + k as u32) {
719 map.insert(code, ch.to_string());
720 }
721 }
722 i += 3;
723 } else {
724 i += 1;
725 }
726 }
727 }
728 _ => i += 1,
729 }
730 }
731 map
732}
733
734fn codes(font: &Font, bytes: &[u8]) -> Vec<u32> {
736 if font.two_byte {
737 bytes
738 .chunks(2)
739 .map(|c| {
740 if c.len() == 2 {
741 ((c[0] as u32) << 8) | c[1] as u32
742 } else {
743 c[0] as u32
744 }
745 })
746 .collect()
747 } else {
748 bytes.iter().map(|&b| b as u32).collect()
749 }
750}
751
752#[derive(Debug, Clone, Copy, PartialEq)]
767pub(crate) struct PageBox {
768 pub l: f32,
770 pub b: f32,
772 pub w: f32,
773 pub h: f32,
774}
775
776impl PageBox {
777 pub fn top(&self) -> f32 {
779 self.b + self.h
780 }
781}
782
783fn inherited_rect(
786 doc: &Document,
787 page_id: lopdf::ObjectId,
788 key: &[u8],
789) -> Option<(f32, f32, f32, f32)> {
790 let mut id = page_id;
791 for _ in 0..32 {
792 let dict = doc.get_object(id).ok()?.as_dict().ok()?;
793 if let Some(Object::Array(a)) = dict.get(key).ok().and_then(|o| deref(doc, o)) {
794 let v: Vec<f32> = a.iter().filter_map(|o| num(o).map(|x| x as f32)).collect();
795 if v.len() == 4 && v.iter().all(|x| x.is_finite()) {
796 return Some((
797 v[0].min(v[2]),
798 v[1].min(v[3]),
799 v[0].max(v[2]),
800 v[1].max(v[3]),
801 ));
802 }
803 return None;
804 }
805 id = dict.get(b"Parent").ok()?.as_reference().ok()?;
806 }
807 None
808}
809
810pub(crate) fn page_box(doc: &Document, page_id: lopdf::ObjectId) -> PageBox {
811 let nonempty = |r: &(f32, f32, f32, f32)| r.2 > r.0 && r.3 > r.1;
812 let media = inherited_rect(doc, page_id, b"MediaBox")
813 .filter(nonempty)
814 .unwrap_or((0.0, 0.0, 612.0, 792.0));
815 let crop = inherited_rect(doc, page_id, b"CropBox")
816 .map(|c| {
817 (
818 c.0.max(media.0),
819 c.1.max(media.1),
820 c.2.min(media.2),
821 c.3.min(media.3),
822 )
823 })
824 .filter(nonempty)
825 .unwrap_or(media);
826 PageBox {
827 l: crop.0,
828 b: crop.1,
829 w: crop.2 - crop.0,
830 h: crop.3 - crop.1,
831 }
832}
833
834fn page_size(doc: &Document, page_id: lopdf::ObjectId) -> (f32, f32) {
837 let pb = page_box(doc, page_id);
838 (pb.w, pb.h)
839}
840
841pub fn content_diagnosis(bytes: &[u8]) -> String {
847 let Some(doc) = load_document(bytes) else {
848 return "document does not load".into();
849 };
850 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
851 pages.sort_by_key(|(n, _)| *n);
852 let mut out = String::new();
853 let mut caches = DocCaches::default();
854 for (n, pid) in pages.into_iter().take(4) {
855 let content_bytes = doc.get_page_content(pid);
856 let ops = lopdf::content::Content::decode(&content_bytes)
857 .map(|c| c.operations.len())
858 .ok();
859 let res = page_res(&doc, pid);
860 let fonts = res.map(|r| fonts_from_res(&doc, r, &mut caches).len());
861 let glyphs = page_glyphs_cached(&doc, pid, &mut caches).len();
862 out.push_str(&format!(
863 "\n page {n}: content {} B, ops {}, resources {}, fonts {}, glyphs {}",
864 content_bytes.len(),
865 ops.map_or("UNDECODABLE".to_string(), |n| n.to_string()),
866 if res.is_some() { "ok" } else { "MISSING" },
867 fonts.map_or("-".to_string(), |n| n.to_string()),
868 glyphs,
869 ));
870 }
871 out
872}
873
874pub fn text_layer_is_vestigial(pages: &[crate::pdfium_backend::PdfPage]) -> bool {
887 let mut tally = TextLayerTally::default();
888 pages.iter().for_each(|p| tally.add(p));
889 tally.is_vestigial()
890}
891
892#[derive(Debug, Default, Clone, Copy)]
895pub struct TextLayerTally {
896 pages: usize,
897 lines: usize,
898 chars: usize,
899}
900
901impl TextLayerTally {
902 pub fn add(&mut self, page: &crate::pdfium_backend::PdfPage) {
903 self.pages += 1;
904 self.lines += page.cells.len();
905 self.chars += page
906 .cells
907 .iter()
908 .map(|c| c.text.chars().count())
909 .sum::<usize>();
910 }
911
912 pub fn is_vestigial(&self) -> bool {
914 self.lines == 0 || (self.lines <= self.pages && self.chars < 32)
915 }
916
917 pub fn proves_text(&self, total_pages: usize) -> bool {
921 self.lines > 0 && (self.lines > total_pages || self.chars >= 32)
922 }
923}
924
925pub fn xref_repair_status(bytes: &[u8]) -> String {
929 if Document::load_mem(bytes).is_ok() {
930 return "loads unaided; no repair needed".into();
931 }
932 match pad_short_xref_entries(bytes) {
933 Ok(fixed) => match Document::load_mem(&fixed) {
934 Ok(_) => "repaired: cross-reference entries padded to 20 bytes".into(),
935 Err(e) => format!("padded the entries, but it still will not load: {e}"),
936 },
937 Err(why) => format!("repair declined — {why}"),
938 }
939}
940
941pub(crate) fn load_document(bytes: &[u8]) -> Option<Document> {
956 open_document(bytes, None).ok()
957}
958
959#[derive(Debug, Clone, Copy, PartialEq, Eq)]
961pub(crate) enum OpenError {
962 Unreadable,
964 Password,
966}
967
968pub(crate) fn open_document(bytes: &[u8], password: Option<&str>) -> Result<Document, OpenError> {
975 let Some(doc) = load_document_raw(bytes, password) else {
976 if password.is_some() && load_document_raw(bytes, None).is_some_and(|d| d.is_encrypted()) {
980 return Err(OpenError::Password);
981 }
982 return Err(OpenError::Unreadable);
983 };
984 if doc.is_encrypted() {
985 return Err(OpenError::Password);
986 }
987 Ok(doc)
988}
989
990fn load_options(password: Option<&str>) -> lopdf::LoadOptions {
991 lopdf::LoadOptions {
992 password: password.map(str::to_string),
993 ..lopdf::LoadOptions::default()
994 }
995}
996
997fn load_document_raw(bytes: &[u8], password: Option<&str>) -> Option<Document> {
998 let mut fallback = None;
1003 if let Some(doc) = best_effort_load(bytes, password, &mut fallback) {
1004 return Some(doc);
1005 }
1006 let xref_fixed = pad_short_xref_entries(bytes).ok();
1007 if let Some(fixed) = &xref_fixed {
1008 if let Some(doc) = best_effort_load(fixed, password, &mut fallback) {
1009 return Some(doc);
1010 }
1011 }
1012 let lengths_fixed = fix_stream_lengths(xref_fixed.as_deref().unwrap_or(bytes));
1015 if let Some(doc) = best_effort_load(&lengths_fixed, password, &mut fallback) {
1016 return Some(doc);
1017 }
1018 fallback
1019}
1020
1021fn best_effort_load(
1024 data: &[u8],
1025 password: Option<&str>,
1026 fallback: &mut Option<Document>,
1027) -> Option<Document> {
1028 match Document::load_mem_with_options(data, load_options(password)) {
1029 Ok(doc) if has_page_content(&doc) => Some(doc),
1030 Ok(doc) => {
1031 fallback.get_or_insert(doc);
1032 None
1033 }
1034 Err(_) => None,
1035 }
1036}
1037
1038fn has_page_content(doc: &Document) -> bool {
1042 doc.get_pages()
1043 .into_values()
1044 .take(4)
1045 .any(|pid| !doc.get_page_content(pid).is_empty())
1046}
1047
1048fn fix_stream_lengths(bytes: &[u8]) -> Vec<u8> {
1061 let mut out = bytes.to_vec();
1062 let mut i = 0;
1063 while let Some(rel) = find(&out[i..], b"stream") {
1064 let kw = i + rel;
1065 i = kw + 6;
1066 if kw >= 3 && &out[kw - 3..kw] == b"end" {
1068 continue;
1069 }
1070 let mut data = kw + 6;
1072 if out.get(data..data + 2) == Some(b"\r\n".as_slice()) {
1073 data += 2;
1074 } else if matches!(out.get(data), Some(b'\n' | b'\r')) {
1075 data += 1;
1076 }
1077 let Some(end) = find(&out[data..], b"endstream").map(|r| data + r) else {
1078 continue;
1079 };
1080 let dict_start = out[..kw].iter().rposition(|&c| c == b'<').unwrap_or(0);
1082 let Some(lrel) = find(&out[dict_start..kw], b"/Length") else {
1083 continue;
1084 };
1085 let mut d = dict_start + lrel + 7;
1086 while matches!(out.get(d), Some(b' ')) {
1087 d += 1;
1088 }
1089 let digits = out[d..].iter().take_while(|c| c.is_ascii_digit()).count();
1090 if digits == 0 {
1091 continue;
1092 }
1093 let declared: usize = match std::str::from_utf8(&out[d..d + digits])
1094 .ok()
1095 .and_then(|s| s.parse().ok())
1096 {
1097 Some(v) => v,
1098 None => continue,
1099 };
1100 let actual = end - data;
1101 let replacement = actual.to_string();
1104 if actual == declared || replacement.len() > digits {
1105 continue;
1106 }
1107 out[d..d + digits].fill(b' ');
1108 out[d..d + replacement.len()].copy_from_slice(replacement.as_bytes());
1109 }
1110 out
1111}
1112
1113fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
1114 haystack.windows(needle.len()).position(|w| w == needle)
1115}
1116
1117fn pad_short_xref_entries(bytes: &[u8]) -> Result<Vec<u8>, &'static str> {
1120 let is_boundary = |i: usize| i == 0 || matches!(bytes[i - 1], b'\n' | b'\r');
1123 let mut starts = (0..bytes.len().saturating_sub(4))
1124 .filter(|&i| &bytes[i..i + 4] == b"xref" && is_boundary(i));
1125 let xref_at = starts
1126 .next()
1127 .ok_or("no classic `xref` section (an xref stream?)")?;
1128 if starts.next().is_some() {
1129 return Err("more than one xref section (incremental update)");
1130 }
1131 let last_obj = bytes
1132 .windows(3)
1133 .rposition(|w| w == b"obj")
1134 .ok_or("no objects found")?;
1135 if last_obj > xref_at {
1136 return Err("an object follows the xref — padding would move it");
1137 }
1138
1139 let mut out = bytes[..xref_at].to_vec();
1140 out.extend_from_slice(b"xref\n");
1141 let mut i = xref_at + 4;
1142 let skip_ws = |i: &mut usize| {
1143 while matches!(bytes.get(*i), Some(b'\r' | b'\n' | b' ')) {
1144 *i += 1;
1145 }
1146 };
1147 loop {
1148 skip_ws(&mut i);
1149 if bytes[i..].starts_with(b"trailer") {
1151 out.extend_from_slice(&bytes[i..]);
1152 return Ok(out);
1153 }
1154 let header_end = i + bytes[i..]
1155 .iter()
1156 .position(|c| matches!(c, b'\n' | b'\r'))
1157 .ok_or("subsection header runs off the end")?;
1158 let header = std::str::from_utf8(&bytes[i..header_end])
1159 .map_err(|_| "subsection header is not text")?
1160 .trim();
1161 let mut parts = header.split_whitespace();
1162 let count: usize = parts
1163 .nth(1)
1164 .and_then(|c| c.parse().ok())
1165 .ok_or("unparseable subsection header")?;
1166 if parts.next().is_some() || count == 0 {
1167 return Err("unexpected subsection header shape");
1168 }
1169 out.extend_from_slice(header.as_bytes());
1170 out.push(b'\n');
1171 i = header_end;
1172 for _ in 0..count {
1173 skip_ws(&mut i);
1174 let entry = bytes.get(i..i + 18).ok_or("xref entry runs off the end")?;
1176 let well_formed = entry[..10].iter().all(u8::is_ascii_digit)
1177 && entry[10] == b' '
1178 && entry[11..16].iter().all(u8::is_ascii_digit)
1179 && entry[16] == b' '
1180 && matches!(entry[17], b'n' | b'f');
1181 if !well_formed {
1182 return Err("xref entry is not `nnnnnnnnnn ggggg n`");
1183 }
1184 out.extend_from_slice(entry);
1185 out.extend_from_slice(b" \n"); i += 18;
1187 }
1188 }
1189}
1190
1191pub fn debug_glyphs(bytes: &[u8], index: usize) -> Vec<(char, f32, f32, f32, f32)> {
1194 let Some(doc) = load_document(bytes) else {
1195 return Vec::new();
1196 };
1197 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
1198 pages.sort_by_key(|(n, _)| *n);
1199 let Some((_, pid)) = pages.get(index) else {
1200 return Vec::new();
1201 };
1202 page_glyphs(&doc, *pid)
1203 .into_iter()
1204 .map(|g| (g.ch, g.ll, g.lr, g.lb, g.lt))
1205 .collect()
1206}
1207
1208pub fn pdf_textlines(bytes: &[u8]) -> Vec<(f32, f32, Vec<crate::pdfium_backend::TextCell>)> {
1212 let Some(doc) = load_document(bytes) else {
1213 return Vec::new();
1214 };
1215 let mut caches = DocCaches::default();
1216 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
1217 pages.sort_by_key(|(n, _)| *n);
1218 pages
1219 .into_iter()
1220 .map(|(_, pid)| {
1221 let (w, h) = page_size(&doc, pid);
1222 let glyphs = page_glyphs_cached(&doc, pid, &mut caches);
1223 let cells = crate::dp_lines::line_cells(&glyphs, h, true);
1224 (w, h, cells)
1225 })
1226 .collect()
1227}
1228
1229pub fn pdf_words(bytes: &[u8]) -> Vec<(f32, f32, Vec<crate::pdfium_backend::TextCell>)> {
1234 let Some(doc) = load_document(bytes) else {
1235 return Vec::new();
1236 };
1237 let mut caches = DocCaches::default();
1238 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
1239 pages.sort_by_key(|(n, _)| *n);
1240 pages
1241 .into_iter()
1242 .map(|(_, pid)| {
1243 let (w, h) = page_size(&doc, pid);
1244 let glyphs = page_glyphs_cached(&doc, pid, &mut caches);
1245 let cells = crate::dp_lines::word_cells(&glyphs, h, true);
1246 (w, h, cells)
1247 })
1248 .collect()
1249}
1250
1251#[derive(Default)]
1255pub struct PageParserCells {
1256 pub prose: Vec<crate::pdfium_backend::TextCell>,
1257 pub words: Vec<crate::pdfium_backend::TextCell>,
1258 pub code: Vec<crate::pdfium_backend::TextCell>,
1259 pub checkboxes: Vec<crate::checkbox::CheckBox>,
1261}
1262
1263pub struct PageTextParser {
1276 doc: Document,
1277 caches: DocCaches,
1278 pages: Vec<lopdf::ObjectId>,
1280}
1281
1282impl PageTextParser {
1283 pub fn open(bytes: &[u8]) -> Option<Self> {
1285 Self::open_with_password(bytes, None)
1286 }
1287
1288 pub fn open_with_password(bytes: &[u8], password: Option<&str>) -> Option<Self> {
1292 let doc = open_document(bytes, password).ok()?;
1293 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
1294 pages.sort_by_key(|(n, _)| *n);
1295 Some(Self {
1296 doc,
1297 caches: DocCaches::default(),
1298 pages: pages.into_iter().map(|(_, pid)| pid).collect(),
1299 })
1300 }
1301
1302 pub(crate) fn glyph_styles(
1310 &mut self,
1311 index: usize,
1312 ) -> Vec<crate::heading_hierarchy::GlyphStyle> {
1313 let Some(&pid) = self.pages.get(index) else {
1314 return Vec::new();
1315 };
1316 let (_w, h) = page_size(&self.doc, pid);
1317 let glyphs = page_glyphs_cached(&self.doc, pid, &mut self.caches);
1318 let styles: HashMap<u64, crate::font_style::FontStyle> = self
1321 .caches
1322 .fonts
1323 .values()
1324 .map(|f| (f.hash, f.style))
1325 .collect();
1326 glyphs
1327 .iter()
1328 .filter(|g| !g.ch.is_whitespace() && g.ll.is_finite())
1329 .map(|g| {
1330 let st = styles.get(&g.font).copied().unwrap_or_default();
1331 crate::heading_hierarchy::GlyphStyle {
1332 l: g.ll,
1333 t: h - g.lt,
1334 r: g.lr,
1335 b: h - g.lb,
1336 height: g.height(),
1337 weight_cls: crate::font_style::weight_class(st.weight),
1338 italic: st.italic,
1339 styled: st.known,
1340 }
1341 })
1342 .collect()
1343 }
1344
1345 pub fn cells(&mut self, index: usize) -> PageParserCells {
1349 let Some(&pid) = self.pages.get(index) else {
1350 return PageParserCells::default();
1351 };
1352 let (_w, h) = page_size(&self.doc, pid);
1353 let (glyphs, inks) = page_glyphs_and_inks(&self.doc, pid, &mut self.caches);
1354 let (prose, words) = crate::dp_lines::line_and_word_cells(&glyphs, h, true);
1355 PageParserCells {
1356 prose,
1357 words,
1358 code: crate::pdfium_backend::code_cells_from_glyphs(&glyphs, h),
1359 checkboxes: crate::checkbox::find(&inks, h),
1360 }
1361 }
1362
1363 pub fn page_count(&self) -> usize {
1365 self.pages.len()
1366 }
1367
1368 pub fn text_page(&mut self, index: usize) -> crate::pdfium_backend::PdfPage {
1374 let (w, h, (glyphs, inks)) = match self.pages.get(index) {
1375 Some(&pid) => {
1376 let (w, h) = page_size(&self.doc, pid);
1377 (w, h, page_glyphs_and_inks(&self.doc, pid, &mut self.caches))
1378 }
1379 None => (0.0, 0.0, Default::default()),
1380 };
1381 let (mut prose, mut words) = crate::dp_lines::line_and_word_cells(&glyphs, h, true);
1382 drop_overpainted_cells(&mut prose);
1383 drop_overpainted_cells(&mut words);
1384 crate::pdfium_backend::PdfPage {
1385 #[cfg(feature = "ocr-prep")]
1386 image_layout: None,
1387 width: w,
1388 height: h,
1389 scale: 1.0,
1391 cells: prose,
1392 code_cells: crate::pdfium_backend::code_cells_from_glyphs(&glyphs, h),
1393 word_cells: words,
1394 checkboxes: crate::checkbox::find(&inks, h),
1395 #[cfg(feature = "ocr-prep")]
1396 image: image::RgbImage::new(1, 1),
1397 links: Vec::new(),
1398 rotation: 0,
1399 #[cfg(feature = "ml")]
1400 pending_image: None,
1401 }
1402 }
1403}
1404
1405pub fn pdf_all_cells(bytes: &[u8]) -> Vec<PageParserCells> {
1410 let Some(mut parser) = PageTextParser::open(bytes) else {
1411 return Vec::new();
1412 };
1413 (0..parser.pages.len()).map(|i| parser.cells(i)).collect()
1414}
1415
1416pub fn pdf_text_pages(bytes: &[u8]) -> Vec<crate::pdfium_backend::PdfPage> {
1423 let Some(mut parser) = PageTextParser::open(bytes) else {
1424 return Vec::new();
1425 };
1426 (0..parser.page_count())
1427 .map(|i| parser.text_page(i))
1428 .collect()
1429}
1430
1431fn drop_overpainted_cells(cells: &mut Vec<crate::pdfium_backend::TextCell>) {
1450 let mut paint = vec![false; cells.len()];
1451 for i in 0..cells.len() {
1452 for j in 0..cells.len() {
1453 if i == j || cells[i].text == cells[j].text {
1454 continue;
1455 }
1456 let (a, b) = (&cells[i], &cells[j]);
1457 let vo = (a.b.min(b.b) - a.t.max(b.t)).max(0.0);
1459 if vo < 0.6 * (a.b - a.t).min(b.b - b.t) {
1460 continue;
1461 }
1462 let ho = (a.r.min(b.r) - a.l.max(b.l)).max(0.0);
1464 if ho >= 0.8 * (a.r - a.l) && (a.r - a.l) <= (b.r - b.l) {
1465 paint[i] = true;
1466 paint[j] = true;
1467 }
1468 }
1469 }
1470 let mut keep = paint.iter().map(|p| !p);
1471 cells.retain(|_| keep.next().unwrap());
1472}
1473
1474#[derive(Clone, Copy)]
1478struct TextState {
1479 tc: f64,
1480 tw: f64,
1481 th: f64,
1482 tl: f64,
1483 trise: f64,
1484 fsize: f64,
1485}
1486
1487impl TextState {
1488 const INIT: TextState = TextState {
1489 tc: 0.0,
1490 tw: 0.0,
1491 th: 1.0,
1492 tl: 0.0,
1493 trise: 0.0,
1494 fsize: 0.0,
1495 };
1496}
1497
1498fn page_res(doc: &Document, page_id: lopdf::ObjectId) -> Option<&Dictionary> {
1501 let (inline, ids) = doc.get_page_resources(page_id).ok()?;
1502 if let Some(d) = inline {
1503 return Some(d);
1504 }
1505 ids.into_iter().find_map(|id| doc.get_dictionary(id).ok())
1506}
1507
1508fn fonts_from_res(
1512 doc: &Document,
1513 res: &Dictionary,
1514 caches: &mut DocCaches,
1515) -> HashMap<Vec<u8>, Arc<Font>> {
1516 let mut map = HashMap::new();
1517 let font_dict = res
1518 .get(b"Font")
1519 .ok()
1520 .and_then(|o| deref(doc, o))
1521 .and_then(|o| o.as_dict().ok());
1522 if let Some(fd) = font_dict {
1523 for (name, value) in fd.iter() {
1524 let font = match value {
1525 Object::Reference(id) => {
1526 let key = (*id, name.clone());
1527 if let Some(f) = caches.fonts.get(&key) {
1528 Arc::clone(f)
1529 } else if let Some(fdict) = deref(doc, value).and_then(|o| o.as_dict().ok()) {
1530 let f = Arc::new(parse_font(doc, name, fdict));
1531 caches.fonts.insert(key, Arc::clone(&f));
1532 f
1533 } else {
1534 continue;
1535 }
1536 }
1537 _ => {
1538 if let Some(fdict) = deref(doc, value).and_then(|o| o.as_dict().ok()) {
1539 Arc::new(parse_font(doc, name, fdict))
1540 } else {
1541 continue;
1542 }
1543 }
1544 };
1545 map.insert(name.clone(), font);
1546 }
1547 }
1548 map
1549}
1550
1551pub(crate) fn glyph_styles(
1557 bytes: &[u8],
1558 pages: &[usize],
1559) -> HashMap<usize, Vec<crate::heading_hierarchy::GlyphStyle>> {
1560 let mut out = HashMap::new();
1561 let Some(mut parser) = PageTextParser::open(bytes) else {
1562 return out;
1563 };
1564 for &page_no in pages {
1565 if page_no == 0 {
1566 continue;
1567 }
1568 let styles = parser.glyph_styles(page_no - 1);
1569 if !styles.is_empty() {
1570 out.insert(page_no, styles);
1571 }
1572 }
1573 out
1574}
1575
1576pub(crate) fn page_glyphs(doc: &Document, page_id: lopdf::ObjectId) -> Vec<Glyph> {
1577 page_glyphs_cached(doc, page_id, &mut DocCaches::default())
1578}
1579
1580fn page_glyphs_cached(
1583 doc: &Document,
1584 page_id: lopdf::ObjectId,
1585 caches: &mut DocCaches,
1586) -> Vec<Glyph> {
1587 page_glyphs_and_inks(doc, page_id, caches).0
1588}
1589
1590fn page_glyphs_and_inks(
1594 doc: &Document,
1595 page_id: lopdf::ObjectId,
1596 caches: &mut DocCaches,
1597) -> (Vec<Glyph>, Vec<crate::checkbox::Ink>) {
1598 let mut out = Vec::new();
1599 let mut inks = Vec::new();
1600 let content_bytes = doc.get_page_content(page_id);
1603 let Ok(content) = lopdf::content::Content::decode(&content_bytes) else {
1604 return (out, inks);
1605 };
1606 if let Some(res) = page_res(doc, page_id) {
1607 let pb = page_box(doc, page_id);
1610 let base = Mat {
1611 e: -(pb.l as f64),
1612 f: -(pb.b as f64),
1613 ..Mat::ID
1614 };
1615 run_content(
1616 doc,
1617 res,
1618 &content,
1619 base,
1620 TextState::INIT,
1621 0,
1622 caches,
1623 &mut out,
1624 &mut inks,
1625 );
1626 out.retain(|g| on_page(g, pb.w, pb.h));
1627 }
1628 (out, inks)
1629}
1630
1631fn on_page(g: &Glyph, w: f32, h: f32) -> bool {
1646 const EPS: f32 = 1e-3;
1648 let (l, b, r, t) = if [g.ll, g.lb, g.lr, g.lt].iter().all(|v| v.is_finite()) {
1649 (g.ll, g.lb, g.lr, g.lt)
1650 } else {
1651 (g.l, g.b, g.r, g.t)
1652 };
1653 if ![l, b, r, t].iter().all(|v| v.is_finite()) {
1654 return true;
1655 }
1656 l >= -EPS && b >= -EPS && r <= w + EPS && t <= h + EPS
1657}
1658
1659#[allow(clippy::too_many_arguments)]
1664fn run_content(
1665 doc: &Document,
1666 res: &Dictionary,
1667 content: &lopdf::content::Content,
1668 base_ctm: Mat,
1669 init: TextState,
1670 depth: u32,
1671 caches: &mut DocCaches,
1672 out: &mut Vec<Glyph>,
1673 inks: &mut Vec<crate::checkbox::Ink>,
1674) {
1675 let fonts = fonts_from_res(doc, res, caches);
1676 let xobjects = res
1677 .get(b"XObject")
1678 .ok()
1679 .and_then(|o| deref(doc, o))
1680 .and_then(|o| o.as_dict().ok());
1681
1682 #[allow(clippy::type_complexity)]
1687 let mut gstate_stack: Vec<(Mat, f64, f64, f64, f64, f64, f64, Option<&Arc<Font>>)> = Vec::new();
1688 let mut ctm = base_ctm;
1689 let mut tm = Mat::ID;
1690 let mut tlm = Mat::ID;
1691 let mut font: Option<&Arc<Font>> = None;
1692 let mut fsize = init.fsize;
1693 let mut tc = init.tc; let mut tw = init.tw; let mut th = init.th; let mut tl = init.tl; let mut trise = init.trise;
1698
1699 let op_f = |operands: &[Object], i: usize| operands.get(i).and_then(num).unwrap_or(0.0);
1700 let mut path: Vec<PathPiece> = Vec::new();
1704 let (mut cur, mut start) = ((0.0, 0.0), (0.0, 0.0));
1705 let mut fill_light = false;
1709 let mut fill_stack: Vec<bool> = Vec::new();
1710
1711 for op in &content.operations {
1712 let operands = &op.operands;
1713 match op.operator.as_str() {
1714 "q" => {
1715 gstate_stack.push((ctm, tc, tw, th, tl, trise, fsize, font));
1716 fill_stack.push(fill_light);
1717 }
1718 "Q" => {
1719 fill_light = fill_stack.pop().unwrap_or(false);
1720 if let Some((c, a, b, h, l, r, fs, f)) = gstate_stack.pop() {
1721 ctm = c;
1722 tc = a;
1723 tw = b;
1724 th = h;
1725 tl = l;
1726 trise = r;
1727 fsize = fs;
1728 font = f;
1729 }
1730 }
1731 "cm" => {
1732 let m = Mat {
1733 a: op_f(operands, 0),
1734 b: op_f(operands, 1),
1735 c: op_f(operands, 2),
1736 d: op_f(operands, 3),
1737 e: op_f(operands, 4),
1738 f: op_f(operands, 5),
1739 };
1740 ctm = m.then(ctm);
1741 }
1742 "BT" => {
1743 tm = Mat::ID;
1744 tlm = Mat::ID;
1745 }
1746 "ET" => {}
1747 "Tf" => {
1748 if let Some(Object::Name(n)) = operands.first() {
1749 font = fonts.get(n.as_slice());
1750 }
1751 fsize = op_f(operands, 1);
1752 }
1753 "Td" => {
1754 tlm = Mat {
1755 a: 1.0,
1756 b: 0.0,
1757 c: 0.0,
1758 d: 1.0,
1759 e: op_f(operands, 0),
1760 f: op_f(operands, 1),
1761 }
1762 .then(tlm);
1763 tm = tlm;
1764 }
1765 "TD" => {
1766 tl = -op_f(operands, 1);
1767 tlm = Mat {
1768 a: 1.0,
1769 b: 0.0,
1770 c: 0.0,
1771 d: 1.0,
1772 e: op_f(operands, 0),
1773 f: op_f(operands, 1),
1774 }
1775 .then(tlm);
1776 tm = tlm;
1777 }
1778 "Tm" => {
1779 tlm = Mat {
1780 a: op_f(operands, 0),
1781 b: op_f(operands, 1),
1782 c: op_f(operands, 2),
1783 d: op_f(operands, 3),
1784 e: op_f(operands, 4),
1785 f: op_f(operands, 5),
1786 };
1787 tm = tlm;
1788 }
1789 "T*" => {
1790 tlm = Mat {
1791 a: 1.0,
1792 b: 0.0,
1793 c: 0.0,
1794 d: 1.0,
1795 e: 0.0,
1796 f: -tl,
1797 }
1798 .then(tlm);
1799 tm = tlm;
1800 }
1801 "Tc" => tc = op_f(operands, 0),
1802 "Tw" => tw = op_f(operands, 0),
1803 "Tz" => th = op_f(operands, 0) / 100.0,
1804 "TL" => tl = op_f(operands, 0),
1805 "Ts" => trise = op_f(operands, 0),
1806 "Tj" | "'" | "\"" => {
1807 if op.operator == "'" || op.operator == "\"" {
1808 tlm = Mat {
1810 a: 1.0,
1811 b: 0.0,
1812 c: 0.0,
1813 d: 1.0,
1814 e: 0.0,
1815 f: -tl,
1816 }
1817 .then(tlm);
1818 tm = tlm;
1819 }
1820 if op.operator == "\"" {
1821 tw = op_f(operands, 0);
1824 tc = op_f(operands, 1);
1825 }
1826 if let (Some(f), Some(Object::String(s, _))) = (font, operands.last()) {
1827 show_text(f, s, fsize, tc, tw, th, trise, &mut tm, ctm, out);
1828 }
1829 }
1830 "TJ" => {
1831 if let (Some(f), Some(Object::Array(arr))) = (font, operands.first()) {
1832 for el in arr {
1833 match el {
1834 Object::String(s, _) => {
1835 show_text(f, s, fsize, tc, tw, th, trise, &mut tm, ctm, out)
1836 }
1837 other => {
1838 if let Some(adj) = num(other) {
1839 let tx = -adj / 1000.0 * fsize * th;
1841 tm = Mat {
1842 a: 1.0,
1843 b: 0.0,
1844 c: 0.0,
1845 d: 1.0,
1846 e: tx,
1847 f: 0.0,
1848 }
1849 .then(tm);
1850 }
1851 }
1852 }
1853 }
1854 }
1855 }
1856 "m" => {
1857 cur = ctm.apply(op_f(operands, 0), op_f(operands, 1));
1858 start = cur;
1859 }
1860 "l" => {
1861 let p = ctm.apply(op_f(operands, 0), op_f(operands, 1));
1862 path.push(PathPiece::Line(cur, p));
1863 cur = p;
1864 }
1865 "c" | "v" | "y" => {
1866 let pts: Vec<(f64, f64)> = (0..operands.len() / 2)
1867 .map(|k| ctm.apply(op_f(operands, 2 * k), op_f(operands, 2 * k + 1)))
1868 .collect();
1869 if let Some(&end) = pts.last() {
1870 path.push(PathPiece::Curve(
1871 std::iter::once(cur).chain(pts.iter().copied()).collect(),
1872 ));
1873 cur = end;
1874 }
1875 }
1876 "h" => {
1877 if cur != start {
1878 path.push(PathPiece::Line(cur, start));
1879 }
1880 cur = start;
1881 }
1882 "re" => {
1883 let (x, y, w, h) = (
1884 op_f(operands, 0),
1885 op_f(operands, 1),
1886 op_f(operands, 2),
1887 op_f(operands, 3),
1888 );
1889 path.push(PathPiece::Rect([
1890 ctm.apply(x, y),
1891 ctm.apply(x + w, y),
1892 ctm.apply(x + w, y + h),
1893 ctm.apply(x, y + h),
1894 ]));
1895 cur = ctm.apply(x, y);
1896 start = cur;
1897 }
1898 "S" | "s" | "f" | "F" | "f*" | "B" | "B*" | "b" | "b*" => {
1899 let op = op.operator.as_str();
1900 if matches!(op, "s" | "b" | "b*") && cur != start {
1901 path.push(PathPiece::Line(cur, start));
1902 }
1903 let stroke = matches!(op, "S" | "s" | "B" | "B*" | "b" | "b*");
1904 let fill = !matches!(op, "S" | "s");
1905 paint_path(&path, stroke, fill, fill_light, inks);
1906 path.clear();
1907 }
1908 "n" => path.clear(),
1909 "g" | "rg" | "k" | "sc" | "scn" => {
1912 let v: Vec<f64> = operands
1913 .iter()
1914 .map(num)
1915 .collect::<Option<_>>()
1916 .unwrap_or_default();
1917 fill_light = match v.as_slice() {
1918 [g] => *g >= 0.9,
1919 [r, g, b] => r.min(*g).min(*b) >= 0.9,
1920 [c, m, y, k] => c.max(*m).max(*y).max(*k) <= 0.1,
1921 _ => false,
1922 };
1923 }
1924 "cs" => fill_light = false,
1925 "Do" => {
1926 if depth >= 8 {
1929 continue;
1930 }
1931 let Some(Object::Name(n)) = operands.first() else {
1932 continue;
1933 };
1934 let obj = xobjects.and_then(|d| d.get(n.as_slice()).ok());
1935 let form_id = match obj {
1936 Some(Object::Reference(id)) => Some(*id),
1937 _ => None,
1938 };
1939 let stream = obj
1940 .and_then(|o| deref(doc, o))
1941 .and_then(|o| o.as_stream().ok());
1942 let Some(stream) = stream else { continue };
1943 let is_form = stream
1944 .dict
1945 .get(b"Subtype")
1946 .ok()
1947 .and_then(|o| o.as_name().ok())
1948 == Some(b"Form".as_slice());
1949 if !is_form {
1950 continue;
1951 }
1952 let cached = form_id.and_then(|id| caches.forms.get(&id).cloned());
1955 let form_content = match cached {
1956 Some(c) => c,
1957 None => {
1958 let Ok(data) = stream.decompressed_content() else {
1959 continue;
1960 };
1961 let Ok(c) = lopdf::content::Content::decode(&data) else {
1962 continue;
1963 };
1964 let c = Arc::new(c);
1965 if let Some(id) = form_id {
1966 caches.forms.insert(id, Arc::clone(&c));
1967 }
1968 c
1969 }
1970 };
1971 let form_mat = match stream.dict.get(b"Matrix").ok() {
1973 Some(Object::Array(a)) if a.len() == 6 => {
1974 let v: Vec<f64> = a.iter().filter_map(num).collect();
1975 if v.len() == 6 {
1976 Mat {
1977 a: v[0],
1978 b: v[1],
1979 c: v[2],
1980 d: v[3],
1981 e: v[4],
1982 f: v[5],
1983 }
1984 } else {
1985 Mat::ID
1986 }
1987 }
1988 _ => Mat::ID,
1989 };
1990 let form_res = stream
1992 .dict
1993 .get(b"Resources")
1994 .ok()
1995 .and_then(|o| deref(doc, o))
1996 .and_then(|o| o.as_dict().ok())
1997 .unwrap_or(res);
1998 let state = TextState {
1999 tc,
2000 tw,
2001 th,
2002 tl,
2003 trise,
2004 fsize,
2005 };
2006 run_content(
2007 doc,
2008 form_res,
2009 &form_content,
2010 form_mat.then(ctm),
2011 state,
2012 depth + 1,
2013 caches,
2014 out,
2015 inks,
2016 );
2017 }
2018 _ => {}
2019 }
2020 }
2021}
2022
2023enum PathPiece {
2025 Line((f64, f64), (f64, f64)),
2026 Rect([(f64, f64); 4]),
2028 Curve(Vec<(f64, f64)>),
2030}
2031
2032fn paint_path(
2040 path: &[PathPiece],
2041 stroke: bool,
2042 fill: bool,
2043 fill_light: bool,
2044 inks: &mut Vec<crate::checkbox::Ink>,
2045) {
2046 use crate::checkbox::Ink;
2047 let bbox = |pts: &mut dyn Iterator<Item = (f64, f64)>| {
2048 pts.fold(
2049 (
2050 f64::INFINITY,
2051 f64::INFINITY,
2052 f64::NEG_INFINITY,
2053 f64::NEG_INFINITY,
2054 ),
2055 |(l, b, r, t), (x, y)| (l.min(x), b.min(y), r.max(x), t.max(y)),
2056 )
2057 };
2058 let mut keep = |ink: Ink| {
2059 if ink.worth_keeping() {
2060 inks.push(ink);
2061 }
2062 };
2063 if fill && !fill_light {
2064 let (l, b, r, t) = bbox(&mut path.iter().flat_map(|p| match p {
2068 PathPiece::Line(a, z) => vec![*a, *z],
2069 PathPiece::Rect(c) => c.to_vec(),
2070 PathPiece::Curve(c) => c.clone(),
2071 }));
2072 if l.is_finite() {
2073 keep(Ink::Blot { l, b, r, t });
2074 }
2075 return;
2076 }
2077 if !stroke {
2078 return;
2079 }
2080 for piece in path {
2081 match piece {
2082 PathPiece::Line(a, z) => keep(Ink::Seg {
2083 x0: a.0,
2084 y0: a.1,
2085 x1: z.0,
2086 y1: z.1,
2087 }),
2088 PathPiece::Rect(c) => {
2089 let axis = ((c[0].1 - c[1].1).abs() < 1e-6 && (c[1].0 - c[2].0).abs() < 1e-6)
2090 || ((c[0].0 - c[1].0).abs() < 1e-6 && (c[1].1 - c[2].1).abs() < 1e-6);
2091 if axis {
2092 let (l, b, r, t) = bbox(&mut c.iter().copied());
2093 keep(Ink::Rect { l, b, r, t });
2094 } else {
2095 for k in 0..4 {
2096 let (a, z) = (c[k], c[(k + 1) % 4]);
2097 keep(Ink::Seg {
2098 x0: a.0,
2099 y0: a.1,
2100 x1: z.0,
2101 y1: z.1,
2102 });
2103 }
2104 }
2105 }
2106 PathPiece::Curve(c) => {
2107 let (l, b, r, t) = bbox(&mut c.iter().copied());
2108 keep(Ink::Blot { l, b, r, t });
2109 }
2110 }
2111 }
2112}
2113
2114#[allow(clippy::too_many_arguments)]
2115fn show_text(
2116 font: &Font,
2117 bytes: &[u8],
2118 fsize: f64,
2119 tc: f64,
2120 tw: f64,
2121 th: f64,
2122 trise: f64,
2123 tm: &mut Mat,
2124 ctm: Mat,
2125 out: &mut Vec<Glyph>,
2126) {
2127 for code in codes(font, bytes) {
2128 let (text, w) = font.decode_code(code);
2129 let w0 = w / 1000.0; let scale = Mat {
2132 a: fsize * th,
2133 b: 0.0,
2134 c: 0.0,
2135 d: fsize,
2136 e: 0.0,
2137 f: trise,
2138 };
2139 let trm = scale.then(*tm).then(ctm);
2140 let (desc, asc) = (font.descent / 1000.0, font.ascent / 1000.0);
2142 let (x0, y0) = trm.apply(0.0, desc);
2143 let (x1, y1) = trm.apply(w0, desc);
2144 let (x2, y2) = trm.apply(w0, asc);
2145 let (x3, y3) = trm.apply(0.0, asc);
2146 let upright = trm.a > 0.0 && trm.b.abs() <= 1e-6 * trm.a;
2154 let (left, bot, right, top, quad) = if upright {
2155 (x0.min(x1), y0.min(y3), x0.max(x1), y0.max(y3), None)
2156 } else {
2157 let (xs, ys) = ([x0, x1, x2, x3], [y0, y1, y2, y3]);
2158 let fold = |v: [f64; 4], f: fn(f64, f64) -> f64| v.into_iter().reduce(f).unwrap();
2159 let q = [x0, y0, x1, y1, x2, y2, x3, y3].map(|v| v as f32);
2160 (
2161 fold(xs, f64::min),
2162 fold(ys, f64::min),
2163 fold(xs, f64::max),
2164 fold(ys, f64::max),
2165 Some(q),
2166 )
2167 };
2168 if let Some(s) = text {
2169 for ch in s.chars() {
2171 if ch != '\u{0}' {
2172 out.push(Glyph {
2173 ch,
2174 l: left as f32,
2175 b: bot as f32,
2176 r: right as f32,
2177 t: top as f32,
2178 ll: left as f32,
2179 lb: bot as f32,
2180 lr: right as f32,
2181 lt: top as f32,
2182 font: font.hash,
2183 quad,
2184 });
2185 }
2186 }
2187 }
2188 let is_space = !font.two_byte && code == 32;
2190 let tx = (w0 * fsize + tc + if is_space { tw } else { 0.0 }) * th;
2191 *tm = Mat {
2192 a: 1.0,
2193 b: 0.0,
2194 c: 0.0,
2195 d: 1.0,
2196 e: tx,
2197 f: 0.0,
2198 }
2199 .then(*tm);
2200 }
2201}
2202
2203fn simple_encoding_table(doc: &Document, fdict: &Dictionary) -> HashMap<u8, char> {
2207 let enc = fdict.get(b"Encoding").ok().and_then(|o| deref(doc, o));
2208 let base_name = match enc {
2209 Some(Object::Name(n)) => n.clone(),
2210 Some(Object::Dictionary(d)) => d
2211 .get(b"BaseEncoding")
2212 .ok()
2213 .and_then(|o| o.as_name().ok())
2214 .map(|n| n.to_vec())
2215 .unwrap_or_default(),
2216 _ => Vec::new(),
2217 };
2218 let mut m = if base_name == b"MacRomanEncoding" {
2219 macroman_table()
2220 } else if base_name.is_empty() {
2221 tex_math_builtin(fdict).unwrap_or_else(winansi_table)
2228 } else {
2229 winansi_table()
2230 };
2231 if let Some(Object::Dictionary(d)) = enc {
2233 if let Some(Object::Array(diffs)) = d.get(b"Differences").ok().and_then(|o| deref(doc, o)) {
2234 let mut code = 0u8;
2235 for el in diffs {
2236 match el {
2237 Object::Integer(i) => code = *i as u8,
2238 Object::Name(name) => {
2239 if let Some(ch) = glyph_name_to_char(name) {
2240 m.insert(code, ch);
2241 }
2242 code = code.wrapping_add(1);
2243 }
2244 _ => {}
2245 }
2246 }
2247 }
2248 }
2249 m
2250}
2251
2252fn tex_math_builtin(fdict: &Dictionary) -> Option<HashMap<u8, char>> {
2258 const CMSY: [char; 128] = [
2259 '−', '·', '×', '∗', '÷', '⋄', '±', '∓', '⊕', '⊖', '⊗', '⊘', '⊙', '◯', '∘', '•', '≍', '≡',
2260 '⊆', '⊇', '≤', '≥', '≼', '≽', '∼', '≈', '⊂', '⊃', '≪', '≫', '≺', '≻', '←', '→', '↑', '↓',
2261 '↔', '↗', '↘', '≃', '⇐', '⇒', '⇑', '⇓', '⇔', '↖', '↙', '∝', '′', '∞', '∈', '∋', '△', '▽',
2262 '\u{338}', '↦', '∀', '∃', '¬', '∅', 'ℜ', 'ℑ', '⊤', '⊥', 'ℵ', 'A', 'B', 'C', 'D', 'E', 'F',
2263 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
2264 'Y', 'Z', '∪', '∩', '⊎', '∧', '∨', '⊢', '⊣', '⌊', '⌋', '⌈', '⌉', '{', '}', '⟨', '⟩', '|',
2265 '∥', '↕', '⇕', '\\', '≀', '√', '∐', '∇', '∫', '⊔', '⊓', '⊑', '⊒', '§', '†', '‡', '¶', '♣',
2266 '♢', '♡', '♠',
2267 ];
2268 const CMMI: [char; 128] = [
2269 'Γ', 'Δ', 'Θ', 'Λ', 'Ξ', 'Π', 'Σ', 'Υ', 'Φ', 'Ψ', 'Ω', 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η',
2270 'θ', 'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'π', 'ρ', 'σ', 'τ', 'υ', 'φ', 'χ', 'ψ', 'ω', 'ϵ', 'ϑ',
2271 'ϖ', 'ϱ', 'ς', 'ϕ', '↼', '↽', '⇀', '⇁', '↩', '↪', '▷', '◁', '0', '1', '2', '3', '4', '5',
2272 '6', '7', '8', '9', '.', ',', '<', '/', '>', '⋆', '∂', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
2273 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y',
2274 'Z', '♭', '♮', '♯', '⌣', '⌢', 'ℓ', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
2275 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'ı', 'ȷ', '℘',
2276 '\u{20d7}', '⁀',
2277 ];
2278 let name = base_font_name(fdict)?;
2279 let up = name.to_ascii_uppercase();
2280 let table: &[char; 128] = if up.starts_with(b"CMSY") || up.starts_with(b"CMBSY") {
2281 &CMSY
2282 } else if up.starts_with(b"CMMI") {
2283 &CMMI
2284 } else {
2285 return None;
2286 };
2287 Some(
2288 table
2289 .iter()
2290 .enumerate()
2291 .map(|(i, &c)| (i as u8, c))
2292 .collect(),
2293 )
2294}
2295
2296const AGL_LATIN: &[(&str, char)] = &[
2304 ("AE", '\u{00C6}'),
2305 ("Aacute", '\u{00C1}'),
2306 ("Abreve", '\u{0102}'),
2307 ("Acircumflex", '\u{00C2}'),
2308 ("Adieresis", '\u{00C4}'),
2309 ("Agrave", '\u{00C0}'),
2310 ("Amacron", '\u{0100}'),
2311 ("Aogonek", '\u{0104}'),
2312 ("Aring", '\u{00C5}'),
2313 ("Atilde", '\u{00C3}'),
2314 ("Cacute", '\u{0106}'),
2315 ("Ccaron", '\u{010C}'),
2316 ("Ccedilla", '\u{00C7}'),
2317 ("Ccircumflex", '\u{0108}'),
2318 ("Cdotaccent", '\u{010A}'),
2319 ("Dcaron", '\u{010E}'),
2320 ("Dcroat", '\u{0110}'),
2321 ("Eacute", '\u{00C9}'),
2322 ("Ebreve", '\u{0114}'),
2323 ("Ecaron", '\u{011A}'),
2324 ("Ecircumflex", '\u{00CA}'),
2325 ("Edieresis", '\u{00CB}'),
2326 ("Edotaccent", '\u{0116}'),
2327 ("Egrave", '\u{00C8}'),
2328 ("Emacron", '\u{0112}'),
2329 ("Eng", '\u{014A}'),
2330 ("Eogonek", '\u{0118}'),
2331 ("Eth", '\u{00D0}'),
2332 ("Euro", '\u{20AC}'),
2333 ("Gbreve", '\u{011E}'),
2334 ("Gcircumflex", '\u{011C}'),
2335 ("Gcommaaccent", '\u{0122}'),
2336 ("Gdotaccent", '\u{0120}'),
2337 ("Hbar", '\u{0126}'),
2338 ("Hcircumflex", '\u{0124}'),
2339 ("IJ", '\u{0132}'),
2340 ("Iacute", '\u{00CD}'),
2341 ("Ibreve", '\u{012C}'),
2342 ("Icircumflex", '\u{00CE}'),
2343 ("Idieresis", '\u{00CF}'),
2344 ("Idotaccent", '\u{0130}'),
2345 ("Igrave", '\u{00CC}'),
2346 ("Imacron", '\u{012A}'),
2347 ("Iogonek", '\u{012E}'),
2348 ("Itilde", '\u{0128}'),
2349 ("Jcircumflex", '\u{0134}'),
2350 ("Kcommaaccent", '\u{0136}'),
2351 ("Lacute", '\u{0139}'),
2352 ("Lcaron", '\u{013D}'),
2353 ("Lcommaaccent", '\u{013B}'),
2354 ("Ldot", '\u{013F}'),
2355 ("Lslash", '\u{0141}'),
2356 ("Nacute", '\u{0143}'),
2357 ("Ncaron", '\u{0147}'),
2358 ("Ncommaaccent", '\u{0145}'),
2359 ("Ntilde", '\u{00D1}'),
2360 ("OE", '\u{0152}'),
2361 ("Oacute", '\u{00D3}'),
2362 ("Obreve", '\u{014E}'),
2363 ("Ocircumflex", '\u{00D4}'),
2364 ("Odieresis", '\u{00D6}'),
2365 ("Ograve", '\u{00D2}'),
2366 ("Ohungarumlaut", '\u{0150}'),
2367 ("Omacron", '\u{014C}'),
2368 ("Oslash", '\u{00D8}'),
2369 ("Otilde", '\u{00D5}'),
2370 ("Racute", '\u{0154}'),
2371 ("Rcaron", '\u{0158}'),
2372 ("Rcommaaccent", '\u{0156}'),
2373 ("Sacute", '\u{015A}'),
2374 ("Scaron", '\u{0160}'),
2375 ("Scedilla", '\u{015E}'),
2376 ("Scircumflex", '\u{015C}'),
2377 ("Scommaaccent", '\u{0218}'),
2378 ("Tbar", '\u{0166}'),
2379 ("Tcaron", '\u{0164}'),
2380 ("Tcommaaccent", '\u{0162}'),
2381 ("Thorn", '\u{00DE}'),
2382 ("Uacute", '\u{00DA}'),
2383 ("Ubreve", '\u{016C}'),
2384 ("Ucircumflex", '\u{00DB}'),
2385 ("Udieresis", '\u{00DC}'),
2386 ("Ugrave", '\u{00D9}'),
2387 ("Uhungarumlaut", '\u{0170}'),
2388 ("Umacron", '\u{016A}'),
2389 ("Uogonek", '\u{0172}'),
2390 ("Uring", '\u{016E}'),
2391 ("Utilde", '\u{0168}'),
2392 ("Wcircumflex", '\u{0174}'),
2393 ("Yacute", '\u{00DD}'),
2394 ("Ycircumflex", '\u{0176}'),
2395 ("Ydieresis", '\u{0178}'),
2396 ("Zacute", '\u{0179}'),
2397 ("Zcaron", '\u{017D}'),
2398 ("Zdotaccent", '\u{017B}'),
2399 ("aacute", '\u{00E1}'),
2400 ("abreve", '\u{0103}'),
2401 ("acircumflex", '\u{00E2}'),
2402 ("acute", '\u{00B4}'),
2403 ("adieresis", '\u{00E4}'),
2404 ("ae", '\u{00E6}'),
2405 ("agrave", '\u{00E0}'),
2406 ("amacron", '\u{0101}'),
2407 ("aogonek", '\u{0105}'),
2408 ("aring", '\u{00E5}'),
2409 ("atilde", '\u{00E3}'),
2410 ("brokenbar", '\u{00A6}'),
2411 ("cacute", '\u{0107}'),
2412 ("ccaron", '\u{010D}'),
2413 ("ccedilla", '\u{00E7}'),
2414 ("ccircumflex", '\u{0109}'),
2415 ("cdotaccent", '\u{010B}'),
2416 ("cedilla", '\u{00B8}'),
2417 ("cent", '\u{00A2}'),
2418 ("currency", '\u{00A4}'),
2419 ("dcaron", '\u{010F}'),
2420 ("dcroat", '\u{0111}'),
2421 ("dieresis", '\u{00A8}'),
2422 ("dotlessi", '\u{0131}'),
2423 ("eacute", '\u{00E9}'),
2424 ("ebreve", '\u{0115}'),
2425 ("ecaron", '\u{011B}'),
2426 ("ecircumflex", '\u{00EA}'),
2427 ("edieresis", '\u{00EB}'),
2428 ("edotaccent", '\u{0117}'),
2429 ("egrave", '\u{00E8}'),
2430 ("emacron", '\u{0113}'),
2431 ("eng", '\u{014B}'),
2432 ("eogonek", '\u{0119}'),
2433 ("eth", '\u{00F0}'),
2434 ("exclamdown", '\u{00A1}'),
2435 ("florin", '\u{0192}'),
2436 ("gbreve", '\u{011F}'),
2437 ("gcircumflex", '\u{011D}'),
2438 ("gcommaaccent", '\u{0123}'),
2439 ("gdotaccent", '\u{0121}'),
2440 ("germandbls", '\u{00DF}'),
2441 ("guillemotleft", '\u{00AB}'),
2442 ("guillemotright", '\u{00BB}'),
2443 ("hbar", '\u{0127}'),
2444 ("hcircumflex", '\u{0125}'),
2445 ("iacute", '\u{00ED}'),
2446 ("ibreve", '\u{012D}'),
2447 ("icircumflex", '\u{00EE}'),
2448 ("idieresis", '\u{00EF}'),
2449 ("igrave", '\u{00EC}'),
2450 ("ij", '\u{0133}'),
2451 ("imacron", '\u{012B}'),
2452 ("iogonek", '\u{012F}'),
2453 ("itilde", '\u{0129}'),
2454 ("jcircumflex", '\u{0135}'),
2455 ("kcommaaccent", '\u{0137}'),
2456 ("kgreenlandic", '\u{0138}'),
2457 ("lacute", '\u{013A}'),
2458 ("lcaron", '\u{013E}'),
2459 ("lcommaaccent", '\u{013C}'),
2460 ("ldot", '\u{0140}'),
2461 ("logicalnot", '\u{00AC}'),
2462 ("longs", '\u{017F}'),
2463 ("lslash", '\u{0142}'),
2464 ("macron", '\u{00AF}'),
2465 ("nacute", '\u{0144}'),
2466 ("napostrophe", '\u{0149}'),
2467 ("ncaron", '\u{0148}'),
2468 ("ncommaaccent", '\u{0146}'),
2469 ("ntilde", '\u{00F1}'),
2470 ("oacute", '\u{00F3}'),
2471 ("obreve", '\u{014F}'),
2472 ("ocircumflex", '\u{00F4}'),
2473 ("odieresis", '\u{00F6}'),
2474 ("oe", '\u{0153}'),
2475 ("ograve", '\u{00F2}'),
2476 ("ohungarumlaut", '\u{0151}'),
2477 ("omacron", '\u{014D}'),
2478 ("onehalf", '\u{00BD}'),
2479 ("onequarter", '\u{00BC}'),
2480 ("onesuperior", '\u{00B9}'),
2481 ("ordfeminine", '\u{00AA}'),
2482 ("ordmasculine", '\u{00BA}'),
2483 ("oslash", '\u{00F8}'),
2484 ("otilde", '\u{00F5}'),
2485 ("paragraph", '\u{00B6}'),
2486 ("questiondown", '\u{00BF}'),
2487 ("racute", '\u{0155}'),
2488 ("rcaron", '\u{0159}'),
2489 ("rcommaaccent", '\u{0157}'),
2490 ("sacute", '\u{015B}'),
2491 ("scaron", '\u{0161}'),
2492 ("scedilla", '\u{015F}'),
2493 ("scircumflex", '\u{015D}'),
2494 ("scommaaccent", '\u{0219}'),
2495 ("section", '\u{00A7}'),
2496 ("sfthyphen", '\u{00AD}'),
2497 ("sterling", '\u{00A3}'),
2498 ("tbar", '\u{0167}'),
2499 ("tcaron", '\u{0165}'),
2500 ("tcommaaccent", '\u{0163}'),
2501 ("thorn", '\u{00FE}'),
2502 ("threequarters", '\u{00BE}'),
2503 ("threesuperior", '\u{00B3}'),
2504 ("twosuperior", '\u{00B2}'),
2505 ("uacute", '\u{00FA}'),
2506 ("ubreve", '\u{016D}'),
2507 ("ucircumflex", '\u{00FB}'),
2508 ("udieresis", '\u{00FC}'),
2509 ("ugrave", '\u{00F9}'),
2510 ("uhungarumlaut", '\u{0171}'),
2511 ("umacron", '\u{016B}'),
2512 ("uogonek", '\u{0173}'),
2513 ("uring", '\u{016F}'),
2514 ("utilde", '\u{0169}'),
2515 ("wcircumflex", '\u{0175}'),
2516 ("yacute", '\u{00FD}'),
2517 ("ycircumflex", '\u{0177}'),
2518 ("ydieresis", '\u{00FF}'),
2519 ("yen", '\u{00A5}'),
2520 ("zacute", '\u{017A}'),
2521 ("zcaron", '\u{017E}'),
2522 ("zdotaccent", '\u{017C}'),
2523];
2524
2525pub(crate) fn glyph_name_to_char(name: &[u8]) -> Option<char> {
2530 let s = std::str::from_utf8(name).ok()?;
2531 if let Some(hex) = s.strip_prefix("uni") {
2532 if let Ok(cp) = u32::from_str_radix(hex.get(0..4)?, 16) {
2533 return char::from_u32(cp);
2534 }
2535 }
2536 if s.len() == 1 {
2538 let b = s.as_bytes()[0];
2539 if b.is_ascii_alphabetic() {
2540 return Some(b as char);
2541 }
2542 }
2543 let resolved = match s {
2544 "space" => ' ',
2545 "exclam" => '!',
2546 "quotedbl" => '"',
2547 "numbersign" => '#',
2548 "dollar" => '$',
2549 "percent" => '%',
2550 "ampersand" => '&',
2551 "quotesingle" => '\'',
2552 "parenleft" => '(',
2553 "parenright" => ')',
2554 "asterisk" => '*',
2555 "plus" => '+',
2556 "comma" => ',',
2557 "hyphen" => '-',
2558 "period" => '.',
2559 "slash" => '/',
2560 "zero" => '0',
2561 "one" => '1',
2562 "two" => '2',
2563 "three" => '3',
2564 "four" => '4',
2565 "five" => '5',
2566 "six" => '6',
2567 "seven" => '7',
2568 "eight" => '8',
2569 "nine" => '9',
2570 "colon" => ':',
2571 "semicolon" => ';',
2572 "less" => '<',
2573 "equal" => '=',
2574 "greater" => '>',
2575 "question" => '?',
2576 "at" => '@',
2577 "bracketleft" => '[',
2578 "backslash" => '\\',
2579 "bracketright" => ']',
2580 "asciicircum" => '^',
2581 "underscore" => '_',
2582 "grave" => '`',
2583 "braceleft" => '{',
2584 "bar" => '|',
2585 "braceright" => '}',
2586 "asciitilde" => '~',
2587 "bullet" => '\u{2022}',
2588 "periodcentered" => '\u{00B7}',
2589 "endash" => '\u{2013}',
2590 "emdash" => '\u{2014}',
2591 "quoteright" => '\u{2019}',
2592 "quoteleft" => '\u{2018}',
2593 "quotedblleft" => '\u{201C}',
2594 "quotedblright" => '\u{201D}',
2595 "quotedblbase" => '\u{201E}',
2596 "quotesinglbase" => '\u{201A}',
2597 "ff" => '\u{FB00}',
2602 "fi" => '\u{FB01}',
2603 "fl" => '\u{FB02}',
2604 "ffi" => '\u{FB03}',
2605 "ffl" => '\u{FB04}',
2606 "ft" => '\u{FB05}',
2607 "st" => '\u{FB06}',
2608 "degree" => '\u{00B0}',
2609 "trademark" => '\u{2122}',
2610 "registered" => '\u{00AE}',
2611 "copyright" => '\u{00A9}',
2612 "ellipsis" => '\u{2026}',
2613 "minus" => '\u{2212}',
2614 "fraction" => '\u{2044}',
2615 "nbspace" => '\u{00A0}',
2616 "alpha" => '\u{03B1}',
2621 "beta" => '\u{03B2}',
2622 "gamma" => '\u{03B3}',
2623 "delta" => '\u{03B4}',
2624 "epsilon" | "epsilon1" => '\u{03B5}',
2625 "zeta" => '\u{03B6}',
2626 "eta" => '\u{03B7}',
2627 "theta" | "theta1" => '\u{03B8}',
2628 "iota" => '\u{03B9}',
2629 "kappa" => '\u{03BA}',
2630 "lambda" => '\u{03BB}',
2631 "mu" => '\u{03BC}',
2632 "nu" => '\u{03BD}',
2633 "xi" => '\u{03BE}',
2634 "omicron" => '\u{03BF}',
2635 "pi" | "pi1" => '\u{03C0}',
2636 "rho" | "rho1" => '\u{03C1}',
2637 "sigma" => '\u{03C3}',
2638 "sigma1" => '\u{03C2}',
2639 "tau" => '\u{03C4}',
2640 "upsilon" => '\u{03C5}',
2641 "phi" | "phi1" => '\u{03C6}',
2642 "chi" => '\u{03C7}',
2643 "psi" => '\u{03C8}',
2644 "omega" | "omega1" => '\u{03C9}',
2645 "Gamma" => '\u{0393}',
2646 "Delta" => '\u{0394}',
2647 "Theta" => '\u{0398}',
2648 "Lambda" => '\u{039B}',
2649 "Xi" => '\u{039E}',
2650 "Pi" => '\u{03A0}',
2651 "Sigma" => '\u{03A3}',
2652 "Upsilon" => '\u{03A5}',
2653 "Phi" => '\u{03A6}',
2654 "Psi" => '\u{03A8}',
2655 "Omega" => '\u{03A9}',
2656 "lessequal" => '\u{2264}',
2657 "greaterequal" => '\u{2265}',
2658 "notequal" => '\u{2260}',
2659 "approxequal" => '\u{2248}',
2660 "equivalence" => '\u{2261}',
2661 "element" => '\u{2208}',
2662 "plusminus" => '\u{00B1}',
2663 "multiply" => '\u{00D7}',
2664 "divide" => '\u{00F7}',
2665 "infinity" => '\u{221E}',
2666 "partialdiff" => '\u{2202}',
2667 "gradient" => '\u{2207}',
2668 "summation" => '\u{2211}',
2669 "product" => '\u{220F}',
2670 "integral" => '\u{222B}',
2671 "radical" => '\u{221A}',
2672 "proportional" => '\u{221D}',
2673 "arrowright" => '\u{2192}',
2674 "arrowleft" => '\u{2190}',
2675 "arrowup" => '\u{2191}',
2676 "arrowdown" => '\u{2193}',
2677 "arrowboth" => '\u{2194}',
2678 "arrowdblright" => '\u{21D2}',
2679 "logicaland" => '\u{2227}',
2680 "logicalor" => '\u{2228}',
2681 "intersection" => '\u{2229}',
2682 "union" => '\u{222A}',
2683 "similar" => '\u{223C}',
2684 "congruent" => '\u{2245}',
2685 "dotmath" => '\u{22C5}',
2686 "asteriskmath" => '\u{2217}',
2687 _ => {
2688 if let Ok(i) = AGL_LATIN.binary_search_by_key(&s, |(n, _)| n) {
2689 return Some(AGL_LATIN[i].1);
2690 }
2691 if let Some((base, _)) = s.split_once('.') {
2693 if !base.is_empty() {
2694 return glyph_name_to_char(base.as_bytes());
2695 }
2696 }
2697 if s.contains('_') {
2701 let joined: String = s.chars().filter(|&c| c != '_').collect();
2702 if !joined.is_empty() && joined.len() < s.len() {
2703 return glyph_name_to_char(joined.as_bytes());
2704 }
2705 }
2706 return None;
2707 }
2708 };
2709 Some(resolved)
2710}
2711
2712fn winansi_table() -> HashMap<u8, char> {
2714 let mut m = HashMap::new();
2715 for b in 0x20u8..=0x7e {
2716 m.insert(b, b as char);
2717 }
2718 let extra: &[(u8, char)] = &[
2720 (0x91, '\u{2018}'),
2721 (0x92, '\u{2019}'),
2722 (0x93, '\u{201C}'),
2723 (0x94, '\u{201D}'),
2724 (0x95, '\u{2022}'),
2725 (0x96, '\u{2013}'),
2726 (0x97, '\u{2014}'),
2727 (0x85, '\u{2026}'),
2728 (0xA0, '\u{00A0}'),
2729 ];
2730 for &(b, c) in extra {
2731 m.insert(b, c);
2732 }
2733 for b in 0xA1u8..=0xFF {
2734 m.entry(b).or_insert(b as char);
2735 }
2736 m
2737}
2738
2739fn macroman_table() -> HashMap<u8, char> {
2746 #[rustfmt::skip]
2747 const HIGH: [char; 128] = [
2748 'Ä', 'Å', 'Ç', 'É', 'Ñ', 'Ö', 'Ü', 'á', 'à', 'â', 'ä', 'ã', 'å', 'ç', 'é', 'è', 'ê', 'ë', 'í', 'ì', 'î', 'ï', 'ñ', 'ó', 'ò', 'ô', 'ö', 'õ', 'ú', 'ù', 'û', 'ü', '†', '°', '¢', '£', '§', '•', '¶', 'ß', '®', '©', '™', '´', '¨', '≠', 'Æ', 'Ø', '∞', '±', '≤', '≥', '¥', 'µ', '∂', '∑', '∏', 'π', '∫', 'ª', 'º', 'Ω', 'æ', 'ø', '¿', '¡', '¬', '√', 'ƒ', '≈', '∆', '«', '»', '…', '\u{00A0}', 'À', 'Ã', 'Õ', 'Œ', 'œ', '–', '—', '“', '”', '‘', '’', '÷', '◊', 'ÿ', 'Ÿ', '⁄', '¤', '‹', '›', 'fi', 'fl', '‡', '·', '‚', '„', '‰', 'Â', 'Ê', 'Á', 'Ë', 'È', 'Í', 'Î', 'Ï', 'Ì', 'Ó', 'Ô', '\0', 'Ò', 'Ú', 'Û', 'Ù', 'ı', 'ˆ', '˜', '¯', '˘', '˙', '˚', '¸', '˝', '˛', 'ˇ', ];
2765 let mut m = HashMap::new();
2766 for b in 0x20u8..=0x7e {
2767 m.insert(b, b as char);
2768 }
2769 for (b, &c) in (0x80u8..=0xFF).zip(HIGH.iter()) {
2770 if c != '\0' {
2771 m.insert(b, c);
2772 }
2773 }
2774 m
2775}
2776
2777#[cfg(test)]
2778mod glyph_names {
2779 #[test]
2783 fn latin_glyph_names_resolve() {
2784 for w in super::AGL_LATIN.windows(2) {
2785 assert!(
2786 w[0].0 < w[1].0,
2787 "AGL_LATIN not sorted at {:?} / {:?}",
2788 w[0].0,
2789 w[1].0
2790 );
2791 }
2792 for (name, ch) in [
2793 ("Lslash", 'Ł'),
2794 ("lslash", 'ł'),
2795 ("Aacute", 'Á'),
2796 ("aacute", 'á'),
2797 ("Scaron", 'Š'),
2798 ("germandbls", 'ß'),
2799 ("oe", 'œ'),
2800 ("Ydieresis", 'Ÿ'),
2801 ("Euro", '€'),
2802 ("Lslash.sc", 'Ł'),
2803 ("fi", '\u{FB01}'),
2804 ("Omega", '\u{03A9}'),
2805 ] {
2806 assert_eq!(
2807 super::glyph_name_to_char(name.as_bytes()),
2808 Some(ch),
2809 "{name}"
2810 );
2811 }
2812 assert_eq!(super::glyph_name_to_char(b"g123"), None);
2813 assert_eq!(super::glyph_name_to_char(b"f_f_i"), Some('\u{FB03}'));
2815 assert_eq!(super::glyph_name_to_char(b"f_l"), Some('\u{FB02}'));
2816 assert_eq!(super::glyph_name_to_char(b"a_b"), None);
2817 }
2818
2819 #[test]
2823 fn a_stale_tounicode_yields_to_differences() {
2824 use lopdf::Object;
2825 use std::collections::HashMap;
2826 let mut tu: HashMap<u32, String> = HashMap::new();
2827 tu.insert(33, "!".into()); tu.insert(34, "x".into()); let diffs = vec![
2830 Object::Integer(33),
2831 Object::Name(b"f_f_i".to_vec()),
2832 Object::Name(b"f_i".to_vec()),
2833 Object::Name(b"f_f".to_vec()),
2834 Object::Integer(65),
2835 Object::Name(b"A".to_vec()), ];
2837 super::reconcile_stale_tounicode(&mut tu, &diffs);
2838 assert_eq!(tu.get(&33).map(String::as_str), Some("\u{FB03}"));
2839 assert_eq!(tu.get(&34).map(String::as_str), Some("x"));
2840 assert_eq!(tu.get(&35), None);
2841 assert_eq!(tu.get(&65), None);
2842 }
2843}
2844
2845#[cfg(test)]
2846mod page_box_frame {
2847 use super::*;
2848
2849 fn pdf(boxes: &str, x: f32, y: f32) -> Vec<u8> {
2852 pdf_content(
2853 boxes,
2854 &format!("BT /F1 12 Tf {x} {y} Td (First printing) Tj ET\n"),
2855 )
2856 }
2857
2858 fn pdf_content(boxes: &str, content: &str) -> Vec<u8> {
2861 let objs: Vec<String> = vec![
2862 "<</Type/Catalog/Pages 2 0 R>>".into(),
2863 format!("<</Type/Pages/Kids[3 0 R]/Count 1{boxes}>>"),
2864 "<</Type/Page/Parent 2 0 R/Contents 4 0 R/Resources<</Font<</F1 5 0 R>>>>>>".into(),
2865 format!("<</Length {}>>stream\n{content}endstream", content.len()),
2866 "<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>".into(),
2867 ];
2868 let mut out = b"%PDF-1.4\n".to_vec();
2869 let mut offsets = Vec::new();
2870 for (i, body) in objs.iter().enumerate() {
2871 offsets.push(out.len());
2872 out.extend_from_slice(format!("{} 0 obj{body}endobj\n", i + 1).as_bytes());
2873 }
2874 let xref_at = out.len();
2875 out.extend_from_slice(
2876 format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes(),
2877 );
2878 for off in &offsets {
2879 out.extend_from_slice(format!("{off:010} 00000 n \n").as_bytes());
2880 }
2881 out.extend_from_slice(
2882 format!(
2883 "trailer<</Size {}/Root 1 0 R>>\nstartxref\n{xref_at}\n%%EOF\n",
2884 objs.len() + 1
2885 )
2886 .as_bytes(),
2887 );
2888 out
2889 }
2890
2891 fn only_page(bytes: &[u8]) -> (PageBox, Vec<Glyph>) {
2892 let doc = load_document(bytes).expect("loads");
2893 let pid = *doc.get_pages().values().next().expect("one page");
2894 (page_box(&doc, pid), page_glyphs(&doc, pid))
2895 }
2896
2897 #[test]
2902 fn glyphs_count_from_the_cropbox_corner_like_pdfium() {
2903 let (pb, shifted) = only_page(&pdf(
2904 "/MediaBox[-56.505 -58.25 576.303 723.31]/CropBox[1.095 -0.65 518.703 665.71]",
2905 37.0 + 1.095,
2906 58.0 - 0.65,
2907 ));
2908 assert!((pb.l - 1.095).abs() < 1e-3 && (pb.b + 0.65).abs() < 1e-3);
2909 assert!(
2910 (pb.w - 517.608).abs() < 1e-3 && (pb.h - 666.36).abs() < 1e-3,
2911 "{pb:?}"
2912 );
2913 let (pb0, plain) = only_page(&pdf("/MediaBox[0 0 517.608 666.36]", 37.0, 58.0));
2914 assert!((pb0.w - pb.w).abs() < 1e-3 && (pb0.h - pb.h).abs() < 1e-3);
2915 assert_eq!(shifted.len(), plain.len());
2916 assert!(!plain.is_empty());
2917 for (a, b) in shifted.iter().zip(&plain) {
2918 assert!(
2919 (a.l - b.l).abs() < 1e-3 && (a.b - b.b).abs() < 1e-3,
2920 "{:?} vs {:?}",
2921 (a.l, a.b),
2922 (b.l, b.b)
2923 );
2924 }
2925 assert!((plain[0].l - 37.0).abs() < 1e-3, "{}", plain[0].l);
2926 }
2927
2928 fn text(glyphs: &[Glyph]) -> String {
2929 glyphs.iter().map(|g| g.ch).collect()
2930 }
2931
2932 #[test]
2937 fn glyphs_outside_the_display_box_are_dropped() {
2938 let (_, g) = only_page(&pdf_content(
2939 "/MediaBox[0 0 400 400]/CropBox[100 100 400 400]",
2940 "BT /F1 14 Tf 120 300 Td (Visible.) Tj ET\nBT /F1 8 Tf 5 40 Td (Slug) Tj ET\n",
2941 ));
2942 assert_eq!(text(&g), "Visible.");
2943 let (_, g) = only_page(&pdf_content(
2944 "/MediaBox[0 0 300 400]",
2945 "BT /F1 14 Tf 40 300 Td (On page) Tj ET\nBT /F1 14 Tf 400 250 Td (Beyond) Tj ET\n",
2946 ));
2947 assert_eq!(text(&g), "On page");
2948 let (_, g) = only_page(&pdf_content(
2950 "/MediaBox[0 0 300 400]",
2951 "BT /F1 14 Tf 200 250 Td (Crossing the right edge) Tj ET\n",
2952 ));
2953 assert_eq!(text(&g), "Crossing the rig");
2954 }
2955
2956 #[test]
2961 fn a_glyph_on_the_edge_stays_one_over_it_goes() {
2962 let w_at = |x: f32, y: f32| {
2963 let (_, g) = only_page(&pdf_content(
2964 "/MediaBox[0 0 300 400]",
2965 &format!("BT /F1 50 Tf {x} {y} Td (W) Tj ET\n"),
2966 ));
2967 text(&g)
2968 };
2969 assert_eq!(w_at(252.8, 200.0), "W");
2970 assert_eq!(w_at(252.81, 200.0), "");
2971 assert_eq!(w_at(0.0, 200.0), "W");
2972 assert_eq!(w_at(-0.01, 200.0), "");
2973 assert_eq!(w_at(100.0, 20.0), "W");
2976 assert_eq!(w_at(100.0, 0.0), "");
2977 assert_eq!(w_at(100.0, 380.0), "");
2978 }
2979
2980 #[test]
2983 fn page_box_follows_pdfium_fallbacks() {
2984 let (pb, _) = only_page(&pdf("", 10.0, 10.0));
2985 assert_eq!((pb.l, pb.b, pb.w, pb.h), (0.0, 0.0, 612.0, 792.0));
2986 let (pb, _) = only_page(&pdf(
2987 "/MediaBox[0 0 500 700]/CropBox[-100 100 600 900]",
2988 10.0,
2989 10.0,
2990 ));
2991 assert_eq!((pb.l, pb.b, pb.w, pb.h), (0.0, 100.0, 500.0, 600.0));
2992 let (pb, _) = only_page(&pdf(
2993 "/MediaBox[0 0 500 700]/CropBox[800 800 900 900]",
2994 10.0,
2995 10.0,
2996 ));
2997 assert_eq!((pb.l, pb.b, pb.w, pb.h), (0.0, 0.0, 500.0, 700.0));
2998 let (pb, _) = only_page(&pdf("/MediaBox[500 700 0 0]", 10.0, 10.0));
3000 assert_eq!((pb.l, pb.b, pb.w, pb.h), (0.0, 0.0, 500.0, 700.0));
3001 }
3002}
3003
3004#[cfg(test)]
3005mod xref_repair {
3006 fn pdf_with_xref(two_byte_eol: bool) -> Vec<u8> {
3010 let content = b"BT /F1 12 Tf 72 700 Td (Invoice 922769430725) Tj ET\n";
3011 let stream = format!("<</Length {}>>stream\n", content.len()).into_bytes();
3012 let objs: Vec<Vec<u8>> = vec![
3013 b"<</Type/Catalog/Pages 2 0 R>>".to_vec(),
3014 b"<</Type/Pages/Kids[3 0 R]/Count 1>>".to_vec(),
3015 b"<</Type/Page/Parent 2 0 R/MediaBox[0 0 595 842]/Contents 4 0 R\
3016 /Resources<</Font<</F1 5 0 R>>>>>>"
3017 .to_vec(),
3018 [stream.as_slice(), content.as_slice(), b"endstream"].concat(),
3019 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>".to_vec(),
3020 ];
3021
3022 let mut out = b"%PDF-1.4\n".to_vec();
3023 let mut offsets = Vec::new();
3024 for (i, body) in objs.iter().enumerate() {
3025 offsets.push(out.len());
3026 out.extend_from_slice(format!("{} 0 obj", i + 1).as_bytes());
3027 out.extend_from_slice(body);
3028 out.extend_from_slice(b"endobj\n");
3029 }
3030 let xref_at = out.len();
3031 let eol: &[u8] = if two_byte_eol { b" \n" } else { b"\n" };
3032 out.extend_from_slice(format!("xref\n0 {}\n", objs.len() + 1).as_bytes());
3033 out.extend_from_slice(b"0000000000 65535 f");
3034 out.extend_from_slice(eol);
3035 for off in &offsets {
3036 out.extend_from_slice(format!("{off:010} 00000 n").as_bytes());
3037 out.extend_from_slice(eol);
3038 }
3039 out.extend_from_slice(
3040 format!("trailer<</Size {}/Root 1 0 R>>\n", objs.len() + 1).as_bytes(),
3041 );
3042 out.extend_from_slice(format!("startxref\n{xref_at}\n%%EOF\n").as_bytes());
3043 out
3044 }
3045
3046 #[test]
3052 fn short_xref_entries_still_parse() {
3053 let good = pdf_with_xref(true);
3054 let broken = pdf_with_xref(false);
3055 assert!(
3056 broken.len() < good.len(),
3057 "the broken file is the shorter one"
3058 );
3059 assert!(
3060 lopdf::Document::load_mem(&good).is_ok(),
3061 "the control file must load unaided"
3062 );
3063 assert!(
3064 lopdf::Document::load_mem(&broken).is_err(),
3065 "lopdf rejects 19-byte entries — if this ever passes, drop the repair"
3066 );
3067
3068 let cells = |b: &[u8]| -> Vec<String> {
3069 super::pdf_textlines(b)
3070 .into_iter()
3071 .flat_map(|(_, _, c)| c.into_iter().map(|c| c.text))
3072 .collect()
3073 };
3074 let from_good = cells(&good);
3075 assert!(
3076 from_good.iter().any(|t| t.contains("922769430725")),
3077 "control text: {from_good:?}"
3078 );
3079 assert_eq!(
3080 cells(&broken),
3081 from_good,
3082 "repair must match the good parse"
3083 );
3084 }
3085
3086 #[test]
3091 fn overstated_stream_length_still_yields_content() {
3092 let good = pdf_with_xref(true);
3093 let broken = {
3096 let at = good
3097 .windows(8)
3098 .position(|w| w == b"/Length ")
3099 .expect("a /Length")
3100 + 8;
3101 let digits = good[at..].iter().take_while(|c| c.is_ascii_digit()).count();
3102 let n: usize = std::str::from_utf8(&good[at..at + digits])
3103 .unwrap()
3104 .parse()
3105 .unwrap();
3106 let inflated = (n + 1).to_string();
3107 assert_eq!(inflated.len(), digits, "keep the digit count");
3108 let mut b = good.clone();
3109 b[at..at + digits].copy_from_slice(inflated.as_bytes());
3110 b
3111 };
3112 assert_eq!(broken.len(), good.len(), "the defect must not move bytes");
3113 let raw = lopdf::Document::load_mem(&broken).expect("still loads");
3115 assert!(
3116 raw.get_pages()
3117 .into_values()
3118 .all(|p| raw.get_page_content(p).is_empty()),
3119 "lopdf should drop the stream — if it stops, drop this repair"
3120 );
3121 let text = |b: &[u8]| -> Vec<String> {
3123 super::pdf_textlines(b)
3124 .into_iter()
3125 .flat_map(|(_, _, c)| c.into_iter().map(|c| c.text))
3126 .collect()
3127 };
3128 let expected = text(&good);
3129 assert!(!expected.is_empty(), "control must produce text");
3130 assert_eq!(text(&broken), expected);
3131 }
3132
3133 #[test]
3137 fn repair_declines_when_padding_would_move_objects() {
3138 let mut incremental = pdf_with_xref(false);
3139 incremental.extend_from_slice(b"6 0 obj<</Type/Whatever>>endobj\n");
3140 let declined = super::pad_short_xref_entries(&incremental).unwrap_err();
3141 assert!(
3142 declined.contains("object follows the xref"),
3143 "reason: {declined}"
3144 );
3145 }
3146}
3147
3148#[cfg(test)]
3154mod base14_fonts {
3155 fn pdf_with_font(fontdict: &[u8], text: &[u8]) -> Vec<u8> {
3157 let content = [b"BT /F1 12 Tf 72 700 Td (".as_slice(), text, b") Tj ET\n"].concat();
3158 pdf_with_content(fontdict, &content)
3159 }
3160
3161 fn pdf_with_content(fontdict: &[u8], content: &[u8]) -> Vec<u8> {
3163 pdf_with_fonts(&[fontdict], content)
3164 }
3165
3166 fn pdf_with_fonts(fontdicts: &[&[u8]], content: &[u8]) -> Vec<u8> {
3169 let stream = format!("<</Length {}>>stream\n", content.len()).into_bytes();
3170 let font_res: String = fontdicts
3171 .iter()
3172 .enumerate()
3173 .map(|(i, _)| format!("/F{} {} 0 R", i + 1, i + 5))
3174 .collect();
3175 let mut objs: Vec<Vec<u8>> = vec![
3176 b"<</Type/Catalog/Pages 2 0 R>>".to_vec(),
3177 b"<</Type/Pages/Kids[3 0 R]/Count 1>>".to_vec(),
3178 format!(
3179 "<</Type/Page/Parent 2 0 R/MediaBox[0 0 595 842]/Contents 4 0 R\
3180 /Resources<</Font<<{font_res}>>>>>>"
3181 )
3182 .into_bytes(),
3183 [stream.as_slice(), content, b"endstream"].concat(),
3184 ];
3185 objs.extend(fontdicts.iter().map(|d| d.to_vec()));
3186 let mut out = b"%PDF-1.4\n".to_vec();
3187 let mut offsets = Vec::new();
3188 for (i, body) in objs.iter().enumerate() {
3189 offsets.push(out.len());
3190 out.extend_from_slice(format!("{} 0 obj", i + 1).as_bytes());
3191 out.extend_from_slice(body);
3192 out.extend_from_slice(b"endobj\n");
3193 }
3194 let xref_at = out.len();
3195 out.extend_from_slice(format!("xref\n0 {}\n", objs.len() + 1).as_bytes());
3196 out.extend_from_slice(b"0000000000 65535 f \n");
3197 for off in &offsets {
3198 out.extend_from_slice(format!("{off:010} 00000 n \n").as_bytes());
3199 }
3200 out.extend_from_slice(
3201 format!("trailer<</Size {}/Root 1 0 R>>\n", objs.len() + 1).as_bytes(),
3202 );
3203 out.extend_from_slice(format!("startxref\n{xref_at}\n%%EOF\n").as_bytes());
3204 out
3205 }
3206
3207 fn cells(pdf: &[u8]) -> Vec<crate::pdfium_backend::TextCell> {
3209 super::pdf_textlines(pdf)
3210 .into_iter()
3211 .flat_map(|(_, _, c)| c)
3212 .collect()
3213 }
3214
3215 #[test]
3222 fn rotated_text_matrix_reads_in_order() {
3223 const HELV: &[u8] = b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>";
3224 const TEXT: &str = "Upright text on a rotated page.";
3225 for (tm, [l, b, r, t]) in [
3226 ("0 14 -14 0 200 100", [189.9, 100.0, 202.9, 289.1]), ("-14 0 0 -14 350 300", [160.9, 289.9, 350.0, 302.9]), ("0 -14 14 0 200 500", [197.1, 310.9, 210.1, 500.0]), (
3230 "9.8995 9.8995 -9.8995 9.8995 100 100",
3231 [92.9, 98.0, 235.8, 240.8],
3232 ), ] {
3234 let content = format!("BT /F1 1 Tf {tm} Tm ({TEXT}) Tj ET\n");
3235 let pdf = pdf_with_content(HELV, content.as_bytes());
3236 let cs = cells(&pdf);
3237 assert_eq!(cs.len(), 1, "{tm}: {cs:?}");
3238 let c = &cs[0];
3239 assert_eq!(c.text, TEXT, "{tm}");
3240 let got = [c.l, 842.0 - c.b, c.r, 842.0 - c.t];
3245 for (g, w) in got.iter().zip([l, b, r, t]) {
3246 assert!(
3247 (g - w).abs() < 1.0,
3248 "{tm}: box {got:?} vs docling-parse {:?}",
3249 [l, b, r, t]
3250 );
3251 }
3252 let words: Vec<String> = super::pdf_words(&pdf)
3253 .into_iter()
3254 .flat_map(|(_, _, c)| c)
3255 .map(|c| c.text)
3256 .collect();
3257 assert_eq!(words, TEXT.split(' ').collect::<Vec<_>>(), "{tm}");
3258 }
3259 }
3260
3261 #[test]
3266 fn rotated_text_off_the_page_is_dropped() {
3267 const HELV: &[u8] = b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>";
3268 let off = pdf_with_content(
3269 HELV,
3270 b"BT /F1 1 Tf 0 14 -14 0 -4 200 Tm (Spine title) Tj ET\n",
3271 );
3272 assert!(cells(&off).is_empty(), "{:?}", cells(&off));
3273 let on = pdf_with_content(
3274 HELV,
3275 b"BT /F1 1 Tf 0 14 -14 0 30 200 Tm (Margin note) Tj ET\n",
3276 );
3277 assert_eq!(cells(&on).len(), 1);
3278 }
3279
3280 #[test]
3283 fn upright_glyphs_carry_no_quad() {
3284 let only_page = |pdf: &[u8]| {
3285 let doc = super::load_document(pdf).expect("loads");
3286 let pid = *doc.get_pages().values().next().expect("one page");
3287 super::page_glyphs(&doc, pid)
3288 };
3289 let pdf = pdf_with_font(b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>", b"Plain");
3290 let glyphs = only_page(&pdf);
3291 assert!(!glyphs.is_empty());
3292 assert!(glyphs.iter().all(|g| g.quad.is_none() && g.lr > g.ll));
3293 let pdf = pdf_with_content(
3295 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>",
3296 b"BT /F1 1 Tf 0 14 -14 0 200 100 Tm (W) Tj ET\n",
3297 );
3298 let glyphs = only_page(&pdf);
3299 let g = &glyphs[0];
3300 assert!(g.quad.is_some());
3301 assert!((g.height() - 14.0).abs() < 0.05, "{}", g.height());
3303 }
3304
3305 #[test]
3310 fn ticks_stacked_by_cm_are_separate_line_cells() {
3311 let mut content = String::new();
3312 for (k, tick) in "345678".chars().enumerate() {
3313 let y = 300.0 + 25.92 * k as f64;
3314 content.push_str(&format!(
3315 "q 1 0 0 1 125 {y} cm BT /F1 10 Tf 1 0 0 1 -5 -4 Tm ({tick}) Tj ET Q\n"
3316 ));
3317 }
3318 let pdf = pdf_with_content(
3319 b"<</Type/Font/Subtype/Type1/BaseFont/Times-Roman>>",
3320 content.as_bytes(),
3321 );
3322 let cells = cells(&pdf);
3323 let texts: Vec<&str> = cells.iter().map(|c| c.text.as_str()).collect();
3324 assert_eq!(texts, ["3", "4", "5", "6", "7", "8"]);
3325 for (k, c) in cells.iter().enumerate() {
3326 let base = 842.0 - (300.0 + 25.92 * k as f32 - 4.0);
3328 assert!((c.l - 120.0).abs() < 0.1, "{k}: l {}", c.l);
3329 assert!(
3330 c.t < base && base - c.t < 10.0,
3331 "{k}: t {} base {base}",
3332 c.t
3333 );
3334 }
3335 }
3336
3337 #[test]
3343 fn drawn_checkbox_squares_are_found() {
3344 let content = b"q 1 0 0 1 119.52 449.04 cm \
3345 n 0 12.96 m 12.96 12.96 l S n 0 0 m 12.96 0 l S \
3346 n 0 0 m 0 12.96 l S n 12.96 0 m 12.96 12.96 l S Q\n\
3347 200 400 10 10 re S 202 405 m 204.5 402 l 208.5 408.5 l S\n\
3348 300 400 12 12 re f 50 50 100 100 re S\n\
3349 q 0.2 0.4 0.8 rg 400 400 12 12 re B Q q 1 g 500 400 12 12 re B Q\n";
3350 let pdf = pdf_with_content(b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>", content);
3351 let mut parser = super::PageTextParser::open(&pdf).expect("parses");
3352 let mut boxes = parser.cells(0).checkboxes;
3353 boxes.sort_by(|a, b| a.l.total_cmp(&b.l));
3354 let got: Vec<([i32; 4], bool)> = boxes
3355 .iter()
3356 .map(|c| {
3357 let r = |v: f32| (v * 100.0).round() as i32;
3358 ([r(c.l), r(c.t), r(c.r), r(c.b)], c.checked)
3359 })
3360 .collect();
3361 assert_eq!(
3363 got,
3364 [
3365 ([11952, 38000, 13248, 39296], false),
3366 ([20000, 43200, 21000, 44200], true),
3367 ([50000, 43000, 51200, 44200], false),
3368 ]
3369 );
3370 }
3371
3372 #[test]
3374 fn standard14_faces_get_builtin_widths() {
3375 for fontdict in [
3376 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/WinAnsiEncoding>>".as_slice(),
3378 b"<</Type/Font/Subtype/Type1/BaseFont/Times-BoldItalic>>",
3380 b"<</Type/Font/Subtype/TrueType/BaseFont/Arial,Bold>>",
3382 b"<</Type/Font/Subtype/Type1/BaseFont/ABCDEF+Courier-Oblique>>",
3383 ] {
3384 let pdf = pdf_with_font(fontdict, b"Words have width now");
3385 let cs = cells(&pdf);
3386 let text: String = cs
3387 .iter()
3388 .map(|c| c.text.as_str())
3389 .collect::<Vec<_>>()
3390 .join(" ");
3391 assert!(
3392 text.contains("Words have width now"),
3393 "{}: text lost: {text:?}",
3394 String::from_utf8_lossy(fontdict)
3395 );
3396 assert!(
3397 cs.iter().all(|c| c.r > c.l),
3398 "{}: zero-width cells: {cs:?}",
3399 String::from_utf8_lossy(fontdict)
3400 );
3401 }
3402 }
3403
3404 #[test]
3409 fn macroman_fonts_keep_accented_letters() {
3410 let sentence = b"Bl\x8cb\xbergr\xbfd p\x8c \xbfen \xd0 caf\x8e M\x9fller, Stra\xa7e \xa4 3";
3411 for (fontdict, want) in [
3412 (
3413 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/MacRomanEncoding>>"
3414 .as_slice(),
3415 "Blåbærgrød på øen – café Müller, Straße § 3",
3416 ),
3417 (
3418 b"<</Type/Font/Subtype/TrueType/BaseFont/ABCDEF+Arial\
3419 /Encoding<</Type/Encoding/BaseEncoding/MacRomanEncoding/Differences[51/four]>>>>",
3420 "Blåbærgrød på øen – café Müller, Straße § 4",
3421 ),
3422 ] {
3423 let text: String = cells(&pdf_with_font(fontdict, sentence))
3424 .iter()
3425 .map(|c| c.text.as_str())
3426 .collect::<Vec<_>>()
3427 .join(" ");
3428 assert_eq!(text, want, "{}", String::from_utf8_lossy(fontdict));
3429 }
3430
3431 let table = super::macroman_table();
3433 assert_eq!(
3434 (0x80u8..=0xFF).filter(|b| table.contains_key(b)).count(),
3435 127
3436 );
3437 assert_eq!(table.get(&0xF0), None);
3438 assert_eq!(table[&0xDB], '¤');
3439 assert_eq!(
3440 [0x80, 0x8C, 0xAE, 0xAF, 0xBE, 0xBF, 0xFF].map(|b| table[&b]),
3441 ['Ä', 'å', 'Æ', 'Ø', 'æ', 'ø', 'ˇ']
3442 );
3443 }
3444
3445 #[test]
3448 fn explicit_widths_win_and_unknown_faces_are_untouched() {
3449 let explicit = pdf_with_font(
3453 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/FirstChar 65\
3454 /Widths[100 100 100 100]/Encoding/WinAnsiEncoding>>",
3455 b"ABBA",
3456 );
3457 let builtin = pdf_with_font(
3458 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/WinAnsiEncoding>>",
3459 b"ABBA",
3460 );
3461 let w = |pdf: &[u8]| {
3462 let cs = cells(pdf);
3463 assert_eq!(cs.len(), 1, "one word cell: {cs:?}");
3464 cs[0].r - cs[0].l
3465 };
3466 let (we, wb) = (w(&explicit), w(&builtin));
3467 assert!(
3468 (we - 4.8).abs() < 0.1,
3469 "explicit widths must win: got {we}, want 4×100×12/1000"
3470 );
3471 assert!(
3472 wb > 2.0 * we,
3473 "built-in Helvetica is much wider: {wb} vs {we}"
3474 );
3475
3476 let unknown = pdf_with_font(
3480 b"<</Type/Font/Subtype/Type1/BaseFont/FancyCorp-Display>>",
3481 b"Mystery",
3482 );
3483 let cs = cells(&unknown);
3484 let text: String = cs.iter().map(|c| c.text.as_str()).collect();
3485 assert!(text.contains("Mystery"), "text still decodes: {cs:?}");
3486 }
3487 #[test]
3496 fn same_face_under_two_resource_keys_is_one_font_for_the_sanitizer() {
3497 let f1 = b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/WinAnsiEncoding>>";
3498 let f2 = b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding<</Type/Encoding\
3499 /BaseEncoding/WinAnsiEncoding/Differences[129/fi]>>>>";
3500 let content = b"BT 12 TL 72 720 Td \
3501 /F1 12 Tf (The ) Tj /F2 12 Tf (\\201nancial position) Tj \
3502 /F1 12 Tf ( of the company is shown in ) Tj /F2 12 Tf (\\201gure 3) Tj \
3503 /F1 12 Tf ( with the ) Tj /F2 12 Tf (\\201rst results.) Tj \
3504 T* T* /F2 12 Tf (Same words in /F2 only: \\201nancial, \\201gure, \\201rst.) Tj ET\n";
3505 let cs = cells(&pdf_with_fonts(&[f1, f2], content));
3506 let lines: Vec<&str> = cs.iter().map(|c| c.text.as_str()).collect();
3507 assert_eq!(
3508 lines,
3509 [
3510 "The financial position of the company is shown in figure 3 with the first results.",
3511 "Same words in /F2 only: financial, figure, first.",
3512 ],
3513 "{cs:?}"
3514 );
3515 }
3516
3517 #[test]
3521 fn different_faces_still_keep_the_font_barrier() {
3522 let f1 = b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/WinAnsiEncoding>>";
3523 let f2 = b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica-Bold/Encoding/WinAnsiEncoding>>";
3524 let content = b"BT 72 720 Td /F2 12 Tf (Total:) Tj /F1 12 Tf (42) Tj ET\n";
3525 let cs = cells(&pdf_with_fonts(&[f1, f2], content));
3526 let texts: Vec<&str> = cs.iter().map(|c| c.text.as_str()).collect();
3527 assert_eq!(texts, ["Total:", "42"], "{cs:?}");
3528 }
3529
3530 #[test]
3535 fn sanitizer_font_name_follows_docling_parse_precedence() {
3536 use lopdf::{dictionary, Document, Object};
3537 let mut doc = Document::with_version("1.4");
3538 let fd_a =
3539 doc.add_object(dictionary! { "Type" => "FontDescriptor", "FontName" => "AAAAAA+Face" });
3540 let fd_b =
3541 doc.add_object(dictionary! { "Type" => "FontDescriptor", "FontName" => "BBBBBB+Face" });
3542 let with = |fd: Option<lopdf::ObjectId>, base: &str| {
3543 let mut d = dictionary! { "Type" => "Font", "Subtype" => "Type1", "BaseFont" => base };
3544 if let Some(id) = fd {
3545 d.set("FontDescriptor", Object::Reference(id));
3546 }
3547 d
3548 };
3549 let name = |d: &lopdf::Dictionary| {
3550 String::from_utf8(super::sanitizer_font_name(&doc, d, false)).unwrap()
3551 };
3552 assert_eq!(name(&with(Some(fd_a), "Face")), "AAAAAA+Face");
3553 assert_eq!(name(&with(Some(fd_b), "Face")), "BBBBBB+Face");
3554 assert_eq!(name(&with(None, "ABCDEF+Face")), "ABCDEF+Face");
3555 assert_eq!(
3556 name(&dictionary! { "Type" => "Font", "Name" => "F7" }),
3557 "F7"
3558 );
3559 assert_eq!(name(&dictionary! { "Type" => "Font" }), "null");
3560 }
3561}
3562
3563#[cfg(test)]
3564mod overpainted {
3565 use crate::pdfium_backend::TextCell;
3566
3567 fn cell(text: &str, l: f32, t: f32, r: f32, b: f32) -> TextCell {
3568 TextCell {
3569 text: text.into(),
3570 l,
3571 t,
3572 r,
3573 b,
3574 }
3575 }
3576
3577 #[test]
3581 fn stacked_logo_glyphs_are_dropped() {
3582 let mut cells = vec![
3583 cell("\"", 72.7, 21.5, 86.4, 31.5),
3584 cell("==", 59.4, 21.5, 99.6, 31.5),
3585 cell("Herr", 65.2, 151.3, 81.7, 161.3),
3586 ];
3587 super::drop_overpainted_cells(&mut cells);
3588 assert_eq!(cells.len(), 1, "cells: {cells:?}");
3589 assert_eq!(cells[0].text, "Herr");
3590 }
3591
3592 #[test]
3596 fn prose_and_double_draw_are_kept() {
3597 let mut cells = vec![
3598 cell("Telefon", 354.3, 133.2, 381.5, 143.2),
3599 cell("0676/2000", 387.3, 133.2, 428.7, 143.2),
3600 cell("Bold", 100.0, 50.0, 130.0, 60.0),
3601 cell("Bold", 100.3, 50.0, 130.3, 60.0),
3602 ];
3603 super::drop_overpainted_cells(&mut cells);
3604 assert_eq!(cells.len(), 4);
3605 }
3606}
3607
3608#[cfg(test)]
3609mod vestigial_layer {
3610 use crate::pdfium_backend::{PdfPage, TextCell};
3611
3612 fn page_with(texts: &[&str]) -> PdfPage {
3613 let cells = texts
3614 .iter()
3615 .enumerate()
3616 .map(|(i, t)| TextCell {
3617 text: t.to_string(),
3618 l: 10.0,
3619 t: 10.0 + 12.0 * i as f32,
3620 r: 90.0,
3621 b: 20.0 + 12.0 * i as f32,
3622 })
3623 .collect();
3624 PdfPage::from_cells(595.0, 842.0, 1.0, cells)
3625 }
3626
3627 #[test]
3632 fn typed_in_form_fields_are_not_a_text_layer() {
3633 let pages = vec![
3634 page_with(&["03", "05", "2025"]),
3635 page_with(&[]),
3636 page_with(&[]),
3637 ];
3638 assert!(super::text_layer_is_vestigial(&pages));
3639 assert!(super::text_layer_is_vestigial(&[page_with(&[])]));
3640 }
3641
3642 #[test]
3645 fn sparse_but_real_documents_pass() {
3646 let one_pager = vec![page_with(&[
3647 "Confidential briefing",
3648 "Prepared for the board meeting",
3649 "Do not distribute",
3650 ])];
3651 assert!(!super::text_layer_is_vestigial(&one_pager));
3652 }
3653}