1use std::collections::HashMap;
17use std::rc::Rc;
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>), Rc<Font>>,
34 forms: HashMap<lopdf::ObjectId, Rc<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}
101
102impl Font {
103 fn decode_code(&self, code: u32) -> (Option<String>, f64) {
104 let w = self
105 .widths
106 .get(&code)
107 .copied()
108 .unwrap_or(self.default_width);
109 if let Some(s) = self.to_unicode.get(&code) {
110 return (Some(decompose_ligatures(s)), w);
111 }
112 if !self.two_byte {
113 if let Some(name) = self.fallback_names.get(&(code as u8)) {
116 return (Some(format!("/{name}")), w);
117 }
118 if let Some(enc) = &self.simple_encoding {
119 if let Some(&ch) = enc.get(&(code as u8)) {
120 return (Some(decompose_ligatures(&ch.to_string())), w);
121 }
122 }
123 if let Some(&ch) = self.program_encoding.get(&(code as u8)) {
131 return (Some(decompose_ligatures(&ch.to_string())), w);
132 }
133 }
134 (None, w)
135 }
136}
137
138fn decompose_ligatures(s: &str) -> String {
142 if !s.chars().any(|c| ('\u{FB00}'..='\u{FB06}').contains(&c)) {
143 return s.to_string();
144 }
145 s.chars()
146 .map(|c| {
147 match c {
148 '\u{FB00}' => "ff",
149 '\u{FB01}' => "fi",
150 '\u{FB02}' => "fl",
151 '\u{FB03}' => "ffi",
152 '\u{FB04}' => "ffl",
153 '\u{FB05}' => "ft",
154 '\u{FB06}' => "st",
155 _ => return c.to_string(),
156 }
157 .to_string()
158 })
159 .collect()
160}
161
162fn hash_name(name: &[u8]) -> u64 {
163 use std::hash::{Hash, Hasher};
164 let mut h = std::collections::hash_map::DefaultHasher::new();
165 name.hash(&mut h);
166 h.finish()
167}
168
169fn as_dict<'a>(doc: &'a Document, obj: &'a Object) -> Option<&'a Dictionary> {
171 match obj {
172 Object::Dictionary(d) => Some(d),
173 Object::Reference(id) => doc.get_object(*id).ok().and_then(|o| o.as_dict().ok()),
174 _ => None,
175 }
176}
177
178fn deref<'a>(doc: &'a Document, obj: &'a Object) -> Option<&'a Object> {
179 match obj {
180 Object::Reference(id) => doc.get_object(*id).ok(),
181 other => Some(other),
182 }
183}
184
185fn parse_font(doc: &Document, name: &[u8], fdict: &Dictionary) -> Font {
187 let subtype: &[u8] = fdict
188 .get(b"Subtype")
189 .ok()
190 .and_then(|o| o.as_name().ok())
191 .unwrap_or(&[]);
192 let two_byte = subtype == b"Type0".as_slice();
193
194 let to_unicode = fdict
195 .get(b"ToUnicode")
196 .ok()
197 .and_then(|o| deref(doc, o))
198 .and_then(|o| o.as_stream().ok())
199 .and_then(|s| s.decompressed_content().ok())
200 .map(|data| parse_tounicode(&data))
201 .unwrap_or_default();
202
203 let (mut widths, mut default_width) = if two_byte {
204 cid_widths(doc, fdict)
205 } else {
206 simple_widths(doc, fdict)
207 };
208
209 let simple_encoding = if two_byte {
210 None
211 } else {
212 Some(simple_encoding_table(doc, fdict))
213 };
214
215 if !two_byte && widths.is_empty() && default_width == 0.0 {
223 if let Some(std14) = base_font_name(fdict).and_then(|n| crate::std14::widths_for(&n)) {
224 if let Some(enc) = &simple_encoding {
225 for (&code, &ch) in enc {
226 if let Some(w) = std14.width(ch) {
227 widths.insert(u32::from(code), w);
228 }
229 }
230 }
231 default_width = 500.0;
234 }
235 }
236 let fallback_names = if two_byte {
237 HashMap::new()
238 } else {
239 differences_gid_names(doc, fdict)
240 };
241 let program_encoding = if two_byte {
242 HashMap::new()
243 } else {
244 type1_program_encoding(doc, fdict)
245 };
246
247 let (ascent, descent) = font_ascent_descent(doc, fdict, two_byte);
248
249 Font {
250 two_byte,
251 to_unicode,
252 widths,
253 default_width,
254 simple_encoding,
255 fallback_names,
256 program_encoding,
257 ascent,
258 descent,
259 hash: hash_name(name),
260 }
261}
262
263fn differences_gid_names(doc: &Document, fdict: &Dictionary) -> HashMap<u8, String> {
270 let mut map = HashMap::new();
271 let Some(Object::Dictionary(enc)) = fdict.get(b"Encoding").ok().and_then(|o| deref(doc, o))
272 else {
273 return map;
274 };
275 let Some(Object::Array(diffs)) = enc.get(b"Differences").ok().and_then(|o| deref(doc, o))
276 else {
277 return map;
278 };
279 let mut code = 0u8;
280 for el in diffs {
281 match el {
282 Object::Integer(i) => code = *i as u8,
283 Object::Name(name) => {
284 if glyph_name_to_char(name).is_none() && is_gid_name(name) {
285 map.insert(code, String::from_utf8_lossy(name).into_owned());
286 }
287 code = code.wrapping_add(1);
288 }
289 _ => {}
290 }
291 }
292 map
293}
294
295fn type1_program_encoding(doc: &Document, fdict: &Dictionary) -> HashMap<u8, char> {
303 let mut map = HashMap::new();
304 let Some(desc) = fdict
305 .get(b"FontDescriptor")
306 .ok()
307 .and_then(|o| deref(doc, o))
308 .and_then(|o| o.as_dict().ok())
309 else {
310 return map;
311 };
312 let Some(data) = desc
313 .get(b"FontFile")
314 .ok()
315 .and_then(|o| deref(doc, o))
316 .and_then(|o| o.as_stream().ok())
317 .and_then(|s| s.decompressed_content().ok())
318 else {
319 return map;
320 };
321 let head_end = data
323 .windows(5)
324 .position(|w| w == b"eexec")
325 .unwrap_or(data.len());
326 let head = String::from_utf8_lossy(&data[..head_end]);
327 let toks: Vec<&str> = head.split_whitespace().collect();
329 for w in toks.windows(4) {
330 if w[0] == "dup" && w[3] == "put" {
331 if let (Ok(code), Some(name)) = (w[1].parse::<u32>(), w[2].strip_prefix('/')) {
332 if code <= 255 {
333 if let Some(ch) = glyph_name_to_char(name.as_bytes()) {
334 map.insert(code as u8, ch);
335 }
336 }
337 }
338 }
339 }
340 map
341}
342
343fn is_gid_name(name: &[u8]) -> bool {
349 let Ok(s) = std::str::from_utf8(name) else {
350 return false;
351 };
352 if s.starts_with("afii") || s.starts_with("uni") {
353 return false;
354 }
355 for prefix in ["g", "G", "cid", "CID", "glyph", "index"] {
356 if let Some(rest) = s.strip_prefix(prefix) {
357 if !rest.is_empty() && rest.bytes().all(|b| b.is_ascii_digit()) {
358 return true;
359 }
360 }
361 }
362 let alpha = s.bytes().take_while(|b| b.is_ascii_alphabetic()).count();
366 let digits = s.len() - alpha;
367 (1..=3).contains(&alpha)
368 && digits >= 3
369 && s.as_bytes()[alpha..].iter().all(|b| b.is_ascii_digit())
370}
371
372fn font_ascent_descent(doc: &Document, fdict: &Dictionary, two_byte: bool) -> (f64, f64) {
373 let descr_owner = if two_byte {
375 fdict
376 .get(b"DescendantFonts")
377 .ok()
378 .and_then(|o| deref(doc, o))
379 .and_then(|o| match o {
380 Object::Array(a) => a.first(),
381 _ => None,
382 })
383 .and_then(|o| as_dict(doc, o))
384 } else {
385 Some(fdict)
386 };
387 let fd = descr_owner
388 .and_then(|d| d.get(b"FontDescriptor").ok())
389 .and_then(|o| as_dict(doc, o));
390 let asc = fd
391 .and_then(|d| d.get(b"Ascent").ok())
392 .and_then(|o| {
393 o.as_float()
394 .ok()
395 .or_else(|| o.as_i64().ok().map(|i| i as f32))
396 })
397 .unwrap_or(750.0) as f64;
398 let desc = fd
399 .and_then(|d| d.get(b"Descent").ok())
400 .and_then(|o| {
401 o.as_float()
402 .ok()
403 .or_else(|| o.as_i64().ok().map(|i| i as f32))
404 })
405 .unwrap_or(-250.0) as f64;
406 if asc - desc <= 1.0 {
413 return (750.0, -250.0);
414 }
415 (asc, desc)
416}
417
418fn base_font_name(fdict: &Dictionary) -> Option<Vec<u8>> {
420 let name = fdict.get(b"BaseFont").ok()?.as_name().ok()?;
421 let stripped = match name.iter().position(|&b| b == b'+') {
422 Some(i) if i == 6 => &name[i + 1..],
423 _ => name,
424 };
425 Some(stripped.to_vec())
426}
427
428fn simple_widths(doc: &Document, fdict: &Dictionary) -> (HashMap<u32, f64>, f64) {
430 let mut map = HashMap::new();
431 let first = fdict
432 .get(b"FirstChar")
433 .ok()
434 .and_then(|o| o.as_i64().ok())
435 .unwrap_or(0) as u32;
436 if let Some(Object::Array(arr)) = fdict.get(b"Widths").ok().and_then(|o| deref(doc, o)) {
437 for (i, w) in arr.iter().enumerate() {
438 if let Some(w) = num(w) {
439 map.insert(first + i as u32, w);
440 }
441 }
442 }
443 let dw = fdict
444 .get(b"FontDescriptor")
445 .ok()
446 .and_then(|o| as_dict(doc, o))
447 .and_then(|d| d.get(b"MissingWidth").ok())
448 .and_then(num)
449 .unwrap_or(0.0);
450 (map, dw)
451}
452
453fn cid_widths(doc: &Document, fdict: &Dictionary) -> (HashMap<u32, f64>, f64) {
455 let mut map = HashMap::new();
456 let Some(desc) = fdict
457 .get(b"DescendantFonts")
458 .ok()
459 .and_then(|o| deref(doc, o))
460 .and_then(|o| match o {
461 Object::Array(a) => a.first(),
462 _ => None,
463 })
464 .and_then(|o| as_dict(doc, o))
465 else {
466 return (map, 1000.0);
467 };
468 let dw = desc.get(b"DW").ok().and_then(num).unwrap_or(1000.0);
469 if let Some(Object::Array(w)) = desc.get(b"W").ok().and_then(|o| deref(doc, o)) {
470 let mut i = 0;
471 while i < w.len() {
472 let c = w.get(i).and_then(num);
473 match (c, w.get(i + 1)) {
474 (Some(c), Some(Object::Array(list))) => {
476 for (k, wv) in list.iter().enumerate() {
477 if let Some(wv) = num(wv) {
478 map.insert(c as u32 + k as u32, wv);
479 }
480 }
481 i += 2;
482 }
483 (Some(c1), Some(o2)) => {
485 if let (Some(c2), Some(wv)) = (num(o2), w.get(i + 2).and_then(num)) {
486 for cid in c1 as u32..=c2 as u32 {
487 map.insert(cid, wv);
488 }
489 }
490 i += 3;
491 }
492 _ => break,
493 }
494 }
495 }
496 (map, dw)
497}
498
499fn num(o: &Object) -> Option<f64> {
500 match o {
501 Object::Integer(i) => Some(*i as f64),
502 Object::Real(r) => Some(*r as f64),
503 _ => None,
504 }
505}
506
507fn parse_tounicode(data: &[u8]) -> HashMap<u32, String> {
509 let text = String::from_utf8_lossy(data);
510 let mut map = HashMap::new();
511 let hex = |s: &str| -> Option<Vec<u16>> {
512 let s = s.trim();
513 if !s.starts_with('<') || !s.ends_with('>') {
514 return None;
515 }
516 let h = &s[1..s.len() - 1];
517 let bytes: Vec<u8> = (0..h.len())
518 .step_by(2)
519 .filter_map(|i| u8::from_str_radix(h.get(i..i + 2)?, 16).ok())
520 .collect();
521 Some(
522 bytes
523 .chunks(2)
524 .map(|c| {
525 if c.len() == 2 {
526 u16::from_be_bytes([c[0], c[1]])
527 } else {
528 c[0] as u16
529 }
530 })
531 .collect(),
532 )
533 };
534 let u16s_to_string = |u: &[u16]| String::from_utf16_lossy(u);
535 let code_of = |u: &[u16]| u.iter().fold(0u32, |acc, &x| (acc << 16) | x as u32);
536
537 let tokens: Vec<String> = {
541 let bytes = text.as_bytes();
542 let mut toks = Vec::new();
543 let mut i = 0;
544 while i < bytes.len() {
545 let c = bytes[i];
546 if c.is_ascii_whitespace() {
547 i += 1;
548 } else if c == b'<' {
549 let start = i;
550 while i < bytes.len() && bytes[i] != b'>' {
551 i += 1;
552 }
553 i += 1; toks.push(String::from_utf8_lossy(&bytes[start..i.min(bytes.len())]).into_owned());
555 } else if c == b'[' || c == b']' {
556 toks.push((c as char).to_string());
557 i += 1;
558 } else {
559 let start = i;
560 while i < bytes.len()
561 && !bytes[i].is_ascii_whitespace()
562 && bytes[i] != b'<'
563 && bytes[i] != b'['
564 && bytes[i] != b']'
565 {
566 i += 1;
567 }
568 toks.push(String::from_utf8_lossy(&bytes[start..i]).into_owned());
569 }
570 }
571 toks
572 };
573 let tokens: Vec<&str> = tokens.iter().map(|s| s.as_str()).collect();
574 let mut i = 0;
575 while i < tokens.len() {
576 match tokens[i] {
577 "beginbfchar" => {
578 i += 1;
579 while i + 1 < tokens.len() && tokens[i] != "endbfchar" {
580 if let (Some(src), Some(dst)) = (hex(tokens[i]), hex(tokens[i + 1])) {
581 map.insert(code_of(&src), u16s_to_string(&dst));
582 }
583 i += 2;
584 }
585 }
586 "beginbfrange" => {
587 i += 1;
588 while i + 2 < tokens.len() && tokens[i] != "endbfrange" {
589 let (Some(lo), Some(hi)) = (hex(tokens[i]), hex(tokens[i + 1])) else {
590 i += 1;
591 continue;
592 };
593 let lo = code_of(&lo);
594 let hi = code_of(&hi);
595 if tokens[i + 2] == "[" {
596 let mut j = i + 3;
598 let mut code = lo;
599 while j < tokens.len() && tokens[j] != "]" {
600 if let Some(dst) = hex(tokens[j]) {
601 map.insert(code, u16s_to_string(&dst));
602 }
603 code += 1;
604 j += 1;
605 }
606 i = j + 1;
607 } else if let Some(dst) = hex(tokens[i + 2]) {
608 let base = code_of(&dst);
610 for (k, code) in (lo..=hi).enumerate() {
611 if let Some(ch) = char::from_u32(base + k as u32) {
612 map.insert(code, ch.to_string());
613 }
614 }
615 i += 3;
616 } else {
617 i += 1;
618 }
619 }
620 }
621 _ => i += 1,
622 }
623 }
624 map
625}
626
627fn codes(font: &Font, bytes: &[u8]) -> Vec<u32> {
629 if font.two_byte {
630 bytes
631 .chunks(2)
632 .map(|c| {
633 if c.len() == 2 {
634 ((c[0] as u32) << 8) | c[1] as u32
635 } else {
636 c[0] as u32
637 }
638 })
639 .collect()
640 } else {
641 bytes.iter().map(|&b| b as u32).collect()
642 }
643}
644
645fn page_size(doc: &Document, page_id: lopdf::ObjectId) -> (f32, f32) {
647 let mb = doc
648 .get_object(page_id)
649 .ok()
650 .and_then(|o| o.as_dict().ok())
651 .and_then(|d| {
652 d.get(b"MediaBox").ok().cloned()
654 })
655 .or_else(|| {
656 doc.get_dictionary(page_id)
657 .ok()
658 .and_then(|d| d.get(b"MediaBox").ok().cloned())
659 });
660 if let Some(Object::Array(a)) = mb {
661 let v: Vec<f32> = a.iter().filter_map(|o| num(o).map(|x| x as f32)).collect();
662 if v.len() == 4 {
663 return ((v[2] - v[0]).abs(), (v[3] - v[1]).abs());
664 }
665 }
666 (612.0, 792.0)
667}
668
669pub fn content_diagnosis(bytes: &[u8]) -> String {
675 let Some(doc) = load_document(bytes) else {
676 return "document does not load".into();
677 };
678 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
679 pages.sort_by_key(|(n, _)| *n);
680 let mut out = String::new();
681 let mut caches = DocCaches::default();
682 for (n, pid) in pages.into_iter().take(4) {
683 let content_bytes = doc.get_page_content(pid);
684 let ops = lopdf::content::Content::decode(&content_bytes)
685 .map(|c| c.operations.len())
686 .ok();
687 let res = page_res(&doc, pid);
688 let fonts = res.map(|r| fonts_from_res(&doc, r, &mut caches).len());
689 let glyphs = page_glyphs_cached(&doc, pid, &mut caches).len();
690 out.push_str(&format!(
691 "\n page {n}: content {} B, ops {}, resources {}, fonts {}, glyphs {}",
692 content_bytes.len(),
693 ops.map_or("UNDECODABLE".to_string(), |n| n.to_string()),
694 if res.is_some() { "ok" } else { "MISSING" },
695 fonts.map_or("-".to_string(), |n| n.to_string()),
696 glyphs,
697 ));
698 }
699 out
700}
701
702pub fn text_layer_is_vestigial(pages: &[crate::pdfium_backend::PdfPage]) -> bool {
715 let lines: usize = pages.iter().map(|p| p.cells.len()).sum();
716 if lines == 0 {
717 return true;
718 }
719 let chars: usize = pages
720 .iter()
721 .flat_map(|p| &p.cells)
722 .map(|c| c.text.chars().count())
723 .sum();
724 lines <= pages.len() && chars < 32
725}
726
727pub fn xref_repair_status(bytes: &[u8]) -> String {
731 if Document::load_mem(bytes).is_ok() {
732 return "loads unaided; no repair needed".into();
733 }
734 match pad_short_xref_entries(bytes) {
735 Ok(fixed) => match Document::load_mem(&fixed) {
736 Ok(_) => "repaired: cross-reference entries padded to 20 bytes".into(),
737 Err(e) => format!("padded the entries, but it still will not load: {e}"),
738 },
739 Err(why) => format!("repair declined — {why}"),
740 }
741}
742
743fn load_document(bytes: &[u8]) -> Option<Document> {
758 let mut fallback = None;
763 if let Some(doc) = best_effort_load(bytes, &mut fallback) {
764 return Some(doc);
765 }
766 let xref_fixed = pad_short_xref_entries(bytes).ok();
767 if let Some(fixed) = &xref_fixed {
768 if let Some(doc) = best_effort_load(fixed, &mut fallback) {
769 return Some(doc);
770 }
771 }
772 let lengths_fixed = fix_stream_lengths(xref_fixed.as_deref().unwrap_or(bytes));
775 if let Some(doc) = best_effort_load(&lengths_fixed, &mut fallback) {
776 return Some(doc);
777 }
778 fallback
779}
780
781fn best_effort_load(data: &[u8], fallback: &mut Option<Document>) -> Option<Document> {
784 match Document::load_mem(data) {
785 Ok(doc) if has_page_content(&doc) => Some(doc),
786 Ok(doc) => {
787 fallback.get_or_insert(doc);
788 None
789 }
790 Err(_) => None,
791 }
792}
793
794fn has_page_content(doc: &Document) -> bool {
798 doc.get_pages()
799 .into_values()
800 .take(4)
801 .any(|pid| !doc.get_page_content(pid).is_empty())
802}
803
804fn fix_stream_lengths(bytes: &[u8]) -> Vec<u8> {
817 let mut out = bytes.to_vec();
818 let mut i = 0;
819 while let Some(rel) = find(&out[i..], b"stream") {
820 let kw = i + rel;
821 i = kw + 6;
822 if kw >= 3 && &out[kw - 3..kw] == b"end" {
824 continue;
825 }
826 let mut data = kw + 6;
828 if out.get(data..data + 2) == Some(b"\r\n".as_slice()) {
829 data += 2;
830 } else if matches!(out.get(data), Some(b'\n' | b'\r')) {
831 data += 1;
832 }
833 let Some(end) = find(&out[data..], b"endstream").map(|r| data + r) else {
834 continue;
835 };
836 let dict_start = out[..kw].iter().rposition(|&c| c == b'<').unwrap_or(0);
838 let Some(lrel) = find(&out[dict_start..kw], b"/Length") else {
839 continue;
840 };
841 let mut d = dict_start + lrel + 7;
842 while matches!(out.get(d), Some(b' ')) {
843 d += 1;
844 }
845 let digits = out[d..].iter().take_while(|c| c.is_ascii_digit()).count();
846 if digits == 0 {
847 continue;
848 }
849 let declared: usize = match std::str::from_utf8(&out[d..d + digits])
850 .ok()
851 .and_then(|s| s.parse().ok())
852 {
853 Some(v) => v,
854 None => continue,
855 };
856 let actual = end - data;
857 let replacement = actual.to_string();
860 if actual == declared || replacement.len() > digits {
861 continue;
862 }
863 out[d..d + digits].fill(b' ');
864 out[d..d + replacement.len()].copy_from_slice(replacement.as_bytes());
865 }
866 out
867}
868
869fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
870 haystack.windows(needle.len()).position(|w| w == needle)
871}
872
873fn pad_short_xref_entries(bytes: &[u8]) -> Result<Vec<u8>, &'static str> {
876 let is_boundary = |i: usize| i == 0 || matches!(bytes[i - 1], b'\n' | b'\r');
879 let mut starts = (0..bytes.len().saturating_sub(4))
880 .filter(|&i| &bytes[i..i + 4] == b"xref" && is_boundary(i));
881 let xref_at = starts
882 .next()
883 .ok_or("no classic `xref` section (an xref stream?)")?;
884 if starts.next().is_some() {
885 return Err("more than one xref section (incremental update)");
886 }
887 let last_obj = bytes
888 .windows(3)
889 .rposition(|w| w == b"obj")
890 .ok_or("no objects found")?;
891 if last_obj > xref_at {
892 return Err("an object follows the xref — padding would move it");
893 }
894
895 let mut out = bytes[..xref_at].to_vec();
896 out.extend_from_slice(b"xref\n");
897 let mut i = xref_at + 4;
898 let skip_ws = |i: &mut usize| {
899 while matches!(bytes.get(*i), Some(b'\r' | b'\n' | b' ')) {
900 *i += 1;
901 }
902 };
903 loop {
904 skip_ws(&mut i);
905 if bytes[i..].starts_with(b"trailer") {
907 out.extend_from_slice(&bytes[i..]);
908 return Ok(out);
909 }
910 let header_end = i + bytes[i..]
911 .iter()
912 .position(|c| matches!(c, b'\n' | b'\r'))
913 .ok_or("subsection header runs off the end")?;
914 let header = std::str::from_utf8(&bytes[i..header_end])
915 .map_err(|_| "subsection header is not text")?
916 .trim();
917 let mut parts = header.split_whitespace();
918 let count: usize = parts
919 .nth(1)
920 .and_then(|c| c.parse().ok())
921 .ok_or("unparseable subsection header")?;
922 if parts.next().is_some() || count == 0 {
923 return Err("unexpected subsection header shape");
924 }
925 out.extend_from_slice(header.as_bytes());
926 out.push(b'\n');
927 i = header_end;
928 for _ in 0..count {
929 skip_ws(&mut i);
930 let entry = bytes.get(i..i + 18).ok_or("xref entry runs off the end")?;
932 let well_formed = entry[..10].iter().all(u8::is_ascii_digit)
933 && entry[10] == b' '
934 && entry[11..16].iter().all(u8::is_ascii_digit)
935 && entry[16] == b' '
936 && matches!(entry[17], b'n' | b'f');
937 if !well_formed {
938 return Err("xref entry is not `nnnnnnnnnn ggggg n`");
939 }
940 out.extend_from_slice(entry);
941 out.extend_from_slice(b" \n"); i += 18;
943 }
944 }
945}
946
947pub fn debug_glyphs(bytes: &[u8], index: usize) -> Vec<(char, f32, f32, f32, f32)> {
950 let Some(doc) = load_document(bytes) else {
951 return Vec::new();
952 };
953 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
954 pages.sort_by_key(|(n, _)| *n);
955 let Some((_, pid)) = pages.get(index) else {
956 return Vec::new();
957 };
958 page_glyphs(&doc, *pid)
959 .into_iter()
960 .map(|g| (g.ch, g.ll, g.lr, g.lb, g.lt))
961 .collect()
962}
963
964pub fn pdf_textlines(bytes: &[u8]) -> Vec<(f32, f32, Vec<crate::pdfium_backend::TextCell>)> {
968 let Some(doc) = load_document(bytes) else {
969 return Vec::new();
970 };
971 let mut caches = DocCaches::default();
972 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
973 pages.sort_by_key(|(n, _)| *n);
974 pages
975 .into_iter()
976 .map(|(_, pid)| {
977 let (w, h) = page_size(&doc, pid);
978 let glyphs = page_glyphs_cached(&doc, pid, &mut caches);
979 let cells = crate::dp_lines::line_cells(&glyphs, h, true);
980 (w, h, cells)
981 })
982 .collect()
983}
984
985pub fn pdf_words(bytes: &[u8]) -> Vec<(f32, f32, Vec<crate::pdfium_backend::TextCell>)> {
990 let Some(doc) = load_document(bytes) else {
991 return Vec::new();
992 };
993 let mut caches = DocCaches::default();
994 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
995 pages.sort_by_key(|(n, _)| *n);
996 pages
997 .into_iter()
998 .map(|(_, pid)| {
999 let (w, h) = page_size(&doc, pid);
1000 let glyphs = page_glyphs_cached(&doc, pid, &mut caches);
1001 let cells = crate::dp_lines::word_cells(&glyphs, h, true);
1002 (w, h, cells)
1003 })
1004 .collect()
1005}
1006
1007#[derive(Default)]
1011pub struct PageParserCells {
1012 pub prose: Vec<crate::pdfium_backend::TextCell>,
1013 pub words: Vec<crate::pdfium_backend::TextCell>,
1014 pub code: Vec<crate::pdfium_backend::TextCell>,
1015}
1016
1017pub fn pdf_all_cells(bytes: &[u8]) -> Vec<PageParserCells> {
1022 let Some(doc) = load_document(bytes) else {
1023 return Vec::new();
1024 };
1025 let mut caches = DocCaches::default();
1026 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
1027 pages.sort_by_key(|(n, _)| *n);
1028 pages
1029 .into_iter()
1030 .map(|(_, pid)| {
1031 let (_w, h) = page_size(&doc, pid);
1032 let glyphs = page_glyphs_cached(&doc, pid, &mut caches);
1033 let (prose, words) = crate::dp_lines::line_and_word_cells(&glyphs, h, true);
1034 PageParserCells {
1035 prose,
1036 words,
1037 code: crate::pdfium_backend::code_cells_from_glyphs(&glyphs, h),
1038 }
1039 })
1040 .collect()
1041}
1042
1043pub fn pdf_text_pages(bytes: &[u8]) -> Vec<crate::pdfium_backend::PdfPage> {
1050 let Some(doc) = load_document(bytes) else {
1051 return Vec::new();
1052 };
1053 let mut caches = DocCaches::default();
1054 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
1055 pages.sort_by_key(|(n, _)| *n);
1056 pages
1057 .into_iter()
1058 .map(|(_, pid)| {
1059 let (w, h) = page_size(&doc, pid);
1060 let glyphs = page_glyphs_cached(&doc, pid, &mut caches);
1061 let (mut prose, mut words) = crate::dp_lines::line_and_word_cells(&glyphs, h, true);
1062 drop_overpainted_cells(&mut prose);
1063 drop_overpainted_cells(&mut words);
1064 crate::pdfium_backend::PdfPage {
1065 width: w,
1066 height: h,
1067 scale: 1.0,
1069 cells: prose,
1070 code_cells: crate::pdfium_backend::code_cells_from_glyphs(&glyphs, h),
1071 word_cells: words,
1072 #[cfg(feature = "ocr-prep")]
1073 image: image::RgbImage::new(1, 1),
1074 links: Vec::new(),
1075 }
1076 })
1077 .collect()
1078}
1079
1080fn drop_overpainted_cells(cells: &mut Vec<crate::pdfium_backend::TextCell>) {
1099 let mut paint = vec![false; cells.len()];
1100 for i in 0..cells.len() {
1101 for j in 0..cells.len() {
1102 if i == j || cells[i].text == cells[j].text {
1103 continue;
1104 }
1105 let (a, b) = (&cells[i], &cells[j]);
1106 let vo = (a.b.min(b.b) - a.t.max(b.t)).max(0.0);
1108 if vo < 0.6 * (a.b - a.t).min(b.b - b.t) {
1109 continue;
1110 }
1111 let ho = (a.r.min(b.r) - a.l.max(b.l)).max(0.0);
1113 if ho >= 0.8 * (a.r - a.l) && (a.r - a.l) <= (b.r - b.l) {
1114 paint[i] = true;
1115 paint[j] = true;
1116 }
1117 }
1118 }
1119 let mut keep = paint.iter().map(|p| !p);
1120 cells.retain(|_| keep.next().unwrap());
1121}
1122
1123#[derive(Clone, Copy)]
1127struct TextState {
1128 tc: f64,
1129 tw: f64,
1130 th: f64,
1131 tl: f64,
1132 trise: f64,
1133 fsize: f64,
1134}
1135
1136impl TextState {
1137 const INIT: TextState = TextState {
1138 tc: 0.0,
1139 tw: 0.0,
1140 th: 1.0,
1141 tl: 0.0,
1142 trise: 0.0,
1143 fsize: 0.0,
1144 };
1145}
1146
1147fn page_res(doc: &Document, page_id: lopdf::ObjectId) -> Option<&Dictionary> {
1150 let (inline, ids) = doc.get_page_resources(page_id).ok()?;
1151 if let Some(d) = inline {
1152 return Some(d);
1153 }
1154 ids.into_iter().find_map(|id| doc.get_dictionary(id).ok())
1155}
1156
1157fn fonts_from_res(
1161 doc: &Document,
1162 res: &Dictionary,
1163 caches: &mut DocCaches,
1164) -> HashMap<Vec<u8>, Rc<Font>> {
1165 let mut map = HashMap::new();
1166 let font_dict = res
1167 .get(b"Font")
1168 .ok()
1169 .and_then(|o| deref(doc, o))
1170 .and_then(|o| o.as_dict().ok());
1171 if let Some(fd) = font_dict {
1172 for (name, value) in fd.iter() {
1173 let font = match value {
1174 Object::Reference(id) => {
1175 let key = (*id, name.clone());
1176 if let Some(f) = caches.fonts.get(&key) {
1177 Rc::clone(f)
1178 } else if let Some(fdict) = deref(doc, value).and_then(|o| o.as_dict().ok()) {
1179 let f = Rc::new(parse_font(doc, name, fdict));
1180 caches.fonts.insert(key, Rc::clone(&f));
1181 f
1182 } else {
1183 continue;
1184 }
1185 }
1186 _ => {
1187 if let Some(fdict) = deref(doc, value).and_then(|o| o.as_dict().ok()) {
1188 Rc::new(parse_font(doc, name, fdict))
1189 } else {
1190 continue;
1191 }
1192 }
1193 };
1194 map.insert(name.clone(), font);
1195 }
1196 }
1197 map
1198}
1199
1200pub(crate) fn page_glyphs(doc: &Document, page_id: lopdf::ObjectId) -> Vec<Glyph> {
1202 page_glyphs_cached(doc, page_id, &mut DocCaches::default())
1203}
1204
1205fn page_glyphs_cached(
1208 doc: &Document,
1209 page_id: lopdf::ObjectId,
1210 caches: &mut DocCaches,
1211) -> Vec<Glyph> {
1212 let mut out = Vec::new();
1213 let content_bytes = doc.get_page_content(page_id);
1216 let Ok(content) = lopdf::content::Content::decode(&content_bytes) else {
1217 return out;
1218 };
1219 if let Some(res) = page_res(doc, page_id) {
1220 run_content(
1221 doc,
1222 res,
1223 &content,
1224 Mat::ID,
1225 TextState::INIT,
1226 0,
1227 caches,
1228 &mut out,
1229 );
1230 }
1231 out
1232}
1233
1234#[allow(clippy::too_many_arguments)]
1239fn run_content(
1240 doc: &Document,
1241 res: &Dictionary,
1242 content: &lopdf::content::Content,
1243 base_ctm: Mat,
1244 init: TextState,
1245 depth: u32,
1246 caches: &mut DocCaches,
1247 out: &mut Vec<Glyph>,
1248) {
1249 let fonts = fonts_from_res(doc, res, caches);
1250 let xobjects = res
1251 .get(b"XObject")
1252 .ok()
1253 .and_then(|o| deref(doc, o))
1254 .and_then(|o| o.as_dict().ok());
1255
1256 #[allow(clippy::type_complexity)]
1261 let mut gstate_stack: Vec<(Mat, f64, f64, f64, f64, f64, f64, Option<&Rc<Font>>)> = Vec::new();
1262 let mut ctm = base_ctm;
1263 let mut tm = Mat::ID;
1264 let mut tlm = Mat::ID;
1265 let mut font: Option<&Rc<Font>> = None;
1266 let mut fsize = init.fsize;
1267 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;
1272
1273 let op_f = |operands: &[Object], i: usize| operands.get(i).and_then(num).unwrap_or(0.0);
1274
1275 for op in &content.operations {
1276 let operands = &op.operands;
1277 match op.operator.as_str() {
1278 "q" => gstate_stack.push((ctm, tc, tw, th, tl, trise, fsize, font)),
1279 "Q" => {
1280 if let Some((c, a, b, h, l, r, fs, f)) = gstate_stack.pop() {
1281 ctm = c;
1282 tc = a;
1283 tw = b;
1284 th = h;
1285 tl = l;
1286 trise = r;
1287 fsize = fs;
1288 font = f;
1289 }
1290 }
1291 "cm" => {
1292 let m = Mat {
1293 a: op_f(operands, 0),
1294 b: op_f(operands, 1),
1295 c: op_f(operands, 2),
1296 d: op_f(operands, 3),
1297 e: op_f(operands, 4),
1298 f: op_f(operands, 5),
1299 };
1300 ctm = m.then(ctm);
1301 }
1302 "BT" => {
1303 tm = Mat::ID;
1304 tlm = Mat::ID;
1305 }
1306 "ET" => {}
1307 "Tf" => {
1308 if let Some(Object::Name(n)) = operands.first() {
1309 font = fonts.get(n.as_slice());
1310 }
1311 fsize = op_f(operands, 1);
1312 }
1313 "Td" => {
1314 tlm = Mat {
1315 a: 1.0,
1316 b: 0.0,
1317 c: 0.0,
1318 d: 1.0,
1319 e: op_f(operands, 0),
1320 f: op_f(operands, 1),
1321 }
1322 .then(tlm);
1323 tm = tlm;
1324 }
1325 "TD" => {
1326 tl = -op_f(operands, 1);
1327 tlm = Mat {
1328 a: 1.0,
1329 b: 0.0,
1330 c: 0.0,
1331 d: 1.0,
1332 e: op_f(operands, 0),
1333 f: op_f(operands, 1),
1334 }
1335 .then(tlm);
1336 tm = tlm;
1337 }
1338 "Tm" => {
1339 tlm = Mat {
1340 a: op_f(operands, 0),
1341 b: op_f(operands, 1),
1342 c: op_f(operands, 2),
1343 d: op_f(operands, 3),
1344 e: op_f(operands, 4),
1345 f: op_f(operands, 5),
1346 };
1347 tm = tlm;
1348 }
1349 "T*" => {
1350 tlm = Mat {
1351 a: 1.0,
1352 b: 0.0,
1353 c: 0.0,
1354 d: 1.0,
1355 e: 0.0,
1356 f: -tl,
1357 }
1358 .then(tlm);
1359 tm = tlm;
1360 }
1361 "Tc" => tc = op_f(operands, 0),
1362 "Tw" => tw = op_f(operands, 0),
1363 "Tz" => th = op_f(operands, 0) / 100.0,
1364 "TL" => tl = op_f(operands, 0),
1365 "Ts" => trise = op_f(operands, 0),
1366 "Tj" | "'" | "\"" => {
1367 if op.operator == "'" || op.operator == "\"" {
1368 tlm = Mat {
1370 a: 1.0,
1371 b: 0.0,
1372 c: 0.0,
1373 d: 1.0,
1374 e: 0.0,
1375 f: -tl,
1376 }
1377 .then(tlm);
1378 tm = tlm;
1379 }
1380 if op.operator == "\"" {
1381 tw = op_f(operands, 0);
1384 tc = op_f(operands, 1);
1385 }
1386 if let (Some(f), Some(Object::String(s, _))) = (font, operands.last()) {
1387 show_text(f, s, fsize, tc, tw, th, trise, &mut tm, ctm, out);
1388 }
1389 }
1390 "TJ" => {
1391 if let (Some(f), Some(Object::Array(arr))) = (font, operands.first()) {
1392 for el in arr {
1393 match el {
1394 Object::String(s, _) => {
1395 show_text(f, s, fsize, tc, tw, th, trise, &mut tm, ctm, out)
1396 }
1397 other => {
1398 if let Some(adj) = num(other) {
1399 let tx = -adj / 1000.0 * fsize * th;
1401 tm = Mat {
1402 a: 1.0,
1403 b: 0.0,
1404 c: 0.0,
1405 d: 1.0,
1406 e: tx,
1407 f: 0.0,
1408 }
1409 .then(tm);
1410 }
1411 }
1412 }
1413 }
1414 }
1415 }
1416 "Do" => {
1417 if depth >= 8 {
1420 continue;
1421 }
1422 let Some(Object::Name(n)) = operands.first() else {
1423 continue;
1424 };
1425 let obj = xobjects.and_then(|d| d.get(n.as_slice()).ok());
1426 let form_id = match obj {
1427 Some(Object::Reference(id)) => Some(*id),
1428 _ => None,
1429 };
1430 let stream = obj
1431 .and_then(|o| deref(doc, o))
1432 .and_then(|o| o.as_stream().ok());
1433 let Some(stream) = stream else { continue };
1434 let is_form = stream
1435 .dict
1436 .get(b"Subtype")
1437 .ok()
1438 .and_then(|o| o.as_name().ok())
1439 == Some(b"Form".as_slice());
1440 if !is_form {
1441 continue;
1442 }
1443 let cached = form_id.and_then(|id| caches.forms.get(&id).cloned());
1446 let form_content = match cached {
1447 Some(c) => c,
1448 None => {
1449 let Ok(data) = stream.decompressed_content() else {
1450 continue;
1451 };
1452 let Ok(c) = lopdf::content::Content::decode(&data) else {
1453 continue;
1454 };
1455 let c = Rc::new(c);
1456 if let Some(id) = form_id {
1457 caches.forms.insert(id, Rc::clone(&c));
1458 }
1459 c
1460 }
1461 };
1462 let form_mat = match stream.dict.get(b"Matrix").ok() {
1464 Some(Object::Array(a)) if a.len() == 6 => {
1465 let v: Vec<f64> = a.iter().filter_map(num).collect();
1466 if v.len() == 6 {
1467 Mat {
1468 a: v[0],
1469 b: v[1],
1470 c: v[2],
1471 d: v[3],
1472 e: v[4],
1473 f: v[5],
1474 }
1475 } else {
1476 Mat::ID
1477 }
1478 }
1479 _ => Mat::ID,
1480 };
1481 let form_res = stream
1483 .dict
1484 .get(b"Resources")
1485 .ok()
1486 .and_then(|o| deref(doc, o))
1487 .and_then(|o| o.as_dict().ok())
1488 .unwrap_or(res);
1489 let state = TextState {
1490 tc,
1491 tw,
1492 th,
1493 tl,
1494 trise,
1495 fsize,
1496 };
1497 run_content(
1498 doc,
1499 form_res,
1500 &form_content,
1501 form_mat.then(ctm),
1502 state,
1503 depth + 1,
1504 caches,
1505 out,
1506 );
1507 }
1508 _ => {}
1509 }
1510 }
1511}
1512
1513#[allow(clippy::too_many_arguments)]
1514fn show_text(
1515 font: &Font,
1516 bytes: &[u8],
1517 fsize: f64,
1518 tc: f64,
1519 tw: f64,
1520 th: f64,
1521 trise: f64,
1522 tm: &mut Mat,
1523 ctm: Mat,
1524 out: &mut Vec<Glyph>,
1525) {
1526 for code in codes(font, bytes) {
1527 let (text, w) = font.decode_code(code);
1528 let w0 = w / 1000.0; let scale = Mat {
1531 a: fsize * th,
1532 b: 0.0,
1533 c: 0.0,
1534 d: fsize,
1535 e: 0.0,
1536 f: trise,
1537 };
1538 let trm = scale.then(*tm).then(ctm);
1539 let (x0, y0) = trm.apply(0.0, font.descent / 1000.0);
1541 let (x1, _y1) = trm.apply(w0, font.descent / 1000.0);
1542 let (_x2, y2) = trm.apply(0.0, font.ascent / 1000.0);
1543 let (left, right) = (x0.min(x1), x0.max(x1));
1544 let (bot, top) = (y0.min(y2), y0.max(y2));
1545 if let Some(s) = text {
1546 for ch in s.chars() {
1548 if ch != '\u{0}' {
1549 out.push(Glyph {
1550 ch,
1551 l: left as f32,
1552 b: bot as f32,
1553 r: right as f32,
1554 t: top as f32,
1555 ll: left as f32,
1556 lb: bot as f32,
1557 lr: right as f32,
1558 lt: top as f32,
1559 font: font.hash,
1560 });
1561 }
1562 }
1563 }
1564 let is_space = !font.two_byte && code == 32;
1566 let tx = (w0 * fsize + tc + if is_space { tw } else { 0.0 }) * th;
1567 *tm = Mat {
1568 a: 1.0,
1569 b: 0.0,
1570 c: 0.0,
1571 d: 1.0,
1572 e: tx,
1573 f: 0.0,
1574 }
1575 .then(*tm);
1576 }
1577}
1578
1579fn simple_encoding_table(doc: &Document, fdict: &Dictionary) -> HashMap<u8, char> {
1583 let enc = fdict.get(b"Encoding").ok().and_then(|o| deref(doc, o));
1584 let base_name = match enc {
1585 Some(Object::Name(n)) => n.clone(),
1586 Some(Object::Dictionary(d)) => d
1587 .get(b"BaseEncoding")
1588 .ok()
1589 .and_then(|o| o.as_name().ok())
1590 .map(|n| n.to_vec())
1591 .unwrap_or_default(),
1592 _ => Vec::new(),
1593 };
1594 let mut m = if base_name == b"MacRomanEncoding" {
1595 macroman_table()
1596 } else {
1597 winansi_table()
1598 };
1599 if let Some(Object::Dictionary(d)) = enc {
1601 if let Some(Object::Array(diffs)) = d.get(b"Differences").ok().and_then(|o| deref(doc, o)) {
1602 let mut code = 0u8;
1603 for el in diffs {
1604 match el {
1605 Object::Integer(i) => code = *i as u8,
1606 Object::Name(name) => {
1607 if let Some(ch) = glyph_name_to_char(name) {
1608 m.insert(code, ch);
1609 }
1610 code = code.wrapping_add(1);
1611 }
1612 _ => {}
1613 }
1614 }
1615 }
1616 }
1617 m
1618}
1619
1620fn glyph_name_to_char(name: &[u8]) -> Option<char> {
1625 let s = std::str::from_utf8(name).ok()?;
1626 if let Some(hex) = s.strip_prefix("uni") {
1627 if let Ok(cp) = u32::from_str_radix(hex.get(0..4)?, 16) {
1628 return char::from_u32(cp);
1629 }
1630 }
1631 if s.len() == 1 {
1633 let b = s.as_bytes()[0];
1634 if b.is_ascii_alphabetic() {
1635 return Some(b as char);
1636 }
1637 }
1638 let resolved = match s {
1639 "space" => ' ',
1640 "exclam" => '!',
1641 "quotedbl" => '"',
1642 "numbersign" => '#',
1643 "dollar" => '$',
1644 "percent" => '%',
1645 "ampersand" => '&',
1646 "quotesingle" => '\'',
1647 "parenleft" => '(',
1648 "parenright" => ')',
1649 "asterisk" => '*',
1650 "plus" => '+',
1651 "comma" => ',',
1652 "hyphen" => '-',
1653 "period" => '.',
1654 "slash" => '/',
1655 "zero" => '0',
1656 "one" => '1',
1657 "two" => '2',
1658 "three" => '3',
1659 "four" => '4',
1660 "five" => '5',
1661 "six" => '6',
1662 "seven" => '7',
1663 "eight" => '8',
1664 "nine" => '9',
1665 "colon" => ':',
1666 "semicolon" => ';',
1667 "less" => '<',
1668 "equal" => '=',
1669 "greater" => '>',
1670 "question" => '?',
1671 "at" => '@',
1672 "bracketleft" => '[',
1673 "backslash" => '\\',
1674 "bracketright" => ']',
1675 "asciicircum" => '^',
1676 "underscore" => '_',
1677 "grave" => '`',
1678 "braceleft" => '{',
1679 "bar" => '|',
1680 "braceright" => '}',
1681 "asciitilde" => '~',
1682 "bullet" => '\u{2022}',
1683 "periodcentered" => '\u{00B7}',
1684 "endash" => '\u{2013}',
1685 "emdash" => '\u{2014}',
1686 "quoteright" => '\u{2019}',
1687 "quoteleft" => '\u{2018}',
1688 "quotedblleft" => '\u{201C}',
1689 "quotedblright" => '\u{201D}',
1690 "quotedblbase" => '\u{201E}',
1691 "quotesinglbase" => '\u{201A}',
1692 "ff" => '\u{FB00}',
1697 "fi" => '\u{FB01}',
1698 "fl" => '\u{FB02}',
1699 "ffi" => '\u{FB03}',
1700 "ffl" => '\u{FB04}',
1701 "ft" => '\u{FB05}',
1702 "st" => '\u{FB06}',
1703 "degree" => '\u{00B0}',
1704 "trademark" => '\u{2122}',
1705 "registered" => '\u{00AE}',
1706 "copyright" => '\u{00A9}',
1707 "ellipsis" => '\u{2026}',
1708 "minus" => '\u{2212}',
1709 "fraction" => '\u{2044}',
1710 "nbspace" => '\u{00A0}',
1711 "alpha" => '\u{03B1}',
1716 "beta" => '\u{03B2}',
1717 "gamma" => '\u{03B3}',
1718 "delta" => '\u{03B4}',
1719 "epsilon" | "epsilon1" => '\u{03B5}',
1720 "zeta" => '\u{03B6}',
1721 "eta" => '\u{03B7}',
1722 "theta" | "theta1" => '\u{03B8}',
1723 "iota" => '\u{03B9}',
1724 "kappa" => '\u{03BA}',
1725 "lambda" => '\u{03BB}',
1726 "mu" => '\u{03BC}',
1727 "nu" => '\u{03BD}',
1728 "xi" => '\u{03BE}',
1729 "omicron" => '\u{03BF}',
1730 "pi" | "pi1" => '\u{03C0}',
1731 "rho" | "rho1" => '\u{03C1}',
1732 "sigma" => '\u{03C3}',
1733 "sigma1" => '\u{03C2}',
1734 "tau" => '\u{03C4}',
1735 "upsilon" => '\u{03C5}',
1736 "phi" | "phi1" => '\u{03C6}',
1737 "chi" => '\u{03C7}',
1738 "psi" => '\u{03C8}',
1739 "omega" | "omega1" => '\u{03C9}',
1740 "Gamma" => '\u{0393}',
1741 "Delta" => '\u{0394}',
1742 "Theta" => '\u{0398}',
1743 "Lambda" => '\u{039B}',
1744 "Xi" => '\u{039E}',
1745 "Pi" => '\u{03A0}',
1746 "Sigma" => '\u{03A3}',
1747 "Upsilon" => '\u{03A5}',
1748 "Phi" => '\u{03A6}',
1749 "Psi" => '\u{03A8}',
1750 "Omega" => '\u{03A9}',
1751 "lessequal" => '\u{2264}',
1752 "greaterequal" => '\u{2265}',
1753 "notequal" => '\u{2260}',
1754 "approxequal" => '\u{2248}',
1755 "equivalence" => '\u{2261}',
1756 "element" => '\u{2208}',
1757 "plusminus" => '\u{00B1}',
1758 "multiply" => '\u{00D7}',
1759 "divide" => '\u{00F7}',
1760 "infinity" => '\u{221E}',
1761 "partialdiff" => '\u{2202}',
1762 "gradient" => '\u{2207}',
1763 "summation" => '\u{2211}',
1764 "product" => '\u{220F}',
1765 "integral" => '\u{222B}',
1766 "radical" => '\u{221A}',
1767 "proportional" => '\u{221D}',
1768 "arrowright" => '\u{2192}',
1769 "arrowleft" => '\u{2190}',
1770 "arrowup" => '\u{2191}',
1771 "arrowdown" => '\u{2193}',
1772 "arrowboth" => '\u{2194}',
1773 "arrowdblright" => '\u{21D2}',
1774 "logicaland" => '\u{2227}',
1775 "logicalor" => '\u{2228}',
1776 "intersection" => '\u{2229}',
1777 "union" => '\u{222A}',
1778 "similar" => '\u{223C}',
1779 "congruent" => '\u{2245}',
1780 "dotmath" => '\u{22C5}',
1781 "asteriskmath" => '\u{2217}',
1782 _ => {
1783 if let Some((base, _)) = s.split_once('.') {
1785 if !base.is_empty() {
1786 return glyph_name_to_char(base.as_bytes());
1787 }
1788 }
1789 return None;
1790 }
1791 };
1792 Some(resolved)
1793}
1794
1795fn winansi_table() -> HashMap<u8, char> {
1797 let mut m = HashMap::new();
1798 for b in 0x20u8..=0x7e {
1799 m.insert(b, b as char);
1800 }
1801 let extra: &[(u8, char)] = &[
1803 (0x91, '\u{2018}'),
1804 (0x92, '\u{2019}'),
1805 (0x93, '\u{201C}'),
1806 (0x94, '\u{201D}'),
1807 (0x95, '\u{2022}'),
1808 (0x96, '\u{2013}'),
1809 (0x97, '\u{2014}'),
1810 (0x85, '\u{2026}'),
1811 (0xA0, '\u{00A0}'),
1812 ];
1813 for &(b, c) in extra {
1814 m.insert(b, c);
1815 }
1816 for b in 0xA1u8..=0xFF {
1817 m.entry(b).or_insert(b as char);
1818 }
1819 m
1820}
1821
1822fn macroman_table() -> HashMap<u8, char> {
1825 let mut m = HashMap::new();
1826 for b in 0x20u8..=0x7e {
1827 m.insert(b, b as char);
1828 }
1829 let high: &[(u8, char)] = &[
1830 (0xA5, '\u{2022}'), (0xD0, '\u{2013}'), (0xD1, '\u{2014}'), (0xD2, '\u{201C}'),
1834 (0xD3, '\u{201D}'),
1835 (0xD4, '\u{2018}'),
1836 (0xD5, '\u{2019}'),
1837 (0xCA, '\u{00A0}'),
1838 (0xC9, '\u{2026}'),
1839 (0xDE, '\u{FB01}'),
1840 (0xDF, '\u{FB02}'),
1841 ];
1842 for &(b, c) in high {
1843 m.insert(b, c);
1844 }
1845 m
1846}
1847
1848#[cfg(test)]
1849mod xref_repair {
1850 fn pdf_with_xref(two_byte_eol: bool) -> Vec<u8> {
1854 let content = b"BT /F1 12 Tf 72 700 Td (Invoice 922769430725) Tj ET\n";
1855 let stream = format!("<</Length {}>>stream\n", content.len()).into_bytes();
1856 let objs: Vec<Vec<u8>> = vec![
1857 b"<</Type/Catalog/Pages 2 0 R>>".to_vec(),
1858 b"<</Type/Pages/Kids[3 0 R]/Count 1>>".to_vec(),
1859 b"<</Type/Page/Parent 2 0 R/MediaBox[0 0 595 842]/Contents 4 0 R\
1860 /Resources<</Font<</F1 5 0 R>>>>>>"
1861 .to_vec(),
1862 [stream.as_slice(), content.as_slice(), b"endstream"].concat(),
1863 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>".to_vec(),
1864 ];
1865
1866 let mut out = b"%PDF-1.4\n".to_vec();
1867 let mut offsets = Vec::new();
1868 for (i, body) in objs.iter().enumerate() {
1869 offsets.push(out.len());
1870 out.extend_from_slice(format!("{} 0 obj", i + 1).as_bytes());
1871 out.extend_from_slice(body);
1872 out.extend_from_slice(b"endobj\n");
1873 }
1874 let xref_at = out.len();
1875 let eol: &[u8] = if two_byte_eol { b" \n" } else { b"\n" };
1876 out.extend_from_slice(format!("xref\n0 {}\n", objs.len() + 1).as_bytes());
1877 out.extend_from_slice(b"0000000000 65535 f");
1878 out.extend_from_slice(eol);
1879 for off in &offsets {
1880 out.extend_from_slice(format!("{off:010} 00000 n").as_bytes());
1881 out.extend_from_slice(eol);
1882 }
1883 out.extend_from_slice(
1884 format!("trailer<</Size {}/Root 1 0 R>>\n", objs.len() + 1).as_bytes(),
1885 );
1886 out.extend_from_slice(format!("startxref\n{xref_at}\n%%EOF\n").as_bytes());
1887 out
1888 }
1889
1890 #[test]
1896 fn short_xref_entries_still_parse() {
1897 let good = pdf_with_xref(true);
1898 let broken = pdf_with_xref(false);
1899 assert!(
1900 broken.len() < good.len(),
1901 "the broken file is the shorter one"
1902 );
1903 assert!(
1904 lopdf::Document::load_mem(&good).is_ok(),
1905 "the control file must load unaided"
1906 );
1907 assert!(
1908 lopdf::Document::load_mem(&broken).is_err(),
1909 "lopdf rejects 19-byte entries — if this ever passes, drop the repair"
1910 );
1911
1912 let cells = |b: &[u8]| -> Vec<String> {
1913 super::pdf_textlines(b)
1914 .into_iter()
1915 .flat_map(|(_, _, c)| c.into_iter().map(|c| c.text))
1916 .collect()
1917 };
1918 let from_good = cells(&good);
1919 assert!(
1920 from_good.iter().any(|t| t.contains("922769430725")),
1921 "control text: {from_good:?}"
1922 );
1923 assert_eq!(
1924 cells(&broken),
1925 from_good,
1926 "repair must match the good parse"
1927 );
1928 }
1929
1930 #[test]
1935 fn overstated_stream_length_still_yields_content() {
1936 let good = pdf_with_xref(true);
1937 let broken = {
1940 let at = good
1941 .windows(8)
1942 .position(|w| w == b"/Length ")
1943 .expect("a /Length")
1944 + 8;
1945 let digits = good[at..].iter().take_while(|c| c.is_ascii_digit()).count();
1946 let n: usize = std::str::from_utf8(&good[at..at + digits])
1947 .unwrap()
1948 .parse()
1949 .unwrap();
1950 let inflated = (n + 1).to_string();
1951 assert_eq!(inflated.len(), digits, "keep the digit count");
1952 let mut b = good.clone();
1953 b[at..at + digits].copy_from_slice(inflated.as_bytes());
1954 b
1955 };
1956 assert_eq!(broken.len(), good.len(), "the defect must not move bytes");
1957 let raw = lopdf::Document::load_mem(&broken).expect("still loads");
1959 assert!(
1960 raw.get_pages()
1961 .into_values()
1962 .all(|p| raw.get_page_content(p).is_empty()),
1963 "lopdf should drop the stream — if it stops, drop this repair"
1964 );
1965 let text = |b: &[u8]| -> Vec<String> {
1967 super::pdf_textlines(b)
1968 .into_iter()
1969 .flat_map(|(_, _, c)| c.into_iter().map(|c| c.text))
1970 .collect()
1971 };
1972 let expected = text(&good);
1973 assert!(!expected.is_empty(), "control must produce text");
1974 assert_eq!(text(&broken), expected);
1975 }
1976
1977 #[test]
1981 fn repair_declines_when_padding_would_move_objects() {
1982 let mut incremental = pdf_with_xref(false);
1983 incremental.extend_from_slice(b"6 0 obj<</Type/Whatever>>endobj\n");
1984 let declined = super::pad_short_xref_entries(&incremental).unwrap_err();
1985 assert!(
1986 declined.contains("object follows the xref"),
1987 "reason: {declined}"
1988 );
1989 }
1990}
1991
1992#[cfg(test)]
1998mod base14_fonts {
1999 fn pdf_with_font(fontdict: &[u8], text: &[u8]) -> Vec<u8> {
2001 let content = [b"BT /F1 12 Tf 72 700 Td (".as_slice(), text, b") Tj ET\n"].concat();
2002 let stream = format!("<</Length {}>>stream\n", content.len()).into_bytes();
2003 let objs: Vec<Vec<u8>> = vec![
2004 b"<</Type/Catalog/Pages 2 0 R>>".to_vec(),
2005 b"<</Type/Pages/Kids[3 0 R]/Count 1>>".to_vec(),
2006 b"<</Type/Page/Parent 2 0 R/MediaBox[0 0 595 842]/Contents 4 0 R\
2007 /Resources<</Font<</F1 5 0 R>>>>>>"
2008 .to_vec(),
2009 [stream.as_slice(), content.as_slice(), b"endstream"].concat(),
2010 fontdict.to_vec(),
2011 ];
2012 let mut out = b"%PDF-1.4\n".to_vec();
2013 let mut offsets = Vec::new();
2014 for (i, body) in objs.iter().enumerate() {
2015 offsets.push(out.len());
2016 out.extend_from_slice(format!("{} 0 obj", i + 1).as_bytes());
2017 out.extend_from_slice(body);
2018 out.extend_from_slice(b"endobj\n");
2019 }
2020 let xref_at = out.len();
2021 out.extend_from_slice(format!("xref\n0 {}\n", objs.len() + 1).as_bytes());
2022 out.extend_from_slice(b"0000000000 65535 f \n");
2023 for off in &offsets {
2024 out.extend_from_slice(format!("{off:010} 00000 n \n").as_bytes());
2025 }
2026 out.extend_from_slice(
2027 format!("trailer<</Size {}/Root 1 0 R>>\n", objs.len() + 1).as_bytes(),
2028 );
2029 out.extend_from_slice(format!("startxref\n{xref_at}\n%%EOF\n").as_bytes());
2030 out
2031 }
2032
2033 fn cells(pdf: &[u8]) -> Vec<crate::pdfium_backend::TextCell> {
2035 super::pdf_textlines(pdf)
2036 .into_iter()
2037 .flat_map(|(_, _, c)| c)
2038 .collect()
2039 }
2040
2041 #[test]
2043 fn standard14_faces_get_builtin_widths() {
2044 for fontdict in [
2045 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/WinAnsiEncoding>>".as_slice(),
2047 b"<</Type/Font/Subtype/Type1/BaseFont/Times-BoldItalic>>",
2049 b"<</Type/Font/Subtype/TrueType/BaseFont/Arial,Bold>>",
2051 b"<</Type/Font/Subtype/Type1/BaseFont/ABCDEF+Courier-Oblique>>",
2052 ] {
2053 let pdf = pdf_with_font(fontdict, b"Words have width now");
2054 let cs = cells(&pdf);
2055 let text: String = cs
2056 .iter()
2057 .map(|c| c.text.as_str())
2058 .collect::<Vec<_>>()
2059 .join(" ");
2060 assert!(
2061 text.contains("Words have width now"),
2062 "{}: text lost: {text:?}",
2063 String::from_utf8_lossy(fontdict)
2064 );
2065 assert!(
2066 cs.iter().all(|c| c.r > c.l),
2067 "{}: zero-width cells: {cs:?}",
2068 String::from_utf8_lossy(fontdict)
2069 );
2070 }
2071 }
2072
2073 #[test]
2076 fn explicit_widths_win_and_unknown_faces_are_untouched() {
2077 let explicit = pdf_with_font(
2081 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/FirstChar 65\
2082 /Widths[100 100 100 100]/Encoding/WinAnsiEncoding>>",
2083 b"ABBA",
2084 );
2085 let builtin = pdf_with_font(
2086 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/WinAnsiEncoding>>",
2087 b"ABBA",
2088 );
2089 let w = |pdf: &[u8]| {
2090 let cs = cells(pdf);
2091 assert_eq!(cs.len(), 1, "one word cell: {cs:?}");
2092 cs[0].r - cs[0].l
2093 };
2094 let (we, wb) = (w(&explicit), w(&builtin));
2095 assert!(
2096 (we - 4.8).abs() < 0.1,
2097 "explicit widths must win: got {we}, want 4×100×12/1000"
2098 );
2099 assert!(
2100 wb > 2.0 * we,
2101 "built-in Helvetica is much wider: {wb} vs {we}"
2102 );
2103
2104 let unknown = pdf_with_font(
2108 b"<</Type/Font/Subtype/Type1/BaseFont/FancyCorp-Display>>",
2109 b"Mystery",
2110 );
2111 let cs = cells(&unknown);
2112 let text: String = cs.iter().map(|c| c.text.as_str()).collect();
2113 assert!(text.contains("Mystery"), "text still decodes: {cs:?}");
2114 }
2115}
2116
2117#[cfg(test)]
2118mod overpainted {
2119 use crate::pdfium_backend::TextCell;
2120
2121 fn cell(text: &str, l: f32, t: f32, r: f32, b: f32) -> TextCell {
2122 TextCell {
2123 text: text.into(),
2124 l,
2125 t,
2126 r,
2127 b,
2128 }
2129 }
2130
2131 #[test]
2135 fn stacked_logo_glyphs_are_dropped() {
2136 let mut cells = vec![
2137 cell("\"", 72.7, 21.5, 86.4, 31.5),
2138 cell("==", 59.4, 21.5, 99.6, 31.5),
2139 cell("Herr", 65.2, 151.3, 81.7, 161.3),
2140 ];
2141 super::drop_overpainted_cells(&mut cells);
2142 assert_eq!(cells.len(), 1, "cells: {cells:?}");
2143 assert_eq!(cells[0].text, "Herr");
2144 }
2145
2146 #[test]
2150 fn prose_and_double_draw_are_kept() {
2151 let mut cells = vec![
2152 cell("Telefon", 354.3, 133.2, 381.5, 143.2),
2153 cell("0676/2000", 387.3, 133.2, 428.7, 143.2),
2154 cell("Bold", 100.0, 50.0, 130.0, 60.0),
2155 cell("Bold", 100.3, 50.0, 130.3, 60.0),
2156 ];
2157 super::drop_overpainted_cells(&mut cells);
2158 assert_eq!(cells.len(), 4);
2159 }
2160}
2161
2162#[cfg(test)]
2163mod vestigial_layer {
2164 use crate::pdfium_backend::{PdfPage, TextCell};
2165
2166 fn page_with(texts: &[&str]) -> PdfPage {
2167 let cells = texts
2168 .iter()
2169 .enumerate()
2170 .map(|(i, t)| TextCell {
2171 text: t.to_string(),
2172 l: 10.0,
2173 t: 10.0 + 12.0 * i as f32,
2174 r: 90.0,
2175 b: 20.0 + 12.0 * i as f32,
2176 })
2177 .collect();
2178 PdfPage::from_cells(595.0, 842.0, 1.0, cells)
2179 }
2180
2181 #[test]
2186 fn typed_in_form_fields_are_not_a_text_layer() {
2187 let pages = vec![
2188 page_with(&["03", "05", "2025"]),
2189 page_with(&[]),
2190 page_with(&[]),
2191 ];
2192 assert!(super::text_layer_is_vestigial(&pages));
2193 assert!(super::text_layer_is_vestigial(&[page_with(&[])]));
2194 }
2195
2196 #[test]
2199 fn sparse_but_real_documents_pass() {
2200 let one_pager = vec![page_with(&[
2201 "Confidential briefing",
2202 "Prepared for the board meeting",
2203 "Do not distribute",
2204 ])];
2205 assert!(!super::text_layer_is_vestigial(&one_pager));
2206 }
2207}