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 (widths, 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 let fallback_names = if two_byte {
215 HashMap::new()
216 } else {
217 differences_gid_names(doc, fdict)
218 };
219 let program_encoding = if two_byte {
220 HashMap::new()
221 } else {
222 type1_program_encoding(doc, fdict)
223 };
224
225 let (ascent, descent) = font_ascent_descent(doc, fdict, two_byte);
226
227 Font {
228 two_byte,
229 to_unicode,
230 widths,
231 default_width,
232 simple_encoding,
233 fallback_names,
234 program_encoding,
235 ascent,
236 descent,
237 hash: hash_name(name),
238 }
239}
240
241fn differences_gid_names(doc: &Document, fdict: &Dictionary) -> HashMap<u8, String> {
248 let mut map = HashMap::new();
249 let Some(Object::Dictionary(enc)) = fdict.get(b"Encoding").ok().and_then(|o| deref(doc, o))
250 else {
251 return map;
252 };
253 let Some(Object::Array(diffs)) = enc.get(b"Differences").ok().and_then(|o| deref(doc, o))
254 else {
255 return map;
256 };
257 let mut code = 0u8;
258 for el in diffs {
259 match el {
260 Object::Integer(i) => code = *i as u8,
261 Object::Name(name) => {
262 if glyph_name_to_char(name).is_none() && is_gid_name(name) {
263 map.insert(code, String::from_utf8_lossy(name).into_owned());
264 }
265 code = code.wrapping_add(1);
266 }
267 _ => {}
268 }
269 }
270 map
271}
272
273fn type1_program_encoding(doc: &Document, fdict: &Dictionary) -> HashMap<u8, char> {
281 let mut map = HashMap::new();
282 let Some(desc) = fdict
283 .get(b"FontDescriptor")
284 .ok()
285 .and_then(|o| deref(doc, o))
286 .and_then(|o| o.as_dict().ok())
287 else {
288 return map;
289 };
290 let Some(data) = desc
291 .get(b"FontFile")
292 .ok()
293 .and_then(|o| deref(doc, o))
294 .and_then(|o| o.as_stream().ok())
295 .and_then(|s| s.decompressed_content().ok())
296 else {
297 return map;
298 };
299 let head_end = data
301 .windows(5)
302 .position(|w| w == b"eexec")
303 .unwrap_or(data.len());
304 let head = String::from_utf8_lossy(&data[..head_end]);
305 let toks: Vec<&str> = head.split_whitespace().collect();
307 for w in toks.windows(4) {
308 if w[0] == "dup" && w[3] == "put" {
309 if let (Ok(code), Some(name)) = (w[1].parse::<u32>(), w[2].strip_prefix('/')) {
310 if code <= 255 {
311 if let Some(ch) = glyph_name_to_char(name.as_bytes()) {
312 map.insert(code as u8, ch);
313 }
314 }
315 }
316 }
317 }
318 map
319}
320
321fn is_gid_name(name: &[u8]) -> bool {
327 let Ok(s) = std::str::from_utf8(name) else {
328 return false;
329 };
330 if s.starts_with("afii") || s.starts_with("uni") {
331 return false;
332 }
333 for prefix in ["g", "G", "cid", "CID", "glyph", "index"] {
334 if let Some(rest) = s.strip_prefix(prefix) {
335 if !rest.is_empty() && rest.bytes().all(|b| b.is_ascii_digit()) {
336 return true;
337 }
338 }
339 }
340 let alpha = s.bytes().take_while(|b| b.is_ascii_alphabetic()).count();
344 let digits = s.len() - alpha;
345 (1..=3).contains(&alpha)
346 && digits >= 3
347 && s.as_bytes()[alpha..].iter().all(|b| b.is_ascii_digit())
348}
349
350fn font_ascent_descent(doc: &Document, fdict: &Dictionary, two_byte: bool) -> (f64, f64) {
351 let descr_owner = if two_byte {
353 fdict
354 .get(b"DescendantFonts")
355 .ok()
356 .and_then(|o| deref(doc, o))
357 .and_then(|o| match o {
358 Object::Array(a) => a.first(),
359 _ => None,
360 })
361 .and_then(|o| as_dict(doc, o))
362 } else {
363 Some(fdict)
364 };
365 let fd = descr_owner
366 .and_then(|d| d.get(b"FontDescriptor").ok())
367 .and_then(|o| as_dict(doc, o));
368 let asc = fd
369 .and_then(|d| d.get(b"Ascent").ok())
370 .and_then(|o| {
371 o.as_float()
372 .ok()
373 .or_else(|| o.as_i64().ok().map(|i| i as f32))
374 })
375 .unwrap_or(750.0) as f64;
376 let desc = fd
377 .and_then(|d| d.get(b"Descent").ok())
378 .and_then(|o| {
379 o.as_float()
380 .ok()
381 .or_else(|| o.as_i64().ok().map(|i| i as f32))
382 })
383 .unwrap_or(-250.0) as f64;
384 if asc - desc <= 1.0 {
391 return (750.0, -250.0);
392 }
393 (asc, desc)
394}
395
396fn simple_widths(doc: &Document, fdict: &Dictionary) -> (HashMap<u32, f64>, f64) {
398 let mut map = HashMap::new();
399 let first = fdict
400 .get(b"FirstChar")
401 .ok()
402 .and_then(|o| o.as_i64().ok())
403 .unwrap_or(0) as u32;
404 if let Some(Object::Array(arr)) = fdict.get(b"Widths").ok().and_then(|o| deref(doc, o)) {
405 for (i, w) in arr.iter().enumerate() {
406 if let Some(w) = num(w) {
407 map.insert(first + i as u32, w);
408 }
409 }
410 }
411 let dw = fdict
412 .get(b"FontDescriptor")
413 .ok()
414 .and_then(|o| as_dict(doc, o))
415 .and_then(|d| d.get(b"MissingWidth").ok())
416 .and_then(num)
417 .unwrap_or(0.0);
418 (map, dw)
419}
420
421fn cid_widths(doc: &Document, fdict: &Dictionary) -> (HashMap<u32, f64>, f64) {
423 let mut map = HashMap::new();
424 let Some(desc) = fdict
425 .get(b"DescendantFonts")
426 .ok()
427 .and_then(|o| deref(doc, o))
428 .and_then(|o| match o {
429 Object::Array(a) => a.first(),
430 _ => None,
431 })
432 .and_then(|o| as_dict(doc, o))
433 else {
434 return (map, 1000.0);
435 };
436 let dw = desc.get(b"DW").ok().and_then(num).unwrap_or(1000.0);
437 if let Some(Object::Array(w)) = desc.get(b"W").ok().and_then(|o| deref(doc, o)) {
438 let mut i = 0;
439 while i < w.len() {
440 let c = w.get(i).and_then(num);
441 match (c, w.get(i + 1)) {
442 (Some(c), Some(Object::Array(list))) => {
444 for (k, wv) in list.iter().enumerate() {
445 if let Some(wv) = num(wv) {
446 map.insert(c as u32 + k as u32, wv);
447 }
448 }
449 i += 2;
450 }
451 (Some(c1), Some(o2)) => {
453 if let (Some(c2), Some(wv)) = (num(o2), w.get(i + 2).and_then(num)) {
454 for cid in c1 as u32..=c2 as u32 {
455 map.insert(cid, wv);
456 }
457 }
458 i += 3;
459 }
460 _ => break,
461 }
462 }
463 }
464 (map, dw)
465}
466
467fn num(o: &Object) -> Option<f64> {
468 match o {
469 Object::Integer(i) => Some(*i as f64),
470 Object::Real(r) => Some(*r as f64),
471 _ => None,
472 }
473}
474
475fn parse_tounicode(data: &[u8]) -> HashMap<u32, String> {
477 let text = String::from_utf8_lossy(data);
478 let mut map = HashMap::new();
479 let hex = |s: &str| -> Option<Vec<u16>> {
480 let s = s.trim();
481 if !s.starts_with('<') || !s.ends_with('>') {
482 return None;
483 }
484 let h = &s[1..s.len() - 1];
485 let bytes: Vec<u8> = (0..h.len())
486 .step_by(2)
487 .filter_map(|i| u8::from_str_radix(h.get(i..i + 2)?, 16).ok())
488 .collect();
489 Some(
490 bytes
491 .chunks(2)
492 .map(|c| {
493 if c.len() == 2 {
494 u16::from_be_bytes([c[0], c[1]])
495 } else {
496 c[0] as u16
497 }
498 })
499 .collect(),
500 )
501 };
502 let u16s_to_string = |u: &[u16]| String::from_utf16_lossy(u);
503 let code_of = |u: &[u16]| u.iter().fold(0u32, |acc, &x| (acc << 16) | x as u32);
504
505 let tokens: Vec<String> = {
509 let bytes = text.as_bytes();
510 let mut toks = Vec::new();
511 let mut i = 0;
512 while i < bytes.len() {
513 let c = bytes[i];
514 if c.is_ascii_whitespace() {
515 i += 1;
516 } else if c == b'<' {
517 let start = i;
518 while i < bytes.len() && bytes[i] != b'>' {
519 i += 1;
520 }
521 i += 1; toks.push(String::from_utf8_lossy(&bytes[start..i.min(bytes.len())]).into_owned());
523 } else if c == b'[' || c == b']' {
524 toks.push((c as char).to_string());
525 i += 1;
526 } else {
527 let start = i;
528 while i < bytes.len()
529 && !bytes[i].is_ascii_whitespace()
530 && bytes[i] != b'<'
531 && bytes[i] != b'['
532 && bytes[i] != b']'
533 {
534 i += 1;
535 }
536 toks.push(String::from_utf8_lossy(&bytes[start..i]).into_owned());
537 }
538 }
539 toks
540 };
541 let tokens: Vec<&str> = tokens.iter().map(|s| s.as_str()).collect();
542 let mut i = 0;
543 while i < tokens.len() {
544 match tokens[i] {
545 "beginbfchar" => {
546 i += 1;
547 while i + 1 < tokens.len() && tokens[i] != "endbfchar" {
548 if let (Some(src), Some(dst)) = (hex(tokens[i]), hex(tokens[i + 1])) {
549 map.insert(code_of(&src), u16s_to_string(&dst));
550 }
551 i += 2;
552 }
553 }
554 "beginbfrange" => {
555 i += 1;
556 while i + 2 < tokens.len() && tokens[i] != "endbfrange" {
557 let (Some(lo), Some(hi)) = (hex(tokens[i]), hex(tokens[i + 1])) else {
558 i += 1;
559 continue;
560 };
561 let lo = code_of(&lo);
562 let hi = code_of(&hi);
563 if tokens[i + 2] == "[" {
564 let mut j = i + 3;
566 let mut code = lo;
567 while j < tokens.len() && tokens[j] != "]" {
568 if let Some(dst) = hex(tokens[j]) {
569 map.insert(code, u16s_to_string(&dst));
570 }
571 code += 1;
572 j += 1;
573 }
574 i = j + 1;
575 } else if let Some(dst) = hex(tokens[i + 2]) {
576 let base = code_of(&dst);
578 for (k, code) in (lo..=hi).enumerate() {
579 if let Some(ch) = char::from_u32(base + k as u32) {
580 map.insert(code, ch.to_string());
581 }
582 }
583 i += 3;
584 } else {
585 i += 1;
586 }
587 }
588 }
589 _ => i += 1,
590 }
591 }
592 map
593}
594
595fn codes(font: &Font, bytes: &[u8]) -> Vec<u32> {
597 if font.two_byte {
598 bytes
599 .chunks(2)
600 .map(|c| {
601 if c.len() == 2 {
602 ((c[0] as u32) << 8) | c[1] as u32
603 } else {
604 c[0] as u32
605 }
606 })
607 .collect()
608 } else {
609 bytes.iter().map(|&b| b as u32).collect()
610 }
611}
612
613fn page_size(doc: &Document, page_id: lopdf::ObjectId) -> (f32, f32) {
615 let mb = doc
616 .get_object(page_id)
617 .ok()
618 .and_then(|o| o.as_dict().ok())
619 .and_then(|d| {
620 d.get(b"MediaBox").ok().cloned()
622 })
623 .or_else(|| {
624 doc.get_dictionary(page_id)
625 .ok()
626 .and_then(|d| d.get(b"MediaBox").ok().cloned())
627 });
628 if let Some(Object::Array(a)) = mb {
629 let v: Vec<f32> = a.iter().filter_map(|o| num(o).map(|x| x as f32)).collect();
630 if v.len() == 4 {
631 return ((v[2] - v[0]).abs(), (v[3] - v[1]).abs());
632 }
633 }
634 (612.0, 792.0)
635}
636
637pub fn content_diagnosis(bytes: &[u8]) -> String {
643 let Some(doc) = load_document(bytes) else {
644 return "document does not load".into();
645 };
646 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
647 pages.sort_by_key(|(n, _)| *n);
648 let mut out = String::new();
649 let mut caches = DocCaches::default();
650 for (n, pid) in pages.into_iter().take(4) {
651 let content_bytes = doc.get_page_content(pid);
652 let ops = lopdf::content::Content::decode(&content_bytes)
653 .map(|c| c.operations.len())
654 .ok();
655 let res = page_res(&doc, pid);
656 let fonts = res.map(|r| fonts_from_res(&doc, r, &mut caches).len());
657 let glyphs = page_glyphs_cached(&doc, pid, &mut caches).len();
658 out.push_str(&format!(
659 "\n page {n}: content {} B, ops {}, resources {}, fonts {}, glyphs {}",
660 content_bytes.len(),
661 ops.map_or("UNDECODABLE".to_string(), |n| n.to_string()),
662 if res.is_some() { "ok" } else { "MISSING" },
663 fonts.map_or("-".to_string(), |n| n.to_string()),
664 glyphs,
665 ));
666 }
667 out
668}
669
670pub fn text_layer_is_vestigial(pages: &[crate::pdfium_backend::PdfPage]) -> bool {
683 let lines: usize = pages.iter().map(|p| p.cells.len()).sum();
684 if lines == 0 {
685 return true;
686 }
687 let chars: usize = pages
688 .iter()
689 .flat_map(|p| &p.cells)
690 .map(|c| c.text.chars().count())
691 .sum();
692 lines <= pages.len() && chars < 32
693}
694
695pub fn xref_repair_status(bytes: &[u8]) -> String {
699 if Document::load_mem(bytes).is_ok() {
700 return "loads unaided; no repair needed".into();
701 }
702 match pad_short_xref_entries(bytes) {
703 Ok(fixed) => match Document::load_mem(&fixed) {
704 Ok(_) => "repaired: cross-reference entries padded to 20 bytes".into(),
705 Err(e) => format!("padded the entries, but it still will not load: {e}"),
706 },
707 Err(why) => format!("repair declined — {why}"),
708 }
709}
710
711fn load_document(bytes: &[u8]) -> Option<Document> {
726 let mut fallback = None;
731 if let Some(doc) = best_effort_load(bytes, &mut fallback) {
732 return Some(doc);
733 }
734 let xref_fixed = pad_short_xref_entries(bytes).ok();
735 if let Some(fixed) = &xref_fixed {
736 if let Some(doc) = best_effort_load(fixed, &mut fallback) {
737 return Some(doc);
738 }
739 }
740 let lengths_fixed = fix_stream_lengths(xref_fixed.as_deref().unwrap_or(bytes));
743 if let Some(doc) = best_effort_load(&lengths_fixed, &mut fallback) {
744 return Some(doc);
745 }
746 fallback
747}
748
749fn best_effort_load(data: &[u8], fallback: &mut Option<Document>) -> Option<Document> {
752 match Document::load_mem(data) {
753 Ok(doc) if has_page_content(&doc) => Some(doc),
754 Ok(doc) => {
755 fallback.get_or_insert(doc);
756 None
757 }
758 Err(_) => None,
759 }
760}
761
762fn has_page_content(doc: &Document) -> bool {
766 doc.get_pages()
767 .into_values()
768 .take(4)
769 .any(|pid| !doc.get_page_content(pid).is_empty())
770}
771
772fn fix_stream_lengths(bytes: &[u8]) -> Vec<u8> {
785 let mut out = bytes.to_vec();
786 let mut i = 0;
787 while let Some(rel) = find(&out[i..], b"stream") {
788 let kw = i + rel;
789 i = kw + 6;
790 if kw >= 3 && &out[kw - 3..kw] == b"end" {
792 continue;
793 }
794 let mut data = kw + 6;
796 if out.get(data..data + 2) == Some(b"\r\n".as_slice()) {
797 data += 2;
798 } else if matches!(out.get(data), Some(b'\n' | b'\r')) {
799 data += 1;
800 }
801 let Some(end) = find(&out[data..], b"endstream").map(|r| data + r) else {
802 continue;
803 };
804 let dict_start = out[..kw].iter().rposition(|&c| c == b'<').unwrap_or(0);
806 let Some(lrel) = find(&out[dict_start..kw], b"/Length") else {
807 continue;
808 };
809 let mut d = dict_start + lrel + 7;
810 while matches!(out.get(d), Some(b' ')) {
811 d += 1;
812 }
813 let digits = out[d..].iter().take_while(|c| c.is_ascii_digit()).count();
814 if digits == 0 {
815 continue;
816 }
817 let declared: usize = match std::str::from_utf8(&out[d..d + digits])
818 .ok()
819 .and_then(|s| s.parse().ok())
820 {
821 Some(v) => v,
822 None => continue,
823 };
824 let actual = end - data;
825 let replacement = actual.to_string();
828 if actual == declared || replacement.len() > digits {
829 continue;
830 }
831 out[d..d + digits].fill(b' ');
832 out[d..d + replacement.len()].copy_from_slice(replacement.as_bytes());
833 }
834 out
835}
836
837fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
838 haystack.windows(needle.len()).position(|w| w == needle)
839}
840
841fn pad_short_xref_entries(bytes: &[u8]) -> Result<Vec<u8>, &'static str> {
844 let is_boundary = |i: usize| i == 0 || matches!(bytes[i - 1], b'\n' | b'\r');
847 let mut starts = (0..bytes.len().saturating_sub(4))
848 .filter(|&i| &bytes[i..i + 4] == b"xref" && is_boundary(i));
849 let xref_at = starts
850 .next()
851 .ok_or("no classic `xref` section (an xref stream?)")?;
852 if starts.next().is_some() {
853 return Err("more than one xref section (incremental update)");
854 }
855 let last_obj = bytes
856 .windows(3)
857 .rposition(|w| w == b"obj")
858 .ok_or("no objects found")?;
859 if last_obj > xref_at {
860 return Err("an object follows the xref — padding would move it");
861 }
862
863 let mut out = bytes[..xref_at].to_vec();
864 out.extend_from_slice(b"xref\n");
865 let mut i = xref_at + 4;
866 let skip_ws = |i: &mut usize| {
867 while matches!(bytes.get(*i), Some(b'\r' | b'\n' | b' ')) {
868 *i += 1;
869 }
870 };
871 loop {
872 skip_ws(&mut i);
873 if bytes[i..].starts_with(b"trailer") {
875 out.extend_from_slice(&bytes[i..]);
876 return Ok(out);
877 }
878 let header_end = i + bytes[i..]
879 .iter()
880 .position(|c| matches!(c, b'\n' | b'\r'))
881 .ok_or("subsection header runs off the end")?;
882 let header = std::str::from_utf8(&bytes[i..header_end])
883 .map_err(|_| "subsection header is not text")?
884 .trim();
885 let mut parts = header.split_whitespace();
886 let count: usize = parts
887 .nth(1)
888 .and_then(|c| c.parse().ok())
889 .ok_or("unparseable subsection header")?;
890 if parts.next().is_some() || count == 0 {
891 return Err("unexpected subsection header shape");
892 }
893 out.extend_from_slice(header.as_bytes());
894 out.push(b'\n');
895 i = header_end;
896 for _ in 0..count {
897 skip_ws(&mut i);
898 let entry = bytes.get(i..i + 18).ok_or("xref entry runs off the end")?;
900 let well_formed = entry[..10].iter().all(u8::is_ascii_digit)
901 && entry[10] == b' '
902 && entry[11..16].iter().all(u8::is_ascii_digit)
903 && entry[16] == b' '
904 && matches!(entry[17], b'n' | b'f');
905 if !well_formed {
906 return Err("xref entry is not `nnnnnnnnnn ggggg n`");
907 }
908 out.extend_from_slice(entry);
909 out.extend_from_slice(b" \n"); i += 18;
911 }
912 }
913}
914
915pub fn debug_glyphs(bytes: &[u8], index: usize) -> Vec<(char, f32, f32, f32, f32)> {
918 let Some(doc) = load_document(bytes) else {
919 return Vec::new();
920 };
921 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
922 pages.sort_by_key(|(n, _)| *n);
923 let Some((_, pid)) = pages.get(index) else {
924 return Vec::new();
925 };
926 page_glyphs(&doc, *pid)
927 .into_iter()
928 .map(|g| (g.ch, g.ll, g.lr, g.lb, g.lt))
929 .collect()
930}
931
932pub fn pdf_textlines(bytes: &[u8]) -> Vec<(f32, f32, Vec<crate::pdfium_backend::TextCell>)> {
936 let Some(doc) = load_document(bytes) else {
937 return Vec::new();
938 };
939 let mut caches = DocCaches::default();
940 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
941 pages.sort_by_key(|(n, _)| *n);
942 pages
943 .into_iter()
944 .map(|(_, pid)| {
945 let (w, h) = page_size(&doc, pid);
946 let glyphs = page_glyphs_cached(&doc, pid, &mut caches);
947 let cells = crate::dp_lines::line_cells(&glyphs, h, true);
948 (w, h, cells)
949 })
950 .collect()
951}
952
953pub fn pdf_words(bytes: &[u8]) -> Vec<(f32, f32, Vec<crate::pdfium_backend::TextCell>)> {
958 let Some(doc) = load_document(bytes) else {
959 return Vec::new();
960 };
961 let mut caches = DocCaches::default();
962 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
963 pages.sort_by_key(|(n, _)| *n);
964 pages
965 .into_iter()
966 .map(|(_, pid)| {
967 let (w, h) = page_size(&doc, pid);
968 let glyphs = page_glyphs_cached(&doc, pid, &mut caches);
969 let cells = crate::dp_lines::word_cells(&glyphs, h, true);
970 (w, h, cells)
971 })
972 .collect()
973}
974
975#[derive(Default)]
979pub struct PageParserCells {
980 pub prose: Vec<crate::pdfium_backend::TextCell>,
981 pub words: Vec<crate::pdfium_backend::TextCell>,
982 pub code: Vec<crate::pdfium_backend::TextCell>,
983}
984
985pub fn pdf_all_cells(bytes: &[u8]) -> Vec<PageParserCells> {
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 (prose, words) = crate::dp_lines::line_and_word_cells(&glyphs, h, true);
1002 PageParserCells {
1003 prose,
1004 words,
1005 code: crate::pdfium_backend::code_cells_from_glyphs(&glyphs, h),
1006 }
1007 })
1008 .collect()
1009}
1010
1011pub fn pdf_text_pages(bytes: &[u8]) -> Vec<crate::pdfium_backend::PdfPage> {
1018 let Some(doc) = load_document(bytes) else {
1019 return Vec::new();
1020 };
1021 let mut caches = DocCaches::default();
1022 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
1023 pages.sort_by_key(|(n, _)| *n);
1024 pages
1025 .into_iter()
1026 .map(|(_, pid)| {
1027 let (w, h) = page_size(&doc, pid);
1028 let glyphs = page_glyphs_cached(&doc, pid, &mut caches);
1029 let (mut prose, mut words) = crate::dp_lines::line_and_word_cells(&glyphs, h, true);
1030 drop_overpainted_cells(&mut prose);
1031 drop_overpainted_cells(&mut words);
1032 crate::pdfium_backend::PdfPage {
1033 width: w,
1034 height: h,
1035 scale: 1.0,
1037 cells: prose,
1038 code_cells: crate::pdfium_backend::code_cells_from_glyphs(&glyphs, h),
1039 word_cells: words,
1040 #[cfg(feature = "ocr-prep")]
1041 image: image::RgbImage::new(1, 1),
1042 links: Vec::new(),
1043 }
1044 })
1045 .collect()
1046}
1047
1048fn drop_overpainted_cells(cells: &mut Vec<crate::pdfium_backend::TextCell>) {
1067 let mut paint = vec![false; cells.len()];
1068 for i in 0..cells.len() {
1069 for j in 0..cells.len() {
1070 if i == j || cells[i].text == cells[j].text {
1071 continue;
1072 }
1073 let (a, b) = (&cells[i], &cells[j]);
1074 let vo = (a.b.min(b.b) - a.t.max(b.t)).max(0.0);
1076 if vo < 0.6 * (a.b - a.t).min(b.b - b.t) {
1077 continue;
1078 }
1079 let ho = (a.r.min(b.r) - a.l.max(b.l)).max(0.0);
1081 if ho >= 0.8 * (a.r - a.l) && (a.r - a.l) <= (b.r - b.l) {
1082 paint[i] = true;
1083 paint[j] = true;
1084 }
1085 }
1086 }
1087 let mut keep = paint.iter().map(|p| !p);
1088 cells.retain(|_| keep.next().unwrap());
1089}
1090
1091#[derive(Clone, Copy)]
1095struct TextState {
1096 tc: f64,
1097 tw: f64,
1098 th: f64,
1099 tl: f64,
1100 trise: f64,
1101 fsize: f64,
1102}
1103
1104impl TextState {
1105 const INIT: TextState = TextState {
1106 tc: 0.0,
1107 tw: 0.0,
1108 th: 1.0,
1109 tl: 0.0,
1110 trise: 0.0,
1111 fsize: 0.0,
1112 };
1113}
1114
1115fn page_res(doc: &Document, page_id: lopdf::ObjectId) -> Option<&Dictionary> {
1118 let (inline, ids) = doc.get_page_resources(page_id).ok()?;
1119 if let Some(d) = inline {
1120 return Some(d);
1121 }
1122 ids.into_iter().find_map(|id| doc.get_dictionary(id).ok())
1123}
1124
1125fn fonts_from_res(
1129 doc: &Document,
1130 res: &Dictionary,
1131 caches: &mut DocCaches,
1132) -> HashMap<Vec<u8>, Rc<Font>> {
1133 let mut map = HashMap::new();
1134 let font_dict = res
1135 .get(b"Font")
1136 .ok()
1137 .and_then(|o| deref(doc, o))
1138 .and_then(|o| o.as_dict().ok());
1139 if let Some(fd) = font_dict {
1140 for (name, value) in fd.iter() {
1141 let font = match value {
1142 Object::Reference(id) => {
1143 let key = (*id, name.clone());
1144 if let Some(f) = caches.fonts.get(&key) {
1145 Rc::clone(f)
1146 } else if let Some(fdict) = deref(doc, value).and_then(|o| o.as_dict().ok()) {
1147 let f = Rc::new(parse_font(doc, name, fdict));
1148 caches.fonts.insert(key, Rc::clone(&f));
1149 f
1150 } else {
1151 continue;
1152 }
1153 }
1154 _ => {
1155 if let Some(fdict) = deref(doc, value).and_then(|o| o.as_dict().ok()) {
1156 Rc::new(parse_font(doc, name, fdict))
1157 } else {
1158 continue;
1159 }
1160 }
1161 };
1162 map.insert(name.clone(), font);
1163 }
1164 }
1165 map
1166}
1167
1168pub(crate) fn page_glyphs(doc: &Document, page_id: lopdf::ObjectId) -> Vec<Glyph> {
1170 page_glyphs_cached(doc, page_id, &mut DocCaches::default())
1171}
1172
1173fn page_glyphs_cached(
1176 doc: &Document,
1177 page_id: lopdf::ObjectId,
1178 caches: &mut DocCaches,
1179) -> Vec<Glyph> {
1180 let mut out = Vec::new();
1181 let content_bytes = doc.get_page_content(page_id);
1184 let Ok(content) = lopdf::content::Content::decode(&content_bytes) else {
1185 return out;
1186 };
1187 if let Some(res) = page_res(doc, page_id) {
1188 run_content(
1189 doc,
1190 res,
1191 &content,
1192 Mat::ID,
1193 TextState::INIT,
1194 0,
1195 caches,
1196 &mut out,
1197 );
1198 }
1199 out
1200}
1201
1202#[allow(clippy::too_many_arguments)]
1207fn run_content(
1208 doc: &Document,
1209 res: &Dictionary,
1210 content: &lopdf::content::Content,
1211 base_ctm: Mat,
1212 init: TextState,
1213 depth: u32,
1214 caches: &mut DocCaches,
1215 out: &mut Vec<Glyph>,
1216) {
1217 let fonts = fonts_from_res(doc, res, caches);
1218 let xobjects = res
1219 .get(b"XObject")
1220 .ok()
1221 .and_then(|o| deref(doc, o))
1222 .and_then(|o| o.as_dict().ok());
1223
1224 #[allow(clippy::type_complexity)]
1229 let mut gstate_stack: Vec<(Mat, f64, f64, f64, f64, f64, f64, Option<&Rc<Font>>)> = Vec::new();
1230 let mut ctm = base_ctm;
1231 let mut tm = Mat::ID;
1232 let mut tlm = Mat::ID;
1233 let mut font: Option<&Rc<Font>> = None;
1234 let mut fsize = init.fsize;
1235 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;
1240
1241 let op_f = |operands: &[Object], i: usize| operands.get(i).and_then(num).unwrap_or(0.0);
1242
1243 for op in &content.operations {
1244 let operands = &op.operands;
1245 match op.operator.as_str() {
1246 "q" => gstate_stack.push((ctm, tc, tw, th, tl, trise, fsize, font)),
1247 "Q" => {
1248 if let Some((c, a, b, h, l, r, fs, f)) = gstate_stack.pop() {
1249 ctm = c;
1250 tc = a;
1251 tw = b;
1252 th = h;
1253 tl = l;
1254 trise = r;
1255 fsize = fs;
1256 font = f;
1257 }
1258 }
1259 "cm" => {
1260 let m = Mat {
1261 a: op_f(operands, 0),
1262 b: op_f(operands, 1),
1263 c: op_f(operands, 2),
1264 d: op_f(operands, 3),
1265 e: op_f(operands, 4),
1266 f: op_f(operands, 5),
1267 };
1268 ctm = m.then(ctm);
1269 }
1270 "BT" => {
1271 tm = Mat::ID;
1272 tlm = Mat::ID;
1273 }
1274 "ET" => {}
1275 "Tf" => {
1276 if let Some(Object::Name(n)) = operands.first() {
1277 font = fonts.get(n.as_slice());
1278 }
1279 fsize = op_f(operands, 1);
1280 }
1281 "Td" => {
1282 tlm = Mat {
1283 a: 1.0,
1284 b: 0.0,
1285 c: 0.0,
1286 d: 1.0,
1287 e: op_f(operands, 0),
1288 f: op_f(operands, 1),
1289 }
1290 .then(tlm);
1291 tm = tlm;
1292 }
1293 "TD" => {
1294 tl = -op_f(operands, 1);
1295 tlm = Mat {
1296 a: 1.0,
1297 b: 0.0,
1298 c: 0.0,
1299 d: 1.0,
1300 e: op_f(operands, 0),
1301 f: op_f(operands, 1),
1302 }
1303 .then(tlm);
1304 tm = tlm;
1305 }
1306 "Tm" => {
1307 tlm = Mat {
1308 a: op_f(operands, 0),
1309 b: op_f(operands, 1),
1310 c: op_f(operands, 2),
1311 d: op_f(operands, 3),
1312 e: op_f(operands, 4),
1313 f: op_f(operands, 5),
1314 };
1315 tm = tlm;
1316 }
1317 "T*" => {
1318 tlm = Mat {
1319 a: 1.0,
1320 b: 0.0,
1321 c: 0.0,
1322 d: 1.0,
1323 e: 0.0,
1324 f: -tl,
1325 }
1326 .then(tlm);
1327 tm = tlm;
1328 }
1329 "Tc" => tc = op_f(operands, 0),
1330 "Tw" => tw = op_f(operands, 0),
1331 "Tz" => th = op_f(operands, 0) / 100.0,
1332 "TL" => tl = op_f(operands, 0),
1333 "Ts" => trise = op_f(operands, 0),
1334 "Tj" | "'" | "\"" => {
1335 if op.operator == "'" || op.operator == "\"" {
1336 tlm = Mat {
1338 a: 1.0,
1339 b: 0.0,
1340 c: 0.0,
1341 d: 1.0,
1342 e: 0.0,
1343 f: -tl,
1344 }
1345 .then(tlm);
1346 tm = tlm;
1347 }
1348 if op.operator == "\"" {
1349 tw = op_f(operands, 0);
1352 tc = op_f(operands, 1);
1353 }
1354 if let (Some(f), Some(Object::String(s, _))) = (font, operands.last()) {
1355 show_text(f, s, fsize, tc, tw, th, trise, &mut tm, ctm, out);
1356 }
1357 }
1358 "TJ" => {
1359 if let (Some(f), Some(Object::Array(arr))) = (font, operands.first()) {
1360 for el in arr {
1361 match el {
1362 Object::String(s, _) => {
1363 show_text(f, s, fsize, tc, tw, th, trise, &mut tm, ctm, out)
1364 }
1365 other => {
1366 if let Some(adj) = num(other) {
1367 let tx = -adj / 1000.0 * fsize * th;
1369 tm = Mat {
1370 a: 1.0,
1371 b: 0.0,
1372 c: 0.0,
1373 d: 1.0,
1374 e: tx,
1375 f: 0.0,
1376 }
1377 .then(tm);
1378 }
1379 }
1380 }
1381 }
1382 }
1383 }
1384 "Do" => {
1385 if depth >= 8 {
1388 continue;
1389 }
1390 let Some(Object::Name(n)) = operands.first() else {
1391 continue;
1392 };
1393 let obj = xobjects.and_then(|d| d.get(n.as_slice()).ok());
1394 let form_id = match obj {
1395 Some(Object::Reference(id)) => Some(*id),
1396 _ => None,
1397 };
1398 let stream = obj
1399 .and_then(|o| deref(doc, o))
1400 .and_then(|o| o.as_stream().ok());
1401 let Some(stream) = stream else { continue };
1402 let is_form = stream
1403 .dict
1404 .get(b"Subtype")
1405 .ok()
1406 .and_then(|o| o.as_name().ok())
1407 == Some(b"Form".as_slice());
1408 if !is_form {
1409 continue;
1410 }
1411 let cached = form_id.and_then(|id| caches.forms.get(&id).cloned());
1414 let form_content = match cached {
1415 Some(c) => c,
1416 None => {
1417 let Ok(data) = stream.decompressed_content() else {
1418 continue;
1419 };
1420 let Ok(c) = lopdf::content::Content::decode(&data) else {
1421 continue;
1422 };
1423 let c = Rc::new(c);
1424 if let Some(id) = form_id {
1425 caches.forms.insert(id, Rc::clone(&c));
1426 }
1427 c
1428 }
1429 };
1430 let form_mat = match stream.dict.get(b"Matrix").ok() {
1432 Some(Object::Array(a)) if a.len() == 6 => {
1433 let v: Vec<f64> = a.iter().filter_map(num).collect();
1434 if v.len() == 6 {
1435 Mat {
1436 a: v[0],
1437 b: v[1],
1438 c: v[2],
1439 d: v[3],
1440 e: v[4],
1441 f: v[5],
1442 }
1443 } else {
1444 Mat::ID
1445 }
1446 }
1447 _ => Mat::ID,
1448 };
1449 let form_res = stream
1451 .dict
1452 .get(b"Resources")
1453 .ok()
1454 .and_then(|o| deref(doc, o))
1455 .and_then(|o| o.as_dict().ok())
1456 .unwrap_or(res);
1457 let state = TextState {
1458 tc,
1459 tw,
1460 th,
1461 tl,
1462 trise,
1463 fsize,
1464 };
1465 run_content(
1466 doc,
1467 form_res,
1468 &form_content,
1469 form_mat.then(ctm),
1470 state,
1471 depth + 1,
1472 caches,
1473 out,
1474 );
1475 }
1476 _ => {}
1477 }
1478 }
1479}
1480
1481#[allow(clippy::too_many_arguments)]
1482fn show_text(
1483 font: &Font,
1484 bytes: &[u8],
1485 fsize: f64,
1486 tc: f64,
1487 tw: f64,
1488 th: f64,
1489 trise: f64,
1490 tm: &mut Mat,
1491 ctm: Mat,
1492 out: &mut Vec<Glyph>,
1493) {
1494 for code in codes(font, bytes) {
1495 let (text, w) = font.decode_code(code);
1496 let w0 = w / 1000.0; let scale = Mat {
1499 a: fsize * th,
1500 b: 0.0,
1501 c: 0.0,
1502 d: fsize,
1503 e: 0.0,
1504 f: trise,
1505 };
1506 let trm = scale.then(*tm).then(ctm);
1507 let (x0, y0) = trm.apply(0.0, font.descent / 1000.0);
1509 let (x1, _y1) = trm.apply(w0, font.descent / 1000.0);
1510 let (_x2, y2) = trm.apply(0.0, font.ascent / 1000.0);
1511 let (left, right) = (x0.min(x1), x0.max(x1));
1512 let (bot, top) = (y0.min(y2), y0.max(y2));
1513 if let Some(s) = text {
1514 for ch in s.chars() {
1516 if ch != '\u{0}' {
1517 out.push(Glyph {
1518 ch,
1519 l: left as f32,
1520 b: bot as f32,
1521 r: right as f32,
1522 t: top as f32,
1523 ll: left as f32,
1524 lb: bot as f32,
1525 lr: right as f32,
1526 lt: top as f32,
1527 font: font.hash,
1528 });
1529 }
1530 }
1531 }
1532 let is_space = !font.two_byte && code == 32;
1534 let tx = (w0 * fsize + tc + if is_space { tw } else { 0.0 }) * th;
1535 *tm = Mat {
1536 a: 1.0,
1537 b: 0.0,
1538 c: 0.0,
1539 d: 1.0,
1540 e: tx,
1541 f: 0.0,
1542 }
1543 .then(*tm);
1544 }
1545}
1546
1547fn simple_encoding_table(doc: &Document, fdict: &Dictionary) -> HashMap<u8, char> {
1551 let enc = fdict.get(b"Encoding").ok().and_then(|o| deref(doc, o));
1552 let base_name = match enc {
1553 Some(Object::Name(n)) => n.clone(),
1554 Some(Object::Dictionary(d)) => d
1555 .get(b"BaseEncoding")
1556 .ok()
1557 .and_then(|o| o.as_name().ok())
1558 .map(|n| n.to_vec())
1559 .unwrap_or_default(),
1560 _ => Vec::new(),
1561 };
1562 let mut m = if base_name == b"MacRomanEncoding" {
1563 macroman_table()
1564 } else {
1565 winansi_table()
1566 };
1567 if let Some(Object::Dictionary(d)) = enc {
1569 if let Some(Object::Array(diffs)) = d.get(b"Differences").ok().and_then(|o| deref(doc, o)) {
1570 let mut code = 0u8;
1571 for el in diffs {
1572 match el {
1573 Object::Integer(i) => code = *i as u8,
1574 Object::Name(name) => {
1575 if let Some(ch) = glyph_name_to_char(name) {
1576 m.insert(code, ch);
1577 }
1578 code = code.wrapping_add(1);
1579 }
1580 _ => {}
1581 }
1582 }
1583 }
1584 }
1585 m
1586}
1587
1588fn glyph_name_to_char(name: &[u8]) -> Option<char> {
1593 let s = std::str::from_utf8(name).ok()?;
1594 if let Some(hex) = s.strip_prefix("uni") {
1595 if let Ok(cp) = u32::from_str_radix(hex.get(0..4)?, 16) {
1596 return char::from_u32(cp);
1597 }
1598 }
1599 if s.len() == 1 {
1601 let b = s.as_bytes()[0];
1602 if b.is_ascii_alphabetic() {
1603 return Some(b as char);
1604 }
1605 }
1606 let resolved = match s {
1607 "space" => ' ',
1608 "exclam" => '!',
1609 "quotedbl" => '"',
1610 "numbersign" => '#',
1611 "dollar" => '$',
1612 "percent" => '%',
1613 "ampersand" => '&',
1614 "quotesingle" => '\'',
1615 "parenleft" => '(',
1616 "parenright" => ')',
1617 "asterisk" => '*',
1618 "plus" => '+',
1619 "comma" => ',',
1620 "hyphen" => '-',
1621 "period" => '.',
1622 "slash" => '/',
1623 "zero" => '0',
1624 "one" => '1',
1625 "two" => '2',
1626 "three" => '3',
1627 "four" => '4',
1628 "five" => '5',
1629 "six" => '6',
1630 "seven" => '7',
1631 "eight" => '8',
1632 "nine" => '9',
1633 "colon" => ':',
1634 "semicolon" => ';',
1635 "less" => '<',
1636 "equal" => '=',
1637 "greater" => '>',
1638 "question" => '?',
1639 "at" => '@',
1640 "bracketleft" => '[',
1641 "backslash" => '\\',
1642 "bracketright" => ']',
1643 "asciicircum" => '^',
1644 "underscore" => '_',
1645 "grave" => '`',
1646 "braceleft" => '{',
1647 "bar" => '|',
1648 "braceright" => '}',
1649 "asciitilde" => '~',
1650 "bullet" => '\u{2022}',
1651 "periodcentered" => '\u{00B7}',
1652 "endash" => '\u{2013}',
1653 "emdash" => '\u{2014}',
1654 "quoteright" => '\u{2019}',
1655 "quoteleft" => '\u{2018}',
1656 "quotedblleft" => '\u{201C}',
1657 "quotedblright" => '\u{201D}',
1658 "quotedblbase" => '\u{201E}',
1659 "quotesinglbase" => '\u{201A}',
1660 "ff" => '\u{FB00}',
1665 "fi" => '\u{FB01}',
1666 "fl" => '\u{FB02}',
1667 "ffi" => '\u{FB03}',
1668 "ffl" => '\u{FB04}',
1669 "ft" => '\u{FB05}',
1670 "st" => '\u{FB06}',
1671 "degree" => '\u{00B0}',
1672 "trademark" => '\u{2122}',
1673 "registered" => '\u{00AE}',
1674 "copyright" => '\u{00A9}',
1675 "ellipsis" => '\u{2026}',
1676 "minus" => '\u{2212}',
1677 "fraction" => '\u{2044}',
1678 "nbspace" => '\u{00A0}',
1679 "alpha" => '\u{03B1}',
1684 "beta" => '\u{03B2}',
1685 "gamma" => '\u{03B3}',
1686 "delta" => '\u{03B4}',
1687 "epsilon" | "epsilon1" => '\u{03B5}',
1688 "zeta" => '\u{03B6}',
1689 "eta" => '\u{03B7}',
1690 "theta" | "theta1" => '\u{03B8}',
1691 "iota" => '\u{03B9}',
1692 "kappa" => '\u{03BA}',
1693 "lambda" => '\u{03BB}',
1694 "mu" => '\u{03BC}',
1695 "nu" => '\u{03BD}',
1696 "xi" => '\u{03BE}',
1697 "omicron" => '\u{03BF}',
1698 "pi" | "pi1" => '\u{03C0}',
1699 "rho" | "rho1" => '\u{03C1}',
1700 "sigma" => '\u{03C3}',
1701 "sigma1" => '\u{03C2}',
1702 "tau" => '\u{03C4}',
1703 "upsilon" => '\u{03C5}',
1704 "phi" | "phi1" => '\u{03C6}',
1705 "chi" => '\u{03C7}',
1706 "psi" => '\u{03C8}',
1707 "omega" | "omega1" => '\u{03C9}',
1708 "Gamma" => '\u{0393}',
1709 "Delta" => '\u{0394}',
1710 "Theta" => '\u{0398}',
1711 "Lambda" => '\u{039B}',
1712 "Xi" => '\u{039E}',
1713 "Pi" => '\u{03A0}',
1714 "Sigma" => '\u{03A3}',
1715 "Upsilon" => '\u{03A5}',
1716 "Phi" => '\u{03A6}',
1717 "Psi" => '\u{03A8}',
1718 "Omega" => '\u{03A9}',
1719 "lessequal" => '\u{2264}',
1720 "greaterequal" => '\u{2265}',
1721 "notequal" => '\u{2260}',
1722 "approxequal" => '\u{2248}',
1723 "equivalence" => '\u{2261}',
1724 "element" => '\u{2208}',
1725 "plusminus" => '\u{00B1}',
1726 "multiply" => '\u{00D7}',
1727 "divide" => '\u{00F7}',
1728 "infinity" => '\u{221E}',
1729 "partialdiff" => '\u{2202}',
1730 "gradient" => '\u{2207}',
1731 "summation" => '\u{2211}',
1732 "product" => '\u{220F}',
1733 "integral" => '\u{222B}',
1734 "radical" => '\u{221A}',
1735 "proportional" => '\u{221D}',
1736 "arrowright" => '\u{2192}',
1737 "arrowleft" => '\u{2190}',
1738 "arrowup" => '\u{2191}',
1739 "arrowdown" => '\u{2193}',
1740 "arrowboth" => '\u{2194}',
1741 "arrowdblright" => '\u{21D2}',
1742 "logicaland" => '\u{2227}',
1743 "logicalor" => '\u{2228}',
1744 "intersection" => '\u{2229}',
1745 "union" => '\u{222A}',
1746 "similar" => '\u{223C}',
1747 "congruent" => '\u{2245}',
1748 "dotmath" => '\u{22C5}',
1749 "asteriskmath" => '\u{2217}',
1750 _ => {
1751 if let Some((base, _)) = s.split_once('.') {
1753 if !base.is_empty() {
1754 return glyph_name_to_char(base.as_bytes());
1755 }
1756 }
1757 return None;
1758 }
1759 };
1760 Some(resolved)
1761}
1762
1763fn winansi_table() -> HashMap<u8, char> {
1765 let mut m = HashMap::new();
1766 for b in 0x20u8..=0x7e {
1767 m.insert(b, b as char);
1768 }
1769 let extra: &[(u8, char)] = &[
1771 (0x91, '\u{2018}'),
1772 (0x92, '\u{2019}'),
1773 (0x93, '\u{201C}'),
1774 (0x94, '\u{201D}'),
1775 (0x95, '\u{2022}'),
1776 (0x96, '\u{2013}'),
1777 (0x97, '\u{2014}'),
1778 (0x85, '\u{2026}'),
1779 (0xA0, '\u{00A0}'),
1780 ];
1781 for &(b, c) in extra {
1782 m.insert(b, c);
1783 }
1784 for b in 0xA1u8..=0xFF {
1785 m.entry(b).or_insert(b as char);
1786 }
1787 m
1788}
1789
1790fn macroman_table() -> HashMap<u8, char> {
1793 let mut m = HashMap::new();
1794 for b in 0x20u8..=0x7e {
1795 m.insert(b, b as char);
1796 }
1797 let high: &[(u8, char)] = &[
1798 (0xA5, '\u{2022}'), (0xD0, '\u{2013}'), (0xD1, '\u{2014}'), (0xD2, '\u{201C}'),
1802 (0xD3, '\u{201D}'),
1803 (0xD4, '\u{2018}'),
1804 (0xD5, '\u{2019}'),
1805 (0xCA, '\u{00A0}'),
1806 (0xC9, '\u{2026}'),
1807 (0xDE, '\u{FB01}'),
1808 (0xDF, '\u{FB02}'),
1809 ];
1810 for &(b, c) in high {
1811 m.insert(b, c);
1812 }
1813 m
1814}
1815
1816#[cfg(test)]
1817mod xref_repair {
1818 fn pdf_with_xref(two_byte_eol: bool) -> Vec<u8> {
1822 let content = b"BT /F1 12 Tf 72 700 Td (Invoice 922769430725) Tj ET\n";
1823 let stream = format!("<</Length {}>>stream\n", content.len()).into_bytes();
1824 let objs: Vec<Vec<u8>> = vec![
1825 b"<</Type/Catalog/Pages 2 0 R>>".to_vec(),
1826 b"<</Type/Pages/Kids[3 0 R]/Count 1>>".to_vec(),
1827 b"<</Type/Page/Parent 2 0 R/MediaBox[0 0 595 842]/Contents 4 0 R\
1828 /Resources<</Font<</F1 5 0 R>>>>>>"
1829 .to_vec(),
1830 [stream.as_slice(), content.as_slice(), b"endstream"].concat(),
1831 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>".to_vec(),
1832 ];
1833
1834 let mut out = b"%PDF-1.4\n".to_vec();
1835 let mut offsets = Vec::new();
1836 for (i, body) in objs.iter().enumerate() {
1837 offsets.push(out.len());
1838 out.extend_from_slice(format!("{} 0 obj", i + 1).as_bytes());
1839 out.extend_from_slice(body);
1840 out.extend_from_slice(b"endobj\n");
1841 }
1842 let xref_at = out.len();
1843 let eol: &[u8] = if two_byte_eol { b" \n" } else { b"\n" };
1844 out.extend_from_slice(format!("xref\n0 {}\n", objs.len() + 1).as_bytes());
1845 out.extend_from_slice(b"0000000000 65535 f");
1846 out.extend_from_slice(eol);
1847 for off in &offsets {
1848 out.extend_from_slice(format!("{off:010} 00000 n").as_bytes());
1849 out.extend_from_slice(eol);
1850 }
1851 out.extend_from_slice(
1852 format!("trailer<</Size {}/Root 1 0 R>>\n", objs.len() + 1).as_bytes(),
1853 );
1854 out.extend_from_slice(format!("startxref\n{xref_at}\n%%EOF\n").as_bytes());
1855 out
1856 }
1857
1858 #[test]
1864 fn short_xref_entries_still_parse() {
1865 let good = pdf_with_xref(true);
1866 let broken = pdf_with_xref(false);
1867 assert!(
1868 broken.len() < good.len(),
1869 "the broken file is the shorter one"
1870 );
1871 assert!(
1872 lopdf::Document::load_mem(&good).is_ok(),
1873 "the control file must load unaided"
1874 );
1875 assert!(
1876 lopdf::Document::load_mem(&broken).is_err(),
1877 "lopdf rejects 19-byte entries — if this ever passes, drop the repair"
1878 );
1879
1880 let cells = |b: &[u8]| -> Vec<String> {
1881 super::pdf_textlines(b)
1882 .into_iter()
1883 .flat_map(|(_, _, c)| c.into_iter().map(|c| c.text))
1884 .collect()
1885 };
1886 let from_good = cells(&good);
1887 assert!(
1888 from_good.iter().any(|t| t.contains("922769430725")),
1889 "control text: {from_good:?}"
1890 );
1891 assert_eq!(
1892 cells(&broken),
1893 from_good,
1894 "repair must match the good parse"
1895 );
1896 }
1897
1898 #[test]
1903 fn overstated_stream_length_still_yields_content() {
1904 let good = pdf_with_xref(true);
1905 let broken = {
1908 let at = good
1909 .windows(8)
1910 .position(|w| w == b"/Length ")
1911 .expect("a /Length")
1912 + 8;
1913 let digits = good[at..].iter().take_while(|c| c.is_ascii_digit()).count();
1914 let n: usize = std::str::from_utf8(&good[at..at + digits])
1915 .unwrap()
1916 .parse()
1917 .unwrap();
1918 let inflated = (n + 1).to_string();
1919 assert_eq!(inflated.len(), digits, "keep the digit count");
1920 let mut b = good.clone();
1921 b[at..at + digits].copy_from_slice(inflated.as_bytes());
1922 b
1923 };
1924 assert_eq!(broken.len(), good.len(), "the defect must not move bytes");
1925 let raw = lopdf::Document::load_mem(&broken).expect("still loads");
1927 assert!(
1928 raw.get_pages()
1929 .into_values()
1930 .all(|p| raw.get_page_content(p).is_empty()),
1931 "lopdf should drop the stream — if it stops, drop this repair"
1932 );
1933 let text = |b: &[u8]| -> Vec<String> {
1935 super::pdf_textlines(b)
1936 .into_iter()
1937 .flat_map(|(_, _, c)| c.into_iter().map(|c| c.text))
1938 .collect()
1939 };
1940 let expected = text(&good);
1941 assert!(!expected.is_empty(), "control must produce text");
1942 assert_eq!(text(&broken), expected);
1943 }
1944
1945 #[test]
1949 fn repair_declines_when_padding_would_move_objects() {
1950 let mut incremental = pdf_with_xref(false);
1951 incremental.extend_from_slice(b"6 0 obj<</Type/Whatever>>endobj\n");
1952 let declined = super::pad_short_xref_entries(&incremental).unwrap_err();
1953 assert!(
1954 declined.contains("object follows the xref"),
1955 "reason: {declined}"
1956 );
1957 }
1958}
1959
1960#[cfg(test)]
1961mod overpainted {
1962 use crate::pdfium_backend::TextCell;
1963
1964 fn cell(text: &str, l: f32, t: f32, r: f32, b: f32) -> TextCell {
1965 TextCell {
1966 text: text.into(),
1967 l,
1968 t,
1969 r,
1970 b,
1971 }
1972 }
1973
1974 #[test]
1978 fn stacked_logo_glyphs_are_dropped() {
1979 let mut cells = vec![
1980 cell("\"", 72.7, 21.5, 86.4, 31.5),
1981 cell("==", 59.4, 21.5, 99.6, 31.5),
1982 cell("Herr", 65.2, 151.3, 81.7, 161.3),
1983 ];
1984 super::drop_overpainted_cells(&mut cells);
1985 assert_eq!(cells.len(), 1, "cells: {cells:?}");
1986 assert_eq!(cells[0].text, "Herr");
1987 }
1988
1989 #[test]
1993 fn prose_and_double_draw_are_kept() {
1994 let mut cells = vec![
1995 cell("Telefon", 354.3, 133.2, 381.5, 143.2),
1996 cell("0676/2000", 387.3, 133.2, 428.7, 143.2),
1997 cell("Bold", 100.0, 50.0, 130.0, 60.0),
1998 cell("Bold", 100.3, 50.0, 130.3, 60.0),
1999 ];
2000 super::drop_overpainted_cells(&mut cells);
2001 assert_eq!(cells.len(), 4);
2002 }
2003}
2004
2005#[cfg(test)]
2006mod vestigial_layer {
2007 use crate::pdfium_backend::{PdfPage, TextCell};
2008
2009 fn page_with(texts: &[&str]) -> PdfPage {
2010 let cells = texts
2011 .iter()
2012 .enumerate()
2013 .map(|(i, t)| TextCell {
2014 text: t.to_string(),
2015 l: 10.0,
2016 t: 10.0 + 12.0 * i as f32,
2017 r: 90.0,
2018 b: 20.0 + 12.0 * i as f32,
2019 })
2020 .collect();
2021 PdfPage::from_cells(595.0, 842.0, 1.0, cells)
2022 }
2023
2024 #[test]
2029 fn typed_in_form_fields_are_not_a_text_layer() {
2030 let pages = vec![
2031 page_with(&["03", "05", "2025"]),
2032 page_with(&[]),
2033 page_with(&[]),
2034 ];
2035 assert!(super::text_layer_is_vestigial(&pages));
2036 assert!(super::text_layer_is_vestigial(&[page_with(&[])]));
2037 }
2038
2039 #[test]
2042 fn sparse_but_real_documents_pass() {
2043 let one_pager = vec![page_with(&[
2044 "Confidential briefing",
2045 "Prepared for the board meeting",
2046 "Do not distribute",
2047 ])];
2048 assert!(!super::text_layer_is_vestigial(&one_pager));
2049 }
2050}