1use std::collections::HashMap;
17use std::sync::Arc;
18
19use lopdf::{Dictionary, Document, Object};
20
21use crate::pdfium_backend::Glyph;
22
23#[derive(Default)]
32struct DocCaches {
33 fonts: HashMap<(lopdf::ObjectId, Vec<u8>), Arc<Font>>,
34 forms: HashMap<lopdf::ObjectId, Arc<lopdf::content::Content>>,
35}
36
37#[derive(Clone, Copy)]
39struct Mat {
40 a: f64,
41 b: f64,
42 c: f64,
43 d: f64,
44 e: f64,
45 f: f64,
46}
47
48impl Mat {
49 const ID: Mat = Mat {
50 a: 1.0,
51 b: 0.0,
52 c: 0.0,
53 d: 1.0,
54 e: 0.0,
55 f: 0.0,
56 };
57
58 fn then(self, m: Mat) -> Mat {
60 Mat {
61 a: self.a * m.a + self.b * m.c,
62 b: self.a * m.b + self.b * m.d,
63 c: self.c * m.a + self.d * m.c,
64 d: self.c * m.b + self.d * m.d,
65 e: self.e * m.a + self.f * m.c + m.e,
66 f: self.e * m.b + self.f * m.d + m.f,
67 }
68 }
69
70 fn apply(self, x: f64, y: f64) -> (f64, f64) {
71 (
72 self.a * x + self.c * y + self.e,
73 self.b * x + self.d * y + self.f,
74 )
75 }
76}
77
78struct Font {
80 two_byte: bool,
82 to_unicode: HashMap<u32, String>,
84 widths: HashMap<u32, f64>,
86 default_width: f64,
87 simple_encoding: Option<HashMap<u8, char>>,
89 fallback_names: HashMap<u8, String>,
93 program_encoding: HashMap<u8, char>,
97 ascent: f64,
98 descent: f64,
99 hash: u64,
100}
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
645#[derive(Debug, Clone, Copy, PartialEq)]
660pub(crate) struct PageBox {
661 pub l: f32,
663 pub b: f32,
665 pub w: f32,
666 pub h: f32,
667}
668
669impl PageBox {
670 pub fn top(&self) -> f32 {
672 self.b + self.h
673 }
674}
675
676fn inherited_rect(
679 doc: &Document,
680 page_id: lopdf::ObjectId,
681 key: &[u8],
682) -> Option<(f32, f32, f32, f32)> {
683 let mut id = page_id;
684 for _ in 0..32 {
685 let dict = doc.get_object(id).ok()?.as_dict().ok()?;
686 if let Some(Object::Array(a)) = dict.get(key).ok().and_then(|o| deref(doc, o)) {
687 let v: Vec<f32> = a.iter().filter_map(|o| num(o).map(|x| x as f32)).collect();
688 if v.len() == 4 && v.iter().all(|x| x.is_finite()) {
689 return Some((
690 v[0].min(v[2]),
691 v[1].min(v[3]),
692 v[0].max(v[2]),
693 v[1].max(v[3]),
694 ));
695 }
696 return None;
697 }
698 id = dict.get(b"Parent").ok()?.as_reference().ok()?;
699 }
700 None
701}
702
703pub(crate) fn page_box(doc: &Document, page_id: lopdf::ObjectId) -> PageBox {
704 let nonempty = |r: &(f32, f32, f32, f32)| r.2 > r.0 && r.3 > r.1;
705 let media = inherited_rect(doc, page_id, b"MediaBox")
706 .filter(nonempty)
707 .unwrap_or((0.0, 0.0, 612.0, 792.0));
708 let crop = inherited_rect(doc, page_id, b"CropBox")
709 .map(|c| {
710 (
711 c.0.max(media.0),
712 c.1.max(media.1),
713 c.2.min(media.2),
714 c.3.min(media.3),
715 )
716 })
717 .filter(nonempty)
718 .unwrap_or(media);
719 PageBox {
720 l: crop.0,
721 b: crop.1,
722 w: crop.2 - crop.0,
723 h: crop.3 - crop.1,
724 }
725}
726
727fn page_size(doc: &Document, page_id: lopdf::ObjectId) -> (f32, f32) {
730 let pb = page_box(doc, page_id);
731 (pb.w, pb.h)
732}
733
734pub fn content_diagnosis(bytes: &[u8]) -> String {
740 let Some(doc) = load_document(bytes) else {
741 return "document does not load".into();
742 };
743 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
744 pages.sort_by_key(|(n, _)| *n);
745 let mut out = String::new();
746 let mut caches = DocCaches::default();
747 for (n, pid) in pages.into_iter().take(4) {
748 let content_bytes = doc.get_page_content(pid);
749 let ops = lopdf::content::Content::decode(&content_bytes)
750 .map(|c| c.operations.len())
751 .ok();
752 let res = page_res(&doc, pid);
753 let fonts = res.map(|r| fonts_from_res(&doc, r, &mut caches).len());
754 let glyphs = page_glyphs_cached(&doc, pid, &mut caches).len();
755 out.push_str(&format!(
756 "\n page {n}: content {} B, ops {}, resources {}, fonts {}, glyphs {}",
757 content_bytes.len(),
758 ops.map_or("UNDECODABLE".to_string(), |n| n.to_string()),
759 if res.is_some() { "ok" } else { "MISSING" },
760 fonts.map_or("-".to_string(), |n| n.to_string()),
761 glyphs,
762 ));
763 }
764 out
765}
766
767pub fn text_layer_is_vestigial(pages: &[crate::pdfium_backend::PdfPage]) -> bool {
780 let lines: usize = pages.iter().map(|p| p.cells.len()).sum();
781 if lines == 0 {
782 return true;
783 }
784 let chars: usize = pages
785 .iter()
786 .flat_map(|p| &p.cells)
787 .map(|c| c.text.chars().count())
788 .sum();
789 lines <= pages.len() && chars < 32
790}
791
792pub fn xref_repair_status(bytes: &[u8]) -> String {
796 if Document::load_mem(bytes).is_ok() {
797 return "loads unaided; no repair needed".into();
798 }
799 match pad_short_xref_entries(bytes) {
800 Ok(fixed) => match Document::load_mem(&fixed) {
801 Ok(_) => "repaired: cross-reference entries padded to 20 bytes".into(),
802 Err(e) => format!("padded the entries, but it still will not load: {e}"),
803 },
804 Err(why) => format!("repair declined — {why}"),
805 }
806}
807
808fn load_document(bytes: &[u8]) -> Option<Document> {
823 let mut fallback = None;
828 if let Some(doc) = best_effort_load(bytes, &mut fallback) {
829 return Some(doc);
830 }
831 let xref_fixed = pad_short_xref_entries(bytes).ok();
832 if let Some(fixed) = &xref_fixed {
833 if let Some(doc) = best_effort_load(fixed, &mut fallback) {
834 return Some(doc);
835 }
836 }
837 let lengths_fixed = fix_stream_lengths(xref_fixed.as_deref().unwrap_or(bytes));
840 if let Some(doc) = best_effort_load(&lengths_fixed, &mut fallback) {
841 return Some(doc);
842 }
843 fallback
844}
845
846fn best_effort_load(data: &[u8], fallback: &mut Option<Document>) -> Option<Document> {
849 match Document::load_mem(data) {
850 Ok(doc) if has_page_content(&doc) => Some(doc),
851 Ok(doc) => {
852 fallback.get_or_insert(doc);
853 None
854 }
855 Err(_) => None,
856 }
857}
858
859fn has_page_content(doc: &Document) -> bool {
863 doc.get_pages()
864 .into_values()
865 .take(4)
866 .any(|pid| !doc.get_page_content(pid).is_empty())
867}
868
869fn fix_stream_lengths(bytes: &[u8]) -> Vec<u8> {
882 let mut out = bytes.to_vec();
883 let mut i = 0;
884 while let Some(rel) = find(&out[i..], b"stream") {
885 let kw = i + rel;
886 i = kw + 6;
887 if kw >= 3 && &out[kw - 3..kw] == b"end" {
889 continue;
890 }
891 let mut data = kw + 6;
893 if out.get(data..data + 2) == Some(b"\r\n".as_slice()) {
894 data += 2;
895 } else if matches!(out.get(data), Some(b'\n' | b'\r')) {
896 data += 1;
897 }
898 let Some(end) = find(&out[data..], b"endstream").map(|r| data + r) else {
899 continue;
900 };
901 let dict_start = out[..kw].iter().rposition(|&c| c == b'<').unwrap_or(0);
903 let Some(lrel) = find(&out[dict_start..kw], b"/Length") else {
904 continue;
905 };
906 let mut d = dict_start + lrel + 7;
907 while matches!(out.get(d), Some(b' ')) {
908 d += 1;
909 }
910 let digits = out[d..].iter().take_while(|c| c.is_ascii_digit()).count();
911 if digits == 0 {
912 continue;
913 }
914 let declared: usize = match std::str::from_utf8(&out[d..d + digits])
915 .ok()
916 .and_then(|s| s.parse().ok())
917 {
918 Some(v) => v,
919 None => continue,
920 };
921 let actual = end - data;
922 let replacement = actual.to_string();
925 if actual == declared || replacement.len() > digits {
926 continue;
927 }
928 out[d..d + digits].fill(b' ');
929 out[d..d + replacement.len()].copy_from_slice(replacement.as_bytes());
930 }
931 out
932}
933
934fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
935 haystack.windows(needle.len()).position(|w| w == needle)
936}
937
938fn pad_short_xref_entries(bytes: &[u8]) -> Result<Vec<u8>, &'static str> {
941 let is_boundary = |i: usize| i == 0 || matches!(bytes[i - 1], b'\n' | b'\r');
944 let mut starts = (0..bytes.len().saturating_sub(4))
945 .filter(|&i| &bytes[i..i + 4] == b"xref" && is_boundary(i));
946 let xref_at = starts
947 .next()
948 .ok_or("no classic `xref` section (an xref stream?)")?;
949 if starts.next().is_some() {
950 return Err("more than one xref section (incremental update)");
951 }
952 let last_obj = bytes
953 .windows(3)
954 .rposition(|w| w == b"obj")
955 .ok_or("no objects found")?;
956 if last_obj > xref_at {
957 return Err("an object follows the xref — padding would move it");
958 }
959
960 let mut out = bytes[..xref_at].to_vec();
961 out.extend_from_slice(b"xref\n");
962 let mut i = xref_at + 4;
963 let skip_ws = |i: &mut usize| {
964 while matches!(bytes.get(*i), Some(b'\r' | b'\n' | b' ')) {
965 *i += 1;
966 }
967 };
968 loop {
969 skip_ws(&mut i);
970 if bytes[i..].starts_with(b"trailer") {
972 out.extend_from_slice(&bytes[i..]);
973 return Ok(out);
974 }
975 let header_end = i + bytes[i..]
976 .iter()
977 .position(|c| matches!(c, b'\n' | b'\r'))
978 .ok_or("subsection header runs off the end")?;
979 let header = std::str::from_utf8(&bytes[i..header_end])
980 .map_err(|_| "subsection header is not text")?
981 .trim();
982 let mut parts = header.split_whitespace();
983 let count: usize = parts
984 .nth(1)
985 .and_then(|c| c.parse().ok())
986 .ok_or("unparseable subsection header")?;
987 if parts.next().is_some() || count == 0 {
988 return Err("unexpected subsection header shape");
989 }
990 out.extend_from_slice(header.as_bytes());
991 out.push(b'\n');
992 i = header_end;
993 for _ in 0..count {
994 skip_ws(&mut i);
995 let entry = bytes.get(i..i + 18).ok_or("xref entry runs off the end")?;
997 let well_formed = entry[..10].iter().all(u8::is_ascii_digit)
998 && entry[10] == b' '
999 && entry[11..16].iter().all(u8::is_ascii_digit)
1000 && entry[16] == b' '
1001 && matches!(entry[17], b'n' | b'f');
1002 if !well_formed {
1003 return Err("xref entry is not `nnnnnnnnnn ggggg n`");
1004 }
1005 out.extend_from_slice(entry);
1006 out.extend_from_slice(b" \n"); i += 18;
1008 }
1009 }
1010}
1011
1012pub fn debug_glyphs(bytes: &[u8], index: usize) -> Vec<(char, f32, f32, f32, f32)> {
1015 let Some(doc) = load_document(bytes) else {
1016 return Vec::new();
1017 };
1018 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
1019 pages.sort_by_key(|(n, _)| *n);
1020 let Some((_, pid)) = pages.get(index) else {
1021 return Vec::new();
1022 };
1023 page_glyphs(&doc, *pid)
1024 .into_iter()
1025 .map(|g| (g.ch, g.ll, g.lr, g.lb, g.lt))
1026 .collect()
1027}
1028
1029pub fn pdf_textlines(bytes: &[u8]) -> Vec<(f32, f32, Vec<crate::pdfium_backend::TextCell>)> {
1033 let Some(doc) = load_document(bytes) else {
1034 return Vec::new();
1035 };
1036 let mut caches = DocCaches::default();
1037 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
1038 pages.sort_by_key(|(n, _)| *n);
1039 pages
1040 .into_iter()
1041 .map(|(_, pid)| {
1042 let (w, h) = page_size(&doc, pid);
1043 let glyphs = page_glyphs_cached(&doc, pid, &mut caches);
1044 let cells = crate::dp_lines::line_cells(&glyphs, h, true);
1045 (w, h, cells)
1046 })
1047 .collect()
1048}
1049
1050pub fn pdf_words(bytes: &[u8]) -> Vec<(f32, f32, Vec<crate::pdfium_backend::TextCell>)> {
1055 let Some(doc) = load_document(bytes) else {
1056 return Vec::new();
1057 };
1058 let mut caches = DocCaches::default();
1059 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
1060 pages.sort_by_key(|(n, _)| *n);
1061 pages
1062 .into_iter()
1063 .map(|(_, pid)| {
1064 let (w, h) = page_size(&doc, pid);
1065 let glyphs = page_glyphs_cached(&doc, pid, &mut caches);
1066 let cells = crate::dp_lines::word_cells(&glyphs, h, true);
1067 (w, h, cells)
1068 })
1069 .collect()
1070}
1071
1072#[derive(Default)]
1076pub struct PageParserCells {
1077 pub prose: Vec<crate::pdfium_backend::TextCell>,
1078 pub words: Vec<crate::pdfium_backend::TextCell>,
1079 pub code: Vec<crate::pdfium_backend::TextCell>,
1080}
1081
1082pub struct PageTextParser {
1095 doc: Document,
1096 caches: DocCaches,
1097 pages: Vec<lopdf::ObjectId>,
1099}
1100
1101impl PageTextParser {
1102 pub fn open(bytes: &[u8]) -> Option<Self> {
1105 let doc = load_document(bytes)?;
1106 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
1107 pages.sort_by_key(|(n, _)| *n);
1108 Some(Self {
1109 doc,
1110 caches: DocCaches::default(),
1111 pages: pages.into_iter().map(|(_, pid)| pid).collect(),
1112 })
1113 }
1114
1115 pub fn cells(&mut self, index: usize) -> PageParserCells {
1119 let Some(&pid) = self.pages.get(index) else {
1120 return PageParserCells::default();
1121 };
1122 let (_w, h) = page_size(&self.doc, pid);
1123 let glyphs = page_glyphs_cached(&self.doc, pid, &mut self.caches);
1124 let (prose, words) = crate::dp_lines::line_and_word_cells(&glyphs, h, true);
1125 PageParserCells {
1126 prose,
1127 words,
1128 code: crate::pdfium_backend::code_cells_from_glyphs(&glyphs, h),
1129 }
1130 }
1131}
1132
1133pub fn pdf_all_cells(bytes: &[u8]) -> Vec<PageParserCells> {
1138 let Some(mut parser) = PageTextParser::open(bytes) else {
1139 return Vec::new();
1140 };
1141 (0..parser.pages.len()).map(|i| parser.cells(i)).collect()
1142}
1143
1144pub fn pdf_text_pages(bytes: &[u8]) -> Vec<crate::pdfium_backend::PdfPage> {
1151 let Some(doc) = load_document(bytes) else {
1152 return Vec::new();
1153 };
1154 let mut caches = DocCaches::default();
1155 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
1156 pages.sort_by_key(|(n, _)| *n);
1157 pages
1158 .into_iter()
1159 .map(|(_, pid)| {
1160 let (w, h) = page_size(&doc, pid);
1161 let glyphs = page_glyphs_cached(&doc, pid, &mut caches);
1162 let (mut prose, mut words) = crate::dp_lines::line_and_word_cells(&glyphs, h, true);
1163 drop_overpainted_cells(&mut prose);
1164 drop_overpainted_cells(&mut words);
1165 crate::pdfium_backend::PdfPage {
1166 #[cfg(feature = "ocr-prep")]
1167 image_layout: None,
1168 width: w,
1169 height: h,
1170 scale: 1.0,
1172 cells: prose,
1173 code_cells: crate::pdfium_backend::code_cells_from_glyphs(&glyphs, h),
1174 word_cells: words,
1175 #[cfg(feature = "ocr-prep")]
1176 image: image::RgbImage::new(1, 1),
1177 links: Vec::new(),
1178 rotation: 0,
1179 }
1180 })
1181 .collect()
1182}
1183
1184fn drop_overpainted_cells(cells: &mut Vec<crate::pdfium_backend::TextCell>) {
1203 let mut paint = vec![false; cells.len()];
1204 for i in 0..cells.len() {
1205 for j in 0..cells.len() {
1206 if i == j || cells[i].text == cells[j].text {
1207 continue;
1208 }
1209 let (a, b) = (&cells[i], &cells[j]);
1210 let vo = (a.b.min(b.b) - a.t.max(b.t)).max(0.0);
1212 if vo < 0.6 * (a.b - a.t).min(b.b - b.t) {
1213 continue;
1214 }
1215 let ho = (a.r.min(b.r) - a.l.max(b.l)).max(0.0);
1217 if ho >= 0.8 * (a.r - a.l) && (a.r - a.l) <= (b.r - b.l) {
1218 paint[i] = true;
1219 paint[j] = true;
1220 }
1221 }
1222 }
1223 let mut keep = paint.iter().map(|p| !p);
1224 cells.retain(|_| keep.next().unwrap());
1225}
1226
1227#[derive(Clone, Copy)]
1231struct TextState {
1232 tc: f64,
1233 tw: f64,
1234 th: f64,
1235 tl: f64,
1236 trise: f64,
1237 fsize: f64,
1238}
1239
1240impl TextState {
1241 const INIT: TextState = TextState {
1242 tc: 0.0,
1243 tw: 0.0,
1244 th: 1.0,
1245 tl: 0.0,
1246 trise: 0.0,
1247 fsize: 0.0,
1248 };
1249}
1250
1251fn page_res(doc: &Document, page_id: lopdf::ObjectId) -> Option<&Dictionary> {
1254 let (inline, ids) = doc.get_page_resources(page_id).ok()?;
1255 if let Some(d) = inline {
1256 return Some(d);
1257 }
1258 ids.into_iter().find_map(|id| doc.get_dictionary(id).ok())
1259}
1260
1261fn fonts_from_res(
1265 doc: &Document,
1266 res: &Dictionary,
1267 caches: &mut DocCaches,
1268) -> HashMap<Vec<u8>, Arc<Font>> {
1269 let mut map = HashMap::new();
1270 let font_dict = res
1271 .get(b"Font")
1272 .ok()
1273 .and_then(|o| deref(doc, o))
1274 .and_then(|o| o.as_dict().ok());
1275 if let Some(fd) = font_dict {
1276 for (name, value) in fd.iter() {
1277 let font = match value {
1278 Object::Reference(id) => {
1279 let key = (*id, name.clone());
1280 if let Some(f) = caches.fonts.get(&key) {
1281 Arc::clone(f)
1282 } else if let Some(fdict) = deref(doc, value).and_then(|o| o.as_dict().ok()) {
1283 let f = Arc::new(parse_font(doc, name, fdict));
1284 caches.fonts.insert(key, Arc::clone(&f));
1285 f
1286 } else {
1287 continue;
1288 }
1289 }
1290 _ => {
1291 if let Some(fdict) = deref(doc, value).and_then(|o| o.as_dict().ok()) {
1292 Arc::new(parse_font(doc, name, fdict))
1293 } else {
1294 continue;
1295 }
1296 }
1297 };
1298 map.insert(name.clone(), font);
1299 }
1300 }
1301 map
1302}
1303
1304pub(crate) fn page_glyphs(doc: &Document, page_id: lopdf::ObjectId) -> Vec<Glyph> {
1306 page_glyphs_cached(doc, page_id, &mut DocCaches::default())
1307}
1308
1309fn page_glyphs_cached(
1312 doc: &Document,
1313 page_id: lopdf::ObjectId,
1314 caches: &mut DocCaches,
1315) -> Vec<Glyph> {
1316 let mut out = Vec::new();
1317 let content_bytes = doc.get_page_content(page_id);
1320 let Ok(content) = lopdf::content::Content::decode(&content_bytes) else {
1321 return out;
1322 };
1323 if let Some(res) = page_res(doc, page_id) {
1324 let pb = page_box(doc, page_id);
1327 let base = Mat {
1328 e: -(pb.l as f64),
1329 f: -(pb.b as f64),
1330 ..Mat::ID
1331 };
1332 run_content(
1333 doc,
1334 res,
1335 &content,
1336 base,
1337 TextState::INIT,
1338 0,
1339 caches,
1340 &mut out,
1341 );
1342 }
1343 out
1344}
1345
1346#[allow(clippy::too_many_arguments)]
1351fn run_content(
1352 doc: &Document,
1353 res: &Dictionary,
1354 content: &lopdf::content::Content,
1355 base_ctm: Mat,
1356 init: TextState,
1357 depth: u32,
1358 caches: &mut DocCaches,
1359 out: &mut Vec<Glyph>,
1360) {
1361 let fonts = fonts_from_res(doc, res, caches);
1362 let xobjects = res
1363 .get(b"XObject")
1364 .ok()
1365 .and_then(|o| deref(doc, o))
1366 .and_then(|o| o.as_dict().ok());
1367
1368 #[allow(clippy::type_complexity)]
1373 let mut gstate_stack: Vec<(Mat, f64, f64, f64, f64, f64, f64, Option<&Arc<Font>>)> = Vec::new();
1374 let mut ctm = base_ctm;
1375 let mut tm = Mat::ID;
1376 let mut tlm = Mat::ID;
1377 let mut font: Option<&Arc<Font>> = None;
1378 let mut fsize = init.fsize;
1379 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;
1384
1385 let op_f = |operands: &[Object], i: usize| operands.get(i).and_then(num).unwrap_or(0.0);
1386
1387 for op in &content.operations {
1388 let operands = &op.operands;
1389 match op.operator.as_str() {
1390 "q" => gstate_stack.push((ctm, tc, tw, th, tl, trise, fsize, font)),
1391 "Q" => {
1392 if let Some((c, a, b, h, l, r, fs, f)) = gstate_stack.pop() {
1393 ctm = c;
1394 tc = a;
1395 tw = b;
1396 th = h;
1397 tl = l;
1398 trise = r;
1399 fsize = fs;
1400 font = f;
1401 }
1402 }
1403 "cm" => {
1404 let m = Mat {
1405 a: op_f(operands, 0),
1406 b: op_f(operands, 1),
1407 c: op_f(operands, 2),
1408 d: op_f(operands, 3),
1409 e: op_f(operands, 4),
1410 f: op_f(operands, 5),
1411 };
1412 ctm = m.then(ctm);
1413 }
1414 "BT" => {
1415 tm = Mat::ID;
1416 tlm = Mat::ID;
1417 }
1418 "ET" => {}
1419 "Tf" => {
1420 if let Some(Object::Name(n)) = operands.first() {
1421 font = fonts.get(n.as_slice());
1422 }
1423 fsize = op_f(operands, 1);
1424 }
1425 "Td" => {
1426 tlm = Mat {
1427 a: 1.0,
1428 b: 0.0,
1429 c: 0.0,
1430 d: 1.0,
1431 e: op_f(operands, 0),
1432 f: op_f(operands, 1),
1433 }
1434 .then(tlm);
1435 tm = tlm;
1436 }
1437 "TD" => {
1438 tl = -op_f(operands, 1);
1439 tlm = Mat {
1440 a: 1.0,
1441 b: 0.0,
1442 c: 0.0,
1443 d: 1.0,
1444 e: op_f(operands, 0),
1445 f: op_f(operands, 1),
1446 }
1447 .then(tlm);
1448 tm = tlm;
1449 }
1450 "Tm" => {
1451 tlm = Mat {
1452 a: op_f(operands, 0),
1453 b: op_f(operands, 1),
1454 c: op_f(operands, 2),
1455 d: op_f(operands, 3),
1456 e: op_f(operands, 4),
1457 f: op_f(operands, 5),
1458 };
1459 tm = tlm;
1460 }
1461 "T*" => {
1462 tlm = Mat {
1463 a: 1.0,
1464 b: 0.0,
1465 c: 0.0,
1466 d: 1.0,
1467 e: 0.0,
1468 f: -tl,
1469 }
1470 .then(tlm);
1471 tm = tlm;
1472 }
1473 "Tc" => tc = op_f(operands, 0),
1474 "Tw" => tw = op_f(operands, 0),
1475 "Tz" => th = op_f(operands, 0) / 100.0,
1476 "TL" => tl = op_f(operands, 0),
1477 "Ts" => trise = op_f(operands, 0),
1478 "Tj" | "'" | "\"" => {
1479 if op.operator == "'" || op.operator == "\"" {
1480 tlm = Mat {
1482 a: 1.0,
1483 b: 0.0,
1484 c: 0.0,
1485 d: 1.0,
1486 e: 0.0,
1487 f: -tl,
1488 }
1489 .then(tlm);
1490 tm = tlm;
1491 }
1492 if op.operator == "\"" {
1493 tw = op_f(operands, 0);
1496 tc = op_f(operands, 1);
1497 }
1498 if let (Some(f), Some(Object::String(s, _))) = (font, operands.last()) {
1499 show_text(f, s, fsize, tc, tw, th, trise, &mut tm, ctm, out);
1500 }
1501 }
1502 "TJ" => {
1503 if let (Some(f), Some(Object::Array(arr))) = (font, operands.first()) {
1504 for el in arr {
1505 match el {
1506 Object::String(s, _) => {
1507 show_text(f, s, fsize, tc, tw, th, trise, &mut tm, ctm, out)
1508 }
1509 other => {
1510 if let Some(adj) = num(other) {
1511 let tx = -adj / 1000.0 * fsize * th;
1513 tm = Mat {
1514 a: 1.0,
1515 b: 0.0,
1516 c: 0.0,
1517 d: 1.0,
1518 e: tx,
1519 f: 0.0,
1520 }
1521 .then(tm);
1522 }
1523 }
1524 }
1525 }
1526 }
1527 }
1528 "Do" => {
1529 if depth >= 8 {
1532 continue;
1533 }
1534 let Some(Object::Name(n)) = operands.first() else {
1535 continue;
1536 };
1537 let obj = xobjects.and_then(|d| d.get(n.as_slice()).ok());
1538 let form_id = match obj {
1539 Some(Object::Reference(id)) => Some(*id),
1540 _ => None,
1541 };
1542 let stream = obj
1543 .and_then(|o| deref(doc, o))
1544 .and_then(|o| o.as_stream().ok());
1545 let Some(stream) = stream else { continue };
1546 let is_form = stream
1547 .dict
1548 .get(b"Subtype")
1549 .ok()
1550 .and_then(|o| o.as_name().ok())
1551 == Some(b"Form".as_slice());
1552 if !is_form {
1553 continue;
1554 }
1555 let cached = form_id.and_then(|id| caches.forms.get(&id).cloned());
1558 let form_content = match cached {
1559 Some(c) => c,
1560 None => {
1561 let Ok(data) = stream.decompressed_content() else {
1562 continue;
1563 };
1564 let Ok(c) = lopdf::content::Content::decode(&data) else {
1565 continue;
1566 };
1567 let c = Arc::new(c);
1568 if let Some(id) = form_id {
1569 caches.forms.insert(id, Arc::clone(&c));
1570 }
1571 c
1572 }
1573 };
1574 let form_mat = match stream.dict.get(b"Matrix").ok() {
1576 Some(Object::Array(a)) if a.len() == 6 => {
1577 let v: Vec<f64> = a.iter().filter_map(num).collect();
1578 if v.len() == 6 {
1579 Mat {
1580 a: v[0],
1581 b: v[1],
1582 c: v[2],
1583 d: v[3],
1584 e: v[4],
1585 f: v[5],
1586 }
1587 } else {
1588 Mat::ID
1589 }
1590 }
1591 _ => Mat::ID,
1592 };
1593 let form_res = stream
1595 .dict
1596 .get(b"Resources")
1597 .ok()
1598 .and_then(|o| deref(doc, o))
1599 .and_then(|o| o.as_dict().ok())
1600 .unwrap_or(res);
1601 let state = TextState {
1602 tc,
1603 tw,
1604 th,
1605 tl,
1606 trise,
1607 fsize,
1608 };
1609 run_content(
1610 doc,
1611 form_res,
1612 &form_content,
1613 form_mat.then(ctm),
1614 state,
1615 depth + 1,
1616 caches,
1617 out,
1618 );
1619 }
1620 _ => {}
1621 }
1622 }
1623}
1624
1625#[allow(clippy::too_many_arguments)]
1626fn show_text(
1627 font: &Font,
1628 bytes: &[u8],
1629 fsize: f64,
1630 tc: f64,
1631 tw: f64,
1632 th: f64,
1633 trise: f64,
1634 tm: &mut Mat,
1635 ctm: Mat,
1636 out: &mut Vec<Glyph>,
1637) {
1638 for code in codes(font, bytes) {
1639 let (text, w) = font.decode_code(code);
1640 let w0 = w / 1000.0; let scale = Mat {
1643 a: fsize * th,
1644 b: 0.0,
1645 c: 0.0,
1646 d: fsize,
1647 e: 0.0,
1648 f: trise,
1649 };
1650 let trm = scale.then(*tm).then(ctm);
1651 let (x0, y0) = trm.apply(0.0, font.descent / 1000.0);
1653 let (x1, _y1) = trm.apply(w0, font.descent / 1000.0);
1654 let (_x2, y2) = trm.apply(0.0, font.ascent / 1000.0);
1655 let (left, right) = (x0.min(x1), x0.max(x1));
1656 let (bot, top) = (y0.min(y2), y0.max(y2));
1657 if let Some(s) = text {
1658 for ch in s.chars() {
1660 if ch != '\u{0}' {
1661 out.push(Glyph {
1662 ch,
1663 l: left as f32,
1664 b: bot as f32,
1665 r: right as f32,
1666 t: top as f32,
1667 ll: left as f32,
1668 lb: bot as f32,
1669 lr: right as f32,
1670 lt: top as f32,
1671 font: font.hash,
1672 });
1673 }
1674 }
1675 }
1676 let is_space = !font.two_byte && code == 32;
1678 let tx = (w0 * fsize + tc + if is_space { tw } else { 0.0 }) * th;
1679 *tm = Mat {
1680 a: 1.0,
1681 b: 0.0,
1682 c: 0.0,
1683 d: 1.0,
1684 e: tx,
1685 f: 0.0,
1686 }
1687 .then(*tm);
1688 }
1689}
1690
1691fn simple_encoding_table(doc: &Document, fdict: &Dictionary) -> HashMap<u8, char> {
1695 let enc = fdict.get(b"Encoding").ok().and_then(|o| deref(doc, o));
1696 let base_name = match enc {
1697 Some(Object::Name(n)) => n.clone(),
1698 Some(Object::Dictionary(d)) => d
1699 .get(b"BaseEncoding")
1700 .ok()
1701 .and_then(|o| o.as_name().ok())
1702 .map(|n| n.to_vec())
1703 .unwrap_or_default(),
1704 _ => Vec::new(),
1705 };
1706 let mut m = if base_name == b"MacRomanEncoding" {
1707 macroman_table()
1708 } else if base_name.is_empty() {
1709 tex_math_builtin(fdict).unwrap_or_else(winansi_table)
1716 } else {
1717 winansi_table()
1718 };
1719 if let Some(Object::Dictionary(d)) = enc {
1721 if let Some(Object::Array(diffs)) = d.get(b"Differences").ok().and_then(|o| deref(doc, o)) {
1722 let mut code = 0u8;
1723 for el in diffs {
1724 match el {
1725 Object::Integer(i) => code = *i as u8,
1726 Object::Name(name) => {
1727 if let Some(ch) = glyph_name_to_char(name) {
1728 m.insert(code, ch);
1729 }
1730 code = code.wrapping_add(1);
1731 }
1732 _ => {}
1733 }
1734 }
1735 }
1736 }
1737 m
1738}
1739
1740fn tex_math_builtin(fdict: &Dictionary) -> Option<HashMap<u8, char>> {
1746 const CMSY: [char; 128] = [
1747 '−', '·', '×', '∗', '÷', '⋄', '±', '∓', '⊕', '⊖', '⊗', '⊘', '⊙', '◯', '∘', '•', '≍', '≡',
1748 '⊆', '⊇', '≤', '≥', '≼', '≽', '∼', '≈', '⊂', '⊃', '≪', '≫', '≺', '≻', '←', '→', '↑', '↓',
1749 '↔', '↗', '↘', '≃', '⇐', '⇒', '⇑', '⇓', '⇔', '↖', '↙', '∝', '′', '∞', '∈', '∋', '△', '▽',
1750 '\u{338}', '↦', '∀', '∃', '¬', '∅', 'ℜ', 'ℑ', '⊤', '⊥', 'ℵ', 'A', 'B', 'C', 'D', 'E', 'F',
1751 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
1752 'Y', 'Z', '∪', '∩', '⊎', '∧', '∨', '⊢', '⊣', '⌊', '⌋', '⌈', '⌉', '{', '}', '⟨', '⟩', '|',
1753 '∥', '↕', '⇕', '\\', '≀', '√', '∐', '∇', '∫', '⊔', '⊓', '⊑', '⊒', '§', '†', '‡', '¶', '♣',
1754 '♢', '♡', '♠',
1755 ];
1756 const CMMI: [char; 128] = [
1757 'Γ', 'Δ', 'Θ', 'Λ', 'Ξ', 'Π', 'Σ', 'Υ', 'Φ', 'Ψ', 'Ω', 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η',
1758 'θ', 'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'π', 'ρ', 'σ', 'τ', 'υ', 'φ', 'χ', 'ψ', 'ω', 'ϵ', 'ϑ',
1759 'ϖ', 'ϱ', 'ς', 'ϕ', '↼', '↽', '⇀', '⇁', '↩', '↪', '▷', '◁', '0', '1', '2', '3', '4', '5',
1760 '6', '7', '8', '9', '.', ',', '<', '/', '>', '⋆', '∂', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
1761 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y',
1762 'Z', '♭', '♮', '♯', '⌣', '⌢', 'ℓ', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
1763 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'ı', 'ȷ', '℘',
1764 '\u{20d7}', '⁀',
1765 ];
1766 let name = base_font_name(fdict)?;
1767 let up = name.to_ascii_uppercase();
1768 let table: &[char; 128] = if up.starts_with(b"CMSY") || up.starts_with(b"CMBSY") {
1769 &CMSY
1770 } else if up.starts_with(b"CMMI") {
1771 &CMMI
1772 } else {
1773 return None;
1774 };
1775 Some(
1776 table
1777 .iter()
1778 .enumerate()
1779 .map(|(i, &c)| (i as u8, c))
1780 .collect(),
1781 )
1782}
1783
1784fn glyph_name_to_char(name: &[u8]) -> Option<char> {
1789 let s = std::str::from_utf8(name).ok()?;
1790 if let Some(hex) = s.strip_prefix("uni") {
1791 if let Ok(cp) = u32::from_str_radix(hex.get(0..4)?, 16) {
1792 return char::from_u32(cp);
1793 }
1794 }
1795 if s.len() == 1 {
1797 let b = s.as_bytes()[0];
1798 if b.is_ascii_alphabetic() {
1799 return Some(b as char);
1800 }
1801 }
1802 let resolved = match s {
1803 "space" => ' ',
1804 "exclam" => '!',
1805 "quotedbl" => '"',
1806 "numbersign" => '#',
1807 "dollar" => '$',
1808 "percent" => '%',
1809 "ampersand" => '&',
1810 "quotesingle" => '\'',
1811 "parenleft" => '(',
1812 "parenright" => ')',
1813 "asterisk" => '*',
1814 "plus" => '+',
1815 "comma" => ',',
1816 "hyphen" => '-',
1817 "period" => '.',
1818 "slash" => '/',
1819 "zero" => '0',
1820 "one" => '1',
1821 "two" => '2',
1822 "three" => '3',
1823 "four" => '4',
1824 "five" => '5',
1825 "six" => '6',
1826 "seven" => '7',
1827 "eight" => '8',
1828 "nine" => '9',
1829 "colon" => ':',
1830 "semicolon" => ';',
1831 "less" => '<',
1832 "equal" => '=',
1833 "greater" => '>',
1834 "question" => '?',
1835 "at" => '@',
1836 "bracketleft" => '[',
1837 "backslash" => '\\',
1838 "bracketright" => ']',
1839 "asciicircum" => '^',
1840 "underscore" => '_',
1841 "grave" => '`',
1842 "braceleft" => '{',
1843 "bar" => '|',
1844 "braceright" => '}',
1845 "asciitilde" => '~',
1846 "bullet" => '\u{2022}',
1847 "periodcentered" => '\u{00B7}',
1848 "endash" => '\u{2013}',
1849 "emdash" => '\u{2014}',
1850 "quoteright" => '\u{2019}',
1851 "quoteleft" => '\u{2018}',
1852 "quotedblleft" => '\u{201C}',
1853 "quotedblright" => '\u{201D}',
1854 "quotedblbase" => '\u{201E}',
1855 "quotesinglbase" => '\u{201A}',
1856 "ff" => '\u{FB00}',
1861 "fi" => '\u{FB01}',
1862 "fl" => '\u{FB02}',
1863 "ffi" => '\u{FB03}',
1864 "ffl" => '\u{FB04}',
1865 "ft" => '\u{FB05}',
1866 "st" => '\u{FB06}',
1867 "degree" => '\u{00B0}',
1868 "trademark" => '\u{2122}',
1869 "registered" => '\u{00AE}',
1870 "copyright" => '\u{00A9}',
1871 "ellipsis" => '\u{2026}',
1872 "minus" => '\u{2212}',
1873 "fraction" => '\u{2044}',
1874 "nbspace" => '\u{00A0}',
1875 "alpha" => '\u{03B1}',
1880 "beta" => '\u{03B2}',
1881 "gamma" => '\u{03B3}',
1882 "delta" => '\u{03B4}',
1883 "epsilon" | "epsilon1" => '\u{03B5}',
1884 "zeta" => '\u{03B6}',
1885 "eta" => '\u{03B7}',
1886 "theta" | "theta1" => '\u{03B8}',
1887 "iota" => '\u{03B9}',
1888 "kappa" => '\u{03BA}',
1889 "lambda" => '\u{03BB}',
1890 "mu" => '\u{03BC}',
1891 "nu" => '\u{03BD}',
1892 "xi" => '\u{03BE}',
1893 "omicron" => '\u{03BF}',
1894 "pi" | "pi1" => '\u{03C0}',
1895 "rho" | "rho1" => '\u{03C1}',
1896 "sigma" => '\u{03C3}',
1897 "sigma1" => '\u{03C2}',
1898 "tau" => '\u{03C4}',
1899 "upsilon" => '\u{03C5}',
1900 "phi" | "phi1" => '\u{03C6}',
1901 "chi" => '\u{03C7}',
1902 "psi" => '\u{03C8}',
1903 "omega" | "omega1" => '\u{03C9}',
1904 "Gamma" => '\u{0393}',
1905 "Delta" => '\u{0394}',
1906 "Theta" => '\u{0398}',
1907 "Lambda" => '\u{039B}',
1908 "Xi" => '\u{039E}',
1909 "Pi" => '\u{03A0}',
1910 "Sigma" => '\u{03A3}',
1911 "Upsilon" => '\u{03A5}',
1912 "Phi" => '\u{03A6}',
1913 "Psi" => '\u{03A8}',
1914 "Omega" => '\u{03A9}',
1915 "lessequal" => '\u{2264}',
1916 "greaterequal" => '\u{2265}',
1917 "notequal" => '\u{2260}',
1918 "approxequal" => '\u{2248}',
1919 "equivalence" => '\u{2261}',
1920 "element" => '\u{2208}',
1921 "plusminus" => '\u{00B1}',
1922 "multiply" => '\u{00D7}',
1923 "divide" => '\u{00F7}',
1924 "infinity" => '\u{221E}',
1925 "partialdiff" => '\u{2202}',
1926 "gradient" => '\u{2207}',
1927 "summation" => '\u{2211}',
1928 "product" => '\u{220F}',
1929 "integral" => '\u{222B}',
1930 "radical" => '\u{221A}',
1931 "proportional" => '\u{221D}',
1932 "arrowright" => '\u{2192}',
1933 "arrowleft" => '\u{2190}',
1934 "arrowup" => '\u{2191}',
1935 "arrowdown" => '\u{2193}',
1936 "arrowboth" => '\u{2194}',
1937 "arrowdblright" => '\u{21D2}',
1938 "logicaland" => '\u{2227}',
1939 "logicalor" => '\u{2228}',
1940 "intersection" => '\u{2229}',
1941 "union" => '\u{222A}',
1942 "similar" => '\u{223C}',
1943 "congruent" => '\u{2245}',
1944 "dotmath" => '\u{22C5}',
1945 "asteriskmath" => '\u{2217}',
1946 _ => {
1947 if let Some((base, _)) = s.split_once('.') {
1949 if !base.is_empty() {
1950 return glyph_name_to_char(base.as_bytes());
1951 }
1952 }
1953 return None;
1954 }
1955 };
1956 Some(resolved)
1957}
1958
1959fn winansi_table() -> HashMap<u8, char> {
1961 let mut m = HashMap::new();
1962 for b in 0x20u8..=0x7e {
1963 m.insert(b, b as char);
1964 }
1965 let extra: &[(u8, char)] = &[
1967 (0x91, '\u{2018}'),
1968 (0x92, '\u{2019}'),
1969 (0x93, '\u{201C}'),
1970 (0x94, '\u{201D}'),
1971 (0x95, '\u{2022}'),
1972 (0x96, '\u{2013}'),
1973 (0x97, '\u{2014}'),
1974 (0x85, '\u{2026}'),
1975 (0xA0, '\u{00A0}'),
1976 ];
1977 for &(b, c) in extra {
1978 m.insert(b, c);
1979 }
1980 for b in 0xA1u8..=0xFF {
1981 m.entry(b).or_insert(b as char);
1982 }
1983 m
1984}
1985
1986fn macroman_table() -> HashMap<u8, char> {
1989 let mut m = HashMap::new();
1990 for b in 0x20u8..=0x7e {
1991 m.insert(b, b as char);
1992 }
1993 let high: &[(u8, char)] = &[
1994 (0xA5, '\u{2022}'), (0xD0, '\u{2013}'), (0xD1, '\u{2014}'), (0xD2, '\u{201C}'),
1998 (0xD3, '\u{201D}'),
1999 (0xD4, '\u{2018}'),
2000 (0xD5, '\u{2019}'),
2001 (0xCA, '\u{00A0}'),
2002 (0xC9, '\u{2026}'),
2003 (0xDE, '\u{FB01}'),
2004 (0xDF, '\u{FB02}'),
2005 ];
2006 for &(b, c) in high {
2007 m.insert(b, c);
2008 }
2009 m
2010}
2011
2012#[cfg(test)]
2013mod page_box_frame {
2014 use super::*;
2015
2016 fn pdf(boxes: &str, x: f32, y: f32) -> Vec<u8> {
2019 let content = format!("BT /F1 12 Tf {x} {y} Td (First printing) Tj ET\n");
2020 let objs: Vec<String> = vec![
2021 "<</Type/Catalog/Pages 2 0 R>>".into(),
2022 format!("<</Type/Pages/Kids[3 0 R]/Count 1{boxes}>>"),
2023 "<</Type/Page/Parent 2 0 R/Contents 4 0 R/Resources<</Font<</F1 5 0 R>>>>>>".into(),
2024 format!("<</Length {}>>stream\n{content}endstream", content.len()),
2025 "<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>".into(),
2026 ];
2027 let mut out = b"%PDF-1.4\n".to_vec();
2028 let mut offsets = Vec::new();
2029 for (i, body) in objs.iter().enumerate() {
2030 offsets.push(out.len());
2031 out.extend_from_slice(format!("{} 0 obj{body}endobj\n", i + 1).as_bytes());
2032 }
2033 let xref_at = out.len();
2034 out.extend_from_slice(
2035 format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes(),
2036 );
2037 for off in &offsets {
2038 out.extend_from_slice(format!("{off:010} 00000 n \n").as_bytes());
2039 }
2040 out.extend_from_slice(
2041 format!(
2042 "trailer<</Size {}/Root 1 0 R>>\nstartxref\n{xref_at}\n%%EOF\n",
2043 objs.len() + 1
2044 )
2045 .as_bytes(),
2046 );
2047 out
2048 }
2049
2050 fn only_page(bytes: &[u8]) -> (PageBox, Vec<Glyph>) {
2051 let doc = load_document(bytes).expect("loads");
2052 let pid = *doc.get_pages().values().next().expect("one page");
2053 (page_box(&doc, pid), page_glyphs(&doc, pid))
2054 }
2055
2056 #[test]
2061 fn glyphs_count_from_the_cropbox_corner_like_pdfium() {
2062 let (pb, shifted) = only_page(&pdf(
2063 "/MediaBox[-56.505 -58.25 576.303 723.31]/CropBox[1.095 -0.65 518.703 665.71]",
2064 37.0 + 1.095,
2065 58.0 - 0.65,
2066 ));
2067 assert!((pb.l - 1.095).abs() < 1e-3 && (pb.b + 0.65).abs() < 1e-3);
2068 assert!(
2069 (pb.w - 517.608).abs() < 1e-3 && (pb.h - 666.36).abs() < 1e-3,
2070 "{pb:?}"
2071 );
2072 let (pb0, plain) = only_page(&pdf("/MediaBox[0 0 517.608 666.36]", 37.0, 58.0));
2073 assert!((pb0.w - pb.w).abs() < 1e-3 && (pb0.h - pb.h).abs() < 1e-3);
2074 assert_eq!(shifted.len(), plain.len());
2075 assert!(!plain.is_empty());
2076 for (a, b) in shifted.iter().zip(&plain) {
2077 assert!(
2078 (a.l - b.l).abs() < 1e-3 && (a.b - b.b).abs() < 1e-3,
2079 "{:?} vs {:?}",
2080 (a.l, a.b),
2081 (b.l, b.b)
2082 );
2083 }
2084 assert!((plain[0].l - 37.0).abs() < 1e-3, "{}", plain[0].l);
2085 }
2086
2087 #[test]
2090 fn page_box_follows_pdfium_fallbacks() {
2091 let (pb, _) = only_page(&pdf("", 10.0, 10.0));
2092 assert_eq!((pb.l, pb.b, pb.w, pb.h), (0.0, 0.0, 612.0, 792.0));
2093 let (pb, _) = only_page(&pdf(
2094 "/MediaBox[0 0 500 700]/CropBox[-100 100 600 900]",
2095 10.0,
2096 10.0,
2097 ));
2098 assert_eq!((pb.l, pb.b, pb.w, pb.h), (0.0, 100.0, 500.0, 600.0));
2099 let (pb, _) = only_page(&pdf(
2100 "/MediaBox[0 0 500 700]/CropBox[800 800 900 900]",
2101 10.0,
2102 10.0,
2103 ));
2104 assert_eq!((pb.l, pb.b, pb.w, pb.h), (0.0, 0.0, 500.0, 700.0));
2105 let (pb, _) = only_page(&pdf("/MediaBox[500 700 0 0]", 10.0, 10.0));
2107 assert_eq!((pb.l, pb.b, pb.w, pb.h), (0.0, 0.0, 500.0, 700.0));
2108 }
2109}
2110
2111#[cfg(test)]
2112mod xref_repair {
2113 fn pdf_with_xref(two_byte_eol: bool) -> Vec<u8> {
2117 let content = b"BT /F1 12 Tf 72 700 Td (Invoice 922769430725) Tj ET\n";
2118 let stream = format!("<</Length {}>>stream\n", content.len()).into_bytes();
2119 let objs: Vec<Vec<u8>> = vec![
2120 b"<</Type/Catalog/Pages 2 0 R>>".to_vec(),
2121 b"<</Type/Pages/Kids[3 0 R]/Count 1>>".to_vec(),
2122 b"<</Type/Page/Parent 2 0 R/MediaBox[0 0 595 842]/Contents 4 0 R\
2123 /Resources<</Font<</F1 5 0 R>>>>>>"
2124 .to_vec(),
2125 [stream.as_slice(), content.as_slice(), b"endstream"].concat(),
2126 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>".to_vec(),
2127 ];
2128
2129 let mut out = b"%PDF-1.4\n".to_vec();
2130 let mut offsets = Vec::new();
2131 for (i, body) in objs.iter().enumerate() {
2132 offsets.push(out.len());
2133 out.extend_from_slice(format!("{} 0 obj", i + 1).as_bytes());
2134 out.extend_from_slice(body);
2135 out.extend_from_slice(b"endobj\n");
2136 }
2137 let xref_at = out.len();
2138 let eol: &[u8] = if two_byte_eol { b" \n" } else { b"\n" };
2139 out.extend_from_slice(format!("xref\n0 {}\n", objs.len() + 1).as_bytes());
2140 out.extend_from_slice(b"0000000000 65535 f");
2141 out.extend_from_slice(eol);
2142 for off in &offsets {
2143 out.extend_from_slice(format!("{off:010} 00000 n").as_bytes());
2144 out.extend_from_slice(eol);
2145 }
2146 out.extend_from_slice(
2147 format!("trailer<</Size {}/Root 1 0 R>>\n", objs.len() + 1).as_bytes(),
2148 );
2149 out.extend_from_slice(format!("startxref\n{xref_at}\n%%EOF\n").as_bytes());
2150 out
2151 }
2152
2153 #[test]
2159 fn short_xref_entries_still_parse() {
2160 let good = pdf_with_xref(true);
2161 let broken = pdf_with_xref(false);
2162 assert!(
2163 broken.len() < good.len(),
2164 "the broken file is the shorter one"
2165 );
2166 assert!(
2167 lopdf::Document::load_mem(&good).is_ok(),
2168 "the control file must load unaided"
2169 );
2170 assert!(
2171 lopdf::Document::load_mem(&broken).is_err(),
2172 "lopdf rejects 19-byte entries — if this ever passes, drop the repair"
2173 );
2174
2175 let cells = |b: &[u8]| -> Vec<String> {
2176 super::pdf_textlines(b)
2177 .into_iter()
2178 .flat_map(|(_, _, c)| c.into_iter().map(|c| c.text))
2179 .collect()
2180 };
2181 let from_good = cells(&good);
2182 assert!(
2183 from_good.iter().any(|t| t.contains("922769430725")),
2184 "control text: {from_good:?}"
2185 );
2186 assert_eq!(
2187 cells(&broken),
2188 from_good,
2189 "repair must match the good parse"
2190 );
2191 }
2192
2193 #[test]
2198 fn overstated_stream_length_still_yields_content() {
2199 let good = pdf_with_xref(true);
2200 let broken = {
2203 let at = good
2204 .windows(8)
2205 .position(|w| w == b"/Length ")
2206 .expect("a /Length")
2207 + 8;
2208 let digits = good[at..].iter().take_while(|c| c.is_ascii_digit()).count();
2209 let n: usize = std::str::from_utf8(&good[at..at + digits])
2210 .unwrap()
2211 .parse()
2212 .unwrap();
2213 let inflated = (n + 1).to_string();
2214 assert_eq!(inflated.len(), digits, "keep the digit count");
2215 let mut b = good.clone();
2216 b[at..at + digits].copy_from_slice(inflated.as_bytes());
2217 b
2218 };
2219 assert_eq!(broken.len(), good.len(), "the defect must not move bytes");
2220 let raw = lopdf::Document::load_mem(&broken).expect("still loads");
2222 assert!(
2223 raw.get_pages()
2224 .into_values()
2225 .all(|p| raw.get_page_content(p).is_empty()),
2226 "lopdf should drop the stream — if it stops, drop this repair"
2227 );
2228 let text = |b: &[u8]| -> Vec<String> {
2230 super::pdf_textlines(b)
2231 .into_iter()
2232 .flat_map(|(_, _, c)| c.into_iter().map(|c| c.text))
2233 .collect()
2234 };
2235 let expected = text(&good);
2236 assert!(!expected.is_empty(), "control must produce text");
2237 assert_eq!(text(&broken), expected);
2238 }
2239
2240 #[test]
2244 fn repair_declines_when_padding_would_move_objects() {
2245 let mut incremental = pdf_with_xref(false);
2246 incremental.extend_from_slice(b"6 0 obj<</Type/Whatever>>endobj\n");
2247 let declined = super::pad_short_xref_entries(&incremental).unwrap_err();
2248 assert!(
2249 declined.contains("object follows the xref"),
2250 "reason: {declined}"
2251 );
2252 }
2253}
2254
2255#[cfg(test)]
2261mod base14_fonts {
2262 fn pdf_with_font(fontdict: &[u8], text: &[u8]) -> Vec<u8> {
2264 let content = [b"BT /F1 12 Tf 72 700 Td (".as_slice(), text, b") Tj ET\n"].concat();
2265 let stream = format!("<</Length {}>>stream\n", content.len()).into_bytes();
2266 let objs: Vec<Vec<u8>> = vec![
2267 b"<</Type/Catalog/Pages 2 0 R>>".to_vec(),
2268 b"<</Type/Pages/Kids[3 0 R]/Count 1>>".to_vec(),
2269 b"<</Type/Page/Parent 2 0 R/MediaBox[0 0 595 842]/Contents 4 0 R\
2270 /Resources<</Font<</F1 5 0 R>>>>>>"
2271 .to_vec(),
2272 [stream.as_slice(), content.as_slice(), b"endstream"].concat(),
2273 fontdict.to_vec(),
2274 ];
2275 let mut out = b"%PDF-1.4\n".to_vec();
2276 let mut offsets = Vec::new();
2277 for (i, body) in objs.iter().enumerate() {
2278 offsets.push(out.len());
2279 out.extend_from_slice(format!("{} 0 obj", i + 1).as_bytes());
2280 out.extend_from_slice(body);
2281 out.extend_from_slice(b"endobj\n");
2282 }
2283 let xref_at = out.len();
2284 out.extend_from_slice(format!("xref\n0 {}\n", objs.len() + 1).as_bytes());
2285 out.extend_from_slice(b"0000000000 65535 f \n");
2286 for off in &offsets {
2287 out.extend_from_slice(format!("{off:010} 00000 n \n").as_bytes());
2288 }
2289 out.extend_from_slice(
2290 format!("trailer<</Size {}/Root 1 0 R>>\n", objs.len() + 1).as_bytes(),
2291 );
2292 out.extend_from_slice(format!("startxref\n{xref_at}\n%%EOF\n").as_bytes());
2293 out
2294 }
2295
2296 fn cells(pdf: &[u8]) -> Vec<crate::pdfium_backend::TextCell> {
2298 super::pdf_textlines(pdf)
2299 .into_iter()
2300 .flat_map(|(_, _, c)| c)
2301 .collect()
2302 }
2303
2304 #[test]
2306 fn standard14_faces_get_builtin_widths() {
2307 for fontdict in [
2308 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/WinAnsiEncoding>>".as_slice(),
2310 b"<</Type/Font/Subtype/Type1/BaseFont/Times-BoldItalic>>",
2312 b"<</Type/Font/Subtype/TrueType/BaseFont/Arial,Bold>>",
2314 b"<</Type/Font/Subtype/Type1/BaseFont/ABCDEF+Courier-Oblique>>",
2315 ] {
2316 let pdf = pdf_with_font(fontdict, b"Words have width now");
2317 let cs = cells(&pdf);
2318 let text: String = cs
2319 .iter()
2320 .map(|c| c.text.as_str())
2321 .collect::<Vec<_>>()
2322 .join(" ");
2323 assert!(
2324 text.contains("Words have width now"),
2325 "{}: text lost: {text:?}",
2326 String::from_utf8_lossy(fontdict)
2327 );
2328 assert!(
2329 cs.iter().all(|c| c.r > c.l),
2330 "{}: zero-width cells: {cs:?}",
2331 String::from_utf8_lossy(fontdict)
2332 );
2333 }
2334 }
2335
2336 #[test]
2339 fn explicit_widths_win_and_unknown_faces_are_untouched() {
2340 let explicit = pdf_with_font(
2344 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/FirstChar 65\
2345 /Widths[100 100 100 100]/Encoding/WinAnsiEncoding>>",
2346 b"ABBA",
2347 );
2348 let builtin = pdf_with_font(
2349 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/WinAnsiEncoding>>",
2350 b"ABBA",
2351 );
2352 let w = |pdf: &[u8]| {
2353 let cs = cells(pdf);
2354 assert_eq!(cs.len(), 1, "one word cell: {cs:?}");
2355 cs[0].r - cs[0].l
2356 };
2357 let (we, wb) = (w(&explicit), w(&builtin));
2358 assert!(
2359 (we - 4.8).abs() < 0.1,
2360 "explicit widths must win: got {we}, want 4×100×12/1000"
2361 );
2362 assert!(
2363 wb > 2.0 * we,
2364 "built-in Helvetica is much wider: {wb} vs {we}"
2365 );
2366
2367 let unknown = pdf_with_font(
2371 b"<</Type/Font/Subtype/Type1/BaseFont/FancyCorp-Display>>",
2372 b"Mystery",
2373 );
2374 let cs = cells(&unknown);
2375 let text: String = cs.iter().map(|c| c.text.as_str()).collect();
2376 assert!(text.contains("Mystery"), "text still decodes: {cs:?}");
2377 }
2378}
2379
2380#[cfg(test)]
2381mod overpainted {
2382 use crate::pdfium_backend::TextCell;
2383
2384 fn cell(text: &str, l: f32, t: f32, r: f32, b: f32) -> TextCell {
2385 TextCell {
2386 text: text.into(),
2387 l,
2388 t,
2389 r,
2390 b,
2391 }
2392 }
2393
2394 #[test]
2398 fn stacked_logo_glyphs_are_dropped() {
2399 let mut cells = vec![
2400 cell("\"", 72.7, 21.5, 86.4, 31.5),
2401 cell("==", 59.4, 21.5, 99.6, 31.5),
2402 cell("Herr", 65.2, 151.3, 81.7, 161.3),
2403 ];
2404 super::drop_overpainted_cells(&mut cells);
2405 assert_eq!(cells.len(), 1, "cells: {cells:?}");
2406 assert_eq!(cells[0].text, "Herr");
2407 }
2408
2409 #[test]
2413 fn prose_and_double_draw_are_kept() {
2414 let mut cells = vec![
2415 cell("Telefon", 354.3, 133.2, 381.5, 143.2),
2416 cell("0676/2000", 387.3, 133.2, 428.7, 143.2),
2417 cell("Bold", 100.0, 50.0, 130.0, 60.0),
2418 cell("Bold", 100.3, 50.0, 130.3, 60.0),
2419 ];
2420 super::drop_overpainted_cells(&mut cells);
2421 assert_eq!(cells.len(), 4);
2422 }
2423}
2424
2425#[cfg(test)]
2426mod vestigial_layer {
2427 use crate::pdfium_backend::{PdfPage, TextCell};
2428
2429 fn page_with(texts: &[&str]) -> PdfPage {
2430 let cells = texts
2431 .iter()
2432 .enumerate()
2433 .map(|(i, t)| TextCell {
2434 text: t.to_string(),
2435 l: 10.0,
2436 t: 10.0 + 12.0 * i as f32,
2437 r: 90.0,
2438 b: 20.0 + 12.0 * i as f32,
2439 })
2440 .collect();
2441 PdfPage::from_cells(595.0, 842.0, 1.0, cells)
2442 }
2443
2444 #[test]
2449 fn typed_in_form_fields_are_not_a_text_layer() {
2450 let pages = vec![
2451 page_with(&["03", "05", "2025"]),
2452 page_with(&[]),
2453 page_with(&[]),
2454 ];
2455 assert!(super::text_layer_is_vestigial(&pages));
2456 assert!(super::text_layer_is_vestigial(&[page_with(&[])]));
2457 }
2458
2459 #[test]
2462 fn sparse_but_real_documents_pass() {
2463 let one_pager = vec![page_with(&[
2464 "Confidential briefing",
2465 "Prepared for the board meeting",
2466 "Do not distribute",
2467 ])];
2468 assert!(!super::text_layer_is_vestigial(&one_pager));
2469 }
2470}