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 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
261 let (mut widths, mut default_width) = if two_byte {
262 cid_widths(doc, fdict)
263 } else {
264 simple_widths(doc, fdict)
265 };
266
267 let simple_encoding = if two_byte {
268 None
269 } else {
270 Some(simple_encoding_table(doc, fdict))
271 };
272
273 if !two_byte && widths.is_empty() && default_width == 0.0 {
281 if let Some(std14) = base_font_name(fdict).and_then(|n| crate::std14::widths_for(&n)) {
282 if let Some(enc) = &simple_encoding {
283 for (&code, &ch) in enc {
284 if let Some(w) = std14.width(ch) {
285 widths.insert(u32::from(code), w);
286 }
287 }
288 }
289 default_width = 500.0;
292 }
293 }
294 let fallback_names = if two_byte {
295 HashMap::new()
296 } else {
297 differences_gid_names(doc, fdict)
298 };
299 let program_encoding = if two_byte {
300 HashMap::new()
301 } else {
302 type1_program_encoding(doc, fdict)
303 };
304
305 let (ascent, descent) = font_ascent_descent(doc, fdict, two_byte);
306
307 Font {
308 two_byte,
309 to_unicode,
310 widths,
311 default_width,
312 simple_encoding,
313 fallback_names,
314 program_encoding,
315 ascent,
316 descent,
317 hash: hash_name(&sanitizer_font_name(doc, fdict, two_byte)),
318 style: crate::font_style::parse_font_style(&String::from_utf8_lossy(
319 &base_font_name(fdict).unwrap_or_else(|| name.to_vec()),
320 )),
321 }
322}
323
324fn differences_gid_names(doc: &Document, fdict: &Dictionary) -> HashMap<u8, String> {
331 let mut map = HashMap::new();
332 let Some(Object::Dictionary(enc)) = fdict.get(b"Encoding").ok().and_then(|o| deref(doc, o))
333 else {
334 return map;
335 };
336 let Some(Object::Array(diffs)) = enc.get(b"Differences").ok().and_then(|o| deref(doc, o))
337 else {
338 return map;
339 };
340 let mut code = 0u8;
341 for el in diffs {
342 match el {
343 Object::Integer(i) => code = *i as u8,
344 Object::Name(name) => {
345 if glyph_name_to_char(name).is_none() && is_gid_name(name) {
346 map.insert(code, String::from_utf8_lossy(name).into_owned());
347 }
348 code = code.wrapping_add(1);
349 }
350 _ => {}
351 }
352 }
353 map
354}
355
356fn type1_program_encoding(doc: &Document, fdict: &Dictionary) -> HashMap<u8, char> {
364 let mut map = HashMap::new();
365 let Some(desc) = fdict
366 .get(b"FontDescriptor")
367 .ok()
368 .and_then(|o| deref(doc, o))
369 .and_then(|o| o.as_dict().ok())
370 else {
371 return map;
372 };
373 let Some(data) = desc
374 .get(b"FontFile")
375 .ok()
376 .and_then(|o| deref(doc, o))
377 .and_then(|o| o.as_stream().ok())
378 .and_then(|s| s.decompressed_content().ok())
379 else {
380 return map;
381 };
382 let head_end = data
384 .windows(5)
385 .position(|w| w == b"eexec")
386 .unwrap_or(data.len());
387 let head = String::from_utf8_lossy(&data[..head_end]);
388 let toks: Vec<&str> = head.split_whitespace().collect();
390 for w in toks.windows(4) {
391 if w[0] == "dup" && w[3] == "put" {
392 if let (Ok(code), Some(name)) = (w[1].parse::<u32>(), w[2].strip_prefix('/')) {
393 if code <= 255 {
394 if let Some(ch) = glyph_name_to_char(name.as_bytes()) {
395 map.insert(code as u8, ch);
396 }
397 }
398 }
399 }
400 }
401 map
402}
403
404fn is_gid_name(name: &[u8]) -> bool {
410 let Ok(s) = std::str::from_utf8(name) else {
411 return false;
412 };
413 if s.starts_with("afii") || s.starts_with("uni") {
414 return false;
415 }
416 for prefix in ["g", "G", "cid", "CID", "glyph", "index"] {
417 if let Some(rest) = s.strip_prefix(prefix) {
418 if !rest.is_empty() && rest.bytes().all(|b| b.is_ascii_digit()) {
419 return true;
420 }
421 }
422 }
423 let alpha = s.bytes().take_while(|b| b.is_ascii_alphabetic()).count();
427 let digits = s.len() - alpha;
428 (1..=3).contains(&alpha)
429 && digits >= 3
430 && s.as_bytes()[alpha..].iter().all(|b| b.is_ascii_digit())
431}
432
433fn font_ascent_descent(doc: &Document, fdict: &Dictionary, two_byte: bool) -> (f64, f64) {
434 let descr_owner = if two_byte {
436 fdict
437 .get(b"DescendantFonts")
438 .ok()
439 .and_then(|o| deref(doc, o))
440 .and_then(|o| match o {
441 Object::Array(a) => a.first(),
442 _ => None,
443 })
444 .and_then(|o| as_dict(doc, o))
445 } else {
446 Some(fdict)
447 };
448 let fd = descr_owner
449 .and_then(|d| d.get(b"FontDescriptor").ok())
450 .and_then(|o| as_dict(doc, o));
451 let asc = fd
452 .and_then(|d| d.get(b"Ascent").ok())
453 .and_then(|o| {
454 o.as_float()
455 .ok()
456 .or_else(|| o.as_i64().ok().map(|i| i as f32))
457 })
458 .unwrap_or(750.0) as f64;
459 let desc = fd
460 .and_then(|d| d.get(b"Descent").ok())
461 .and_then(|o| {
462 o.as_float()
463 .ok()
464 .or_else(|| o.as_i64().ok().map(|i| i as f32))
465 })
466 .unwrap_or(-250.0) as f64;
467 if asc - desc <= 1.0 {
474 return (750.0, -250.0);
475 }
476 (asc, desc)
477}
478
479fn base_font_name(fdict: &Dictionary) -> Option<Vec<u8>> {
481 let name = fdict.get(b"BaseFont").ok()?.as_name().ok()?;
482 let stripped = match name.iter().position(|&b| b == b'+') {
483 Some(i) if i == 6 => &name[i + 1..],
484 _ => name,
485 };
486 Some(stripped.to_vec())
487}
488
489fn simple_widths(doc: &Document, fdict: &Dictionary) -> (HashMap<u32, f64>, f64) {
491 let mut map = HashMap::new();
492 let first = fdict
493 .get(b"FirstChar")
494 .ok()
495 .and_then(|o| o.as_i64().ok())
496 .unwrap_or(0) as u32;
497 if let Some(Object::Array(arr)) = fdict.get(b"Widths").ok().and_then(|o| deref(doc, o)) {
498 for (i, w) in arr.iter().enumerate() {
499 if let Some(w) = num(w) {
500 map.insert(first + i as u32, w);
501 }
502 }
503 }
504 let dw = fdict
505 .get(b"FontDescriptor")
506 .ok()
507 .and_then(|o| as_dict(doc, o))
508 .and_then(|d| d.get(b"MissingWidth").ok())
509 .and_then(num)
510 .unwrap_or(0.0);
511 (map, dw)
512}
513
514fn cid_widths(doc: &Document, fdict: &Dictionary) -> (HashMap<u32, f64>, f64) {
516 let mut map = HashMap::new();
517 let Some(desc) = fdict
518 .get(b"DescendantFonts")
519 .ok()
520 .and_then(|o| deref(doc, o))
521 .and_then(|o| match o {
522 Object::Array(a) => a.first(),
523 _ => None,
524 })
525 .and_then(|o| as_dict(doc, o))
526 else {
527 return (map, 1000.0);
528 };
529 let dw = desc.get(b"DW").ok().and_then(num).unwrap_or(1000.0);
530 if let Some(Object::Array(w)) = desc.get(b"W").ok().and_then(|o| deref(doc, o)) {
531 let mut i = 0;
532 while i < w.len() {
533 let c = w.get(i).and_then(num);
534 match (c, w.get(i + 1)) {
535 (Some(c), Some(Object::Array(list))) => {
537 for (k, wv) in list.iter().enumerate() {
538 if let Some(wv) = num(wv) {
539 map.insert(c as u32 + k as u32, wv);
540 }
541 }
542 i += 2;
543 }
544 (Some(c1), Some(o2)) => {
546 if let (Some(c2), Some(wv)) = (num(o2), w.get(i + 2).and_then(num)) {
547 for cid in c1 as u32..=c2 as u32 {
548 map.insert(cid, wv);
549 }
550 }
551 i += 3;
552 }
553 _ => break,
554 }
555 }
556 }
557 (map, dw)
558}
559
560fn num(o: &Object) -> Option<f64> {
561 match o {
562 Object::Integer(i) => Some(*i as f64),
563 Object::Real(r) => Some(*r as f64),
564 _ => None,
565 }
566}
567
568pub(crate) fn parse_tounicode(data: &[u8]) -> HashMap<u32, String> {
570 let text = String::from_utf8_lossy(data);
571 let mut map = HashMap::new();
572 let hex = |s: &str| -> Option<Vec<u16>> {
573 let s = s.trim();
574 if !s.starts_with('<') || !s.ends_with('>') {
575 return None;
576 }
577 let h = &s[1..s.len() - 1];
578 let bytes: Vec<u8> = (0..h.len())
579 .step_by(2)
580 .filter_map(|i| u8::from_str_radix(h.get(i..i + 2)?, 16).ok())
581 .collect();
582 Some(
583 bytes
584 .chunks(2)
585 .map(|c| {
586 if c.len() == 2 {
587 u16::from_be_bytes([c[0], c[1]])
588 } else {
589 c[0] as u16
590 }
591 })
592 .collect(),
593 )
594 };
595 let u16s_to_string = |u: &[u16]| String::from_utf16_lossy(u);
596 let code_of = |u: &[u16]| u.iter().fold(0u32, |acc, &x| (acc << 16) | x as u32);
597
598 let tokens: Vec<String> = {
602 let bytes = text.as_bytes();
603 let mut toks = Vec::new();
604 let mut i = 0;
605 while i < bytes.len() {
606 let c = bytes[i];
607 if c.is_ascii_whitespace() {
608 i += 1;
609 } else if c == b'<' {
610 let start = i;
611 while i < bytes.len() && bytes[i] != b'>' {
612 i += 1;
613 }
614 i += 1; toks.push(String::from_utf8_lossy(&bytes[start..i.min(bytes.len())]).into_owned());
616 } else if c == b'[' || c == b']' {
617 toks.push((c as char).to_string());
618 i += 1;
619 } else {
620 let start = i;
621 while i < bytes.len()
622 && !bytes[i].is_ascii_whitespace()
623 && bytes[i] != b'<'
624 && bytes[i] != b'['
625 && bytes[i] != b']'
626 {
627 i += 1;
628 }
629 toks.push(String::from_utf8_lossy(&bytes[start..i]).into_owned());
630 }
631 }
632 toks
633 };
634 let tokens: Vec<&str> = tokens.iter().map(|s| s.as_str()).collect();
635 let mut i = 0;
636 while i < tokens.len() {
637 match tokens[i] {
638 "beginbfchar" => {
639 i += 1;
640 while i + 1 < tokens.len() && tokens[i] != "endbfchar" {
641 if let (Some(src), Some(dst)) = (hex(tokens[i]), hex(tokens[i + 1])) {
642 map.insert(code_of(&src), u16s_to_string(&dst));
643 }
644 i += 2;
645 }
646 }
647 "beginbfrange" => {
648 i += 1;
649 while i + 2 < tokens.len() && tokens[i] != "endbfrange" {
650 let (Some(lo), Some(hi)) = (hex(tokens[i]), hex(tokens[i + 1])) else {
651 i += 1;
652 continue;
653 };
654 let lo = code_of(&lo);
655 let hi = code_of(&hi);
656 if tokens[i + 2] == "[" {
657 let mut j = i + 3;
659 let mut code = lo;
660 while j < tokens.len() && tokens[j] != "]" {
661 if let Some(dst) = hex(tokens[j]) {
662 map.insert(code, u16s_to_string(&dst));
663 }
664 code += 1;
665 j += 1;
666 }
667 i = j + 1;
668 } else if let Some(dst) = hex(tokens[i + 2]) {
669 let base = code_of(&dst);
671 for (k, code) in (lo..=hi).enumerate() {
672 if let Some(ch) = char::from_u32(base + k as u32) {
673 map.insert(code, ch.to_string());
674 }
675 }
676 i += 3;
677 } else {
678 i += 1;
679 }
680 }
681 }
682 _ => i += 1,
683 }
684 }
685 map
686}
687
688fn codes(font: &Font, bytes: &[u8]) -> Vec<u32> {
690 if font.two_byte {
691 bytes
692 .chunks(2)
693 .map(|c| {
694 if c.len() == 2 {
695 ((c[0] as u32) << 8) | c[1] as u32
696 } else {
697 c[0] as u32
698 }
699 })
700 .collect()
701 } else {
702 bytes.iter().map(|&b| b as u32).collect()
703 }
704}
705
706#[derive(Debug, Clone, Copy, PartialEq)]
721pub(crate) struct PageBox {
722 pub l: f32,
724 pub b: f32,
726 pub w: f32,
727 pub h: f32,
728}
729
730impl PageBox {
731 pub fn top(&self) -> f32 {
733 self.b + self.h
734 }
735}
736
737fn inherited_rect(
740 doc: &Document,
741 page_id: lopdf::ObjectId,
742 key: &[u8],
743) -> Option<(f32, f32, f32, f32)> {
744 let mut id = page_id;
745 for _ in 0..32 {
746 let dict = doc.get_object(id).ok()?.as_dict().ok()?;
747 if let Some(Object::Array(a)) = dict.get(key).ok().and_then(|o| deref(doc, o)) {
748 let v: Vec<f32> = a.iter().filter_map(|o| num(o).map(|x| x as f32)).collect();
749 if v.len() == 4 && v.iter().all(|x| x.is_finite()) {
750 return Some((
751 v[0].min(v[2]),
752 v[1].min(v[3]),
753 v[0].max(v[2]),
754 v[1].max(v[3]),
755 ));
756 }
757 return None;
758 }
759 id = dict.get(b"Parent").ok()?.as_reference().ok()?;
760 }
761 None
762}
763
764pub(crate) fn page_box(doc: &Document, page_id: lopdf::ObjectId) -> PageBox {
765 let nonempty = |r: &(f32, f32, f32, f32)| r.2 > r.0 && r.3 > r.1;
766 let media = inherited_rect(doc, page_id, b"MediaBox")
767 .filter(nonempty)
768 .unwrap_or((0.0, 0.0, 612.0, 792.0));
769 let crop = inherited_rect(doc, page_id, b"CropBox")
770 .map(|c| {
771 (
772 c.0.max(media.0),
773 c.1.max(media.1),
774 c.2.min(media.2),
775 c.3.min(media.3),
776 )
777 })
778 .filter(nonempty)
779 .unwrap_or(media);
780 PageBox {
781 l: crop.0,
782 b: crop.1,
783 w: crop.2 - crop.0,
784 h: crop.3 - crop.1,
785 }
786}
787
788fn page_size(doc: &Document, page_id: lopdf::ObjectId) -> (f32, f32) {
791 let pb = page_box(doc, page_id);
792 (pb.w, pb.h)
793}
794
795pub fn content_diagnosis(bytes: &[u8]) -> String {
801 let Some(doc) = load_document(bytes) else {
802 return "document does not load".into();
803 };
804 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
805 pages.sort_by_key(|(n, _)| *n);
806 let mut out = String::new();
807 let mut caches = DocCaches::default();
808 for (n, pid) in pages.into_iter().take(4) {
809 let content_bytes = doc.get_page_content(pid);
810 let ops = lopdf::content::Content::decode(&content_bytes)
811 .map(|c| c.operations.len())
812 .ok();
813 let res = page_res(&doc, pid);
814 let fonts = res.map(|r| fonts_from_res(&doc, r, &mut caches).len());
815 let glyphs = page_glyphs_cached(&doc, pid, &mut caches).len();
816 out.push_str(&format!(
817 "\n page {n}: content {} B, ops {}, resources {}, fonts {}, glyphs {}",
818 content_bytes.len(),
819 ops.map_or("UNDECODABLE".to_string(), |n| n.to_string()),
820 if res.is_some() { "ok" } else { "MISSING" },
821 fonts.map_or("-".to_string(), |n| n.to_string()),
822 glyphs,
823 ));
824 }
825 out
826}
827
828pub fn text_layer_is_vestigial(pages: &[crate::pdfium_backend::PdfPage]) -> bool {
841 let mut tally = TextLayerTally::default();
842 pages.iter().for_each(|p| tally.add(p));
843 tally.is_vestigial()
844}
845
846#[derive(Debug, Default, Clone, Copy)]
849pub struct TextLayerTally {
850 pages: usize,
851 lines: usize,
852 chars: usize,
853}
854
855impl TextLayerTally {
856 pub fn add(&mut self, page: &crate::pdfium_backend::PdfPage) {
857 self.pages += 1;
858 self.lines += page.cells.len();
859 self.chars += page
860 .cells
861 .iter()
862 .map(|c| c.text.chars().count())
863 .sum::<usize>();
864 }
865
866 pub fn is_vestigial(&self) -> bool {
868 self.lines == 0 || (self.lines <= self.pages && self.chars < 32)
869 }
870
871 pub fn proves_text(&self, total_pages: usize) -> bool {
875 self.lines > 0 && (self.lines > total_pages || self.chars >= 32)
876 }
877}
878
879pub fn xref_repair_status(bytes: &[u8]) -> String {
883 if Document::load_mem(bytes).is_ok() {
884 return "loads unaided; no repair needed".into();
885 }
886 match pad_short_xref_entries(bytes) {
887 Ok(fixed) => match Document::load_mem(&fixed) {
888 Ok(_) => "repaired: cross-reference entries padded to 20 bytes".into(),
889 Err(e) => format!("padded the entries, but it still will not load: {e}"),
890 },
891 Err(why) => format!("repair declined — {why}"),
892 }
893}
894
895pub(crate) fn load_document(bytes: &[u8]) -> Option<Document> {
910 open_document(bytes, None).ok()
911}
912
913#[derive(Debug, Clone, Copy, PartialEq, Eq)]
915pub(crate) enum OpenError {
916 Unreadable,
918 Password,
920}
921
922pub(crate) fn open_document(bytes: &[u8], password: Option<&str>) -> Result<Document, OpenError> {
929 let Some(doc) = load_document_raw(bytes, password) else {
930 if password.is_some() && load_document_raw(bytes, None).is_some_and(|d| d.is_encrypted()) {
934 return Err(OpenError::Password);
935 }
936 return Err(OpenError::Unreadable);
937 };
938 if doc.is_encrypted() {
939 return Err(OpenError::Password);
940 }
941 Ok(doc)
942}
943
944fn load_options(password: Option<&str>) -> lopdf::LoadOptions {
945 lopdf::LoadOptions {
946 password: password.map(str::to_string),
947 ..lopdf::LoadOptions::default()
948 }
949}
950
951fn load_document_raw(bytes: &[u8], password: Option<&str>) -> Option<Document> {
952 let mut fallback = None;
957 if let Some(doc) = best_effort_load(bytes, password, &mut fallback) {
958 return Some(doc);
959 }
960 let xref_fixed = pad_short_xref_entries(bytes).ok();
961 if let Some(fixed) = &xref_fixed {
962 if let Some(doc) = best_effort_load(fixed, password, &mut fallback) {
963 return Some(doc);
964 }
965 }
966 let lengths_fixed = fix_stream_lengths(xref_fixed.as_deref().unwrap_or(bytes));
969 if let Some(doc) = best_effort_load(&lengths_fixed, password, &mut fallback) {
970 return Some(doc);
971 }
972 fallback
973}
974
975fn best_effort_load(
978 data: &[u8],
979 password: Option<&str>,
980 fallback: &mut Option<Document>,
981) -> Option<Document> {
982 match Document::load_mem_with_options(data, load_options(password)) {
983 Ok(doc) if has_page_content(&doc) => Some(doc),
984 Ok(doc) => {
985 fallback.get_or_insert(doc);
986 None
987 }
988 Err(_) => None,
989 }
990}
991
992fn has_page_content(doc: &Document) -> bool {
996 doc.get_pages()
997 .into_values()
998 .take(4)
999 .any(|pid| !doc.get_page_content(pid).is_empty())
1000}
1001
1002fn fix_stream_lengths(bytes: &[u8]) -> Vec<u8> {
1015 let mut out = bytes.to_vec();
1016 let mut i = 0;
1017 while let Some(rel) = find(&out[i..], b"stream") {
1018 let kw = i + rel;
1019 i = kw + 6;
1020 if kw >= 3 && &out[kw - 3..kw] == b"end" {
1022 continue;
1023 }
1024 let mut data = kw + 6;
1026 if out.get(data..data + 2) == Some(b"\r\n".as_slice()) {
1027 data += 2;
1028 } else if matches!(out.get(data), Some(b'\n' | b'\r')) {
1029 data += 1;
1030 }
1031 let Some(end) = find(&out[data..], b"endstream").map(|r| data + r) else {
1032 continue;
1033 };
1034 let dict_start = out[..kw].iter().rposition(|&c| c == b'<').unwrap_or(0);
1036 let Some(lrel) = find(&out[dict_start..kw], b"/Length") else {
1037 continue;
1038 };
1039 let mut d = dict_start + lrel + 7;
1040 while matches!(out.get(d), Some(b' ')) {
1041 d += 1;
1042 }
1043 let digits = out[d..].iter().take_while(|c| c.is_ascii_digit()).count();
1044 if digits == 0 {
1045 continue;
1046 }
1047 let declared: usize = match std::str::from_utf8(&out[d..d + digits])
1048 .ok()
1049 .and_then(|s| s.parse().ok())
1050 {
1051 Some(v) => v,
1052 None => continue,
1053 };
1054 let actual = end - data;
1055 let replacement = actual.to_string();
1058 if actual == declared || replacement.len() > digits {
1059 continue;
1060 }
1061 out[d..d + digits].fill(b' ');
1062 out[d..d + replacement.len()].copy_from_slice(replacement.as_bytes());
1063 }
1064 out
1065}
1066
1067fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
1068 haystack.windows(needle.len()).position(|w| w == needle)
1069}
1070
1071fn pad_short_xref_entries(bytes: &[u8]) -> Result<Vec<u8>, &'static str> {
1074 let is_boundary = |i: usize| i == 0 || matches!(bytes[i - 1], b'\n' | b'\r');
1077 let mut starts = (0..bytes.len().saturating_sub(4))
1078 .filter(|&i| &bytes[i..i + 4] == b"xref" && is_boundary(i));
1079 let xref_at = starts
1080 .next()
1081 .ok_or("no classic `xref` section (an xref stream?)")?;
1082 if starts.next().is_some() {
1083 return Err("more than one xref section (incremental update)");
1084 }
1085 let last_obj = bytes
1086 .windows(3)
1087 .rposition(|w| w == b"obj")
1088 .ok_or("no objects found")?;
1089 if last_obj > xref_at {
1090 return Err("an object follows the xref — padding would move it");
1091 }
1092
1093 let mut out = bytes[..xref_at].to_vec();
1094 out.extend_from_slice(b"xref\n");
1095 let mut i = xref_at + 4;
1096 let skip_ws = |i: &mut usize| {
1097 while matches!(bytes.get(*i), Some(b'\r' | b'\n' | b' ')) {
1098 *i += 1;
1099 }
1100 };
1101 loop {
1102 skip_ws(&mut i);
1103 if bytes[i..].starts_with(b"trailer") {
1105 out.extend_from_slice(&bytes[i..]);
1106 return Ok(out);
1107 }
1108 let header_end = i + bytes[i..]
1109 .iter()
1110 .position(|c| matches!(c, b'\n' | b'\r'))
1111 .ok_or("subsection header runs off the end")?;
1112 let header = std::str::from_utf8(&bytes[i..header_end])
1113 .map_err(|_| "subsection header is not text")?
1114 .trim();
1115 let mut parts = header.split_whitespace();
1116 let count: usize = parts
1117 .nth(1)
1118 .and_then(|c| c.parse().ok())
1119 .ok_or("unparseable subsection header")?;
1120 if parts.next().is_some() || count == 0 {
1121 return Err("unexpected subsection header shape");
1122 }
1123 out.extend_from_slice(header.as_bytes());
1124 out.push(b'\n');
1125 i = header_end;
1126 for _ in 0..count {
1127 skip_ws(&mut i);
1128 let entry = bytes.get(i..i + 18).ok_or("xref entry runs off the end")?;
1130 let well_formed = entry[..10].iter().all(u8::is_ascii_digit)
1131 && entry[10] == b' '
1132 && entry[11..16].iter().all(u8::is_ascii_digit)
1133 && entry[16] == b' '
1134 && matches!(entry[17], b'n' | b'f');
1135 if !well_formed {
1136 return Err("xref entry is not `nnnnnnnnnn ggggg n`");
1137 }
1138 out.extend_from_slice(entry);
1139 out.extend_from_slice(b" \n"); i += 18;
1141 }
1142 }
1143}
1144
1145pub fn debug_glyphs(bytes: &[u8], index: usize) -> Vec<(char, f32, f32, f32, f32)> {
1148 let Some(doc) = load_document(bytes) else {
1149 return Vec::new();
1150 };
1151 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
1152 pages.sort_by_key(|(n, _)| *n);
1153 let Some((_, pid)) = pages.get(index) else {
1154 return Vec::new();
1155 };
1156 page_glyphs(&doc, *pid)
1157 .into_iter()
1158 .map(|g| (g.ch, g.ll, g.lr, g.lb, g.lt))
1159 .collect()
1160}
1161
1162pub fn pdf_textlines(bytes: &[u8]) -> Vec<(f32, f32, Vec<crate::pdfium_backend::TextCell>)> {
1166 let Some(doc) = load_document(bytes) else {
1167 return Vec::new();
1168 };
1169 let mut caches = DocCaches::default();
1170 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
1171 pages.sort_by_key(|(n, _)| *n);
1172 pages
1173 .into_iter()
1174 .map(|(_, pid)| {
1175 let (w, h) = page_size(&doc, pid);
1176 let glyphs = page_glyphs_cached(&doc, pid, &mut caches);
1177 let cells = crate::dp_lines::line_cells(&glyphs, h, true);
1178 (w, h, cells)
1179 })
1180 .collect()
1181}
1182
1183pub fn pdf_words(bytes: &[u8]) -> Vec<(f32, f32, Vec<crate::pdfium_backend::TextCell>)> {
1188 let Some(doc) = load_document(bytes) else {
1189 return Vec::new();
1190 };
1191 let mut caches = DocCaches::default();
1192 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
1193 pages.sort_by_key(|(n, _)| *n);
1194 pages
1195 .into_iter()
1196 .map(|(_, pid)| {
1197 let (w, h) = page_size(&doc, pid);
1198 let glyphs = page_glyphs_cached(&doc, pid, &mut caches);
1199 let cells = crate::dp_lines::word_cells(&glyphs, h, true);
1200 (w, h, cells)
1201 })
1202 .collect()
1203}
1204
1205#[derive(Default)]
1209pub struct PageParserCells {
1210 pub prose: Vec<crate::pdfium_backend::TextCell>,
1211 pub words: Vec<crate::pdfium_backend::TextCell>,
1212 pub code: Vec<crate::pdfium_backend::TextCell>,
1213 pub checkboxes: Vec<crate::checkbox::CheckBox>,
1215}
1216
1217pub struct PageTextParser {
1230 doc: Document,
1231 caches: DocCaches,
1232 pages: Vec<lopdf::ObjectId>,
1234}
1235
1236impl PageTextParser {
1237 pub fn open(bytes: &[u8]) -> Option<Self> {
1239 Self::open_with_password(bytes, None)
1240 }
1241
1242 pub fn open_with_password(bytes: &[u8], password: Option<&str>) -> Option<Self> {
1246 let doc = open_document(bytes, password).ok()?;
1247 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
1248 pages.sort_by_key(|(n, _)| *n);
1249 Some(Self {
1250 doc,
1251 caches: DocCaches::default(),
1252 pages: pages.into_iter().map(|(_, pid)| pid).collect(),
1253 })
1254 }
1255
1256 pub(crate) fn glyph_styles(
1264 &mut self,
1265 index: usize,
1266 ) -> Vec<crate::heading_hierarchy::GlyphStyle> {
1267 let Some(&pid) = self.pages.get(index) else {
1268 return Vec::new();
1269 };
1270 let (_w, h) = page_size(&self.doc, pid);
1271 let glyphs = page_glyphs_cached(&self.doc, pid, &mut self.caches);
1272 let styles: HashMap<u64, crate::font_style::FontStyle> = self
1275 .caches
1276 .fonts
1277 .values()
1278 .map(|f| (f.hash, f.style))
1279 .collect();
1280 glyphs
1281 .iter()
1282 .filter(|g| !g.ch.is_whitespace() && g.ll.is_finite())
1283 .map(|g| {
1284 let st = styles.get(&g.font).copied().unwrap_or_default();
1285 crate::heading_hierarchy::GlyphStyle {
1286 l: g.ll,
1287 t: h - g.lt,
1288 r: g.lr,
1289 b: h - g.lb,
1290 height: g.height(),
1291 weight_cls: crate::font_style::weight_class(st.weight),
1292 italic: st.italic,
1293 styled: st.known,
1294 }
1295 })
1296 .collect()
1297 }
1298
1299 pub fn cells(&mut self, index: usize) -> PageParserCells {
1303 let Some(&pid) = self.pages.get(index) else {
1304 return PageParserCells::default();
1305 };
1306 let (_w, h) = page_size(&self.doc, pid);
1307 let (glyphs, inks) = page_glyphs_and_inks(&self.doc, pid, &mut self.caches);
1308 let (prose, words) = crate::dp_lines::line_and_word_cells(&glyphs, h, true);
1309 PageParserCells {
1310 prose,
1311 words,
1312 code: crate::pdfium_backend::code_cells_from_glyphs(&glyphs, h),
1313 checkboxes: crate::checkbox::find(&inks, h),
1314 }
1315 }
1316
1317 pub fn page_count(&self) -> usize {
1319 self.pages.len()
1320 }
1321
1322 pub fn text_page(&mut self, index: usize) -> crate::pdfium_backend::PdfPage {
1328 let (w, h, (glyphs, inks)) = match self.pages.get(index) {
1329 Some(&pid) => {
1330 let (w, h) = page_size(&self.doc, pid);
1331 (w, h, page_glyphs_and_inks(&self.doc, pid, &mut self.caches))
1332 }
1333 None => (0.0, 0.0, Default::default()),
1334 };
1335 let (mut prose, mut words) = crate::dp_lines::line_and_word_cells(&glyphs, h, true);
1336 drop_overpainted_cells(&mut prose);
1337 drop_overpainted_cells(&mut words);
1338 crate::pdfium_backend::PdfPage {
1339 #[cfg(feature = "ocr-prep")]
1340 image_layout: None,
1341 width: w,
1342 height: h,
1343 scale: 1.0,
1345 cells: prose,
1346 code_cells: crate::pdfium_backend::code_cells_from_glyphs(&glyphs, h),
1347 word_cells: words,
1348 checkboxes: crate::checkbox::find(&inks, h),
1349 #[cfg(feature = "ocr-prep")]
1350 image: image::RgbImage::new(1, 1),
1351 links: Vec::new(),
1352 rotation: 0,
1353 #[cfg(feature = "ml")]
1354 pending_image: None,
1355 }
1356 }
1357}
1358
1359pub fn pdf_all_cells(bytes: &[u8]) -> Vec<PageParserCells> {
1364 let Some(mut parser) = PageTextParser::open(bytes) else {
1365 return Vec::new();
1366 };
1367 (0..parser.pages.len()).map(|i| parser.cells(i)).collect()
1368}
1369
1370pub fn pdf_text_pages(bytes: &[u8]) -> Vec<crate::pdfium_backend::PdfPage> {
1377 let Some(mut parser) = PageTextParser::open(bytes) else {
1378 return Vec::new();
1379 };
1380 (0..parser.page_count())
1381 .map(|i| parser.text_page(i))
1382 .collect()
1383}
1384
1385fn drop_overpainted_cells(cells: &mut Vec<crate::pdfium_backend::TextCell>) {
1404 let mut paint = vec![false; cells.len()];
1405 for i in 0..cells.len() {
1406 for j in 0..cells.len() {
1407 if i == j || cells[i].text == cells[j].text {
1408 continue;
1409 }
1410 let (a, b) = (&cells[i], &cells[j]);
1411 let vo = (a.b.min(b.b) - a.t.max(b.t)).max(0.0);
1413 if vo < 0.6 * (a.b - a.t).min(b.b - b.t) {
1414 continue;
1415 }
1416 let ho = (a.r.min(b.r) - a.l.max(b.l)).max(0.0);
1418 if ho >= 0.8 * (a.r - a.l) && (a.r - a.l) <= (b.r - b.l) {
1419 paint[i] = true;
1420 paint[j] = true;
1421 }
1422 }
1423 }
1424 let mut keep = paint.iter().map(|p| !p);
1425 cells.retain(|_| keep.next().unwrap());
1426}
1427
1428#[derive(Clone, Copy)]
1432struct TextState {
1433 tc: f64,
1434 tw: f64,
1435 th: f64,
1436 tl: f64,
1437 trise: f64,
1438 fsize: f64,
1439}
1440
1441impl TextState {
1442 const INIT: TextState = TextState {
1443 tc: 0.0,
1444 tw: 0.0,
1445 th: 1.0,
1446 tl: 0.0,
1447 trise: 0.0,
1448 fsize: 0.0,
1449 };
1450}
1451
1452fn page_res(doc: &Document, page_id: lopdf::ObjectId) -> Option<&Dictionary> {
1455 let (inline, ids) = doc.get_page_resources(page_id).ok()?;
1456 if let Some(d) = inline {
1457 return Some(d);
1458 }
1459 ids.into_iter().find_map(|id| doc.get_dictionary(id).ok())
1460}
1461
1462fn fonts_from_res(
1466 doc: &Document,
1467 res: &Dictionary,
1468 caches: &mut DocCaches,
1469) -> HashMap<Vec<u8>, Arc<Font>> {
1470 let mut map = HashMap::new();
1471 let font_dict = res
1472 .get(b"Font")
1473 .ok()
1474 .and_then(|o| deref(doc, o))
1475 .and_then(|o| o.as_dict().ok());
1476 if let Some(fd) = font_dict {
1477 for (name, value) in fd.iter() {
1478 let font = match value {
1479 Object::Reference(id) => {
1480 let key = (*id, name.clone());
1481 if let Some(f) = caches.fonts.get(&key) {
1482 Arc::clone(f)
1483 } else if let Some(fdict) = deref(doc, value).and_then(|o| o.as_dict().ok()) {
1484 let f = Arc::new(parse_font(doc, name, fdict));
1485 caches.fonts.insert(key, Arc::clone(&f));
1486 f
1487 } else {
1488 continue;
1489 }
1490 }
1491 _ => {
1492 if let Some(fdict) = deref(doc, value).and_then(|o| o.as_dict().ok()) {
1493 Arc::new(parse_font(doc, name, fdict))
1494 } else {
1495 continue;
1496 }
1497 }
1498 };
1499 map.insert(name.clone(), font);
1500 }
1501 }
1502 map
1503}
1504
1505pub(crate) fn glyph_styles(
1511 bytes: &[u8],
1512 pages: &[usize],
1513) -> HashMap<usize, Vec<crate::heading_hierarchy::GlyphStyle>> {
1514 let mut out = HashMap::new();
1515 let Some(mut parser) = PageTextParser::open(bytes) else {
1516 return out;
1517 };
1518 for &page_no in pages {
1519 if page_no == 0 {
1520 continue;
1521 }
1522 let styles = parser.glyph_styles(page_no - 1);
1523 if !styles.is_empty() {
1524 out.insert(page_no, styles);
1525 }
1526 }
1527 out
1528}
1529
1530pub(crate) fn page_glyphs(doc: &Document, page_id: lopdf::ObjectId) -> Vec<Glyph> {
1531 page_glyphs_cached(doc, page_id, &mut DocCaches::default())
1532}
1533
1534fn page_glyphs_cached(
1537 doc: &Document,
1538 page_id: lopdf::ObjectId,
1539 caches: &mut DocCaches,
1540) -> Vec<Glyph> {
1541 page_glyphs_and_inks(doc, page_id, caches).0
1542}
1543
1544fn page_glyphs_and_inks(
1548 doc: &Document,
1549 page_id: lopdf::ObjectId,
1550 caches: &mut DocCaches,
1551) -> (Vec<Glyph>, Vec<crate::checkbox::Ink>) {
1552 let mut out = Vec::new();
1553 let mut inks = Vec::new();
1554 let content_bytes = doc.get_page_content(page_id);
1557 let Ok(content) = lopdf::content::Content::decode(&content_bytes) else {
1558 return (out, inks);
1559 };
1560 if let Some(res) = page_res(doc, page_id) {
1561 let pb = page_box(doc, page_id);
1564 let base = Mat {
1565 e: -(pb.l as f64),
1566 f: -(pb.b as f64),
1567 ..Mat::ID
1568 };
1569 run_content(
1570 doc,
1571 res,
1572 &content,
1573 base,
1574 TextState::INIT,
1575 0,
1576 caches,
1577 &mut out,
1578 &mut inks,
1579 );
1580 out.retain(|g| on_page(g, pb.w, pb.h));
1581 }
1582 (out, inks)
1583}
1584
1585fn on_page(g: &Glyph, w: f32, h: f32) -> bool {
1600 const EPS: f32 = 1e-3;
1602 let (l, b, r, t) = if [g.ll, g.lb, g.lr, g.lt].iter().all(|v| v.is_finite()) {
1603 (g.ll, g.lb, g.lr, g.lt)
1604 } else {
1605 (g.l, g.b, g.r, g.t)
1606 };
1607 if ![l, b, r, t].iter().all(|v| v.is_finite()) {
1608 return true;
1609 }
1610 l >= -EPS && b >= -EPS && r <= w + EPS && t <= h + EPS
1611}
1612
1613#[allow(clippy::too_many_arguments)]
1618fn run_content(
1619 doc: &Document,
1620 res: &Dictionary,
1621 content: &lopdf::content::Content,
1622 base_ctm: Mat,
1623 init: TextState,
1624 depth: u32,
1625 caches: &mut DocCaches,
1626 out: &mut Vec<Glyph>,
1627 inks: &mut Vec<crate::checkbox::Ink>,
1628) {
1629 let fonts = fonts_from_res(doc, res, caches);
1630 let xobjects = res
1631 .get(b"XObject")
1632 .ok()
1633 .and_then(|o| deref(doc, o))
1634 .and_then(|o| o.as_dict().ok());
1635
1636 #[allow(clippy::type_complexity)]
1641 let mut gstate_stack: Vec<(Mat, f64, f64, f64, f64, f64, f64, Option<&Arc<Font>>)> = Vec::new();
1642 let mut ctm = base_ctm;
1643 let mut tm = Mat::ID;
1644 let mut tlm = Mat::ID;
1645 let mut font: Option<&Arc<Font>> = None;
1646 let mut fsize = init.fsize;
1647 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;
1652
1653 let op_f = |operands: &[Object], i: usize| operands.get(i).and_then(num).unwrap_or(0.0);
1654 let mut path: Vec<PathPiece> = Vec::new();
1658 let (mut cur, mut start) = ((0.0, 0.0), (0.0, 0.0));
1659 let mut fill_light = false;
1663 let mut fill_stack: Vec<bool> = Vec::new();
1664
1665 for op in &content.operations {
1666 let operands = &op.operands;
1667 match op.operator.as_str() {
1668 "q" => {
1669 gstate_stack.push((ctm, tc, tw, th, tl, trise, fsize, font));
1670 fill_stack.push(fill_light);
1671 }
1672 "Q" => {
1673 fill_light = fill_stack.pop().unwrap_or(false);
1674 if let Some((c, a, b, h, l, r, fs, f)) = gstate_stack.pop() {
1675 ctm = c;
1676 tc = a;
1677 tw = b;
1678 th = h;
1679 tl = l;
1680 trise = r;
1681 fsize = fs;
1682 font = f;
1683 }
1684 }
1685 "cm" => {
1686 let m = Mat {
1687 a: op_f(operands, 0),
1688 b: op_f(operands, 1),
1689 c: op_f(operands, 2),
1690 d: op_f(operands, 3),
1691 e: op_f(operands, 4),
1692 f: op_f(operands, 5),
1693 };
1694 ctm = m.then(ctm);
1695 }
1696 "BT" => {
1697 tm = Mat::ID;
1698 tlm = Mat::ID;
1699 }
1700 "ET" => {}
1701 "Tf" => {
1702 if let Some(Object::Name(n)) = operands.first() {
1703 font = fonts.get(n.as_slice());
1704 }
1705 fsize = op_f(operands, 1);
1706 }
1707 "Td" => {
1708 tlm = Mat {
1709 a: 1.0,
1710 b: 0.0,
1711 c: 0.0,
1712 d: 1.0,
1713 e: op_f(operands, 0),
1714 f: op_f(operands, 1),
1715 }
1716 .then(tlm);
1717 tm = tlm;
1718 }
1719 "TD" => {
1720 tl = -op_f(operands, 1);
1721 tlm = Mat {
1722 a: 1.0,
1723 b: 0.0,
1724 c: 0.0,
1725 d: 1.0,
1726 e: op_f(operands, 0),
1727 f: op_f(operands, 1),
1728 }
1729 .then(tlm);
1730 tm = tlm;
1731 }
1732 "Tm" => {
1733 tlm = Mat {
1734 a: op_f(operands, 0),
1735 b: op_f(operands, 1),
1736 c: op_f(operands, 2),
1737 d: op_f(operands, 3),
1738 e: op_f(operands, 4),
1739 f: op_f(operands, 5),
1740 };
1741 tm = tlm;
1742 }
1743 "T*" => {
1744 tlm = Mat {
1745 a: 1.0,
1746 b: 0.0,
1747 c: 0.0,
1748 d: 1.0,
1749 e: 0.0,
1750 f: -tl,
1751 }
1752 .then(tlm);
1753 tm = tlm;
1754 }
1755 "Tc" => tc = op_f(operands, 0),
1756 "Tw" => tw = op_f(operands, 0),
1757 "Tz" => th = op_f(operands, 0) / 100.0,
1758 "TL" => tl = op_f(operands, 0),
1759 "Ts" => trise = op_f(operands, 0),
1760 "Tj" | "'" | "\"" => {
1761 if op.operator == "'" || op.operator == "\"" {
1762 tlm = Mat {
1764 a: 1.0,
1765 b: 0.0,
1766 c: 0.0,
1767 d: 1.0,
1768 e: 0.0,
1769 f: -tl,
1770 }
1771 .then(tlm);
1772 tm = tlm;
1773 }
1774 if op.operator == "\"" {
1775 tw = op_f(operands, 0);
1778 tc = op_f(operands, 1);
1779 }
1780 if let (Some(f), Some(Object::String(s, _))) = (font, operands.last()) {
1781 show_text(f, s, fsize, tc, tw, th, trise, &mut tm, ctm, out);
1782 }
1783 }
1784 "TJ" => {
1785 if let (Some(f), Some(Object::Array(arr))) = (font, operands.first()) {
1786 for el in arr {
1787 match el {
1788 Object::String(s, _) => {
1789 show_text(f, s, fsize, tc, tw, th, trise, &mut tm, ctm, out)
1790 }
1791 other => {
1792 if let Some(adj) = num(other) {
1793 let tx = -adj / 1000.0 * fsize * th;
1795 tm = Mat {
1796 a: 1.0,
1797 b: 0.0,
1798 c: 0.0,
1799 d: 1.0,
1800 e: tx,
1801 f: 0.0,
1802 }
1803 .then(tm);
1804 }
1805 }
1806 }
1807 }
1808 }
1809 }
1810 "m" => {
1811 cur = ctm.apply(op_f(operands, 0), op_f(operands, 1));
1812 start = cur;
1813 }
1814 "l" => {
1815 let p = ctm.apply(op_f(operands, 0), op_f(operands, 1));
1816 path.push(PathPiece::Line(cur, p));
1817 cur = p;
1818 }
1819 "c" | "v" | "y" => {
1820 let pts: Vec<(f64, f64)> = (0..operands.len() / 2)
1821 .map(|k| ctm.apply(op_f(operands, 2 * k), op_f(operands, 2 * k + 1)))
1822 .collect();
1823 if let Some(&end) = pts.last() {
1824 path.push(PathPiece::Curve(
1825 std::iter::once(cur).chain(pts.iter().copied()).collect(),
1826 ));
1827 cur = end;
1828 }
1829 }
1830 "h" => {
1831 if cur != start {
1832 path.push(PathPiece::Line(cur, start));
1833 }
1834 cur = start;
1835 }
1836 "re" => {
1837 let (x, y, w, h) = (
1838 op_f(operands, 0),
1839 op_f(operands, 1),
1840 op_f(operands, 2),
1841 op_f(operands, 3),
1842 );
1843 path.push(PathPiece::Rect([
1844 ctm.apply(x, y),
1845 ctm.apply(x + w, y),
1846 ctm.apply(x + w, y + h),
1847 ctm.apply(x, y + h),
1848 ]));
1849 cur = ctm.apply(x, y);
1850 start = cur;
1851 }
1852 "S" | "s" | "f" | "F" | "f*" | "B" | "B*" | "b" | "b*" => {
1853 let op = op.operator.as_str();
1854 if matches!(op, "s" | "b" | "b*") && cur != start {
1855 path.push(PathPiece::Line(cur, start));
1856 }
1857 let stroke = matches!(op, "S" | "s" | "B" | "B*" | "b" | "b*");
1858 let fill = !matches!(op, "S" | "s");
1859 paint_path(&path, stroke, fill, fill_light, inks);
1860 path.clear();
1861 }
1862 "n" => path.clear(),
1863 "g" | "rg" | "k" | "sc" | "scn" => {
1866 let v: Vec<f64> = operands
1867 .iter()
1868 .map(num)
1869 .collect::<Option<_>>()
1870 .unwrap_or_default();
1871 fill_light = match v.as_slice() {
1872 [g] => *g >= 0.9,
1873 [r, g, b] => r.min(*g).min(*b) >= 0.9,
1874 [c, m, y, k] => c.max(*m).max(*y).max(*k) <= 0.1,
1875 _ => false,
1876 };
1877 }
1878 "cs" => fill_light = false,
1879 "Do" => {
1880 if depth >= 8 {
1883 continue;
1884 }
1885 let Some(Object::Name(n)) = operands.first() else {
1886 continue;
1887 };
1888 let obj = xobjects.and_then(|d| d.get(n.as_slice()).ok());
1889 let form_id = match obj {
1890 Some(Object::Reference(id)) => Some(*id),
1891 _ => None,
1892 };
1893 let stream = obj
1894 .and_then(|o| deref(doc, o))
1895 .and_then(|o| o.as_stream().ok());
1896 let Some(stream) = stream else { continue };
1897 let is_form = stream
1898 .dict
1899 .get(b"Subtype")
1900 .ok()
1901 .and_then(|o| o.as_name().ok())
1902 == Some(b"Form".as_slice());
1903 if !is_form {
1904 continue;
1905 }
1906 let cached = form_id.and_then(|id| caches.forms.get(&id).cloned());
1909 let form_content = match cached {
1910 Some(c) => c,
1911 None => {
1912 let Ok(data) = stream.decompressed_content() else {
1913 continue;
1914 };
1915 let Ok(c) = lopdf::content::Content::decode(&data) else {
1916 continue;
1917 };
1918 let c = Arc::new(c);
1919 if let Some(id) = form_id {
1920 caches.forms.insert(id, Arc::clone(&c));
1921 }
1922 c
1923 }
1924 };
1925 let form_mat = match stream.dict.get(b"Matrix").ok() {
1927 Some(Object::Array(a)) if a.len() == 6 => {
1928 let v: Vec<f64> = a.iter().filter_map(num).collect();
1929 if v.len() == 6 {
1930 Mat {
1931 a: v[0],
1932 b: v[1],
1933 c: v[2],
1934 d: v[3],
1935 e: v[4],
1936 f: v[5],
1937 }
1938 } else {
1939 Mat::ID
1940 }
1941 }
1942 _ => Mat::ID,
1943 };
1944 let form_res = stream
1946 .dict
1947 .get(b"Resources")
1948 .ok()
1949 .and_then(|o| deref(doc, o))
1950 .and_then(|o| o.as_dict().ok())
1951 .unwrap_or(res);
1952 let state = TextState {
1953 tc,
1954 tw,
1955 th,
1956 tl,
1957 trise,
1958 fsize,
1959 };
1960 run_content(
1961 doc,
1962 form_res,
1963 &form_content,
1964 form_mat.then(ctm),
1965 state,
1966 depth + 1,
1967 caches,
1968 out,
1969 inks,
1970 );
1971 }
1972 _ => {}
1973 }
1974 }
1975}
1976
1977enum PathPiece {
1979 Line((f64, f64), (f64, f64)),
1980 Rect([(f64, f64); 4]),
1982 Curve(Vec<(f64, f64)>),
1984}
1985
1986fn paint_path(
1994 path: &[PathPiece],
1995 stroke: bool,
1996 fill: bool,
1997 fill_light: bool,
1998 inks: &mut Vec<crate::checkbox::Ink>,
1999) {
2000 use crate::checkbox::Ink;
2001 let bbox = |pts: &mut dyn Iterator<Item = (f64, f64)>| {
2002 pts.fold(
2003 (
2004 f64::INFINITY,
2005 f64::INFINITY,
2006 f64::NEG_INFINITY,
2007 f64::NEG_INFINITY,
2008 ),
2009 |(l, b, r, t), (x, y)| (l.min(x), b.min(y), r.max(x), t.max(y)),
2010 )
2011 };
2012 let mut keep = |ink: Ink| {
2013 if ink.worth_keeping() {
2014 inks.push(ink);
2015 }
2016 };
2017 if fill && !fill_light {
2018 let (l, b, r, t) = bbox(&mut path.iter().flat_map(|p| match p {
2022 PathPiece::Line(a, z) => vec![*a, *z],
2023 PathPiece::Rect(c) => c.to_vec(),
2024 PathPiece::Curve(c) => c.clone(),
2025 }));
2026 if l.is_finite() {
2027 keep(Ink::Blot { l, b, r, t });
2028 }
2029 return;
2030 }
2031 if !stroke {
2032 return;
2033 }
2034 for piece in path {
2035 match piece {
2036 PathPiece::Line(a, z) => keep(Ink::Seg {
2037 x0: a.0,
2038 y0: a.1,
2039 x1: z.0,
2040 y1: z.1,
2041 }),
2042 PathPiece::Rect(c) => {
2043 let axis = ((c[0].1 - c[1].1).abs() < 1e-6 && (c[1].0 - c[2].0).abs() < 1e-6)
2044 || ((c[0].0 - c[1].0).abs() < 1e-6 && (c[1].1 - c[2].1).abs() < 1e-6);
2045 if axis {
2046 let (l, b, r, t) = bbox(&mut c.iter().copied());
2047 keep(Ink::Rect { l, b, r, t });
2048 } else {
2049 for k in 0..4 {
2050 let (a, z) = (c[k], c[(k + 1) % 4]);
2051 keep(Ink::Seg {
2052 x0: a.0,
2053 y0: a.1,
2054 x1: z.0,
2055 y1: z.1,
2056 });
2057 }
2058 }
2059 }
2060 PathPiece::Curve(c) => {
2061 let (l, b, r, t) = bbox(&mut c.iter().copied());
2062 keep(Ink::Blot { l, b, r, t });
2063 }
2064 }
2065 }
2066}
2067
2068#[allow(clippy::too_many_arguments)]
2069fn show_text(
2070 font: &Font,
2071 bytes: &[u8],
2072 fsize: f64,
2073 tc: f64,
2074 tw: f64,
2075 th: f64,
2076 trise: f64,
2077 tm: &mut Mat,
2078 ctm: Mat,
2079 out: &mut Vec<Glyph>,
2080) {
2081 for code in codes(font, bytes) {
2082 let (text, w) = font.decode_code(code);
2083 let w0 = w / 1000.0; let scale = Mat {
2086 a: fsize * th,
2087 b: 0.0,
2088 c: 0.0,
2089 d: fsize,
2090 e: 0.0,
2091 f: trise,
2092 };
2093 let trm = scale.then(*tm).then(ctm);
2094 let (desc, asc) = (font.descent / 1000.0, font.ascent / 1000.0);
2096 let (x0, y0) = trm.apply(0.0, desc);
2097 let (x1, y1) = trm.apply(w0, desc);
2098 let (x2, y2) = trm.apply(w0, asc);
2099 let (x3, y3) = trm.apply(0.0, asc);
2100 let upright = trm.a > 0.0 && trm.b.abs() <= 1e-6 * trm.a;
2108 let (left, bot, right, top, quad) = if upright {
2109 (x0.min(x1), y0.min(y3), x0.max(x1), y0.max(y3), None)
2110 } else {
2111 let (xs, ys) = ([x0, x1, x2, x3], [y0, y1, y2, y3]);
2112 let fold = |v: [f64; 4], f: fn(f64, f64) -> f64| v.into_iter().reduce(f).unwrap();
2113 let q = [x0, y0, x1, y1, x2, y2, x3, y3].map(|v| v as f32);
2114 (
2115 fold(xs, f64::min),
2116 fold(ys, f64::min),
2117 fold(xs, f64::max),
2118 fold(ys, f64::max),
2119 Some(q),
2120 )
2121 };
2122 if let Some(s) = text {
2123 for ch in s.chars() {
2125 if ch != '\u{0}' {
2126 out.push(Glyph {
2127 ch,
2128 l: left as f32,
2129 b: bot as f32,
2130 r: right as f32,
2131 t: top as f32,
2132 ll: left as f32,
2133 lb: bot as f32,
2134 lr: right as f32,
2135 lt: top as f32,
2136 font: font.hash,
2137 quad,
2138 });
2139 }
2140 }
2141 }
2142 let is_space = !font.two_byte && code == 32;
2144 let tx = (w0 * fsize + tc + if is_space { tw } else { 0.0 }) * th;
2145 *tm = Mat {
2146 a: 1.0,
2147 b: 0.0,
2148 c: 0.0,
2149 d: 1.0,
2150 e: tx,
2151 f: 0.0,
2152 }
2153 .then(*tm);
2154 }
2155}
2156
2157fn simple_encoding_table(doc: &Document, fdict: &Dictionary) -> HashMap<u8, char> {
2161 let enc = fdict.get(b"Encoding").ok().and_then(|o| deref(doc, o));
2162 let base_name = match enc {
2163 Some(Object::Name(n)) => n.clone(),
2164 Some(Object::Dictionary(d)) => d
2165 .get(b"BaseEncoding")
2166 .ok()
2167 .and_then(|o| o.as_name().ok())
2168 .map(|n| n.to_vec())
2169 .unwrap_or_default(),
2170 _ => Vec::new(),
2171 };
2172 let mut m = if base_name == b"MacRomanEncoding" {
2173 macroman_table()
2174 } else if base_name.is_empty() {
2175 tex_math_builtin(fdict).unwrap_or_else(winansi_table)
2182 } else {
2183 winansi_table()
2184 };
2185 if let Some(Object::Dictionary(d)) = enc {
2187 if let Some(Object::Array(diffs)) = d.get(b"Differences").ok().and_then(|o| deref(doc, o)) {
2188 let mut code = 0u8;
2189 for el in diffs {
2190 match el {
2191 Object::Integer(i) => code = *i as u8,
2192 Object::Name(name) => {
2193 if let Some(ch) = glyph_name_to_char(name) {
2194 m.insert(code, ch);
2195 }
2196 code = code.wrapping_add(1);
2197 }
2198 _ => {}
2199 }
2200 }
2201 }
2202 }
2203 m
2204}
2205
2206fn tex_math_builtin(fdict: &Dictionary) -> Option<HashMap<u8, char>> {
2212 const CMSY: [char; 128] = [
2213 '−', '·', '×', '∗', '÷', '⋄', '±', '∓', '⊕', '⊖', '⊗', '⊘', '⊙', '◯', '∘', '•', '≍', '≡',
2214 '⊆', '⊇', '≤', '≥', '≼', '≽', '∼', '≈', '⊂', '⊃', '≪', '≫', '≺', '≻', '←', '→', '↑', '↓',
2215 '↔', '↗', '↘', '≃', '⇐', '⇒', '⇑', '⇓', '⇔', '↖', '↙', '∝', '′', '∞', '∈', '∋', '△', '▽',
2216 '\u{338}', '↦', '∀', '∃', '¬', '∅', 'ℜ', 'ℑ', '⊤', '⊥', 'ℵ', 'A', 'B', 'C', 'D', 'E', 'F',
2217 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
2218 'Y', 'Z', '∪', '∩', '⊎', '∧', '∨', '⊢', '⊣', '⌊', '⌋', '⌈', '⌉', '{', '}', '⟨', '⟩', '|',
2219 '∥', '↕', '⇕', '\\', '≀', '√', '∐', '∇', '∫', '⊔', '⊓', '⊑', '⊒', '§', '†', '‡', '¶', '♣',
2220 '♢', '♡', '♠',
2221 ];
2222 const CMMI: [char; 128] = [
2223 'Γ', 'Δ', 'Θ', 'Λ', 'Ξ', 'Π', 'Σ', 'Υ', 'Φ', 'Ψ', 'Ω', 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η',
2224 'θ', 'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'π', 'ρ', 'σ', 'τ', 'υ', 'φ', 'χ', 'ψ', 'ω', 'ϵ', 'ϑ',
2225 'ϖ', 'ϱ', 'ς', 'ϕ', '↼', '↽', '⇀', '⇁', '↩', '↪', '▷', '◁', '0', '1', '2', '3', '4', '5',
2226 '6', '7', '8', '9', '.', ',', '<', '/', '>', '⋆', '∂', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
2227 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y',
2228 'Z', '♭', '♮', '♯', '⌣', '⌢', 'ℓ', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
2229 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'ı', 'ȷ', '℘',
2230 '\u{20d7}', '⁀',
2231 ];
2232 let name = base_font_name(fdict)?;
2233 let up = name.to_ascii_uppercase();
2234 let table: &[char; 128] = if up.starts_with(b"CMSY") || up.starts_with(b"CMBSY") {
2235 &CMSY
2236 } else if up.starts_with(b"CMMI") {
2237 &CMMI
2238 } else {
2239 return None;
2240 };
2241 Some(
2242 table
2243 .iter()
2244 .enumerate()
2245 .map(|(i, &c)| (i as u8, c))
2246 .collect(),
2247 )
2248}
2249
2250pub(crate) fn glyph_name_to_char(name: &[u8]) -> Option<char> {
2255 let s = std::str::from_utf8(name).ok()?;
2256 if let Some(hex) = s.strip_prefix("uni") {
2257 if let Ok(cp) = u32::from_str_radix(hex.get(0..4)?, 16) {
2258 return char::from_u32(cp);
2259 }
2260 }
2261 if s.len() == 1 {
2263 let b = s.as_bytes()[0];
2264 if b.is_ascii_alphabetic() {
2265 return Some(b as char);
2266 }
2267 }
2268 let resolved = match s {
2269 "space" => ' ',
2270 "exclam" => '!',
2271 "quotedbl" => '"',
2272 "numbersign" => '#',
2273 "dollar" => '$',
2274 "percent" => '%',
2275 "ampersand" => '&',
2276 "quotesingle" => '\'',
2277 "parenleft" => '(',
2278 "parenright" => ')',
2279 "asterisk" => '*',
2280 "plus" => '+',
2281 "comma" => ',',
2282 "hyphen" => '-',
2283 "period" => '.',
2284 "slash" => '/',
2285 "zero" => '0',
2286 "one" => '1',
2287 "two" => '2',
2288 "three" => '3',
2289 "four" => '4',
2290 "five" => '5',
2291 "six" => '6',
2292 "seven" => '7',
2293 "eight" => '8',
2294 "nine" => '9',
2295 "colon" => ':',
2296 "semicolon" => ';',
2297 "less" => '<',
2298 "equal" => '=',
2299 "greater" => '>',
2300 "question" => '?',
2301 "at" => '@',
2302 "bracketleft" => '[',
2303 "backslash" => '\\',
2304 "bracketright" => ']',
2305 "asciicircum" => '^',
2306 "underscore" => '_',
2307 "grave" => '`',
2308 "braceleft" => '{',
2309 "bar" => '|',
2310 "braceright" => '}',
2311 "asciitilde" => '~',
2312 "bullet" => '\u{2022}',
2313 "periodcentered" => '\u{00B7}',
2314 "endash" => '\u{2013}',
2315 "emdash" => '\u{2014}',
2316 "quoteright" => '\u{2019}',
2317 "quoteleft" => '\u{2018}',
2318 "quotedblleft" => '\u{201C}',
2319 "quotedblright" => '\u{201D}',
2320 "quotedblbase" => '\u{201E}',
2321 "quotesinglbase" => '\u{201A}',
2322 "ff" => '\u{FB00}',
2327 "fi" => '\u{FB01}',
2328 "fl" => '\u{FB02}',
2329 "ffi" => '\u{FB03}',
2330 "ffl" => '\u{FB04}',
2331 "ft" => '\u{FB05}',
2332 "st" => '\u{FB06}',
2333 "degree" => '\u{00B0}',
2334 "trademark" => '\u{2122}',
2335 "registered" => '\u{00AE}',
2336 "copyright" => '\u{00A9}',
2337 "ellipsis" => '\u{2026}',
2338 "minus" => '\u{2212}',
2339 "fraction" => '\u{2044}',
2340 "nbspace" => '\u{00A0}',
2341 "alpha" => '\u{03B1}',
2346 "beta" => '\u{03B2}',
2347 "gamma" => '\u{03B3}',
2348 "delta" => '\u{03B4}',
2349 "epsilon" | "epsilon1" => '\u{03B5}',
2350 "zeta" => '\u{03B6}',
2351 "eta" => '\u{03B7}',
2352 "theta" | "theta1" => '\u{03B8}',
2353 "iota" => '\u{03B9}',
2354 "kappa" => '\u{03BA}',
2355 "lambda" => '\u{03BB}',
2356 "mu" => '\u{03BC}',
2357 "nu" => '\u{03BD}',
2358 "xi" => '\u{03BE}',
2359 "omicron" => '\u{03BF}',
2360 "pi" | "pi1" => '\u{03C0}',
2361 "rho" | "rho1" => '\u{03C1}',
2362 "sigma" => '\u{03C3}',
2363 "sigma1" => '\u{03C2}',
2364 "tau" => '\u{03C4}',
2365 "upsilon" => '\u{03C5}',
2366 "phi" | "phi1" => '\u{03C6}',
2367 "chi" => '\u{03C7}',
2368 "psi" => '\u{03C8}',
2369 "omega" | "omega1" => '\u{03C9}',
2370 "Gamma" => '\u{0393}',
2371 "Delta" => '\u{0394}',
2372 "Theta" => '\u{0398}',
2373 "Lambda" => '\u{039B}',
2374 "Xi" => '\u{039E}',
2375 "Pi" => '\u{03A0}',
2376 "Sigma" => '\u{03A3}',
2377 "Upsilon" => '\u{03A5}',
2378 "Phi" => '\u{03A6}',
2379 "Psi" => '\u{03A8}',
2380 "Omega" => '\u{03A9}',
2381 "lessequal" => '\u{2264}',
2382 "greaterequal" => '\u{2265}',
2383 "notequal" => '\u{2260}',
2384 "approxequal" => '\u{2248}',
2385 "equivalence" => '\u{2261}',
2386 "element" => '\u{2208}',
2387 "plusminus" => '\u{00B1}',
2388 "multiply" => '\u{00D7}',
2389 "divide" => '\u{00F7}',
2390 "infinity" => '\u{221E}',
2391 "partialdiff" => '\u{2202}',
2392 "gradient" => '\u{2207}',
2393 "summation" => '\u{2211}',
2394 "product" => '\u{220F}',
2395 "integral" => '\u{222B}',
2396 "radical" => '\u{221A}',
2397 "proportional" => '\u{221D}',
2398 "arrowright" => '\u{2192}',
2399 "arrowleft" => '\u{2190}',
2400 "arrowup" => '\u{2191}',
2401 "arrowdown" => '\u{2193}',
2402 "arrowboth" => '\u{2194}',
2403 "arrowdblright" => '\u{21D2}',
2404 "logicaland" => '\u{2227}',
2405 "logicalor" => '\u{2228}',
2406 "intersection" => '\u{2229}',
2407 "union" => '\u{222A}',
2408 "similar" => '\u{223C}',
2409 "congruent" => '\u{2245}',
2410 "dotmath" => '\u{22C5}',
2411 "asteriskmath" => '\u{2217}',
2412 _ => {
2413 if let Some((base, _)) = s.split_once('.') {
2415 if !base.is_empty() {
2416 return glyph_name_to_char(base.as_bytes());
2417 }
2418 }
2419 return None;
2420 }
2421 };
2422 Some(resolved)
2423}
2424
2425fn winansi_table() -> HashMap<u8, char> {
2427 let mut m = HashMap::new();
2428 for b in 0x20u8..=0x7e {
2429 m.insert(b, b as char);
2430 }
2431 let extra: &[(u8, char)] = &[
2433 (0x91, '\u{2018}'),
2434 (0x92, '\u{2019}'),
2435 (0x93, '\u{201C}'),
2436 (0x94, '\u{201D}'),
2437 (0x95, '\u{2022}'),
2438 (0x96, '\u{2013}'),
2439 (0x97, '\u{2014}'),
2440 (0x85, '\u{2026}'),
2441 (0xA0, '\u{00A0}'),
2442 ];
2443 for &(b, c) in extra {
2444 m.insert(b, c);
2445 }
2446 for b in 0xA1u8..=0xFF {
2447 m.entry(b).or_insert(b as char);
2448 }
2449 m
2450}
2451
2452fn macroman_table() -> HashMap<u8, char> {
2455 let mut m = HashMap::new();
2456 for b in 0x20u8..=0x7e {
2457 m.insert(b, b as char);
2458 }
2459 let high: &[(u8, char)] = &[
2460 (0xA5, '\u{2022}'), (0xD0, '\u{2013}'), (0xD1, '\u{2014}'), (0xD2, '\u{201C}'),
2464 (0xD3, '\u{201D}'),
2465 (0xD4, '\u{2018}'),
2466 (0xD5, '\u{2019}'),
2467 (0xCA, '\u{00A0}'),
2468 (0xC9, '\u{2026}'),
2469 (0xDE, '\u{FB01}'),
2470 (0xDF, '\u{FB02}'),
2471 ];
2472 for &(b, c) in high {
2473 m.insert(b, c);
2474 }
2475 m
2476}
2477
2478#[cfg(test)]
2479mod page_box_frame {
2480 use super::*;
2481
2482 fn pdf(boxes: &str, x: f32, y: f32) -> Vec<u8> {
2485 pdf_content(
2486 boxes,
2487 &format!("BT /F1 12 Tf {x} {y} Td (First printing) Tj ET\n"),
2488 )
2489 }
2490
2491 fn pdf_content(boxes: &str, content: &str) -> Vec<u8> {
2494 let objs: Vec<String> = vec![
2495 "<</Type/Catalog/Pages 2 0 R>>".into(),
2496 format!("<</Type/Pages/Kids[3 0 R]/Count 1{boxes}>>"),
2497 "<</Type/Page/Parent 2 0 R/Contents 4 0 R/Resources<</Font<</F1 5 0 R>>>>>>".into(),
2498 format!("<</Length {}>>stream\n{content}endstream", content.len()),
2499 "<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>".into(),
2500 ];
2501 let mut out = b"%PDF-1.4\n".to_vec();
2502 let mut offsets = Vec::new();
2503 for (i, body) in objs.iter().enumerate() {
2504 offsets.push(out.len());
2505 out.extend_from_slice(format!("{} 0 obj{body}endobj\n", i + 1).as_bytes());
2506 }
2507 let xref_at = out.len();
2508 out.extend_from_slice(
2509 format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes(),
2510 );
2511 for off in &offsets {
2512 out.extend_from_slice(format!("{off:010} 00000 n \n").as_bytes());
2513 }
2514 out.extend_from_slice(
2515 format!(
2516 "trailer<</Size {}/Root 1 0 R>>\nstartxref\n{xref_at}\n%%EOF\n",
2517 objs.len() + 1
2518 )
2519 .as_bytes(),
2520 );
2521 out
2522 }
2523
2524 fn only_page(bytes: &[u8]) -> (PageBox, Vec<Glyph>) {
2525 let doc = load_document(bytes).expect("loads");
2526 let pid = *doc.get_pages().values().next().expect("one page");
2527 (page_box(&doc, pid), page_glyphs(&doc, pid))
2528 }
2529
2530 #[test]
2535 fn glyphs_count_from_the_cropbox_corner_like_pdfium() {
2536 let (pb, shifted) = only_page(&pdf(
2537 "/MediaBox[-56.505 -58.25 576.303 723.31]/CropBox[1.095 -0.65 518.703 665.71]",
2538 37.0 + 1.095,
2539 58.0 - 0.65,
2540 ));
2541 assert!((pb.l - 1.095).abs() < 1e-3 && (pb.b + 0.65).abs() < 1e-3);
2542 assert!(
2543 (pb.w - 517.608).abs() < 1e-3 && (pb.h - 666.36).abs() < 1e-3,
2544 "{pb:?}"
2545 );
2546 let (pb0, plain) = only_page(&pdf("/MediaBox[0 0 517.608 666.36]", 37.0, 58.0));
2547 assert!((pb0.w - pb.w).abs() < 1e-3 && (pb0.h - pb.h).abs() < 1e-3);
2548 assert_eq!(shifted.len(), plain.len());
2549 assert!(!plain.is_empty());
2550 for (a, b) in shifted.iter().zip(&plain) {
2551 assert!(
2552 (a.l - b.l).abs() < 1e-3 && (a.b - b.b).abs() < 1e-3,
2553 "{:?} vs {:?}",
2554 (a.l, a.b),
2555 (b.l, b.b)
2556 );
2557 }
2558 assert!((plain[0].l - 37.0).abs() < 1e-3, "{}", plain[0].l);
2559 }
2560
2561 fn text(glyphs: &[Glyph]) -> String {
2562 glyphs.iter().map(|g| g.ch).collect()
2563 }
2564
2565 #[test]
2570 fn glyphs_outside_the_display_box_are_dropped() {
2571 let (_, g) = only_page(&pdf_content(
2572 "/MediaBox[0 0 400 400]/CropBox[100 100 400 400]",
2573 "BT /F1 14 Tf 120 300 Td (Visible.) Tj ET\nBT /F1 8 Tf 5 40 Td (Slug) Tj ET\n",
2574 ));
2575 assert_eq!(text(&g), "Visible.");
2576 let (_, g) = only_page(&pdf_content(
2577 "/MediaBox[0 0 300 400]",
2578 "BT /F1 14 Tf 40 300 Td (On page) Tj ET\nBT /F1 14 Tf 400 250 Td (Beyond) Tj ET\n",
2579 ));
2580 assert_eq!(text(&g), "On page");
2581 let (_, g) = only_page(&pdf_content(
2583 "/MediaBox[0 0 300 400]",
2584 "BT /F1 14 Tf 200 250 Td (Crossing the right edge) Tj ET\n",
2585 ));
2586 assert_eq!(text(&g), "Crossing the rig");
2587 }
2588
2589 #[test]
2594 fn a_glyph_on_the_edge_stays_one_over_it_goes() {
2595 let w_at = |x: f32, y: f32| {
2596 let (_, g) = only_page(&pdf_content(
2597 "/MediaBox[0 0 300 400]",
2598 &format!("BT /F1 50 Tf {x} {y} Td (W) Tj ET\n"),
2599 ));
2600 text(&g)
2601 };
2602 assert_eq!(w_at(252.8, 200.0), "W");
2603 assert_eq!(w_at(252.81, 200.0), "");
2604 assert_eq!(w_at(0.0, 200.0), "W");
2605 assert_eq!(w_at(-0.01, 200.0), "");
2606 assert_eq!(w_at(100.0, 20.0), "W");
2609 assert_eq!(w_at(100.0, 0.0), "");
2610 assert_eq!(w_at(100.0, 380.0), "");
2611 }
2612
2613 #[test]
2616 fn page_box_follows_pdfium_fallbacks() {
2617 let (pb, _) = only_page(&pdf("", 10.0, 10.0));
2618 assert_eq!((pb.l, pb.b, pb.w, pb.h), (0.0, 0.0, 612.0, 792.0));
2619 let (pb, _) = only_page(&pdf(
2620 "/MediaBox[0 0 500 700]/CropBox[-100 100 600 900]",
2621 10.0,
2622 10.0,
2623 ));
2624 assert_eq!((pb.l, pb.b, pb.w, pb.h), (0.0, 100.0, 500.0, 600.0));
2625 let (pb, _) = only_page(&pdf(
2626 "/MediaBox[0 0 500 700]/CropBox[800 800 900 900]",
2627 10.0,
2628 10.0,
2629 ));
2630 assert_eq!((pb.l, pb.b, pb.w, pb.h), (0.0, 0.0, 500.0, 700.0));
2631 let (pb, _) = only_page(&pdf("/MediaBox[500 700 0 0]", 10.0, 10.0));
2633 assert_eq!((pb.l, pb.b, pb.w, pb.h), (0.0, 0.0, 500.0, 700.0));
2634 }
2635}
2636
2637#[cfg(test)]
2638mod xref_repair {
2639 fn pdf_with_xref(two_byte_eol: bool) -> Vec<u8> {
2643 let content = b"BT /F1 12 Tf 72 700 Td (Invoice 922769430725) Tj ET\n";
2644 let stream = format!("<</Length {}>>stream\n", content.len()).into_bytes();
2645 let objs: Vec<Vec<u8>> = vec![
2646 b"<</Type/Catalog/Pages 2 0 R>>".to_vec(),
2647 b"<</Type/Pages/Kids[3 0 R]/Count 1>>".to_vec(),
2648 b"<</Type/Page/Parent 2 0 R/MediaBox[0 0 595 842]/Contents 4 0 R\
2649 /Resources<</Font<</F1 5 0 R>>>>>>"
2650 .to_vec(),
2651 [stream.as_slice(), content.as_slice(), b"endstream"].concat(),
2652 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>".to_vec(),
2653 ];
2654
2655 let mut out = b"%PDF-1.4\n".to_vec();
2656 let mut offsets = Vec::new();
2657 for (i, body) in objs.iter().enumerate() {
2658 offsets.push(out.len());
2659 out.extend_from_slice(format!("{} 0 obj", i + 1).as_bytes());
2660 out.extend_from_slice(body);
2661 out.extend_from_slice(b"endobj\n");
2662 }
2663 let xref_at = out.len();
2664 let eol: &[u8] = if two_byte_eol { b" \n" } else { b"\n" };
2665 out.extend_from_slice(format!("xref\n0 {}\n", objs.len() + 1).as_bytes());
2666 out.extend_from_slice(b"0000000000 65535 f");
2667 out.extend_from_slice(eol);
2668 for off in &offsets {
2669 out.extend_from_slice(format!("{off:010} 00000 n").as_bytes());
2670 out.extend_from_slice(eol);
2671 }
2672 out.extend_from_slice(
2673 format!("trailer<</Size {}/Root 1 0 R>>\n", objs.len() + 1).as_bytes(),
2674 );
2675 out.extend_from_slice(format!("startxref\n{xref_at}\n%%EOF\n").as_bytes());
2676 out
2677 }
2678
2679 #[test]
2685 fn short_xref_entries_still_parse() {
2686 let good = pdf_with_xref(true);
2687 let broken = pdf_with_xref(false);
2688 assert!(
2689 broken.len() < good.len(),
2690 "the broken file is the shorter one"
2691 );
2692 assert!(
2693 lopdf::Document::load_mem(&good).is_ok(),
2694 "the control file must load unaided"
2695 );
2696 assert!(
2697 lopdf::Document::load_mem(&broken).is_err(),
2698 "lopdf rejects 19-byte entries — if this ever passes, drop the repair"
2699 );
2700
2701 let cells = |b: &[u8]| -> Vec<String> {
2702 super::pdf_textlines(b)
2703 .into_iter()
2704 .flat_map(|(_, _, c)| c.into_iter().map(|c| c.text))
2705 .collect()
2706 };
2707 let from_good = cells(&good);
2708 assert!(
2709 from_good.iter().any(|t| t.contains("922769430725")),
2710 "control text: {from_good:?}"
2711 );
2712 assert_eq!(
2713 cells(&broken),
2714 from_good,
2715 "repair must match the good parse"
2716 );
2717 }
2718
2719 #[test]
2724 fn overstated_stream_length_still_yields_content() {
2725 let good = pdf_with_xref(true);
2726 let broken = {
2729 let at = good
2730 .windows(8)
2731 .position(|w| w == b"/Length ")
2732 .expect("a /Length")
2733 + 8;
2734 let digits = good[at..].iter().take_while(|c| c.is_ascii_digit()).count();
2735 let n: usize = std::str::from_utf8(&good[at..at + digits])
2736 .unwrap()
2737 .parse()
2738 .unwrap();
2739 let inflated = (n + 1).to_string();
2740 assert_eq!(inflated.len(), digits, "keep the digit count");
2741 let mut b = good.clone();
2742 b[at..at + digits].copy_from_slice(inflated.as_bytes());
2743 b
2744 };
2745 assert_eq!(broken.len(), good.len(), "the defect must not move bytes");
2746 let raw = lopdf::Document::load_mem(&broken).expect("still loads");
2748 assert!(
2749 raw.get_pages()
2750 .into_values()
2751 .all(|p| raw.get_page_content(p).is_empty()),
2752 "lopdf should drop the stream — if it stops, drop this repair"
2753 );
2754 let text = |b: &[u8]| -> Vec<String> {
2756 super::pdf_textlines(b)
2757 .into_iter()
2758 .flat_map(|(_, _, c)| c.into_iter().map(|c| c.text))
2759 .collect()
2760 };
2761 let expected = text(&good);
2762 assert!(!expected.is_empty(), "control must produce text");
2763 assert_eq!(text(&broken), expected);
2764 }
2765
2766 #[test]
2770 fn repair_declines_when_padding_would_move_objects() {
2771 let mut incremental = pdf_with_xref(false);
2772 incremental.extend_from_slice(b"6 0 obj<</Type/Whatever>>endobj\n");
2773 let declined = super::pad_short_xref_entries(&incremental).unwrap_err();
2774 assert!(
2775 declined.contains("object follows the xref"),
2776 "reason: {declined}"
2777 );
2778 }
2779}
2780
2781#[cfg(test)]
2787mod base14_fonts {
2788 fn pdf_with_font(fontdict: &[u8], text: &[u8]) -> Vec<u8> {
2790 let content = [b"BT /F1 12 Tf 72 700 Td (".as_slice(), text, b") Tj ET\n"].concat();
2791 pdf_with_content(fontdict, &content)
2792 }
2793
2794 fn pdf_with_content(fontdict: &[u8], content: &[u8]) -> Vec<u8> {
2796 pdf_with_fonts(&[fontdict], content)
2797 }
2798
2799 fn pdf_with_fonts(fontdicts: &[&[u8]], content: &[u8]) -> Vec<u8> {
2802 let stream = format!("<</Length {}>>stream\n", content.len()).into_bytes();
2803 let font_res: String = fontdicts
2804 .iter()
2805 .enumerate()
2806 .map(|(i, _)| format!("/F{} {} 0 R", i + 1, i + 5))
2807 .collect();
2808 let mut objs: Vec<Vec<u8>> = vec![
2809 b"<</Type/Catalog/Pages 2 0 R>>".to_vec(),
2810 b"<</Type/Pages/Kids[3 0 R]/Count 1>>".to_vec(),
2811 format!(
2812 "<</Type/Page/Parent 2 0 R/MediaBox[0 0 595 842]/Contents 4 0 R\
2813 /Resources<</Font<<{font_res}>>>>>>"
2814 )
2815 .into_bytes(),
2816 [stream.as_slice(), content, b"endstream"].concat(),
2817 ];
2818 objs.extend(fontdicts.iter().map(|d| d.to_vec()));
2819 let mut out = b"%PDF-1.4\n".to_vec();
2820 let mut offsets = Vec::new();
2821 for (i, body) in objs.iter().enumerate() {
2822 offsets.push(out.len());
2823 out.extend_from_slice(format!("{} 0 obj", i + 1).as_bytes());
2824 out.extend_from_slice(body);
2825 out.extend_from_slice(b"endobj\n");
2826 }
2827 let xref_at = out.len();
2828 out.extend_from_slice(format!("xref\n0 {}\n", objs.len() + 1).as_bytes());
2829 out.extend_from_slice(b"0000000000 65535 f \n");
2830 for off in &offsets {
2831 out.extend_from_slice(format!("{off:010} 00000 n \n").as_bytes());
2832 }
2833 out.extend_from_slice(
2834 format!("trailer<</Size {}/Root 1 0 R>>\n", objs.len() + 1).as_bytes(),
2835 );
2836 out.extend_from_slice(format!("startxref\n{xref_at}\n%%EOF\n").as_bytes());
2837 out
2838 }
2839
2840 fn cells(pdf: &[u8]) -> Vec<crate::pdfium_backend::TextCell> {
2842 super::pdf_textlines(pdf)
2843 .into_iter()
2844 .flat_map(|(_, _, c)| c)
2845 .collect()
2846 }
2847
2848 #[test]
2855 fn rotated_text_matrix_reads_in_order() {
2856 const HELV: &[u8] = b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>";
2857 const TEXT: &str = "Upright text on a rotated page.";
2858 for (tm, [l, b, r, t]) in [
2859 ("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]), (
2863 "9.8995 9.8995 -9.8995 9.8995 100 100",
2864 [92.9, 98.0, 235.8, 240.8],
2865 ), ] {
2867 let content = format!("BT /F1 1 Tf {tm} Tm ({TEXT}) Tj ET\n");
2868 let pdf = pdf_with_content(HELV, content.as_bytes());
2869 let cs = cells(&pdf);
2870 assert_eq!(cs.len(), 1, "{tm}: {cs:?}");
2871 let c = &cs[0];
2872 assert_eq!(c.text, TEXT, "{tm}");
2873 let got = [c.l, 842.0 - c.b, c.r, 842.0 - c.t];
2878 for (g, w) in got.iter().zip([l, b, r, t]) {
2879 assert!(
2880 (g - w).abs() < 1.0,
2881 "{tm}: box {got:?} vs docling-parse {:?}",
2882 [l, b, r, t]
2883 );
2884 }
2885 let words: Vec<String> = super::pdf_words(&pdf)
2886 .into_iter()
2887 .flat_map(|(_, _, c)| c)
2888 .map(|c| c.text)
2889 .collect();
2890 assert_eq!(words, TEXT.split(' ').collect::<Vec<_>>(), "{tm}");
2891 }
2892 }
2893
2894 #[test]
2899 fn rotated_text_off_the_page_is_dropped() {
2900 const HELV: &[u8] = b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>";
2901 let off = pdf_with_content(
2902 HELV,
2903 b"BT /F1 1 Tf 0 14 -14 0 -4 200 Tm (Spine title) Tj ET\n",
2904 );
2905 assert!(cells(&off).is_empty(), "{:?}", cells(&off));
2906 let on = pdf_with_content(
2907 HELV,
2908 b"BT /F1 1 Tf 0 14 -14 0 30 200 Tm (Margin note) Tj ET\n",
2909 );
2910 assert_eq!(cells(&on).len(), 1);
2911 }
2912
2913 #[test]
2916 fn upright_glyphs_carry_no_quad() {
2917 let only_page = |pdf: &[u8]| {
2918 let doc = super::load_document(pdf).expect("loads");
2919 let pid = *doc.get_pages().values().next().expect("one page");
2920 super::page_glyphs(&doc, pid)
2921 };
2922 let pdf = pdf_with_font(b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>", b"Plain");
2923 let glyphs = only_page(&pdf);
2924 assert!(!glyphs.is_empty());
2925 assert!(glyphs.iter().all(|g| g.quad.is_none() && g.lr > g.ll));
2926 let pdf = pdf_with_content(
2928 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>",
2929 b"BT /F1 1 Tf 0 14 -14 0 200 100 Tm (W) Tj ET\n",
2930 );
2931 let glyphs = only_page(&pdf);
2932 let g = &glyphs[0];
2933 assert!(g.quad.is_some());
2934 assert!((g.height() - 14.0).abs() < 0.05, "{}", g.height());
2936 }
2937
2938 #[test]
2943 fn ticks_stacked_by_cm_are_separate_line_cells() {
2944 let mut content = String::new();
2945 for (k, tick) in "345678".chars().enumerate() {
2946 let y = 300.0 + 25.92 * k as f64;
2947 content.push_str(&format!(
2948 "q 1 0 0 1 125 {y} cm BT /F1 10 Tf 1 0 0 1 -5 -4 Tm ({tick}) Tj ET Q\n"
2949 ));
2950 }
2951 let pdf = pdf_with_content(
2952 b"<</Type/Font/Subtype/Type1/BaseFont/Times-Roman>>",
2953 content.as_bytes(),
2954 );
2955 let cells = cells(&pdf);
2956 let texts: Vec<&str> = cells.iter().map(|c| c.text.as_str()).collect();
2957 assert_eq!(texts, ["3", "4", "5", "6", "7", "8"]);
2958 for (k, c) in cells.iter().enumerate() {
2959 let base = 842.0 - (300.0 + 25.92 * k as f32 - 4.0);
2961 assert!((c.l - 120.0).abs() < 0.1, "{k}: l {}", c.l);
2962 assert!(
2963 c.t < base && base - c.t < 10.0,
2964 "{k}: t {} base {base}",
2965 c.t
2966 );
2967 }
2968 }
2969
2970 #[test]
2976 fn drawn_checkbox_squares_are_found() {
2977 let content = b"q 1 0 0 1 119.52 449.04 cm \
2978 n 0 12.96 m 12.96 12.96 l S n 0 0 m 12.96 0 l S \
2979 n 0 0 m 0 12.96 l S n 12.96 0 m 12.96 12.96 l S Q\n\
2980 200 400 10 10 re S 202 405 m 204.5 402 l 208.5 408.5 l S\n\
2981 300 400 12 12 re f 50 50 100 100 re S\n\
2982 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";
2983 let pdf = pdf_with_content(b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>", content);
2984 let mut parser = super::PageTextParser::open(&pdf).expect("parses");
2985 let mut boxes = parser.cells(0).checkboxes;
2986 boxes.sort_by(|a, b| a.l.total_cmp(&b.l));
2987 let got: Vec<([i32; 4], bool)> = boxes
2988 .iter()
2989 .map(|c| {
2990 let r = |v: f32| (v * 100.0).round() as i32;
2991 ([r(c.l), r(c.t), r(c.r), r(c.b)], c.checked)
2992 })
2993 .collect();
2994 assert_eq!(
2996 got,
2997 [
2998 ([11952, 38000, 13248, 39296], false),
2999 ([20000, 43200, 21000, 44200], true),
3000 ([50000, 43000, 51200, 44200], false),
3001 ]
3002 );
3003 }
3004
3005 #[test]
3007 fn standard14_faces_get_builtin_widths() {
3008 for fontdict in [
3009 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/WinAnsiEncoding>>".as_slice(),
3011 b"<</Type/Font/Subtype/Type1/BaseFont/Times-BoldItalic>>",
3013 b"<</Type/Font/Subtype/TrueType/BaseFont/Arial,Bold>>",
3015 b"<</Type/Font/Subtype/Type1/BaseFont/ABCDEF+Courier-Oblique>>",
3016 ] {
3017 let pdf = pdf_with_font(fontdict, b"Words have width now");
3018 let cs = cells(&pdf);
3019 let text: String = cs
3020 .iter()
3021 .map(|c| c.text.as_str())
3022 .collect::<Vec<_>>()
3023 .join(" ");
3024 assert!(
3025 text.contains("Words have width now"),
3026 "{}: text lost: {text:?}",
3027 String::from_utf8_lossy(fontdict)
3028 );
3029 assert!(
3030 cs.iter().all(|c| c.r > c.l),
3031 "{}: zero-width cells: {cs:?}",
3032 String::from_utf8_lossy(fontdict)
3033 );
3034 }
3035 }
3036
3037 #[test]
3040 fn explicit_widths_win_and_unknown_faces_are_untouched() {
3041 let explicit = pdf_with_font(
3045 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/FirstChar 65\
3046 /Widths[100 100 100 100]/Encoding/WinAnsiEncoding>>",
3047 b"ABBA",
3048 );
3049 let builtin = pdf_with_font(
3050 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/WinAnsiEncoding>>",
3051 b"ABBA",
3052 );
3053 let w = |pdf: &[u8]| {
3054 let cs = cells(pdf);
3055 assert_eq!(cs.len(), 1, "one word cell: {cs:?}");
3056 cs[0].r - cs[0].l
3057 };
3058 let (we, wb) = (w(&explicit), w(&builtin));
3059 assert!(
3060 (we - 4.8).abs() < 0.1,
3061 "explicit widths must win: got {we}, want 4×100×12/1000"
3062 );
3063 assert!(
3064 wb > 2.0 * we,
3065 "built-in Helvetica is much wider: {wb} vs {we}"
3066 );
3067
3068 let unknown = pdf_with_font(
3072 b"<</Type/Font/Subtype/Type1/BaseFont/FancyCorp-Display>>",
3073 b"Mystery",
3074 );
3075 let cs = cells(&unknown);
3076 let text: String = cs.iter().map(|c| c.text.as_str()).collect();
3077 assert!(text.contains("Mystery"), "text still decodes: {cs:?}");
3078 }
3079 #[test]
3088 fn same_face_under_two_resource_keys_is_one_font_for_the_sanitizer() {
3089 let f1 = b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/WinAnsiEncoding>>";
3090 let f2 = b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding<</Type/Encoding\
3091 /BaseEncoding/WinAnsiEncoding/Differences[129/fi]>>>>";
3092 let content = b"BT 12 TL 72 720 Td \
3093 /F1 12 Tf (The ) Tj /F2 12 Tf (\\201nancial position) Tj \
3094 /F1 12 Tf ( of the company is shown in ) Tj /F2 12 Tf (\\201gure 3) Tj \
3095 /F1 12 Tf ( with the ) Tj /F2 12 Tf (\\201rst results.) Tj \
3096 T* T* /F2 12 Tf (Same words in /F2 only: \\201nancial, \\201gure, \\201rst.) Tj ET\n";
3097 let cs = cells(&pdf_with_fonts(&[f1, f2], content));
3098 let lines: Vec<&str> = cs.iter().map(|c| c.text.as_str()).collect();
3099 assert_eq!(
3100 lines,
3101 [
3102 "The financial position of the company is shown in figure 3 with the first results.",
3103 "Same words in /F2 only: financial, figure, first.",
3104 ],
3105 "{cs:?}"
3106 );
3107 }
3108
3109 #[test]
3113 fn different_faces_still_keep_the_font_barrier() {
3114 let f1 = b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/WinAnsiEncoding>>";
3115 let f2 = b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica-Bold/Encoding/WinAnsiEncoding>>";
3116 let content = b"BT 72 720 Td /F2 12 Tf (Total:) Tj /F1 12 Tf (42) Tj ET\n";
3117 let cs = cells(&pdf_with_fonts(&[f1, f2], content));
3118 let texts: Vec<&str> = cs.iter().map(|c| c.text.as_str()).collect();
3119 assert_eq!(texts, ["Total:", "42"], "{cs:?}");
3120 }
3121
3122 #[test]
3127 fn sanitizer_font_name_follows_docling_parse_precedence() {
3128 use lopdf::{dictionary, Document, Object};
3129 let mut doc = Document::with_version("1.4");
3130 let fd_a =
3131 doc.add_object(dictionary! { "Type" => "FontDescriptor", "FontName" => "AAAAAA+Face" });
3132 let fd_b =
3133 doc.add_object(dictionary! { "Type" => "FontDescriptor", "FontName" => "BBBBBB+Face" });
3134 let with = |fd: Option<lopdf::ObjectId>, base: &str| {
3135 let mut d = dictionary! { "Type" => "Font", "Subtype" => "Type1", "BaseFont" => base };
3136 if let Some(id) = fd {
3137 d.set("FontDescriptor", Object::Reference(id));
3138 }
3139 d
3140 };
3141 let name = |d: &lopdf::Dictionary| {
3142 String::from_utf8(super::sanitizer_font_name(&doc, d, false)).unwrap()
3143 };
3144 assert_eq!(name(&with(Some(fd_a), "Face")), "AAAAAA+Face");
3145 assert_eq!(name(&with(Some(fd_b), "Face")), "BBBBBB+Face");
3146 assert_eq!(name(&with(None, "ABCDEF+Face")), "ABCDEF+Face");
3147 assert_eq!(
3148 name(&dictionary! { "Type" => "Font", "Name" => "F7" }),
3149 "F7"
3150 );
3151 assert_eq!(name(&dictionary! { "Type" => "Font" }), "null");
3152 }
3153}
3154
3155#[cfg(test)]
3156mod overpainted {
3157 use crate::pdfium_backend::TextCell;
3158
3159 fn cell(text: &str, l: f32, t: f32, r: f32, b: f32) -> TextCell {
3160 TextCell {
3161 text: text.into(),
3162 l,
3163 t,
3164 r,
3165 b,
3166 }
3167 }
3168
3169 #[test]
3173 fn stacked_logo_glyphs_are_dropped() {
3174 let mut cells = vec![
3175 cell("\"", 72.7, 21.5, 86.4, 31.5),
3176 cell("==", 59.4, 21.5, 99.6, 31.5),
3177 cell("Herr", 65.2, 151.3, 81.7, 161.3),
3178 ];
3179 super::drop_overpainted_cells(&mut cells);
3180 assert_eq!(cells.len(), 1, "cells: {cells:?}");
3181 assert_eq!(cells[0].text, "Herr");
3182 }
3183
3184 #[test]
3188 fn prose_and_double_draw_are_kept() {
3189 let mut cells = vec![
3190 cell("Telefon", 354.3, 133.2, 381.5, 143.2),
3191 cell("0676/2000", 387.3, 133.2, 428.7, 143.2),
3192 cell("Bold", 100.0, 50.0, 130.0, 60.0),
3193 cell("Bold", 100.3, 50.0, 130.3, 60.0),
3194 ];
3195 super::drop_overpainted_cells(&mut cells);
3196 assert_eq!(cells.len(), 4);
3197 }
3198}
3199
3200#[cfg(test)]
3201mod vestigial_layer {
3202 use crate::pdfium_backend::{PdfPage, TextCell};
3203
3204 fn page_with(texts: &[&str]) -> PdfPage {
3205 let cells = texts
3206 .iter()
3207 .enumerate()
3208 .map(|(i, t)| TextCell {
3209 text: t.to_string(),
3210 l: 10.0,
3211 t: 10.0 + 12.0 * i as f32,
3212 r: 90.0,
3213 b: 20.0 + 12.0 * i as f32,
3214 })
3215 .collect();
3216 PdfPage::from_cells(595.0, 842.0, 1.0, cells)
3217 }
3218
3219 #[test]
3224 fn typed_in_form_fields_are_not_a_text_layer() {
3225 let pages = vec![
3226 page_with(&["03", "05", "2025"]),
3227 page_with(&[]),
3228 page_with(&[]),
3229 ];
3230 assert!(super::text_layer_is_vestigial(&pages));
3231 assert!(super::text_layer_is_vestigial(&[page_with(&[])]));
3232 }
3233
3234 #[test]
3237 fn sparse_but_real_documents_pass() {
3238 let one_pager = vec![page_with(&[
3239 "Confidential briefing",
3240 "Prepared for the board meeting",
3241 "Do not distribute",
3242 ])];
3243 assert!(!super::text_layer_is_vestigial(&one_pager));
3244 }
3245}