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 #[cfg(feature = "ocr-prep")]
1066 image_layout: None,
1067 width: w,
1068 height: h,
1069 scale: 1.0,
1071 cells: prose,
1072 code_cells: crate::pdfium_backend::code_cells_from_glyphs(&glyphs, h),
1073 word_cells: words,
1074 #[cfg(feature = "ocr-prep")]
1075 image: image::RgbImage::new(1, 1),
1076 links: Vec::new(),
1077 rotation: 0,
1078 }
1079 })
1080 .collect()
1081}
1082
1083fn drop_overpainted_cells(cells: &mut Vec<crate::pdfium_backend::TextCell>) {
1102 let mut paint = vec![false; cells.len()];
1103 for i in 0..cells.len() {
1104 for j in 0..cells.len() {
1105 if i == j || cells[i].text == cells[j].text {
1106 continue;
1107 }
1108 let (a, b) = (&cells[i], &cells[j]);
1109 let vo = (a.b.min(b.b) - a.t.max(b.t)).max(0.0);
1111 if vo < 0.6 * (a.b - a.t).min(b.b - b.t) {
1112 continue;
1113 }
1114 let ho = (a.r.min(b.r) - a.l.max(b.l)).max(0.0);
1116 if ho >= 0.8 * (a.r - a.l) && (a.r - a.l) <= (b.r - b.l) {
1117 paint[i] = true;
1118 paint[j] = true;
1119 }
1120 }
1121 }
1122 let mut keep = paint.iter().map(|p| !p);
1123 cells.retain(|_| keep.next().unwrap());
1124}
1125
1126#[derive(Clone, Copy)]
1130struct TextState {
1131 tc: f64,
1132 tw: f64,
1133 th: f64,
1134 tl: f64,
1135 trise: f64,
1136 fsize: f64,
1137}
1138
1139impl TextState {
1140 const INIT: TextState = TextState {
1141 tc: 0.0,
1142 tw: 0.0,
1143 th: 1.0,
1144 tl: 0.0,
1145 trise: 0.0,
1146 fsize: 0.0,
1147 };
1148}
1149
1150fn page_res(doc: &Document, page_id: lopdf::ObjectId) -> Option<&Dictionary> {
1153 let (inline, ids) = doc.get_page_resources(page_id).ok()?;
1154 if let Some(d) = inline {
1155 return Some(d);
1156 }
1157 ids.into_iter().find_map(|id| doc.get_dictionary(id).ok())
1158}
1159
1160fn fonts_from_res(
1164 doc: &Document,
1165 res: &Dictionary,
1166 caches: &mut DocCaches,
1167) -> HashMap<Vec<u8>, Rc<Font>> {
1168 let mut map = HashMap::new();
1169 let font_dict = res
1170 .get(b"Font")
1171 .ok()
1172 .and_then(|o| deref(doc, o))
1173 .and_then(|o| o.as_dict().ok());
1174 if let Some(fd) = font_dict {
1175 for (name, value) in fd.iter() {
1176 let font = match value {
1177 Object::Reference(id) => {
1178 let key = (*id, name.clone());
1179 if let Some(f) = caches.fonts.get(&key) {
1180 Rc::clone(f)
1181 } else if let Some(fdict) = deref(doc, value).and_then(|o| o.as_dict().ok()) {
1182 let f = Rc::new(parse_font(doc, name, fdict));
1183 caches.fonts.insert(key, Rc::clone(&f));
1184 f
1185 } else {
1186 continue;
1187 }
1188 }
1189 _ => {
1190 if let Some(fdict) = deref(doc, value).and_then(|o| o.as_dict().ok()) {
1191 Rc::new(parse_font(doc, name, fdict))
1192 } else {
1193 continue;
1194 }
1195 }
1196 };
1197 map.insert(name.clone(), font);
1198 }
1199 }
1200 map
1201}
1202
1203pub(crate) fn page_glyphs(doc: &Document, page_id: lopdf::ObjectId) -> Vec<Glyph> {
1205 page_glyphs_cached(doc, page_id, &mut DocCaches::default())
1206}
1207
1208fn page_glyphs_cached(
1211 doc: &Document,
1212 page_id: lopdf::ObjectId,
1213 caches: &mut DocCaches,
1214) -> Vec<Glyph> {
1215 let mut out = Vec::new();
1216 let content_bytes = doc.get_page_content(page_id);
1219 let Ok(content) = lopdf::content::Content::decode(&content_bytes) else {
1220 return out;
1221 };
1222 if let Some(res) = page_res(doc, page_id) {
1223 run_content(
1224 doc,
1225 res,
1226 &content,
1227 Mat::ID,
1228 TextState::INIT,
1229 0,
1230 caches,
1231 &mut out,
1232 );
1233 }
1234 out
1235}
1236
1237#[allow(clippy::too_many_arguments)]
1242fn run_content(
1243 doc: &Document,
1244 res: &Dictionary,
1245 content: &lopdf::content::Content,
1246 base_ctm: Mat,
1247 init: TextState,
1248 depth: u32,
1249 caches: &mut DocCaches,
1250 out: &mut Vec<Glyph>,
1251) {
1252 let fonts = fonts_from_res(doc, res, caches);
1253 let xobjects = res
1254 .get(b"XObject")
1255 .ok()
1256 .and_then(|o| deref(doc, o))
1257 .and_then(|o| o.as_dict().ok());
1258
1259 #[allow(clippy::type_complexity)]
1264 let mut gstate_stack: Vec<(Mat, f64, f64, f64, f64, f64, f64, Option<&Rc<Font>>)> = Vec::new();
1265 let mut ctm = base_ctm;
1266 let mut tm = Mat::ID;
1267 let mut tlm = Mat::ID;
1268 let mut font: Option<&Rc<Font>> = None;
1269 let mut fsize = init.fsize;
1270 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;
1275
1276 let op_f = |operands: &[Object], i: usize| operands.get(i).and_then(num).unwrap_or(0.0);
1277
1278 for op in &content.operations {
1279 let operands = &op.operands;
1280 match op.operator.as_str() {
1281 "q" => gstate_stack.push((ctm, tc, tw, th, tl, trise, fsize, font)),
1282 "Q" => {
1283 if let Some((c, a, b, h, l, r, fs, f)) = gstate_stack.pop() {
1284 ctm = c;
1285 tc = a;
1286 tw = b;
1287 th = h;
1288 tl = l;
1289 trise = r;
1290 fsize = fs;
1291 font = f;
1292 }
1293 }
1294 "cm" => {
1295 let m = Mat {
1296 a: op_f(operands, 0),
1297 b: op_f(operands, 1),
1298 c: op_f(operands, 2),
1299 d: op_f(operands, 3),
1300 e: op_f(operands, 4),
1301 f: op_f(operands, 5),
1302 };
1303 ctm = m.then(ctm);
1304 }
1305 "BT" => {
1306 tm = Mat::ID;
1307 tlm = Mat::ID;
1308 }
1309 "ET" => {}
1310 "Tf" => {
1311 if let Some(Object::Name(n)) = operands.first() {
1312 font = fonts.get(n.as_slice());
1313 }
1314 fsize = op_f(operands, 1);
1315 }
1316 "Td" => {
1317 tlm = Mat {
1318 a: 1.0,
1319 b: 0.0,
1320 c: 0.0,
1321 d: 1.0,
1322 e: op_f(operands, 0),
1323 f: op_f(operands, 1),
1324 }
1325 .then(tlm);
1326 tm = tlm;
1327 }
1328 "TD" => {
1329 tl = -op_f(operands, 1);
1330 tlm = Mat {
1331 a: 1.0,
1332 b: 0.0,
1333 c: 0.0,
1334 d: 1.0,
1335 e: op_f(operands, 0),
1336 f: op_f(operands, 1),
1337 }
1338 .then(tlm);
1339 tm = tlm;
1340 }
1341 "Tm" => {
1342 tlm = Mat {
1343 a: op_f(operands, 0),
1344 b: op_f(operands, 1),
1345 c: op_f(operands, 2),
1346 d: op_f(operands, 3),
1347 e: op_f(operands, 4),
1348 f: op_f(operands, 5),
1349 };
1350 tm = tlm;
1351 }
1352 "T*" => {
1353 tlm = Mat {
1354 a: 1.0,
1355 b: 0.0,
1356 c: 0.0,
1357 d: 1.0,
1358 e: 0.0,
1359 f: -tl,
1360 }
1361 .then(tlm);
1362 tm = tlm;
1363 }
1364 "Tc" => tc = op_f(operands, 0),
1365 "Tw" => tw = op_f(operands, 0),
1366 "Tz" => th = op_f(operands, 0) / 100.0,
1367 "TL" => tl = op_f(operands, 0),
1368 "Ts" => trise = op_f(operands, 0),
1369 "Tj" | "'" | "\"" => {
1370 if op.operator == "'" || op.operator == "\"" {
1371 tlm = Mat {
1373 a: 1.0,
1374 b: 0.0,
1375 c: 0.0,
1376 d: 1.0,
1377 e: 0.0,
1378 f: -tl,
1379 }
1380 .then(tlm);
1381 tm = tlm;
1382 }
1383 if op.operator == "\"" {
1384 tw = op_f(operands, 0);
1387 tc = op_f(operands, 1);
1388 }
1389 if let (Some(f), Some(Object::String(s, _))) = (font, operands.last()) {
1390 show_text(f, s, fsize, tc, tw, th, trise, &mut tm, ctm, out);
1391 }
1392 }
1393 "TJ" => {
1394 if let (Some(f), Some(Object::Array(arr))) = (font, operands.first()) {
1395 for el in arr {
1396 match el {
1397 Object::String(s, _) => {
1398 show_text(f, s, fsize, tc, tw, th, trise, &mut tm, ctm, out)
1399 }
1400 other => {
1401 if let Some(adj) = num(other) {
1402 let tx = -adj / 1000.0 * fsize * th;
1404 tm = Mat {
1405 a: 1.0,
1406 b: 0.0,
1407 c: 0.0,
1408 d: 1.0,
1409 e: tx,
1410 f: 0.0,
1411 }
1412 .then(tm);
1413 }
1414 }
1415 }
1416 }
1417 }
1418 }
1419 "Do" => {
1420 if depth >= 8 {
1423 continue;
1424 }
1425 let Some(Object::Name(n)) = operands.first() else {
1426 continue;
1427 };
1428 let obj = xobjects.and_then(|d| d.get(n.as_slice()).ok());
1429 let form_id = match obj {
1430 Some(Object::Reference(id)) => Some(*id),
1431 _ => None,
1432 };
1433 let stream = obj
1434 .and_then(|o| deref(doc, o))
1435 .and_then(|o| o.as_stream().ok());
1436 let Some(stream) = stream else { continue };
1437 let is_form = stream
1438 .dict
1439 .get(b"Subtype")
1440 .ok()
1441 .and_then(|o| o.as_name().ok())
1442 == Some(b"Form".as_slice());
1443 if !is_form {
1444 continue;
1445 }
1446 let cached = form_id.and_then(|id| caches.forms.get(&id).cloned());
1449 let form_content = match cached {
1450 Some(c) => c,
1451 None => {
1452 let Ok(data) = stream.decompressed_content() else {
1453 continue;
1454 };
1455 let Ok(c) = lopdf::content::Content::decode(&data) else {
1456 continue;
1457 };
1458 let c = Rc::new(c);
1459 if let Some(id) = form_id {
1460 caches.forms.insert(id, Rc::clone(&c));
1461 }
1462 c
1463 }
1464 };
1465 let form_mat = match stream.dict.get(b"Matrix").ok() {
1467 Some(Object::Array(a)) if a.len() == 6 => {
1468 let v: Vec<f64> = a.iter().filter_map(num).collect();
1469 if v.len() == 6 {
1470 Mat {
1471 a: v[0],
1472 b: v[1],
1473 c: v[2],
1474 d: v[3],
1475 e: v[4],
1476 f: v[5],
1477 }
1478 } else {
1479 Mat::ID
1480 }
1481 }
1482 _ => Mat::ID,
1483 };
1484 let form_res = stream
1486 .dict
1487 .get(b"Resources")
1488 .ok()
1489 .and_then(|o| deref(doc, o))
1490 .and_then(|o| o.as_dict().ok())
1491 .unwrap_or(res);
1492 let state = TextState {
1493 tc,
1494 tw,
1495 th,
1496 tl,
1497 trise,
1498 fsize,
1499 };
1500 run_content(
1501 doc,
1502 form_res,
1503 &form_content,
1504 form_mat.then(ctm),
1505 state,
1506 depth + 1,
1507 caches,
1508 out,
1509 );
1510 }
1511 _ => {}
1512 }
1513 }
1514}
1515
1516#[allow(clippy::too_many_arguments)]
1517fn show_text(
1518 font: &Font,
1519 bytes: &[u8],
1520 fsize: f64,
1521 tc: f64,
1522 tw: f64,
1523 th: f64,
1524 trise: f64,
1525 tm: &mut Mat,
1526 ctm: Mat,
1527 out: &mut Vec<Glyph>,
1528) {
1529 for code in codes(font, bytes) {
1530 let (text, w) = font.decode_code(code);
1531 let w0 = w / 1000.0; let scale = Mat {
1534 a: fsize * th,
1535 b: 0.0,
1536 c: 0.0,
1537 d: fsize,
1538 e: 0.0,
1539 f: trise,
1540 };
1541 let trm = scale.then(*tm).then(ctm);
1542 let (x0, y0) = trm.apply(0.0, font.descent / 1000.0);
1544 let (x1, _y1) = trm.apply(w0, font.descent / 1000.0);
1545 let (_x2, y2) = trm.apply(0.0, font.ascent / 1000.0);
1546 let (left, right) = (x0.min(x1), x0.max(x1));
1547 let (bot, top) = (y0.min(y2), y0.max(y2));
1548 if let Some(s) = text {
1549 for ch in s.chars() {
1551 if ch != '\u{0}' {
1552 out.push(Glyph {
1553 ch,
1554 l: left as f32,
1555 b: bot as f32,
1556 r: right as f32,
1557 t: top as f32,
1558 ll: left as f32,
1559 lb: bot as f32,
1560 lr: right as f32,
1561 lt: top as f32,
1562 font: font.hash,
1563 });
1564 }
1565 }
1566 }
1567 let is_space = !font.two_byte && code == 32;
1569 let tx = (w0 * fsize + tc + if is_space { tw } else { 0.0 }) * th;
1570 *tm = Mat {
1571 a: 1.0,
1572 b: 0.0,
1573 c: 0.0,
1574 d: 1.0,
1575 e: tx,
1576 f: 0.0,
1577 }
1578 .then(*tm);
1579 }
1580}
1581
1582fn simple_encoding_table(doc: &Document, fdict: &Dictionary) -> HashMap<u8, char> {
1586 let enc = fdict.get(b"Encoding").ok().and_then(|o| deref(doc, o));
1587 let base_name = match enc {
1588 Some(Object::Name(n)) => n.clone(),
1589 Some(Object::Dictionary(d)) => d
1590 .get(b"BaseEncoding")
1591 .ok()
1592 .and_then(|o| o.as_name().ok())
1593 .map(|n| n.to_vec())
1594 .unwrap_or_default(),
1595 _ => Vec::new(),
1596 };
1597 let mut m = if base_name == b"MacRomanEncoding" {
1598 macroman_table()
1599 } else if base_name.is_empty() {
1600 tex_math_builtin(fdict).unwrap_or_else(winansi_table)
1607 } else {
1608 winansi_table()
1609 };
1610 if let Some(Object::Dictionary(d)) = enc {
1612 if let Some(Object::Array(diffs)) = d.get(b"Differences").ok().and_then(|o| deref(doc, o)) {
1613 let mut code = 0u8;
1614 for el in diffs {
1615 match el {
1616 Object::Integer(i) => code = *i as u8,
1617 Object::Name(name) => {
1618 if let Some(ch) = glyph_name_to_char(name) {
1619 m.insert(code, ch);
1620 }
1621 code = code.wrapping_add(1);
1622 }
1623 _ => {}
1624 }
1625 }
1626 }
1627 }
1628 m
1629}
1630
1631fn tex_math_builtin(fdict: &Dictionary) -> Option<HashMap<u8, char>> {
1637 const CMSY: [char; 128] = [
1638 '−', '·', '×', '∗', '÷', '⋄', '±', '∓', '⊕', '⊖', '⊗', '⊘', '⊙', '◯', '∘', '•', '≍', '≡',
1639 '⊆', '⊇', '≤', '≥', '≼', '≽', '∼', '≈', '⊂', '⊃', '≪', '≫', '≺', '≻', '←', '→', '↑', '↓',
1640 '↔', '↗', '↘', '≃', '⇐', '⇒', '⇑', '⇓', '⇔', '↖', '↙', '∝', '′', '∞', '∈', '∋', '△', '▽',
1641 '\u{338}', '↦', '∀', '∃', '¬', '∅', 'ℜ', 'ℑ', '⊤', '⊥', 'ℵ', 'A', 'B', 'C', 'D', 'E', 'F',
1642 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
1643 'Y', 'Z', '∪', '∩', '⊎', '∧', '∨', '⊢', '⊣', '⌊', '⌋', '⌈', '⌉', '{', '}', '⟨', '⟩', '|',
1644 '∥', '↕', '⇕', '\\', '≀', '√', '∐', '∇', '∫', '⊔', '⊓', '⊑', '⊒', '§', '†', '‡', '¶', '♣',
1645 '♢', '♡', '♠',
1646 ];
1647 const CMMI: [char; 128] = [
1648 'Γ', 'Δ', 'Θ', 'Λ', 'Ξ', 'Π', 'Σ', 'Υ', 'Φ', 'Ψ', 'Ω', 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η',
1649 'θ', 'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'π', 'ρ', 'σ', 'τ', 'υ', 'φ', 'χ', 'ψ', 'ω', 'ϵ', 'ϑ',
1650 'ϖ', 'ϱ', 'ς', 'ϕ', '↼', '↽', '⇀', '⇁', '↩', '↪', '▷', '◁', '0', '1', '2', '3', '4', '5',
1651 '6', '7', '8', '9', '.', ',', '<', '/', '>', '⋆', '∂', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
1652 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y',
1653 'Z', '♭', '♮', '♯', '⌣', '⌢', 'ℓ', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
1654 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'ı', 'ȷ', '℘',
1655 '\u{20d7}', '⁀',
1656 ];
1657 let name = base_font_name(fdict)?;
1658 let up = name.to_ascii_uppercase();
1659 let table: &[char; 128] = if up.starts_with(b"CMSY") || up.starts_with(b"CMBSY") {
1660 &CMSY
1661 } else if up.starts_with(b"CMMI") {
1662 &CMMI
1663 } else {
1664 return None;
1665 };
1666 Some(
1667 table
1668 .iter()
1669 .enumerate()
1670 .map(|(i, &c)| (i as u8, c))
1671 .collect(),
1672 )
1673}
1674
1675fn glyph_name_to_char(name: &[u8]) -> Option<char> {
1680 let s = std::str::from_utf8(name).ok()?;
1681 if let Some(hex) = s.strip_prefix("uni") {
1682 if let Ok(cp) = u32::from_str_radix(hex.get(0..4)?, 16) {
1683 return char::from_u32(cp);
1684 }
1685 }
1686 if s.len() == 1 {
1688 let b = s.as_bytes()[0];
1689 if b.is_ascii_alphabetic() {
1690 return Some(b as char);
1691 }
1692 }
1693 let resolved = match s {
1694 "space" => ' ',
1695 "exclam" => '!',
1696 "quotedbl" => '"',
1697 "numbersign" => '#',
1698 "dollar" => '$',
1699 "percent" => '%',
1700 "ampersand" => '&',
1701 "quotesingle" => '\'',
1702 "parenleft" => '(',
1703 "parenright" => ')',
1704 "asterisk" => '*',
1705 "plus" => '+',
1706 "comma" => ',',
1707 "hyphen" => '-',
1708 "period" => '.',
1709 "slash" => '/',
1710 "zero" => '0',
1711 "one" => '1',
1712 "two" => '2',
1713 "three" => '3',
1714 "four" => '4',
1715 "five" => '5',
1716 "six" => '6',
1717 "seven" => '7',
1718 "eight" => '8',
1719 "nine" => '9',
1720 "colon" => ':',
1721 "semicolon" => ';',
1722 "less" => '<',
1723 "equal" => '=',
1724 "greater" => '>',
1725 "question" => '?',
1726 "at" => '@',
1727 "bracketleft" => '[',
1728 "backslash" => '\\',
1729 "bracketright" => ']',
1730 "asciicircum" => '^',
1731 "underscore" => '_',
1732 "grave" => '`',
1733 "braceleft" => '{',
1734 "bar" => '|',
1735 "braceright" => '}',
1736 "asciitilde" => '~',
1737 "bullet" => '\u{2022}',
1738 "periodcentered" => '\u{00B7}',
1739 "endash" => '\u{2013}',
1740 "emdash" => '\u{2014}',
1741 "quoteright" => '\u{2019}',
1742 "quoteleft" => '\u{2018}',
1743 "quotedblleft" => '\u{201C}',
1744 "quotedblright" => '\u{201D}',
1745 "quotedblbase" => '\u{201E}',
1746 "quotesinglbase" => '\u{201A}',
1747 "ff" => '\u{FB00}',
1752 "fi" => '\u{FB01}',
1753 "fl" => '\u{FB02}',
1754 "ffi" => '\u{FB03}',
1755 "ffl" => '\u{FB04}',
1756 "ft" => '\u{FB05}',
1757 "st" => '\u{FB06}',
1758 "degree" => '\u{00B0}',
1759 "trademark" => '\u{2122}',
1760 "registered" => '\u{00AE}',
1761 "copyright" => '\u{00A9}',
1762 "ellipsis" => '\u{2026}',
1763 "minus" => '\u{2212}',
1764 "fraction" => '\u{2044}',
1765 "nbspace" => '\u{00A0}',
1766 "alpha" => '\u{03B1}',
1771 "beta" => '\u{03B2}',
1772 "gamma" => '\u{03B3}',
1773 "delta" => '\u{03B4}',
1774 "epsilon" | "epsilon1" => '\u{03B5}',
1775 "zeta" => '\u{03B6}',
1776 "eta" => '\u{03B7}',
1777 "theta" | "theta1" => '\u{03B8}',
1778 "iota" => '\u{03B9}',
1779 "kappa" => '\u{03BA}',
1780 "lambda" => '\u{03BB}',
1781 "mu" => '\u{03BC}',
1782 "nu" => '\u{03BD}',
1783 "xi" => '\u{03BE}',
1784 "omicron" => '\u{03BF}',
1785 "pi" | "pi1" => '\u{03C0}',
1786 "rho" | "rho1" => '\u{03C1}',
1787 "sigma" => '\u{03C3}',
1788 "sigma1" => '\u{03C2}',
1789 "tau" => '\u{03C4}',
1790 "upsilon" => '\u{03C5}',
1791 "phi" | "phi1" => '\u{03C6}',
1792 "chi" => '\u{03C7}',
1793 "psi" => '\u{03C8}',
1794 "omega" | "omega1" => '\u{03C9}',
1795 "Gamma" => '\u{0393}',
1796 "Delta" => '\u{0394}',
1797 "Theta" => '\u{0398}',
1798 "Lambda" => '\u{039B}',
1799 "Xi" => '\u{039E}',
1800 "Pi" => '\u{03A0}',
1801 "Sigma" => '\u{03A3}',
1802 "Upsilon" => '\u{03A5}',
1803 "Phi" => '\u{03A6}',
1804 "Psi" => '\u{03A8}',
1805 "Omega" => '\u{03A9}',
1806 "lessequal" => '\u{2264}',
1807 "greaterequal" => '\u{2265}',
1808 "notequal" => '\u{2260}',
1809 "approxequal" => '\u{2248}',
1810 "equivalence" => '\u{2261}',
1811 "element" => '\u{2208}',
1812 "plusminus" => '\u{00B1}',
1813 "multiply" => '\u{00D7}',
1814 "divide" => '\u{00F7}',
1815 "infinity" => '\u{221E}',
1816 "partialdiff" => '\u{2202}',
1817 "gradient" => '\u{2207}',
1818 "summation" => '\u{2211}',
1819 "product" => '\u{220F}',
1820 "integral" => '\u{222B}',
1821 "radical" => '\u{221A}',
1822 "proportional" => '\u{221D}',
1823 "arrowright" => '\u{2192}',
1824 "arrowleft" => '\u{2190}',
1825 "arrowup" => '\u{2191}',
1826 "arrowdown" => '\u{2193}',
1827 "arrowboth" => '\u{2194}',
1828 "arrowdblright" => '\u{21D2}',
1829 "logicaland" => '\u{2227}',
1830 "logicalor" => '\u{2228}',
1831 "intersection" => '\u{2229}',
1832 "union" => '\u{222A}',
1833 "similar" => '\u{223C}',
1834 "congruent" => '\u{2245}',
1835 "dotmath" => '\u{22C5}',
1836 "asteriskmath" => '\u{2217}',
1837 _ => {
1838 if let Some((base, _)) = s.split_once('.') {
1840 if !base.is_empty() {
1841 return glyph_name_to_char(base.as_bytes());
1842 }
1843 }
1844 return None;
1845 }
1846 };
1847 Some(resolved)
1848}
1849
1850fn winansi_table() -> HashMap<u8, char> {
1852 let mut m = HashMap::new();
1853 for b in 0x20u8..=0x7e {
1854 m.insert(b, b as char);
1855 }
1856 let extra: &[(u8, char)] = &[
1858 (0x91, '\u{2018}'),
1859 (0x92, '\u{2019}'),
1860 (0x93, '\u{201C}'),
1861 (0x94, '\u{201D}'),
1862 (0x95, '\u{2022}'),
1863 (0x96, '\u{2013}'),
1864 (0x97, '\u{2014}'),
1865 (0x85, '\u{2026}'),
1866 (0xA0, '\u{00A0}'),
1867 ];
1868 for &(b, c) in extra {
1869 m.insert(b, c);
1870 }
1871 for b in 0xA1u8..=0xFF {
1872 m.entry(b).or_insert(b as char);
1873 }
1874 m
1875}
1876
1877fn macroman_table() -> HashMap<u8, char> {
1880 let mut m = HashMap::new();
1881 for b in 0x20u8..=0x7e {
1882 m.insert(b, b as char);
1883 }
1884 let high: &[(u8, char)] = &[
1885 (0xA5, '\u{2022}'), (0xD0, '\u{2013}'), (0xD1, '\u{2014}'), (0xD2, '\u{201C}'),
1889 (0xD3, '\u{201D}'),
1890 (0xD4, '\u{2018}'),
1891 (0xD5, '\u{2019}'),
1892 (0xCA, '\u{00A0}'),
1893 (0xC9, '\u{2026}'),
1894 (0xDE, '\u{FB01}'),
1895 (0xDF, '\u{FB02}'),
1896 ];
1897 for &(b, c) in high {
1898 m.insert(b, c);
1899 }
1900 m
1901}
1902
1903#[cfg(test)]
1904mod xref_repair {
1905 fn pdf_with_xref(two_byte_eol: bool) -> Vec<u8> {
1909 let content = b"BT /F1 12 Tf 72 700 Td (Invoice 922769430725) Tj ET\n";
1910 let stream = format!("<</Length {}>>stream\n", content.len()).into_bytes();
1911 let objs: Vec<Vec<u8>> = vec![
1912 b"<</Type/Catalog/Pages 2 0 R>>".to_vec(),
1913 b"<</Type/Pages/Kids[3 0 R]/Count 1>>".to_vec(),
1914 b"<</Type/Page/Parent 2 0 R/MediaBox[0 0 595 842]/Contents 4 0 R\
1915 /Resources<</Font<</F1 5 0 R>>>>>>"
1916 .to_vec(),
1917 [stream.as_slice(), content.as_slice(), b"endstream"].concat(),
1918 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>".to_vec(),
1919 ];
1920
1921 let mut out = b"%PDF-1.4\n".to_vec();
1922 let mut offsets = Vec::new();
1923 for (i, body) in objs.iter().enumerate() {
1924 offsets.push(out.len());
1925 out.extend_from_slice(format!("{} 0 obj", i + 1).as_bytes());
1926 out.extend_from_slice(body);
1927 out.extend_from_slice(b"endobj\n");
1928 }
1929 let xref_at = out.len();
1930 let eol: &[u8] = if two_byte_eol { b" \n" } else { b"\n" };
1931 out.extend_from_slice(format!("xref\n0 {}\n", objs.len() + 1).as_bytes());
1932 out.extend_from_slice(b"0000000000 65535 f");
1933 out.extend_from_slice(eol);
1934 for off in &offsets {
1935 out.extend_from_slice(format!("{off:010} 00000 n").as_bytes());
1936 out.extend_from_slice(eol);
1937 }
1938 out.extend_from_slice(
1939 format!("trailer<</Size {}/Root 1 0 R>>\n", objs.len() + 1).as_bytes(),
1940 );
1941 out.extend_from_slice(format!("startxref\n{xref_at}\n%%EOF\n").as_bytes());
1942 out
1943 }
1944
1945 #[test]
1951 fn short_xref_entries_still_parse() {
1952 let good = pdf_with_xref(true);
1953 let broken = pdf_with_xref(false);
1954 assert!(
1955 broken.len() < good.len(),
1956 "the broken file is the shorter one"
1957 );
1958 assert!(
1959 lopdf::Document::load_mem(&good).is_ok(),
1960 "the control file must load unaided"
1961 );
1962 assert!(
1963 lopdf::Document::load_mem(&broken).is_err(),
1964 "lopdf rejects 19-byte entries — if this ever passes, drop the repair"
1965 );
1966
1967 let cells = |b: &[u8]| -> Vec<String> {
1968 super::pdf_textlines(b)
1969 .into_iter()
1970 .flat_map(|(_, _, c)| c.into_iter().map(|c| c.text))
1971 .collect()
1972 };
1973 let from_good = cells(&good);
1974 assert!(
1975 from_good.iter().any(|t| t.contains("922769430725")),
1976 "control text: {from_good:?}"
1977 );
1978 assert_eq!(
1979 cells(&broken),
1980 from_good,
1981 "repair must match the good parse"
1982 );
1983 }
1984
1985 #[test]
1990 fn overstated_stream_length_still_yields_content() {
1991 let good = pdf_with_xref(true);
1992 let broken = {
1995 let at = good
1996 .windows(8)
1997 .position(|w| w == b"/Length ")
1998 .expect("a /Length")
1999 + 8;
2000 let digits = good[at..].iter().take_while(|c| c.is_ascii_digit()).count();
2001 let n: usize = std::str::from_utf8(&good[at..at + digits])
2002 .unwrap()
2003 .parse()
2004 .unwrap();
2005 let inflated = (n + 1).to_string();
2006 assert_eq!(inflated.len(), digits, "keep the digit count");
2007 let mut b = good.clone();
2008 b[at..at + digits].copy_from_slice(inflated.as_bytes());
2009 b
2010 };
2011 assert_eq!(broken.len(), good.len(), "the defect must not move bytes");
2012 let raw = lopdf::Document::load_mem(&broken).expect("still loads");
2014 assert!(
2015 raw.get_pages()
2016 .into_values()
2017 .all(|p| raw.get_page_content(p).is_empty()),
2018 "lopdf should drop the stream — if it stops, drop this repair"
2019 );
2020 let text = |b: &[u8]| -> Vec<String> {
2022 super::pdf_textlines(b)
2023 .into_iter()
2024 .flat_map(|(_, _, c)| c.into_iter().map(|c| c.text))
2025 .collect()
2026 };
2027 let expected = text(&good);
2028 assert!(!expected.is_empty(), "control must produce text");
2029 assert_eq!(text(&broken), expected);
2030 }
2031
2032 #[test]
2036 fn repair_declines_when_padding_would_move_objects() {
2037 let mut incremental = pdf_with_xref(false);
2038 incremental.extend_from_slice(b"6 0 obj<</Type/Whatever>>endobj\n");
2039 let declined = super::pad_short_xref_entries(&incremental).unwrap_err();
2040 assert!(
2041 declined.contains("object follows the xref"),
2042 "reason: {declined}"
2043 );
2044 }
2045}
2046
2047#[cfg(test)]
2053mod base14_fonts {
2054 fn pdf_with_font(fontdict: &[u8], text: &[u8]) -> Vec<u8> {
2056 let content = [b"BT /F1 12 Tf 72 700 Td (".as_slice(), text, b") Tj ET\n"].concat();
2057 let stream = format!("<</Length {}>>stream\n", content.len()).into_bytes();
2058 let objs: Vec<Vec<u8>> = vec![
2059 b"<</Type/Catalog/Pages 2 0 R>>".to_vec(),
2060 b"<</Type/Pages/Kids[3 0 R]/Count 1>>".to_vec(),
2061 b"<</Type/Page/Parent 2 0 R/MediaBox[0 0 595 842]/Contents 4 0 R\
2062 /Resources<</Font<</F1 5 0 R>>>>>>"
2063 .to_vec(),
2064 [stream.as_slice(), content.as_slice(), b"endstream"].concat(),
2065 fontdict.to_vec(),
2066 ];
2067 let mut out = b"%PDF-1.4\n".to_vec();
2068 let mut offsets = Vec::new();
2069 for (i, body) in objs.iter().enumerate() {
2070 offsets.push(out.len());
2071 out.extend_from_slice(format!("{} 0 obj", i + 1).as_bytes());
2072 out.extend_from_slice(body);
2073 out.extend_from_slice(b"endobj\n");
2074 }
2075 let xref_at = out.len();
2076 out.extend_from_slice(format!("xref\n0 {}\n", objs.len() + 1).as_bytes());
2077 out.extend_from_slice(b"0000000000 65535 f \n");
2078 for off in &offsets {
2079 out.extend_from_slice(format!("{off:010} 00000 n \n").as_bytes());
2080 }
2081 out.extend_from_slice(
2082 format!("trailer<</Size {}/Root 1 0 R>>\n", objs.len() + 1).as_bytes(),
2083 );
2084 out.extend_from_slice(format!("startxref\n{xref_at}\n%%EOF\n").as_bytes());
2085 out
2086 }
2087
2088 fn cells(pdf: &[u8]) -> Vec<crate::pdfium_backend::TextCell> {
2090 super::pdf_textlines(pdf)
2091 .into_iter()
2092 .flat_map(|(_, _, c)| c)
2093 .collect()
2094 }
2095
2096 #[test]
2098 fn standard14_faces_get_builtin_widths() {
2099 for fontdict in [
2100 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/WinAnsiEncoding>>".as_slice(),
2102 b"<</Type/Font/Subtype/Type1/BaseFont/Times-BoldItalic>>",
2104 b"<</Type/Font/Subtype/TrueType/BaseFont/Arial,Bold>>",
2106 b"<</Type/Font/Subtype/Type1/BaseFont/ABCDEF+Courier-Oblique>>",
2107 ] {
2108 let pdf = pdf_with_font(fontdict, b"Words have width now");
2109 let cs = cells(&pdf);
2110 let text: String = cs
2111 .iter()
2112 .map(|c| c.text.as_str())
2113 .collect::<Vec<_>>()
2114 .join(" ");
2115 assert!(
2116 text.contains("Words have width now"),
2117 "{}: text lost: {text:?}",
2118 String::from_utf8_lossy(fontdict)
2119 );
2120 assert!(
2121 cs.iter().all(|c| c.r > c.l),
2122 "{}: zero-width cells: {cs:?}",
2123 String::from_utf8_lossy(fontdict)
2124 );
2125 }
2126 }
2127
2128 #[test]
2131 fn explicit_widths_win_and_unknown_faces_are_untouched() {
2132 let explicit = pdf_with_font(
2136 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/FirstChar 65\
2137 /Widths[100 100 100 100]/Encoding/WinAnsiEncoding>>",
2138 b"ABBA",
2139 );
2140 let builtin = pdf_with_font(
2141 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/WinAnsiEncoding>>",
2142 b"ABBA",
2143 );
2144 let w = |pdf: &[u8]| {
2145 let cs = cells(pdf);
2146 assert_eq!(cs.len(), 1, "one word cell: {cs:?}");
2147 cs[0].r - cs[0].l
2148 };
2149 let (we, wb) = (w(&explicit), w(&builtin));
2150 assert!(
2151 (we - 4.8).abs() < 0.1,
2152 "explicit widths must win: got {we}, want 4×100×12/1000"
2153 );
2154 assert!(
2155 wb > 2.0 * we,
2156 "built-in Helvetica is much wider: {wb} vs {we}"
2157 );
2158
2159 let unknown = pdf_with_font(
2163 b"<</Type/Font/Subtype/Type1/BaseFont/FancyCorp-Display>>",
2164 b"Mystery",
2165 );
2166 let cs = cells(&unknown);
2167 let text: String = cs.iter().map(|c| c.text.as_str()).collect();
2168 assert!(text.contains("Mystery"), "text still decodes: {cs:?}");
2169 }
2170}
2171
2172#[cfg(test)]
2173mod overpainted {
2174 use crate::pdfium_backend::TextCell;
2175
2176 fn cell(text: &str, l: f32, t: f32, r: f32, b: f32) -> TextCell {
2177 TextCell {
2178 text: text.into(),
2179 l,
2180 t,
2181 r,
2182 b,
2183 }
2184 }
2185
2186 #[test]
2190 fn stacked_logo_glyphs_are_dropped() {
2191 let mut cells = vec![
2192 cell("\"", 72.7, 21.5, 86.4, 31.5),
2193 cell("==", 59.4, 21.5, 99.6, 31.5),
2194 cell("Herr", 65.2, 151.3, 81.7, 161.3),
2195 ];
2196 super::drop_overpainted_cells(&mut cells);
2197 assert_eq!(cells.len(), 1, "cells: {cells:?}");
2198 assert_eq!(cells[0].text, "Herr");
2199 }
2200
2201 #[test]
2205 fn prose_and_double_draw_are_kept() {
2206 let mut cells = vec![
2207 cell("Telefon", 354.3, 133.2, 381.5, 143.2),
2208 cell("0676/2000", 387.3, 133.2, 428.7, 143.2),
2209 cell("Bold", 100.0, 50.0, 130.0, 60.0),
2210 cell("Bold", 100.3, 50.0, 130.3, 60.0),
2211 ];
2212 super::drop_overpainted_cells(&mut cells);
2213 assert_eq!(cells.len(), 4);
2214 }
2215}
2216
2217#[cfg(test)]
2218mod vestigial_layer {
2219 use crate::pdfium_backend::{PdfPage, TextCell};
2220
2221 fn page_with(texts: &[&str]) -> PdfPage {
2222 let cells = texts
2223 .iter()
2224 .enumerate()
2225 .map(|(i, t)| TextCell {
2226 text: t.to_string(),
2227 l: 10.0,
2228 t: 10.0 + 12.0 * i as f32,
2229 r: 90.0,
2230 b: 20.0 + 12.0 * i as f32,
2231 })
2232 .collect();
2233 PdfPage::from_cells(595.0, 842.0, 1.0, cells)
2234 }
2235
2236 #[test]
2241 fn typed_in_form_fields_are_not_a_text_layer() {
2242 let pages = vec![
2243 page_with(&["03", "05", "2025"]),
2244 page_with(&[]),
2245 page_with(&[]),
2246 ];
2247 assert!(super::text_layer_is_vestigial(&pages));
2248 assert!(super::text_layer_is_vestigial(&[page_with(&[])]));
2249 }
2250
2251 #[test]
2254 fn sparse_but_real_documents_pass() {
2255 let one_pager = vec![page_with(&[
2256 "Confidential briefing",
2257 "Prepared for the board meeting",
2258 "Do not distribute",
2259 ])];
2260 assert!(!super::text_layer_is_vestigial(&one_pager));
2261 }
2262}