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> {
2459 #[rustfmt::skip]
2460 const HIGH: [char; 128] = [
2461 'Ä', 'Å', 'Ç', 'É', 'Ñ', 'Ö', 'Ü', 'á', 'à', 'â', 'ä', 'ã', 'å', 'ç', 'é', 'è', 'ê', 'ë', 'í', 'ì', 'î', 'ï', 'ñ', 'ó', 'ò', 'ô', 'ö', 'õ', 'ú', 'ù', 'û', 'ü', '†', '°', '¢', '£', '§', '•', '¶', 'ß', '®', '©', '™', '´', '¨', '≠', 'Æ', 'Ø', '∞', '±', '≤', '≥', '¥', 'µ', '∂', '∑', '∏', 'π', '∫', 'ª', 'º', 'Ω', 'æ', 'ø', '¿', '¡', '¬', '√', 'ƒ', '≈', '∆', '«', '»', '…', '\u{00A0}', 'À', 'Ã', 'Õ', 'Œ', 'œ', '–', '—', '“', '”', '‘', '’', '÷', '◊', 'ÿ', 'Ÿ', '⁄', '¤', '‹', '›', 'fi', 'fl', '‡', '·', '‚', '„', '‰', 'Â', 'Ê', 'Á', 'Ë', 'È', 'Í', 'Î', 'Ï', 'Ì', 'Ó', 'Ô', '\0', 'Ò', 'Ú', 'Û', 'Ù', 'ı', 'ˆ', '˜', '¯', '˘', '˙', '˚', '¸', '˝', '˛', 'ˇ', ];
2478 let mut m = HashMap::new();
2479 for b in 0x20u8..=0x7e {
2480 m.insert(b, b as char);
2481 }
2482 for (b, &c) in (0x80u8..=0xFF).zip(HIGH.iter()) {
2483 if c != '\0' {
2484 m.insert(b, c);
2485 }
2486 }
2487 m
2488}
2489
2490#[cfg(test)]
2491mod page_box_frame {
2492 use super::*;
2493
2494 fn pdf(boxes: &str, x: f32, y: f32) -> Vec<u8> {
2497 pdf_content(
2498 boxes,
2499 &format!("BT /F1 12 Tf {x} {y} Td (First printing) Tj ET\n"),
2500 )
2501 }
2502
2503 fn pdf_content(boxes: &str, content: &str) -> Vec<u8> {
2506 let objs: Vec<String> = vec![
2507 "<</Type/Catalog/Pages 2 0 R>>".into(),
2508 format!("<</Type/Pages/Kids[3 0 R]/Count 1{boxes}>>"),
2509 "<</Type/Page/Parent 2 0 R/Contents 4 0 R/Resources<</Font<</F1 5 0 R>>>>>>".into(),
2510 format!("<</Length {}>>stream\n{content}endstream", content.len()),
2511 "<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>".into(),
2512 ];
2513 let mut out = b"%PDF-1.4\n".to_vec();
2514 let mut offsets = Vec::new();
2515 for (i, body) in objs.iter().enumerate() {
2516 offsets.push(out.len());
2517 out.extend_from_slice(format!("{} 0 obj{body}endobj\n", i + 1).as_bytes());
2518 }
2519 let xref_at = out.len();
2520 out.extend_from_slice(
2521 format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes(),
2522 );
2523 for off in &offsets {
2524 out.extend_from_slice(format!("{off:010} 00000 n \n").as_bytes());
2525 }
2526 out.extend_from_slice(
2527 format!(
2528 "trailer<</Size {}/Root 1 0 R>>\nstartxref\n{xref_at}\n%%EOF\n",
2529 objs.len() + 1
2530 )
2531 .as_bytes(),
2532 );
2533 out
2534 }
2535
2536 fn only_page(bytes: &[u8]) -> (PageBox, Vec<Glyph>) {
2537 let doc = load_document(bytes).expect("loads");
2538 let pid = *doc.get_pages().values().next().expect("one page");
2539 (page_box(&doc, pid), page_glyphs(&doc, pid))
2540 }
2541
2542 #[test]
2547 fn glyphs_count_from_the_cropbox_corner_like_pdfium() {
2548 let (pb, shifted) = only_page(&pdf(
2549 "/MediaBox[-56.505 -58.25 576.303 723.31]/CropBox[1.095 -0.65 518.703 665.71]",
2550 37.0 + 1.095,
2551 58.0 - 0.65,
2552 ));
2553 assert!((pb.l - 1.095).abs() < 1e-3 && (pb.b + 0.65).abs() < 1e-3);
2554 assert!(
2555 (pb.w - 517.608).abs() < 1e-3 && (pb.h - 666.36).abs() < 1e-3,
2556 "{pb:?}"
2557 );
2558 let (pb0, plain) = only_page(&pdf("/MediaBox[0 0 517.608 666.36]", 37.0, 58.0));
2559 assert!((pb0.w - pb.w).abs() < 1e-3 && (pb0.h - pb.h).abs() < 1e-3);
2560 assert_eq!(shifted.len(), plain.len());
2561 assert!(!plain.is_empty());
2562 for (a, b) in shifted.iter().zip(&plain) {
2563 assert!(
2564 (a.l - b.l).abs() < 1e-3 && (a.b - b.b).abs() < 1e-3,
2565 "{:?} vs {:?}",
2566 (a.l, a.b),
2567 (b.l, b.b)
2568 );
2569 }
2570 assert!((plain[0].l - 37.0).abs() < 1e-3, "{}", plain[0].l);
2571 }
2572
2573 fn text(glyphs: &[Glyph]) -> String {
2574 glyphs.iter().map(|g| g.ch).collect()
2575 }
2576
2577 #[test]
2582 fn glyphs_outside_the_display_box_are_dropped() {
2583 let (_, g) = only_page(&pdf_content(
2584 "/MediaBox[0 0 400 400]/CropBox[100 100 400 400]",
2585 "BT /F1 14 Tf 120 300 Td (Visible.) Tj ET\nBT /F1 8 Tf 5 40 Td (Slug) Tj ET\n",
2586 ));
2587 assert_eq!(text(&g), "Visible.");
2588 let (_, g) = only_page(&pdf_content(
2589 "/MediaBox[0 0 300 400]",
2590 "BT /F1 14 Tf 40 300 Td (On page) Tj ET\nBT /F1 14 Tf 400 250 Td (Beyond) Tj ET\n",
2591 ));
2592 assert_eq!(text(&g), "On page");
2593 let (_, g) = only_page(&pdf_content(
2595 "/MediaBox[0 0 300 400]",
2596 "BT /F1 14 Tf 200 250 Td (Crossing the right edge) Tj ET\n",
2597 ));
2598 assert_eq!(text(&g), "Crossing the rig");
2599 }
2600
2601 #[test]
2606 fn a_glyph_on_the_edge_stays_one_over_it_goes() {
2607 let w_at = |x: f32, y: f32| {
2608 let (_, g) = only_page(&pdf_content(
2609 "/MediaBox[0 0 300 400]",
2610 &format!("BT /F1 50 Tf {x} {y} Td (W) Tj ET\n"),
2611 ));
2612 text(&g)
2613 };
2614 assert_eq!(w_at(252.8, 200.0), "W");
2615 assert_eq!(w_at(252.81, 200.0), "");
2616 assert_eq!(w_at(0.0, 200.0), "W");
2617 assert_eq!(w_at(-0.01, 200.0), "");
2618 assert_eq!(w_at(100.0, 20.0), "W");
2621 assert_eq!(w_at(100.0, 0.0), "");
2622 assert_eq!(w_at(100.0, 380.0), "");
2623 }
2624
2625 #[test]
2628 fn page_box_follows_pdfium_fallbacks() {
2629 let (pb, _) = only_page(&pdf("", 10.0, 10.0));
2630 assert_eq!((pb.l, pb.b, pb.w, pb.h), (0.0, 0.0, 612.0, 792.0));
2631 let (pb, _) = only_page(&pdf(
2632 "/MediaBox[0 0 500 700]/CropBox[-100 100 600 900]",
2633 10.0,
2634 10.0,
2635 ));
2636 assert_eq!((pb.l, pb.b, pb.w, pb.h), (0.0, 100.0, 500.0, 600.0));
2637 let (pb, _) = only_page(&pdf(
2638 "/MediaBox[0 0 500 700]/CropBox[800 800 900 900]",
2639 10.0,
2640 10.0,
2641 ));
2642 assert_eq!((pb.l, pb.b, pb.w, pb.h), (0.0, 0.0, 500.0, 700.0));
2643 let (pb, _) = only_page(&pdf("/MediaBox[500 700 0 0]", 10.0, 10.0));
2645 assert_eq!((pb.l, pb.b, pb.w, pb.h), (0.0, 0.0, 500.0, 700.0));
2646 }
2647}
2648
2649#[cfg(test)]
2650mod xref_repair {
2651 fn pdf_with_xref(two_byte_eol: bool) -> Vec<u8> {
2655 let content = b"BT /F1 12 Tf 72 700 Td (Invoice 922769430725) Tj ET\n";
2656 let stream = format!("<</Length {}>>stream\n", content.len()).into_bytes();
2657 let objs: Vec<Vec<u8>> = vec![
2658 b"<</Type/Catalog/Pages 2 0 R>>".to_vec(),
2659 b"<</Type/Pages/Kids[3 0 R]/Count 1>>".to_vec(),
2660 b"<</Type/Page/Parent 2 0 R/MediaBox[0 0 595 842]/Contents 4 0 R\
2661 /Resources<</Font<</F1 5 0 R>>>>>>"
2662 .to_vec(),
2663 [stream.as_slice(), content.as_slice(), b"endstream"].concat(),
2664 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>".to_vec(),
2665 ];
2666
2667 let mut out = b"%PDF-1.4\n".to_vec();
2668 let mut offsets = Vec::new();
2669 for (i, body) in objs.iter().enumerate() {
2670 offsets.push(out.len());
2671 out.extend_from_slice(format!("{} 0 obj", i + 1).as_bytes());
2672 out.extend_from_slice(body);
2673 out.extend_from_slice(b"endobj\n");
2674 }
2675 let xref_at = out.len();
2676 let eol: &[u8] = if two_byte_eol { b" \n" } else { b"\n" };
2677 out.extend_from_slice(format!("xref\n0 {}\n", objs.len() + 1).as_bytes());
2678 out.extend_from_slice(b"0000000000 65535 f");
2679 out.extend_from_slice(eol);
2680 for off in &offsets {
2681 out.extend_from_slice(format!("{off:010} 00000 n").as_bytes());
2682 out.extend_from_slice(eol);
2683 }
2684 out.extend_from_slice(
2685 format!("trailer<</Size {}/Root 1 0 R>>\n", objs.len() + 1).as_bytes(),
2686 );
2687 out.extend_from_slice(format!("startxref\n{xref_at}\n%%EOF\n").as_bytes());
2688 out
2689 }
2690
2691 #[test]
2697 fn short_xref_entries_still_parse() {
2698 let good = pdf_with_xref(true);
2699 let broken = pdf_with_xref(false);
2700 assert!(
2701 broken.len() < good.len(),
2702 "the broken file is the shorter one"
2703 );
2704 assert!(
2705 lopdf::Document::load_mem(&good).is_ok(),
2706 "the control file must load unaided"
2707 );
2708 assert!(
2709 lopdf::Document::load_mem(&broken).is_err(),
2710 "lopdf rejects 19-byte entries — if this ever passes, drop the repair"
2711 );
2712
2713 let cells = |b: &[u8]| -> Vec<String> {
2714 super::pdf_textlines(b)
2715 .into_iter()
2716 .flat_map(|(_, _, c)| c.into_iter().map(|c| c.text))
2717 .collect()
2718 };
2719 let from_good = cells(&good);
2720 assert!(
2721 from_good.iter().any(|t| t.contains("922769430725")),
2722 "control text: {from_good:?}"
2723 );
2724 assert_eq!(
2725 cells(&broken),
2726 from_good,
2727 "repair must match the good parse"
2728 );
2729 }
2730
2731 #[test]
2736 fn overstated_stream_length_still_yields_content() {
2737 let good = pdf_with_xref(true);
2738 let broken = {
2741 let at = good
2742 .windows(8)
2743 .position(|w| w == b"/Length ")
2744 .expect("a /Length")
2745 + 8;
2746 let digits = good[at..].iter().take_while(|c| c.is_ascii_digit()).count();
2747 let n: usize = std::str::from_utf8(&good[at..at + digits])
2748 .unwrap()
2749 .parse()
2750 .unwrap();
2751 let inflated = (n + 1).to_string();
2752 assert_eq!(inflated.len(), digits, "keep the digit count");
2753 let mut b = good.clone();
2754 b[at..at + digits].copy_from_slice(inflated.as_bytes());
2755 b
2756 };
2757 assert_eq!(broken.len(), good.len(), "the defect must not move bytes");
2758 let raw = lopdf::Document::load_mem(&broken).expect("still loads");
2760 assert!(
2761 raw.get_pages()
2762 .into_values()
2763 .all(|p| raw.get_page_content(p).is_empty()),
2764 "lopdf should drop the stream — if it stops, drop this repair"
2765 );
2766 let text = |b: &[u8]| -> Vec<String> {
2768 super::pdf_textlines(b)
2769 .into_iter()
2770 .flat_map(|(_, _, c)| c.into_iter().map(|c| c.text))
2771 .collect()
2772 };
2773 let expected = text(&good);
2774 assert!(!expected.is_empty(), "control must produce text");
2775 assert_eq!(text(&broken), expected);
2776 }
2777
2778 #[test]
2782 fn repair_declines_when_padding_would_move_objects() {
2783 let mut incremental = pdf_with_xref(false);
2784 incremental.extend_from_slice(b"6 0 obj<</Type/Whatever>>endobj\n");
2785 let declined = super::pad_short_xref_entries(&incremental).unwrap_err();
2786 assert!(
2787 declined.contains("object follows the xref"),
2788 "reason: {declined}"
2789 );
2790 }
2791}
2792
2793#[cfg(test)]
2799mod base14_fonts {
2800 fn pdf_with_font(fontdict: &[u8], text: &[u8]) -> Vec<u8> {
2802 let content = [b"BT /F1 12 Tf 72 700 Td (".as_slice(), text, b") Tj ET\n"].concat();
2803 pdf_with_content(fontdict, &content)
2804 }
2805
2806 fn pdf_with_content(fontdict: &[u8], content: &[u8]) -> Vec<u8> {
2808 pdf_with_fonts(&[fontdict], content)
2809 }
2810
2811 fn pdf_with_fonts(fontdicts: &[&[u8]], content: &[u8]) -> Vec<u8> {
2814 let stream = format!("<</Length {}>>stream\n", content.len()).into_bytes();
2815 let font_res: String = fontdicts
2816 .iter()
2817 .enumerate()
2818 .map(|(i, _)| format!("/F{} {} 0 R", i + 1, i + 5))
2819 .collect();
2820 let mut objs: Vec<Vec<u8>> = vec![
2821 b"<</Type/Catalog/Pages 2 0 R>>".to_vec(),
2822 b"<</Type/Pages/Kids[3 0 R]/Count 1>>".to_vec(),
2823 format!(
2824 "<</Type/Page/Parent 2 0 R/MediaBox[0 0 595 842]/Contents 4 0 R\
2825 /Resources<</Font<<{font_res}>>>>>>"
2826 )
2827 .into_bytes(),
2828 [stream.as_slice(), content, b"endstream"].concat(),
2829 ];
2830 objs.extend(fontdicts.iter().map(|d| d.to_vec()));
2831 let mut out = b"%PDF-1.4\n".to_vec();
2832 let mut offsets = Vec::new();
2833 for (i, body) in objs.iter().enumerate() {
2834 offsets.push(out.len());
2835 out.extend_from_slice(format!("{} 0 obj", i + 1).as_bytes());
2836 out.extend_from_slice(body);
2837 out.extend_from_slice(b"endobj\n");
2838 }
2839 let xref_at = out.len();
2840 out.extend_from_slice(format!("xref\n0 {}\n", objs.len() + 1).as_bytes());
2841 out.extend_from_slice(b"0000000000 65535 f \n");
2842 for off in &offsets {
2843 out.extend_from_slice(format!("{off:010} 00000 n \n").as_bytes());
2844 }
2845 out.extend_from_slice(
2846 format!("trailer<</Size {}/Root 1 0 R>>\n", objs.len() + 1).as_bytes(),
2847 );
2848 out.extend_from_slice(format!("startxref\n{xref_at}\n%%EOF\n").as_bytes());
2849 out
2850 }
2851
2852 fn cells(pdf: &[u8]) -> Vec<crate::pdfium_backend::TextCell> {
2854 super::pdf_textlines(pdf)
2855 .into_iter()
2856 .flat_map(|(_, _, c)| c)
2857 .collect()
2858 }
2859
2860 #[test]
2867 fn rotated_text_matrix_reads_in_order() {
2868 const HELV: &[u8] = b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>";
2869 const TEXT: &str = "Upright text on a rotated page.";
2870 for (tm, [l, b, r, t]) in [
2871 ("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]), (
2875 "9.8995 9.8995 -9.8995 9.8995 100 100",
2876 [92.9, 98.0, 235.8, 240.8],
2877 ), ] {
2879 let content = format!("BT /F1 1 Tf {tm} Tm ({TEXT}) Tj ET\n");
2880 let pdf = pdf_with_content(HELV, content.as_bytes());
2881 let cs = cells(&pdf);
2882 assert_eq!(cs.len(), 1, "{tm}: {cs:?}");
2883 let c = &cs[0];
2884 assert_eq!(c.text, TEXT, "{tm}");
2885 let got = [c.l, 842.0 - c.b, c.r, 842.0 - c.t];
2890 for (g, w) in got.iter().zip([l, b, r, t]) {
2891 assert!(
2892 (g - w).abs() < 1.0,
2893 "{tm}: box {got:?} vs docling-parse {:?}",
2894 [l, b, r, t]
2895 );
2896 }
2897 let words: Vec<String> = super::pdf_words(&pdf)
2898 .into_iter()
2899 .flat_map(|(_, _, c)| c)
2900 .map(|c| c.text)
2901 .collect();
2902 assert_eq!(words, TEXT.split(' ').collect::<Vec<_>>(), "{tm}");
2903 }
2904 }
2905
2906 #[test]
2911 fn rotated_text_off_the_page_is_dropped() {
2912 const HELV: &[u8] = b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>";
2913 let off = pdf_with_content(
2914 HELV,
2915 b"BT /F1 1 Tf 0 14 -14 0 -4 200 Tm (Spine title) Tj ET\n",
2916 );
2917 assert!(cells(&off).is_empty(), "{:?}", cells(&off));
2918 let on = pdf_with_content(
2919 HELV,
2920 b"BT /F1 1 Tf 0 14 -14 0 30 200 Tm (Margin note) Tj ET\n",
2921 );
2922 assert_eq!(cells(&on).len(), 1);
2923 }
2924
2925 #[test]
2928 fn upright_glyphs_carry_no_quad() {
2929 let only_page = |pdf: &[u8]| {
2930 let doc = super::load_document(pdf).expect("loads");
2931 let pid = *doc.get_pages().values().next().expect("one page");
2932 super::page_glyphs(&doc, pid)
2933 };
2934 let pdf = pdf_with_font(b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>", b"Plain");
2935 let glyphs = only_page(&pdf);
2936 assert!(!glyphs.is_empty());
2937 assert!(glyphs.iter().all(|g| g.quad.is_none() && g.lr > g.ll));
2938 let pdf = pdf_with_content(
2940 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>",
2941 b"BT /F1 1 Tf 0 14 -14 0 200 100 Tm (W) Tj ET\n",
2942 );
2943 let glyphs = only_page(&pdf);
2944 let g = &glyphs[0];
2945 assert!(g.quad.is_some());
2946 assert!((g.height() - 14.0).abs() < 0.05, "{}", g.height());
2948 }
2949
2950 #[test]
2955 fn ticks_stacked_by_cm_are_separate_line_cells() {
2956 let mut content = String::new();
2957 for (k, tick) in "345678".chars().enumerate() {
2958 let y = 300.0 + 25.92 * k as f64;
2959 content.push_str(&format!(
2960 "q 1 0 0 1 125 {y} cm BT /F1 10 Tf 1 0 0 1 -5 -4 Tm ({tick}) Tj ET Q\n"
2961 ));
2962 }
2963 let pdf = pdf_with_content(
2964 b"<</Type/Font/Subtype/Type1/BaseFont/Times-Roman>>",
2965 content.as_bytes(),
2966 );
2967 let cells = cells(&pdf);
2968 let texts: Vec<&str> = cells.iter().map(|c| c.text.as_str()).collect();
2969 assert_eq!(texts, ["3", "4", "5", "6", "7", "8"]);
2970 for (k, c) in cells.iter().enumerate() {
2971 let base = 842.0 - (300.0 + 25.92 * k as f32 - 4.0);
2973 assert!((c.l - 120.0).abs() < 0.1, "{k}: l {}", c.l);
2974 assert!(
2975 c.t < base && base - c.t < 10.0,
2976 "{k}: t {} base {base}",
2977 c.t
2978 );
2979 }
2980 }
2981
2982 #[test]
2988 fn drawn_checkbox_squares_are_found() {
2989 let content = b"q 1 0 0 1 119.52 449.04 cm \
2990 n 0 12.96 m 12.96 12.96 l S n 0 0 m 12.96 0 l S \
2991 n 0 0 m 0 12.96 l S n 12.96 0 m 12.96 12.96 l S Q\n\
2992 200 400 10 10 re S 202 405 m 204.5 402 l 208.5 408.5 l S\n\
2993 300 400 12 12 re f 50 50 100 100 re S\n\
2994 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";
2995 let pdf = pdf_with_content(b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>", content);
2996 let mut parser = super::PageTextParser::open(&pdf).expect("parses");
2997 let mut boxes = parser.cells(0).checkboxes;
2998 boxes.sort_by(|a, b| a.l.total_cmp(&b.l));
2999 let got: Vec<([i32; 4], bool)> = boxes
3000 .iter()
3001 .map(|c| {
3002 let r = |v: f32| (v * 100.0).round() as i32;
3003 ([r(c.l), r(c.t), r(c.r), r(c.b)], c.checked)
3004 })
3005 .collect();
3006 assert_eq!(
3008 got,
3009 [
3010 ([11952, 38000, 13248, 39296], false),
3011 ([20000, 43200, 21000, 44200], true),
3012 ([50000, 43000, 51200, 44200], false),
3013 ]
3014 );
3015 }
3016
3017 #[test]
3019 fn standard14_faces_get_builtin_widths() {
3020 for fontdict in [
3021 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/WinAnsiEncoding>>".as_slice(),
3023 b"<</Type/Font/Subtype/Type1/BaseFont/Times-BoldItalic>>",
3025 b"<</Type/Font/Subtype/TrueType/BaseFont/Arial,Bold>>",
3027 b"<</Type/Font/Subtype/Type1/BaseFont/ABCDEF+Courier-Oblique>>",
3028 ] {
3029 let pdf = pdf_with_font(fontdict, b"Words have width now");
3030 let cs = cells(&pdf);
3031 let text: String = cs
3032 .iter()
3033 .map(|c| c.text.as_str())
3034 .collect::<Vec<_>>()
3035 .join(" ");
3036 assert!(
3037 text.contains("Words have width now"),
3038 "{}: text lost: {text:?}",
3039 String::from_utf8_lossy(fontdict)
3040 );
3041 assert!(
3042 cs.iter().all(|c| c.r > c.l),
3043 "{}: zero-width cells: {cs:?}",
3044 String::from_utf8_lossy(fontdict)
3045 );
3046 }
3047 }
3048
3049 #[test]
3054 fn macroman_fonts_keep_accented_letters() {
3055 let sentence = b"Bl\x8cb\xbergr\xbfd p\x8c \xbfen \xd0 caf\x8e M\x9fller, Stra\xa7e \xa4 3";
3056 for (fontdict, want) in [
3057 (
3058 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/MacRomanEncoding>>"
3059 .as_slice(),
3060 "Blåbærgrød på øen – café Müller, Straße § 3",
3061 ),
3062 (
3063 b"<</Type/Font/Subtype/TrueType/BaseFont/ABCDEF+Arial\
3064 /Encoding<</Type/Encoding/BaseEncoding/MacRomanEncoding/Differences[51/four]>>>>",
3065 "Blåbærgrød på øen – café Müller, Straße § 4",
3066 ),
3067 ] {
3068 let text: String = cells(&pdf_with_font(fontdict, sentence))
3069 .iter()
3070 .map(|c| c.text.as_str())
3071 .collect::<Vec<_>>()
3072 .join(" ");
3073 assert_eq!(text, want, "{}", String::from_utf8_lossy(fontdict));
3074 }
3075
3076 let table = super::macroman_table();
3078 assert_eq!(
3079 (0x80u8..=0xFF).filter(|b| table.contains_key(b)).count(),
3080 127
3081 );
3082 assert_eq!(table.get(&0xF0), None);
3083 assert_eq!(table[&0xDB], '¤');
3084 assert_eq!(
3085 [0x80, 0x8C, 0xAE, 0xAF, 0xBE, 0xBF, 0xFF].map(|b| table[&b]),
3086 ['Ä', 'å', 'Æ', 'Ø', 'æ', 'ø', 'ˇ']
3087 );
3088 }
3089
3090 #[test]
3093 fn explicit_widths_win_and_unknown_faces_are_untouched() {
3094 let explicit = pdf_with_font(
3098 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/FirstChar 65\
3099 /Widths[100 100 100 100]/Encoding/WinAnsiEncoding>>",
3100 b"ABBA",
3101 );
3102 let builtin = pdf_with_font(
3103 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/WinAnsiEncoding>>",
3104 b"ABBA",
3105 );
3106 let w = |pdf: &[u8]| {
3107 let cs = cells(pdf);
3108 assert_eq!(cs.len(), 1, "one word cell: {cs:?}");
3109 cs[0].r - cs[0].l
3110 };
3111 let (we, wb) = (w(&explicit), w(&builtin));
3112 assert!(
3113 (we - 4.8).abs() < 0.1,
3114 "explicit widths must win: got {we}, want 4×100×12/1000"
3115 );
3116 assert!(
3117 wb > 2.0 * we,
3118 "built-in Helvetica is much wider: {wb} vs {we}"
3119 );
3120
3121 let unknown = pdf_with_font(
3125 b"<</Type/Font/Subtype/Type1/BaseFont/FancyCorp-Display>>",
3126 b"Mystery",
3127 );
3128 let cs = cells(&unknown);
3129 let text: String = cs.iter().map(|c| c.text.as_str()).collect();
3130 assert!(text.contains("Mystery"), "text still decodes: {cs:?}");
3131 }
3132 #[test]
3141 fn same_face_under_two_resource_keys_is_one_font_for_the_sanitizer() {
3142 let f1 = b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/WinAnsiEncoding>>";
3143 let f2 = b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding<</Type/Encoding\
3144 /BaseEncoding/WinAnsiEncoding/Differences[129/fi]>>>>";
3145 let content = b"BT 12 TL 72 720 Td \
3146 /F1 12 Tf (The ) Tj /F2 12 Tf (\\201nancial position) Tj \
3147 /F1 12 Tf ( of the company is shown in ) Tj /F2 12 Tf (\\201gure 3) Tj \
3148 /F1 12 Tf ( with the ) Tj /F2 12 Tf (\\201rst results.) Tj \
3149 T* T* /F2 12 Tf (Same words in /F2 only: \\201nancial, \\201gure, \\201rst.) Tj ET\n";
3150 let cs = cells(&pdf_with_fonts(&[f1, f2], content));
3151 let lines: Vec<&str> = cs.iter().map(|c| c.text.as_str()).collect();
3152 assert_eq!(
3153 lines,
3154 [
3155 "The financial position of the company is shown in figure 3 with the first results.",
3156 "Same words in /F2 only: financial, figure, first.",
3157 ],
3158 "{cs:?}"
3159 );
3160 }
3161
3162 #[test]
3166 fn different_faces_still_keep_the_font_barrier() {
3167 let f1 = b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/WinAnsiEncoding>>";
3168 let f2 = b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica-Bold/Encoding/WinAnsiEncoding>>";
3169 let content = b"BT 72 720 Td /F2 12 Tf (Total:) Tj /F1 12 Tf (42) Tj ET\n";
3170 let cs = cells(&pdf_with_fonts(&[f1, f2], content));
3171 let texts: Vec<&str> = cs.iter().map(|c| c.text.as_str()).collect();
3172 assert_eq!(texts, ["Total:", "42"], "{cs:?}");
3173 }
3174
3175 #[test]
3180 fn sanitizer_font_name_follows_docling_parse_precedence() {
3181 use lopdf::{dictionary, Document, Object};
3182 let mut doc = Document::with_version("1.4");
3183 let fd_a =
3184 doc.add_object(dictionary! { "Type" => "FontDescriptor", "FontName" => "AAAAAA+Face" });
3185 let fd_b =
3186 doc.add_object(dictionary! { "Type" => "FontDescriptor", "FontName" => "BBBBBB+Face" });
3187 let with = |fd: Option<lopdf::ObjectId>, base: &str| {
3188 let mut d = dictionary! { "Type" => "Font", "Subtype" => "Type1", "BaseFont" => base };
3189 if let Some(id) = fd {
3190 d.set("FontDescriptor", Object::Reference(id));
3191 }
3192 d
3193 };
3194 let name = |d: &lopdf::Dictionary| {
3195 String::from_utf8(super::sanitizer_font_name(&doc, d, false)).unwrap()
3196 };
3197 assert_eq!(name(&with(Some(fd_a), "Face")), "AAAAAA+Face");
3198 assert_eq!(name(&with(Some(fd_b), "Face")), "BBBBBB+Face");
3199 assert_eq!(name(&with(None, "ABCDEF+Face")), "ABCDEF+Face");
3200 assert_eq!(
3201 name(&dictionary! { "Type" => "Font", "Name" => "F7" }),
3202 "F7"
3203 );
3204 assert_eq!(name(&dictionary! { "Type" => "Font" }), "null");
3205 }
3206}
3207
3208#[cfg(test)]
3209mod overpainted {
3210 use crate::pdfium_backend::TextCell;
3211
3212 fn cell(text: &str, l: f32, t: f32, r: f32, b: f32) -> TextCell {
3213 TextCell {
3214 text: text.into(),
3215 l,
3216 t,
3217 r,
3218 b,
3219 }
3220 }
3221
3222 #[test]
3226 fn stacked_logo_glyphs_are_dropped() {
3227 let mut cells = vec![
3228 cell("\"", 72.7, 21.5, 86.4, 31.5),
3229 cell("==", 59.4, 21.5, 99.6, 31.5),
3230 cell("Herr", 65.2, 151.3, 81.7, 161.3),
3231 ];
3232 super::drop_overpainted_cells(&mut cells);
3233 assert_eq!(cells.len(), 1, "cells: {cells:?}");
3234 assert_eq!(cells[0].text, "Herr");
3235 }
3236
3237 #[test]
3241 fn prose_and_double_draw_are_kept() {
3242 let mut cells = vec![
3243 cell("Telefon", 354.3, 133.2, 381.5, 143.2),
3244 cell("0676/2000", 387.3, 133.2, 428.7, 143.2),
3245 cell("Bold", 100.0, 50.0, 130.0, 60.0),
3246 cell("Bold", 100.3, 50.0, 130.3, 60.0),
3247 ];
3248 super::drop_overpainted_cells(&mut cells);
3249 assert_eq!(cells.len(), 4);
3250 }
3251}
3252
3253#[cfg(test)]
3254mod vestigial_layer {
3255 use crate::pdfium_backend::{PdfPage, TextCell};
3256
3257 fn page_with(texts: &[&str]) -> PdfPage {
3258 let cells = texts
3259 .iter()
3260 .enumerate()
3261 .map(|(i, t)| TextCell {
3262 text: t.to_string(),
3263 l: 10.0,
3264 t: 10.0 + 12.0 * i as f32,
3265 r: 90.0,
3266 b: 20.0 + 12.0 * i as f32,
3267 })
3268 .collect();
3269 PdfPage::from_cells(595.0, 842.0, 1.0, cells)
3270 }
3271
3272 #[test]
3277 fn typed_in_form_fields_are_not_a_text_layer() {
3278 let pages = vec![
3279 page_with(&["03", "05", "2025"]),
3280 page_with(&[]),
3281 page_with(&[]),
3282 ];
3283 assert!(super::text_layer_is_vestigial(&pages));
3284 assert!(super::text_layer_is_vestigial(&[page_with(&[])]));
3285 }
3286
3287 #[test]
3290 fn sparse_but_real_documents_pass() {
3291 let one_pager = vec![page_with(&[
3292 "Confidential briefing",
3293 "Prepared for the board meeting",
3294 "Do not distribute",
3295 ])];
3296 assert!(!super::text_layer_is_vestigial(&one_pager));
3297 }
3298}