1use std::collections::HashMap;
17use std::sync::Arc;
18
19use lopdf::{Dictionary, Document, Object};
20
21use crate::pdfium_backend::Glyph;
22
23#[derive(Default)]
32struct DocCaches {
33 fonts: HashMap<(lopdf::ObjectId, Vec<u8>), Arc<Font>>,
34 forms: HashMap<lopdf::ObjectId, Arc<lopdf::content::Content>>,
35}
36
37#[derive(Clone, Copy)]
39struct Mat {
40 a: f64,
41 b: f64,
42 c: f64,
43 d: f64,
44 e: f64,
45 f: f64,
46}
47
48impl Mat {
49 const ID: Mat = Mat {
50 a: 1.0,
51 b: 0.0,
52 c: 0.0,
53 d: 1.0,
54 e: 0.0,
55 f: 0.0,
56 };
57
58 fn then(self, m: Mat) -> Mat {
60 Mat {
61 a: self.a * m.a + self.b * m.c,
62 b: self.a * m.b + self.b * m.d,
63 c: self.c * m.a + self.d * m.c,
64 d: self.c * m.b + self.d * m.d,
65 e: self.e * m.a + self.f * m.c + m.e,
66 f: self.e * m.b + self.f * m.d + m.f,
67 }
68 }
69
70 fn apply(self, x: f64, y: f64) -> (f64, f64) {
71 (
72 self.a * x + self.c * y + self.e,
73 self.b * x + self.d * y + self.f,
74 )
75 }
76}
77
78struct Font {
80 two_byte: bool,
82 to_unicode: HashMap<u32, String>,
84 widths: HashMap<u32, f64>,
86 default_width: f64,
87 simple_encoding: Option<HashMap<u8, char>>,
89 fallback_names: HashMap<u8, String>,
93 program_encoding: HashMap<u8, char>,
97 ascent: f64,
98 descent: f64,
99 hash: u64,
100 style: crate::font_style::FontStyle,
103}
104
105impl Font {
106 fn decode_code(&self, code: u32) -> (Option<String>, f64) {
107 let w = self
108 .widths
109 .get(&code)
110 .copied()
111 .unwrap_or(self.default_width);
112 if let Some(s) = self.to_unicode.get(&code) {
113 return (Some(decompose_ligatures(s)), w);
114 }
115 if !self.two_byte {
116 if let Some(name) = self.fallback_names.get(&(code as u8)) {
119 return (Some(format!("/{name}")), w);
120 }
121 if let Some(enc) = &self.simple_encoding {
122 if let Some(&ch) = enc.get(&(code as u8)) {
123 return (Some(decompose_ligatures(&ch.to_string())), w);
124 }
125 }
126 if let Some(&ch) = self.program_encoding.get(&(code as u8)) {
134 return (Some(decompose_ligatures(&ch.to_string())), w);
135 }
136 }
137 (None, w)
138 }
139}
140
141fn decompose_ligatures(s: &str) -> String {
145 if !s.chars().any(|c| ('\u{FB00}'..='\u{FB06}').contains(&c)) {
146 return s.to_string();
147 }
148 s.chars()
149 .map(|c| {
150 match c {
151 '\u{FB00}' => "ff",
152 '\u{FB01}' => "fi",
153 '\u{FB02}' => "fl",
154 '\u{FB03}' => "ffi",
155 '\u{FB04}' => "ffl",
156 '\u{FB05}' => "ft",
157 '\u{FB06}' => "st",
158 _ => return c.to_string(),
159 }
160 .to_string()
161 })
162 .collect()
163}
164
165fn hash_name(name: &[u8]) -> u64 {
166 use std::hash::{Hash, Hasher};
167 let mut h = std::collections::hash_map::DefaultHasher::new();
168 name.hash(&mut h);
169 h.finish()
170}
171
172fn as_dict<'a>(doc: &'a Document, obj: &'a Object) -> Option<&'a Dictionary> {
174 match obj {
175 Object::Dictionary(d) => Some(d),
176 Object::Reference(id) => doc.get_object(*id).ok().and_then(|o| o.as_dict().ok()),
177 _ => None,
178 }
179}
180
181fn deref<'a>(doc: &'a Document, obj: &'a Object) -> Option<&'a Object> {
182 match obj {
183 Object::Reference(id) => doc.get_object(*id).ok(),
184 other => Some(other),
185 }
186}
187
188fn parse_font(doc: &Document, name: &[u8], fdict: &Dictionary) -> Font {
190 let subtype: &[u8] = fdict
191 .get(b"Subtype")
192 .ok()
193 .and_then(|o| o.as_name().ok())
194 .unwrap_or(&[]);
195 let two_byte = subtype == b"Type0".as_slice();
196
197 let to_unicode = fdict
198 .get(b"ToUnicode")
199 .ok()
200 .and_then(|o| deref(doc, o))
201 .and_then(|o| o.as_stream().ok())
202 .and_then(|s| s.decompressed_content().ok())
203 .map(|data| parse_tounicode(&data))
204 .unwrap_or_default();
205
206 let (mut widths, mut default_width) = if two_byte {
207 cid_widths(doc, fdict)
208 } else {
209 simple_widths(doc, fdict)
210 };
211
212 let simple_encoding = if two_byte {
213 None
214 } else {
215 Some(simple_encoding_table(doc, fdict))
216 };
217
218 if !two_byte && widths.is_empty() && default_width == 0.0 {
226 if let Some(std14) = base_font_name(fdict).and_then(|n| crate::std14::widths_for(&n)) {
227 if let Some(enc) = &simple_encoding {
228 for (&code, &ch) in enc {
229 if let Some(w) = std14.width(ch) {
230 widths.insert(u32::from(code), w);
231 }
232 }
233 }
234 default_width = 500.0;
237 }
238 }
239 let fallback_names = if two_byte {
240 HashMap::new()
241 } else {
242 differences_gid_names(doc, fdict)
243 };
244 let program_encoding = if two_byte {
245 HashMap::new()
246 } else {
247 type1_program_encoding(doc, fdict)
248 };
249
250 let (ascent, descent) = font_ascent_descent(doc, fdict, two_byte);
251
252 Font {
253 two_byte,
254 to_unicode,
255 widths,
256 default_width,
257 simple_encoding,
258 fallback_names,
259 program_encoding,
260 ascent,
261 descent,
262 hash: hash_name(name),
263 style: crate::font_style::parse_font_style(&String::from_utf8_lossy(
264 &base_font_name(fdict).unwrap_or_else(|| name.to_vec()),
265 )),
266 }
267}
268
269fn differences_gid_names(doc: &Document, fdict: &Dictionary) -> HashMap<u8, String> {
276 let mut map = HashMap::new();
277 let Some(Object::Dictionary(enc)) = fdict.get(b"Encoding").ok().and_then(|o| deref(doc, o))
278 else {
279 return map;
280 };
281 let Some(Object::Array(diffs)) = enc.get(b"Differences").ok().and_then(|o| deref(doc, o))
282 else {
283 return map;
284 };
285 let mut code = 0u8;
286 for el in diffs {
287 match el {
288 Object::Integer(i) => code = *i as u8,
289 Object::Name(name) => {
290 if glyph_name_to_char(name).is_none() && is_gid_name(name) {
291 map.insert(code, String::from_utf8_lossy(name).into_owned());
292 }
293 code = code.wrapping_add(1);
294 }
295 _ => {}
296 }
297 }
298 map
299}
300
301fn type1_program_encoding(doc: &Document, fdict: &Dictionary) -> HashMap<u8, char> {
309 let mut map = HashMap::new();
310 let Some(desc) = fdict
311 .get(b"FontDescriptor")
312 .ok()
313 .and_then(|o| deref(doc, o))
314 .and_then(|o| o.as_dict().ok())
315 else {
316 return map;
317 };
318 let Some(data) = desc
319 .get(b"FontFile")
320 .ok()
321 .and_then(|o| deref(doc, o))
322 .and_then(|o| o.as_stream().ok())
323 .and_then(|s| s.decompressed_content().ok())
324 else {
325 return map;
326 };
327 let head_end = data
329 .windows(5)
330 .position(|w| w == b"eexec")
331 .unwrap_or(data.len());
332 let head = String::from_utf8_lossy(&data[..head_end]);
333 let toks: Vec<&str> = head.split_whitespace().collect();
335 for w in toks.windows(4) {
336 if w[0] == "dup" && w[3] == "put" {
337 if let (Ok(code), Some(name)) = (w[1].parse::<u32>(), w[2].strip_prefix('/')) {
338 if code <= 255 {
339 if let Some(ch) = glyph_name_to_char(name.as_bytes()) {
340 map.insert(code as u8, ch);
341 }
342 }
343 }
344 }
345 }
346 map
347}
348
349fn is_gid_name(name: &[u8]) -> bool {
355 let Ok(s) = std::str::from_utf8(name) else {
356 return false;
357 };
358 if s.starts_with("afii") || s.starts_with("uni") {
359 return false;
360 }
361 for prefix in ["g", "G", "cid", "CID", "glyph", "index"] {
362 if let Some(rest) = s.strip_prefix(prefix) {
363 if !rest.is_empty() && rest.bytes().all(|b| b.is_ascii_digit()) {
364 return true;
365 }
366 }
367 }
368 let alpha = s.bytes().take_while(|b| b.is_ascii_alphabetic()).count();
372 let digits = s.len() - alpha;
373 (1..=3).contains(&alpha)
374 && digits >= 3
375 && s.as_bytes()[alpha..].iter().all(|b| b.is_ascii_digit())
376}
377
378fn font_ascent_descent(doc: &Document, fdict: &Dictionary, two_byte: bool) -> (f64, f64) {
379 let descr_owner = if two_byte {
381 fdict
382 .get(b"DescendantFonts")
383 .ok()
384 .and_then(|o| deref(doc, o))
385 .and_then(|o| match o {
386 Object::Array(a) => a.first(),
387 _ => None,
388 })
389 .and_then(|o| as_dict(doc, o))
390 } else {
391 Some(fdict)
392 };
393 let fd = descr_owner
394 .and_then(|d| d.get(b"FontDescriptor").ok())
395 .and_then(|o| as_dict(doc, o));
396 let asc = fd
397 .and_then(|d| d.get(b"Ascent").ok())
398 .and_then(|o| {
399 o.as_float()
400 .ok()
401 .or_else(|| o.as_i64().ok().map(|i| i as f32))
402 })
403 .unwrap_or(750.0) as f64;
404 let desc = fd
405 .and_then(|d| d.get(b"Descent").ok())
406 .and_then(|o| {
407 o.as_float()
408 .ok()
409 .or_else(|| o.as_i64().ok().map(|i| i as f32))
410 })
411 .unwrap_or(-250.0) as f64;
412 if asc - desc <= 1.0 {
419 return (750.0, -250.0);
420 }
421 (asc, desc)
422}
423
424fn base_font_name(fdict: &Dictionary) -> Option<Vec<u8>> {
426 let name = fdict.get(b"BaseFont").ok()?.as_name().ok()?;
427 let stripped = match name.iter().position(|&b| b == b'+') {
428 Some(i) if i == 6 => &name[i + 1..],
429 _ => name,
430 };
431 Some(stripped.to_vec())
432}
433
434fn simple_widths(doc: &Document, fdict: &Dictionary) -> (HashMap<u32, f64>, f64) {
436 let mut map = HashMap::new();
437 let first = fdict
438 .get(b"FirstChar")
439 .ok()
440 .and_then(|o| o.as_i64().ok())
441 .unwrap_or(0) as u32;
442 if let Some(Object::Array(arr)) = fdict.get(b"Widths").ok().and_then(|o| deref(doc, o)) {
443 for (i, w) in arr.iter().enumerate() {
444 if let Some(w) = num(w) {
445 map.insert(first + i as u32, w);
446 }
447 }
448 }
449 let dw = fdict
450 .get(b"FontDescriptor")
451 .ok()
452 .and_then(|o| as_dict(doc, o))
453 .and_then(|d| d.get(b"MissingWidth").ok())
454 .and_then(num)
455 .unwrap_or(0.0);
456 (map, dw)
457}
458
459fn cid_widths(doc: &Document, fdict: &Dictionary) -> (HashMap<u32, f64>, f64) {
461 let mut map = HashMap::new();
462 let Some(desc) = fdict
463 .get(b"DescendantFonts")
464 .ok()
465 .and_then(|o| deref(doc, o))
466 .and_then(|o| match o {
467 Object::Array(a) => a.first(),
468 _ => None,
469 })
470 .and_then(|o| as_dict(doc, o))
471 else {
472 return (map, 1000.0);
473 };
474 let dw = desc.get(b"DW").ok().and_then(num).unwrap_or(1000.0);
475 if let Some(Object::Array(w)) = desc.get(b"W").ok().and_then(|o| deref(doc, o)) {
476 let mut i = 0;
477 while i < w.len() {
478 let c = w.get(i).and_then(num);
479 match (c, w.get(i + 1)) {
480 (Some(c), Some(Object::Array(list))) => {
482 for (k, wv) in list.iter().enumerate() {
483 if let Some(wv) = num(wv) {
484 map.insert(c as u32 + k as u32, wv);
485 }
486 }
487 i += 2;
488 }
489 (Some(c1), Some(o2)) => {
491 if let (Some(c2), Some(wv)) = (num(o2), w.get(i + 2).and_then(num)) {
492 for cid in c1 as u32..=c2 as u32 {
493 map.insert(cid, wv);
494 }
495 }
496 i += 3;
497 }
498 _ => break,
499 }
500 }
501 }
502 (map, dw)
503}
504
505fn num(o: &Object) -> Option<f64> {
506 match o {
507 Object::Integer(i) => Some(*i as f64),
508 Object::Real(r) => Some(*r as f64),
509 _ => None,
510 }
511}
512
513pub(crate) fn parse_tounicode(data: &[u8]) -> HashMap<u32, String> {
515 let text = String::from_utf8_lossy(data);
516 let mut map = HashMap::new();
517 let hex = |s: &str| -> Option<Vec<u16>> {
518 let s = s.trim();
519 if !s.starts_with('<') || !s.ends_with('>') {
520 return None;
521 }
522 let h = &s[1..s.len() - 1];
523 let bytes: Vec<u8> = (0..h.len())
524 .step_by(2)
525 .filter_map(|i| u8::from_str_radix(h.get(i..i + 2)?, 16).ok())
526 .collect();
527 Some(
528 bytes
529 .chunks(2)
530 .map(|c| {
531 if c.len() == 2 {
532 u16::from_be_bytes([c[0], c[1]])
533 } else {
534 c[0] as u16
535 }
536 })
537 .collect(),
538 )
539 };
540 let u16s_to_string = |u: &[u16]| String::from_utf16_lossy(u);
541 let code_of = |u: &[u16]| u.iter().fold(0u32, |acc, &x| (acc << 16) | x as u32);
542
543 let tokens: Vec<String> = {
547 let bytes = text.as_bytes();
548 let mut toks = Vec::new();
549 let mut i = 0;
550 while i < bytes.len() {
551 let c = bytes[i];
552 if c.is_ascii_whitespace() {
553 i += 1;
554 } else if c == b'<' {
555 let start = i;
556 while i < bytes.len() && bytes[i] != b'>' {
557 i += 1;
558 }
559 i += 1; toks.push(String::from_utf8_lossy(&bytes[start..i.min(bytes.len())]).into_owned());
561 } else if c == b'[' || c == b']' {
562 toks.push((c as char).to_string());
563 i += 1;
564 } else {
565 let start = i;
566 while i < bytes.len()
567 && !bytes[i].is_ascii_whitespace()
568 && bytes[i] != b'<'
569 && bytes[i] != b'['
570 && bytes[i] != b']'
571 {
572 i += 1;
573 }
574 toks.push(String::from_utf8_lossy(&bytes[start..i]).into_owned());
575 }
576 }
577 toks
578 };
579 let tokens: Vec<&str> = tokens.iter().map(|s| s.as_str()).collect();
580 let mut i = 0;
581 while i < tokens.len() {
582 match tokens[i] {
583 "beginbfchar" => {
584 i += 1;
585 while i + 1 < tokens.len() && tokens[i] != "endbfchar" {
586 if let (Some(src), Some(dst)) = (hex(tokens[i]), hex(tokens[i + 1])) {
587 map.insert(code_of(&src), u16s_to_string(&dst));
588 }
589 i += 2;
590 }
591 }
592 "beginbfrange" => {
593 i += 1;
594 while i + 2 < tokens.len() && tokens[i] != "endbfrange" {
595 let (Some(lo), Some(hi)) = (hex(tokens[i]), hex(tokens[i + 1])) else {
596 i += 1;
597 continue;
598 };
599 let lo = code_of(&lo);
600 let hi = code_of(&hi);
601 if tokens[i + 2] == "[" {
602 let mut j = i + 3;
604 let mut code = lo;
605 while j < tokens.len() && tokens[j] != "]" {
606 if let Some(dst) = hex(tokens[j]) {
607 map.insert(code, u16s_to_string(&dst));
608 }
609 code += 1;
610 j += 1;
611 }
612 i = j + 1;
613 } else if let Some(dst) = hex(tokens[i + 2]) {
614 let base = code_of(&dst);
616 for (k, code) in (lo..=hi).enumerate() {
617 if let Some(ch) = char::from_u32(base + k as u32) {
618 map.insert(code, ch.to_string());
619 }
620 }
621 i += 3;
622 } else {
623 i += 1;
624 }
625 }
626 }
627 _ => i += 1,
628 }
629 }
630 map
631}
632
633fn codes(font: &Font, bytes: &[u8]) -> Vec<u32> {
635 if font.two_byte {
636 bytes
637 .chunks(2)
638 .map(|c| {
639 if c.len() == 2 {
640 ((c[0] as u32) << 8) | c[1] as u32
641 } else {
642 c[0] as u32
643 }
644 })
645 .collect()
646 } else {
647 bytes.iter().map(|&b| b as u32).collect()
648 }
649}
650
651#[derive(Debug, Clone, Copy, PartialEq)]
666pub(crate) struct PageBox {
667 pub l: f32,
669 pub b: f32,
671 pub w: f32,
672 pub h: f32,
673}
674
675impl PageBox {
676 pub fn top(&self) -> f32 {
678 self.b + self.h
679 }
680}
681
682fn inherited_rect(
685 doc: &Document,
686 page_id: lopdf::ObjectId,
687 key: &[u8],
688) -> Option<(f32, f32, f32, f32)> {
689 let mut id = page_id;
690 for _ in 0..32 {
691 let dict = doc.get_object(id).ok()?.as_dict().ok()?;
692 if let Some(Object::Array(a)) = dict.get(key).ok().and_then(|o| deref(doc, o)) {
693 let v: Vec<f32> = a.iter().filter_map(|o| num(o).map(|x| x as f32)).collect();
694 if v.len() == 4 && v.iter().all(|x| x.is_finite()) {
695 return Some((
696 v[0].min(v[2]),
697 v[1].min(v[3]),
698 v[0].max(v[2]),
699 v[1].max(v[3]),
700 ));
701 }
702 return None;
703 }
704 id = dict.get(b"Parent").ok()?.as_reference().ok()?;
705 }
706 None
707}
708
709pub(crate) fn page_box(doc: &Document, page_id: lopdf::ObjectId) -> PageBox {
710 let nonempty = |r: &(f32, f32, f32, f32)| r.2 > r.0 && r.3 > r.1;
711 let media = inherited_rect(doc, page_id, b"MediaBox")
712 .filter(nonempty)
713 .unwrap_or((0.0, 0.0, 612.0, 792.0));
714 let crop = inherited_rect(doc, page_id, b"CropBox")
715 .map(|c| {
716 (
717 c.0.max(media.0),
718 c.1.max(media.1),
719 c.2.min(media.2),
720 c.3.min(media.3),
721 )
722 })
723 .filter(nonempty)
724 .unwrap_or(media);
725 PageBox {
726 l: crop.0,
727 b: crop.1,
728 w: crop.2 - crop.0,
729 h: crop.3 - crop.1,
730 }
731}
732
733fn page_size(doc: &Document, page_id: lopdf::ObjectId) -> (f32, f32) {
736 let pb = page_box(doc, page_id);
737 (pb.w, pb.h)
738}
739
740pub fn content_diagnosis(bytes: &[u8]) -> String {
746 let Some(doc) = load_document(bytes) else {
747 return "document does not load".into();
748 };
749 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
750 pages.sort_by_key(|(n, _)| *n);
751 let mut out = String::new();
752 let mut caches = DocCaches::default();
753 for (n, pid) in pages.into_iter().take(4) {
754 let content_bytes = doc.get_page_content(pid);
755 let ops = lopdf::content::Content::decode(&content_bytes)
756 .map(|c| c.operations.len())
757 .ok();
758 let res = page_res(&doc, pid);
759 let fonts = res.map(|r| fonts_from_res(&doc, r, &mut caches).len());
760 let glyphs = page_glyphs_cached(&doc, pid, &mut caches).len();
761 out.push_str(&format!(
762 "\n page {n}: content {} B, ops {}, resources {}, fonts {}, glyphs {}",
763 content_bytes.len(),
764 ops.map_or("UNDECODABLE".to_string(), |n| n.to_string()),
765 if res.is_some() { "ok" } else { "MISSING" },
766 fonts.map_or("-".to_string(), |n| n.to_string()),
767 glyphs,
768 ));
769 }
770 out
771}
772
773pub fn text_layer_is_vestigial(pages: &[crate::pdfium_backend::PdfPage]) -> bool {
786 let mut tally = TextLayerTally::default();
787 pages.iter().for_each(|p| tally.add(p));
788 tally.is_vestigial()
789}
790
791#[derive(Debug, Default, Clone, Copy)]
794pub struct TextLayerTally {
795 pages: usize,
796 lines: usize,
797 chars: usize,
798}
799
800impl TextLayerTally {
801 pub fn add(&mut self, page: &crate::pdfium_backend::PdfPage) {
802 self.pages += 1;
803 self.lines += page.cells.len();
804 self.chars += page
805 .cells
806 .iter()
807 .map(|c| c.text.chars().count())
808 .sum::<usize>();
809 }
810
811 pub fn is_vestigial(&self) -> bool {
813 self.lines == 0 || (self.lines <= self.pages && self.chars < 32)
814 }
815
816 pub fn proves_text(&self, total_pages: usize) -> bool {
820 self.lines > 0 && (self.lines > total_pages || self.chars >= 32)
821 }
822}
823
824pub fn xref_repair_status(bytes: &[u8]) -> String {
828 if Document::load_mem(bytes).is_ok() {
829 return "loads unaided; no repair needed".into();
830 }
831 match pad_short_xref_entries(bytes) {
832 Ok(fixed) => match Document::load_mem(&fixed) {
833 Ok(_) => "repaired: cross-reference entries padded to 20 bytes".into(),
834 Err(e) => format!("padded the entries, but it still will not load: {e}"),
835 },
836 Err(why) => format!("repair declined — {why}"),
837 }
838}
839
840pub(crate) fn load_document(bytes: &[u8]) -> Option<Document> {
855 open_document(bytes, None).ok()
856}
857
858#[derive(Debug, Clone, Copy, PartialEq, Eq)]
860pub(crate) enum OpenError {
861 Unreadable,
863 Password,
865}
866
867pub(crate) fn open_document(bytes: &[u8], password: Option<&str>) -> Result<Document, OpenError> {
874 let Some(doc) = load_document_raw(bytes, password) else {
875 if password.is_some() && load_document_raw(bytes, None).is_some_and(|d| d.is_encrypted()) {
879 return Err(OpenError::Password);
880 }
881 return Err(OpenError::Unreadable);
882 };
883 if doc.is_encrypted() {
884 return Err(OpenError::Password);
885 }
886 Ok(doc)
887}
888
889fn load_options(password: Option<&str>) -> lopdf::LoadOptions {
890 lopdf::LoadOptions {
891 password: password.map(str::to_string),
892 ..lopdf::LoadOptions::default()
893 }
894}
895
896fn load_document_raw(bytes: &[u8], password: Option<&str>) -> Option<Document> {
897 let mut fallback = None;
902 if let Some(doc) = best_effort_load(bytes, password, &mut fallback) {
903 return Some(doc);
904 }
905 let xref_fixed = pad_short_xref_entries(bytes).ok();
906 if let Some(fixed) = &xref_fixed {
907 if let Some(doc) = best_effort_load(fixed, password, &mut fallback) {
908 return Some(doc);
909 }
910 }
911 let lengths_fixed = fix_stream_lengths(xref_fixed.as_deref().unwrap_or(bytes));
914 if let Some(doc) = best_effort_load(&lengths_fixed, password, &mut fallback) {
915 return Some(doc);
916 }
917 fallback
918}
919
920fn best_effort_load(
923 data: &[u8],
924 password: Option<&str>,
925 fallback: &mut Option<Document>,
926) -> Option<Document> {
927 match Document::load_mem_with_options(data, load_options(password)) {
928 Ok(doc) if has_page_content(&doc) => Some(doc),
929 Ok(doc) => {
930 fallback.get_or_insert(doc);
931 None
932 }
933 Err(_) => None,
934 }
935}
936
937fn has_page_content(doc: &Document) -> bool {
941 doc.get_pages()
942 .into_values()
943 .take(4)
944 .any(|pid| !doc.get_page_content(pid).is_empty())
945}
946
947fn fix_stream_lengths(bytes: &[u8]) -> Vec<u8> {
960 let mut out = bytes.to_vec();
961 let mut i = 0;
962 while let Some(rel) = find(&out[i..], b"stream") {
963 let kw = i + rel;
964 i = kw + 6;
965 if kw >= 3 && &out[kw - 3..kw] == b"end" {
967 continue;
968 }
969 let mut data = kw + 6;
971 if out.get(data..data + 2) == Some(b"\r\n".as_slice()) {
972 data += 2;
973 } else if matches!(out.get(data), Some(b'\n' | b'\r')) {
974 data += 1;
975 }
976 let Some(end) = find(&out[data..], b"endstream").map(|r| data + r) else {
977 continue;
978 };
979 let dict_start = out[..kw].iter().rposition(|&c| c == b'<').unwrap_or(0);
981 let Some(lrel) = find(&out[dict_start..kw], b"/Length") else {
982 continue;
983 };
984 let mut d = dict_start + lrel + 7;
985 while matches!(out.get(d), Some(b' ')) {
986 d += 1;
987 }
988 let digits = out[d..].iter().take_while(|c| c.is_ascii_digit()).count();
989 if digits == 0 {
990 continue;
991 }
992 let declared: usize = match std::str::from_utf8(&out[d..d + digits])
993 .ok()
994 .and_then(|s| s.parse().ok())
995 {
996 Some(v) => v,
997 None => continue,
998 };
999 let actual = end - data;
1000 let replacement = actual.to_string();
1003 if actual == declared || replacement.len() > digits {
1004 continue;
1005 }
1006 out[d..d + digits].fill(b' ');
1007 out[d..d + replacement.len()].copy_from_slice(replacement.as_bytes());
1008 }
1009 out
1010}
1011
1012fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
1013 haystack.windows(needle.len()).position(|w| w == needle)
1014}
1015
1016fn pad_short_xref_entries(bytes: &[u8]) -> Result<Vec<u8>, &'static str> {
1019 let is_boundary = |i: usize| i == 0 || matches!(bytes[i - 1], b'\n' | b'\r');
1022 let mut starts = (0..bytes.len().saturating_sub(4))
1023 .filter(|&i| &bytes[i..i + 4] == b"xref" && is_boundary(i));
1024 let xref_at = starts
1025 .next()
1026 .ok_or("no classic `xref` section (an xref stream?)")?;
1027 if starts.next().is_some() {
1028 return Err("more than one xref section (incremental update)");
1029 }
1030 let last_obj = bytes
1031 .windows(3)
1032 .rposition(|w| w == b"obj")
1033 .ok_or("no objects found")?;
1034 if last_obj > xref_at {
1035 return Err("an object follows the xref — padding would move it");
1036 }
1037
1038 let mut out = bytes[..xref_at].to_vec();
1039 out.extend_from_slice(b"xref\n");
1040 let mut i = xref_at + 4;
1041 let skip_ws = |i: &mut usize| {
1042 while matches!(bytes.get(*i), Some(b'\r' | b'\n' | b' ')) {
1043 *i += 1;
1044 }
1045 };
1046 loop {
1047 skip_ws(&mut i);
1048 if bytes[i..].starts_with(b"trailer") {
1050 out.extend_from_slice(&bytes[i..]);
1051 return Ok(out);
1052 }
1053 let header_end = i + bytes[i..]
1054 .iter()
1055 .position(|c| matches!(c, b'\n' | b'\r'))
1056 .ok_or("subsection header runs off the end")?;
1057 let header = std::str::from_utf8(&bytes[i..header_end])
1058 .map_err(|_| "subsection header is not text")?
1059 .trim();
1060 let mut parts = header.split_whitespace();
1061 let count: usize = parts
1062 .nth(1)
1063 .and_then(|c| c.parse().ok())
1064 .ok_or("unparseable subsection header")?;
1065 if parts.next().is_some() || count == 0 {
1066 return Err("unexpected subsection header shape");
1067 }
1068 out.extend_from_slice(header.as_bytes());
1069 out.push(b'\n');
1070 i = header_end;
1071 for _ in 0..count {
1072 skip_ws(&mut i);
1073 let entry = bytes.get(i..i + 18).ok_or("xref entry runs off the end")?;
1075 let well_formed = entry[..10].iter().all(u8::is_ascii_digit)
1076 && entry[10] == b' '
1077 && entry[11..16].iter().all(u8::is_ascii_digit)
1078 && entry[16] == b' '
1079 && matches!(entry[17], b'n' | b'f');
1080 if !well_formed {
1081 return Err("xref entry is not `nnnnnnnnnn ggggg n`");
1082 }
1083 out.extend_from_slice(entry);
1084 out.extend_from_slice(b" \n"); i += 18;
1086 }
1087 }
1088}
1089
1090pub fn debug_glyphs(bytes: &[u8], index: usize) -> Vec<(char, f32, f32, f32, f32)> {
1093 let Some(doc) = load_document(bytes) else {
1094 return Vec::new();
1095 };
1096 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
1097 pages.sort_by_key(|(n, _)| *n);
1098 let Some((_, pid)) = pages.get(index) else {
1099 return Vec::new();
1100 };
1101 page_glyphs(&doc, *pid)
1102 .into_iter()
1103 .map(|g| (g.ch, g.ll, g.lr, g.lb, g.lt))
1104 .collect()
1105}
1106
1107pub fn pdf_textlines(bytes: &[u8]) -> Vec<(f32, f32, Vec<crate::pdfium_backend::TextCell>)> {
1111 let Some(doc) = load_document(bytes) else {
1112 return Vec::new();
1113 };
1114 let mut caches = DocCaches::default();
1115 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
1116 pages.sort_by_key(|(n, _)| *n);
1117 pages
1118 .into_iter()
1119 .map(|(_, pid)| {
1120 let (w, h) = page_size(&doc, pid);
1121 let glyphs = page_glyphs_cached(&doc, pid, &mut caches);
1122 let cells = crate::dp_lines::line_cells(&glyphs, h, true);
1123 (w, h, cells)
1124 })
1125 .collect()
1126}
1127
1128pub fn pdf_words(bytes: &[u8]) -> Vec<(f32, f32, Vec<crate::pdfium_backend::TextCell>)> {
1133 let Some(doc) = load_document(bytes) else {
1134 return Vec::new();
1135 };
1136 let mut caches = DocCaches::default();
1137 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
1138 pages.sort_by_key(|(n, _)| *n);
1139 pages
1140 .into_iter()
1141 .map(|(_, pid)| {
1142 let (w, h) = page_size(&doc, pid);
1143 let glyphs = page_glyphs_cached(&doc, pid, &mut caches);
1144 let cells = crate::dp_lines::word_cells(&glyphs, h, true);
1145 (w, h, cells)
1146 })
1147 .collect()
1148}
1149
1150#[derive(Default)]
1154pub struct PageParserCells {
1155 pub prose: Vec<crate::pdfium_backend::TextCell>,
1156 pub words: Vec<crate::pdfium_backend::TextCell>,
1157 pub code: Vec<crate::pdfium_backend::TextCell>,
1158 pub checkboxes: Vec<crate::checkbox::CheckBox>,
1160}
1161
1162pub struct PageTextParser {
1175 doc: Document,
1176 caches: DocCaches,
1177 pages: Vec<lopdf::ObjectId>,
1179}
1180
1181impl PageTextParser {
1182 pub fn open(bytes: &[u8]) -> Option<Self> {
1184 Self::open_with_password(bytes, None)
1185 }
1186
1187 pub fn open_with_password(bytes: &[u8], password: Option<&str>) -> Option<Self> {
1191 let doc = open_document(bytes, password).ok()?;
1192 let mut pages: Vec<_> = doc.get_pages().into_iter().collect();
1193 pages.sort_by_key(|(n, _)| *n);
1194 Some(Self {
1195 doc,
1196 caches: DocCaches::default(),
1197 pages: pages.into_iter().map(|(_, pid)| pid).collect(),
1198 })
1199 }
1200
1201 pub(crate) fn glyph_styles(
1209 &mut self,
1210 index: usize,
1211 ) -> Vec<crate::heading_hierarchy::GlyphStyle> {
1212 let Some(&pid) = self.pages.get(index) else {
1213 return Vec::new();
1214 };
1215 let (_w, h) = page_size(&self.doc, pid);
1216 let glyphs = page_glyphs_cached(&self.doc, pid, &mut self.caches);
1217 let styles: HashMap<u64, crate::font_style::FontStyle> = self
1220 .caches
1221 .fonts
1222 .values()
1223 .map(|f| (f.hash, f.style))
1224 .collect();
1225 glyphs
1226 .iter()
1227 .filter(|g| !g.ch.is_whitespace() && g.ll.is_finite())
1228 .map(|g| {
1229 let st = styles.get(&g.font).copied().unwrap_or_default();
1230 crate::heading_hierarchy::GlyphStyle {
1231 l: g.ll,
1232 t: h - g.lt,
1233 r: g.lr,
1234 b: h - g.lb,
1235 height: g.height(),
1236 weight_cls: crate::font_style::weight_class(st.weight),
1237 italic: st.italic,
1238 styled: st.known,
1239 }
1240 })
1241 .collect()
1242 }
1243
1244 pub fn cells(&mut self, index: usize) -> PageParserCells {
1248 let Some(&pid) = self.pages.get(index) else {
1249 return PageParserCells::default();
1250 };
1251 let (_w, h) = page_size(&self.doc, pid);
1252 let (glyphs, inks) = page_glyphs_and_inks(&self.doc, pid, &mut self.caches);
1253 let (prose, words) = crate::dp_lines::line_and_word_cells(&glyphs, h, true);
1254 PageParserCells {
1255 prose,
1256 words,
1257 code: crate::pdfium_backend::code_cells_from_glyphs(&glyphs, h),
1258 checkboxes: crate::checkbox::find(&inks, h),
1259 }
1260 }
1261
1262 pub fn page_count(&self) -> usize {
1264 self.pages.len()
1265 }
1266
1267 pub fn text_page(&mut self, index: usize) -> crate::pdfium_backend::PdfPage {
1273 let (w, h, (glyphs, inks)) = match self.pages.get(index) {
1274 Some(&pid) => {
1275 let (w, h) = page_size(&self.doc, pid);
1276 (w, h, page_glyphs_and_inks(&self.doc, pid, &mut self.caches))
1277 }
1278 None => (0.0, 0.0, Default::default()),
1279 };
1280 let (mut prose, mut words) = crate::dp_lines::line_and_word_cells(&glyphs, h, true);
1281 drop_overpainted_cells(&mut prose);
1282 drop_overpainted_cells(&mut words);
1283 crate::pdfium_backend::PdfPage {
1284 #[cfg(feature = "ocr-prep")]
1285 image_layout: None,
1286 width: w,
1287 height: h,
1288 scale: 1.0,
1290 cells: prose,
1291 code_cells: crate::pdfium_backend::code_cells_from_glyphs(&glyphs, h),
1292 word_cells: words,
1293 checkboxes: crate::checkbox::find(&inks, h),
1294 #[cfg(feature = "ocr-prep")]
1295 image: image::RgbImage::new(1, 1),
1296 links: Vec::new(),
1297 rotation: 0,
1298 #[cfg(feature = "ml")]
1299 pending_image: None,
1300 }
1301 }
1302}
1303
1304pub fn pdf_all_cells(bytes: &[u8]) -> Vec<PageParserCells> {
1309 let Some(mut parser) = PageTextParser::open(bytes) else {
1310 return Vec::new();
1311 };
1312 (0..parser.pages.len()).map(|i| parser.cells(i)).collect()
1313}
1314
1315pub fn pdf_text_pages(bytes: &[u8]) -> Vec<crate::pdfium_backend::PdfPage> {
1322 let Some(mut parser) = PageTextParser::open(bytes) else {
1323 return Vec::new();
1324 };
1325 (0..parser.page_count())
1326 .map(|i| parser.text_page(i))
1327 .collect()
1328}
1329
1330fn drop_overpainted_cells(cells: &mut Vec<crate::pdfium_backend::TextCell>) {
1349 let mut paint = vec![false; cells.len()];
1350 for i in 0..cells.len() {
1351 for j in 0..cells.len() {
1352 if i == j || cells[i].text == cells[j].text {
1353 continue;
1354 }
1355 let (a, b) = (&cells[i], &cells[j]);
1356 let vo = (a.b.min(b.b) - a.t.max(b.t)).max(0.0);
1358 if vo < 0.6 * (a.b - a.t).min(b.b - b.t) {
1359 continue;
1360 }
1361 let ho = (a.r.min(b.r) - a.l.max(b.l)).max(0.0);
1363 if ho >= 0.8 * (a.r - a.l) && (a.r - a.l) <= (b.r - b.l) {
1364 paint[i] = true;
1365 paint[j] = true;
1366 }
1367 }
1368 }
1369 let mut keep = paint.iter().map(|p| !p);
1370 cells.retain(|_| keep.next().unwrap());
1371}
1372
1373#[derive(Clone, Copy)]
1377struct TextState {
1378 tc: f64,
1379 tw: f64,
1380 th: f64,
1381 tl: f64,
1382 trise: f64,
1383 fsize: f64,
1384}
1385
1386impl TextState {
1387 const INIT: TextState = TextState {
1388 tc: 0.0,
1389 tw: 0.0,
1390 th: 1.0,
1391 tl: 0.0,
1392 trise: 0.0,
1393 fsize: 0.0,
1394 };
1395}
1396
1397fn page_res(doc: &Document, page_id: lopdf::ObjectId) -> Option<&Dictionary> {
1400 let (inline, ids) = doc.get_page_resources(page_id).ok()?;
1401 if let Some(d) = inline {
1402 return Some(d);
1403 }
1404 ids.into_iter().find_map(|id| doc.get_dictionary(id).ok())
1405}
1406
1407fn fonts_from_res(
1411 doc: &Document,
1412 res: &Dictionary,
1413 caches: &mut DocCaches,
1414) -> HashMap<Vec<u8>, Arc<Font>> {
1415 let mut map = HashMap::new();
1416 let font_dict = res
1417 .get(b"Font")
1418 .ok()
1419 .and_then(|o| deref(doc, o))
1420 .and_then(|o| o.as_dict().ok());
1421 if let Some(fd) = font_dict {
1422 for (name, value) in fd.iter() {
1423 let font = match value {
1424 Object::Reference(id) => {
1425 let key = (*id, name.clone());
1426 if let Some(f) = caches.fonts.get(&key) {
1427 Arc::clone(f)
1428 } else if let Some(fdict) = deref(doc, value).and_then(|o| o.as_dict().ok()) {
1429 let f = Arc::new(parse_font(doc, name, fdict));
1430 caches.fonts.insert(key, Arc::clone(&f));
1431 f
1432 } else {
1433 continue;
1434 }
1435 }
1436 _ => {
1437 if let Some(fdict) = deref(doc, value).and_then(|o| o.as_dict().ok()) {
1438 Arc::new(parse_font(doc, name, fdict))
1439 } else {
1440 continue;
1441 }
1442 }
1443 };
1444 map.insert(name.clone(), font);
1445 }
1446 }
1447 map
1448}
1449
1450pub(crate) fn glyph_styles(
1456 bytes: &[u8],
1457 pages: &[usize],
1458) -> HashMap<usize, Vec<crate::heading_hierarchy::GlyphStyle>> {
1459 let mut out = HashMap::new();
1460 let Some(mut parser) = PageTextParser::open(bytes) else {
1461 return out;
1462 };
1463 for &page_no in pages {
1464 if page_no == 0 {
1465 continue;
1466 }
1467 let styles = parser.glyph_styles(page_no - 1);
1468 if !styles.is_empty() {
1469 out.insert(page_no, styles);
1470 }
1471 }
1472 out
1473}
1474
1475pub(crate) fn page_glyphs(doc: &Document, page_id: lopdf::ObjectId) -> Vec<Glyph> {
1476 page_glyphs_cached(doc, page_id, &mut DocCaches::default())
1477}
1478
1479fn page_glyphs_cached(
1482 doc: &Document,
1483 page_id: lopdf::ObjectId,
1484 caches: &mut DocCaches,
1485) -> Vec<Glyph> {
1486 page_glyphs_and_inks(doc, page_id, caches).0
1487}
1488
1489fn page_glyphs_and_inks(
1493 doc: &Document,
1494 page_id: lopdf::ObjectId,
1495 caches: &mut DocCaches,
1496) -> (Vec<Glyph>, Vec<crate::checkbox::Ink>) {
1497 let mut out = Vec::new();
1498 let mut inks = Vec::new();
1499 let content_bytes = doc.get_page_content(page_id);
1502 let Ok(content) = lopdf::content::Content::decode(&content_bytes) else {
1503 return (out, inks);
1504 };
1505 if let Some(res) = page_res(doc, page_id) {
1506 let pb = page_box(doc, page_id);
1509 let base = Mat {
1510 e: -(pb.l as f64),
1511 f: -(pb.b as f64),
1512 ..Mat::ID
1513 };
1514 run_content(
1515 doc,
1516 res,
1517 &content,
1518 base,
1519 TextState::INIT,
1520 0,
1521 caches,
1522 &mut out,
1523 &mut inks,
1524 );
1525 out.retain(|g| on_page(g, pb.w, pb.h));
1526 }
1527 (out, inks)
1528}
1529
1530fn on_page(g: &Glyph, w: f32, h: f32) -> bool {
1545 const EPS: f32 = 1e-3;
1547 let (l, b, r, t) = if [g.ll, g.lb, g.lr, g.lt].iter().all(|v| v.is_finite()) {
1548 (g.ll, g.lb, g.lr, g.lt)
1549 } else {
1550 (g.l, g.b, g.r, g.t)
1551 };
1552 if ![l, b, r, t].iter().all(|v| v.is_finite()) {
1553 return true;
1554 }
1555 l >= -EPS && b >= -EPS && r <= w + EPS && t <= h + EPS
1556}
1557
1558#[allow(clippy::too_many_arguments)]
1563fn run_content(
1564 doc: &Document,
1565 res: &Dictionary,
1566 content: &lopdf::content::Content,
1567 base_ctm: Mat,
1568 init: TextState,
1569 depth: u32,
1570 caches: &mut DocCaches,
1571 out: &mut Vec<Glyph>,
1572 inks: &mut Vec<crate::checkbox::Ink>,
1573) {
1574 let fonts = fonts_from_res(doc, res, caches);
1575 let xobjects = res
1576 .get(b"XObject")
1577 .ok()
1578 .and_then(|o| deref(doc, o))
1579 .and_then(|o| o.as_dict().ok());
1580
1581 #[allow(clippy::type_complexity)]
1586 let mut gstate_stack: Vec<(Mat, f64, f64, f64, f64, f64, f64, Option<&Arc<Font>>)> = Vec::new();
1587 let mut ctm = base_ctm;
1588 let mut tm = Mat::ID;
1589 let mut tlm = Mat::ID;
1590 let mut font: Option<&Arc<Font>> = None;
1591 let mut fsize = init.fsize;
1592 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;
1597
1598 let op_f = |operands: &[Object], i: usize| operands.get(i).and_then(num).unwrap_or(0.0);
1599 let mut path: Vec<PathPiece> = Vec::new();
1603 let (mut cur, mut start) = ((0.0, 0.0), (0.0, 0.0));
1604 let mut fill_light = false;
1608 let mut fill_stack: Vec<bool> = Vec::new();
1609
1610 for op in &content.operations {
1611 let operands = &op.operands;
1612 match op.operator.as_str() {
1613 "q" => {
1614 gstate_stack.push((ctm, tc, tw, th, tl, trise, fsize, font));
1615 fill_stack.push(fill_light);
1616 }
1617 "Q" => {
1618 fill_light = fill_stack.pop().unwrap_or(false);
1619 if let Some((c, a, b, h, l, r, fs, f)) = gstate_stack.pop() {
1620 ctm = c;
1621 tc = a;
1622 tw = b;
1623 th = h;
1624 tl = l;
1625 trise = r;
1626 fsize = fs;
1627 font = f;
1628 }
1629 }
1630 "cm" => {
1631 let m = Mat {
1632 a: op_f(operands, 0),
1633 b: op_f(operands, 1),
1634 c: op_f(operands, 2),
1635 d: op_f(operands, 3),
1636 e: op_f(operands, 4),
1637 f: op_f(operands, 5),
1638 };
1639 ctm = m.then(ctm);
1640 }
1641 "BT" => {
1642 tm = Mat::ID;
1643 tlm = Mat::ID;
1644 }
1645 "ET" => {}
1646 "Tf" => {
1647 if let Some(Object::Name(n)) = operands.first() {
1648 font = fonts.get(n.as_slice());
1649 }
1650 fsize = op_f(operands, 1);
1651 }
1652 "Td" => {
1653 tlm = Mat {
1654 a: 1.0,
1655 b: 0.0,
1656 c: 0.0,
1657 d: 1.0,
1658 e: op_f(operands, 0),
1659 f: op_f(operands, 1),
1660 }
1661 .then(tlm);
1662 tm = tlm;
1663 }
1664 "TD" => {
1665 tl = -op_f(operands, 1);
1666 tlm = Mat {
1667 a: 1.0,
1668 b: 0.0,
1669 c: 0.0,
1670 d: 1.0,
1671 e: op_f(operands, 0),
1672 f: op_f(operands, 1),
1673 }
1674 .then(tlm);
1675 tm = tlm;
1676 }
1677 "Tm" => {
1678 tlm = Mat {
1679 a: op_f(operands, 0),
1680 b: op_f(operands, 1),
1681 c: op_f(operands, 2),
1682 d: op_f(operands, 3),
1683 e: op_f(operands, 4),
1684 f: op_f(operands, 5),
1685 };
1686 tm = tlm;
1687 }
1688 "T*" => {
1689 tlm = Mat {
1690 a: 1.0,
1691 b: 0.0,
1692 c: 0.0,
1693 d: 1.0,
1694 e: 0.0,
1695 f: -tl,
1696 }
1697 .then(tlm);
1698 tm = tlm;
1699 }
1700 "Tc" => tc = op_f(operands, 0),
1701 "Tw" => tw = op_f(operands, 0),
1702 "Tz" => th = op_f(operands, 0) / 100.0,
1703 "TL" => tl = op_f(operands, 0),
1704 "Ts" => trise = op_f(operands, 0),
1705 "Tj" | "'" | "\"" => {
1706 if op.operator == "'" || op.operator == "\"" {
1707 tlm = Mat {
1709 a: 1.0,
1710 b: 0.0,
1711 c: 0.0,
1712 d: 1.0,
1713 e: 0.0,
1714 f: -tl,
1715 }
1716 .then(tlm);
1717 tm = tlm;
1718 }
1719 if op.operator == "\"" {
1720 tw = op_f(operands, 0);
1723 tc = op_f(operands, 1);
1724 }
1725 if let (Some(f), Some(Object::String(s, _))) = (font, operands.last()) {
1726 show_text(f, s, fsize, tc, tw, th, trise, &mut tm, ctm, out);
1727 }
1728 }
1729 "TJ" => {
1730 if let (Some(f), Some(Object::Array(arr))) = (font, operands.first()) {
1731 for el in arr {
1732 match el {
1733 Object::String(s, _) => {
1734 show_text(f, s, fsize, tc, tw, th, trise, &mut tm, ctm, out)
1735 }
1736 other => {
1737 if let Some(adj) = num(other) {
1738 let tx = -adj / 1000.0 * fsize * th;
1740 tm = Mat {
1741 a: 1.0,
1742 b: 0.0,
1743 c: 0.0,
1744 d: 1.0,
1745 e: tx,
1746 f: 0.0,
1747 }
1748 .then(tm);
1749 }
1750 }
1751 }
1752 }
1753 }
1754 }
1755 "m" => {
1756 cur = ctm.apply(op_f(operands, 0), op_f(operands, 1));
1757 start = cur;
1758 }
1759 "l" => {
1760 let p = ctm.apply(op_f(operands, 0), op_f(operands, 1));
1761 path.push(PathPiece::Line(cur, p));
1762 cur = p;
1763 }
1764 "c" | "v" | "y" => {
1765 let pts: Vec<(f64, f64)> = (0..operands.len() / 2)
1766 .map(|k| ctm.apply(op_f(operands, 2 * k), op_f(operands, 2 * k + 1)))
1767 .collect();
1768 if let Some(&end) = pts.last() {
1769 path.push(PathPiece::Curve(
1770 std::iter::once(cur).chain(pts.iter().copied()).collect(),
1771 ));
1772 cur = end;
1773 }
1774 }
1775 "h" => {
1776 if cur != start {
1777 path.push(PathPiece::Line(cur, start));
1778 }
1779 cur = start;
1780 }
1781 "re" => {
1782 let (x, y, w, h) = (
1783 op_f(operands, 0),
1784 op_f(operands, 1),
1785 op_f(operands, 2),
1786 op_f(operands, 3),
1787 );
1788 path.push(PathPiece::Rect([
1789 ctm.apply(x, y),
1790 ctm.apply(x + w, y),
1791 ctm.apply(x + w, y + h),
1792 ctm.apply(x, y + h),
1793 ]));
1794 cur = ctm.apply(x, y);
1795 start = cur;
1796 }
1797 "S" | "s" | "f" | "F" | "f*" | "B" | "B*" | "b" | "b*" => {
1798 let op = op.operator.as_str();
1799 if matches!(op, "s" | "b" | "b*") && cur != start {
1800 path.push(PathPiece::Line(cur, start));
1801 }
1802 let stroke = matches!(op, "S" | "s" | "B" | "B*" | "b" | "b*");
1803 let fill = !matches!(op, "S" | "s");
1804 paint_path(&path, stroke, fill, fill_light, inks);
1805 path.clear();
1806 }
1807 "n" => path.clear(),
1808 "g" | "rg" | "k" | "sc" | "scn" => {
1811 let v: Vec<f64> = operands
1812 .iter()
1813 .map(num)
1814 .collect::<Option<_>>()
1815 .unwrap_or_default();
1816 fill_light = match v.as_slice() {
1817 [g] => *g >= 0.9,
1818 [r, g, b] => r.min(*g).min(*b) >= 0.9,
1819 [c, m, y, k] => c.max(*m).max(*y).max(*k) <= 0.1,
1820 _ => false,
1821 };
1822 }
1823 "cs" => fill_light = false,
1824 "Do" => {
1825 if depth >= 8 {
1828 continue;
1829 }
1830 let Some(Object::Name(n)) = operands.first() else {
1831 continue;
1832 };
1833 let obj = xobjects.and_then(|d| d.get(n.as_slice()).ok());
1834 let form_id = match obj {
1835 Some(Object::Reference(id)) => Some(*id),
1836 _ => None,
1837 };
1838 let stream = obj
1839 .and_then(|o| deref(doc, o))
1840 .and_then(|o| o.as_stream().ok());
1841 let Some(stream) = stream else { continue };
1842 let is_form = stream
1843 .dict
1844 .get(b"Subtype")
1845 .ok()
1846 .and_then(|o| o.as_name().ok())
1847 == Some(b"Form".as_slice());
1848 if !is_form {
1849 continue;
1850 }
1851 let cached = form_id.and_then(|id| caches.forms.get(&id).cloned());
1854 let form_content = match cached {
1855 Some(c) => c,
1856 None => {
1857 let Ok(data) = stream.decompressed_content() else {
1858 continue;
1859 };
1860 let Ok(c) = lopdf::content::Content::decode(&data) else {
1861 continue;
1862 };
1863 let c = Arc::new(c);
1864 if let Some(id) = form_id {
1865 caches.forms.insert(id, Arc::clone(&c));
1866 }
1867 c
1868 }
1869 };
1870 let form_mat = match stream.dict.get(b"Matrix").ok() {
1872 Some(Object::Array(a)) if a.len() == 6 => {
1873 let v: Vec<f64> = a.iter().filter_map(num).collect();
1874 if v.len() == 6 {
1875 Mat {
1876 a: v[0],
1877 b: v[1],
1878 c: v[2],
1879 d: v[3],
1880 e: v[4],
1881 f: v[5],
1882 }
1883 } else {
1884 Mat::ID
1885 }
1886 }
1887 _ => Mat::ID,
1888 };
1889 let form_res = stream
1891 .dict
1892 .get(b"Resources")
1893 .ok()
1894 .and_then(|o| deref(doc, o))
1895 .and_then(|o| o.as_dict().ok())
1896 .unwrap_or(res);
1897 let state = TextState {
1898 tc,
1899 tw,
1900 th,
1901 tl,
1902 trise,
1903 fsize,
1904 };
1905 run_content(
1906 doc,
1907 form_res,
1908 &form_content,
1909 form_mat.then(ctm),
1910 state,
1911 depth + 1,
1912 caches,
1913 out,
1914 inks,
1915 );
1916 }
1917 _ => {}
1918 }
1919 }
1920}
1921
1922enum PathPiece {
1924 Line((f64, f64), (f64, f64)),
1925 Rect([(f64, f64); 4]),
1927 Curve(Vec<(f64, f64)>),
1929}
1930
1931fn paint_path(
1939 path: &[PathPiece],
1940 stroke: bool,
1941 fill: bool,
1942 fill_light: bool,
1943 inks: &mut Vec<crate::checkbox::Ink>,
1944) {
1945 use crate::checkbox::Ink;
1946 let bbox = |pts: &mut dyn Iterator<Item = (f64, f64)>| {
1947 pts.fold(
1948 (
1949 f64::INFINITY,
1950 f64::INFINITY,
1951 f64::NEG_INFINITY,
1952 f64::NEG_INFINITY,
1953 ),
1954 |(l, b, r, t), (x, y)| (l.min(x), b.min(y), r.max(x), t.max(y)),
1955 )
1956 };
1957 let mut keep = |ink: Ink| {
1958 if ink.worth_keeping() {
1959 inks.push(ink);
1960 }
1961 };
1962 if fill && !fill_light {
1963 let (l, b, r, t) = bbox(&mut path.iter().flat_map(|p| match p {
1967 PathPiece::Line(a, z) => vec![*a, *z],
1968 PathPiece::Rect(c) => c.to_vec(),
1969 PathPiece::Curve(c) => c.clone(),
1970 }));
1971 if l.is_finite() {
1972 keep(Ink::Blot { l, b, r, t });
1973 }
1974 return;
1975 }
1976 if !stroke {
1977 return;
1978 }
1979 for piece in path {
1980 match piece {
1981 PathPiece::Line(a, z) => keep(Ink::Seg {
1982 x0: a.0,
1983 y0: a.1,
1984 x1: z.0,
1985 y1: z.1,
1986 }),
1987 PathPiece::Rect(c) => {
1988 let axis = ((c[0].1 - c[1].1).abs() < 1e-6 && (c[1].0 - c[2].0).abs() < 1e-6)
1989 || ((c[0].0 - c[1].0).abs() < 1e-6 && (c[1].1 - c[2].1).abs() < 1e-6);
1990 if axis {
1991 let (l, b, r, t) = bbox(&mut c.iter().copied());
1992 keep(Ink::Rect { l, b, r, t });
1993 } else {
1994 for k in 0..4 {
1995 let (a, z) = (c[k], c[(k + 1) % 4]);
1996 keep(Ink::Seg {
1997 x0: a.0,
1998 y0: a.1,
1999 x1: z.0,
2000 y1: z.1,
2001 });
2002 }
2003 }
2004 }
2005 PathPiece::Curve(c) => {
2006 let (l, b, r, t) = bbox(&mut c.iter().copied());
2007 keep(Ink::Blot { l, b, r, t });
2008 }
2009 }
2010 }
2011}
2012
2013#[allow(clippy::too_many_arguments)]
2014fn show_text(
2015 font: &Font,
2016 bytes: &[u8],
2017 fsize: f64,
2018 tc: f64,
2019 tw: f64,
2020 th: f64,
2021 trise: f64,
2022 tm: &mut Mat,
2023 ctm: Mat,
2024 out: &mut Vec<Glyph>,
2025) {
2026 for code in codes(font, bytes) {
2027 let (text, w) = font.decode_code(code);
2028 let w0 = w / 1000.0; let scale = Mat {
2031 a: fsize * th,
2032 b: 0.0,
2033 c: 0.0,
2034 d: fsize,
2035 e: 0.0,
2036 f: trise,
2037 };
2038 let trm = scale.then(*tm).then(ctm);
2039 let (desc, asc) = (font.descent / 1000.0, font.ascent / 1000.0);
2041 let (x0, y0) = trm.apply(0.0, desc);
2042 let (x1, y1) = trm.apply(w0, desc);
2043 let (x2, y2) = trm.apply(w0, asc);
2044 let (x3, y3) = trm.apply(0.0, asc);
2045 let upright = trm.a > 0.0 && trm.b.abs() <= 1e-6 * trm.a;
2053 let (left, bot, right, top, quad) = if upright {
2054 (x0.min(x1), y0.min(y3), x0.max(x1), y0.max(y3), None)
2055 } else {
2056 let (xs, ys) = ([x0, x1, x2, x3], [y0, y1, y2, y3]);
2057 let fold = |v: [f64; 4], f: fn(f64, f64) -> f64| v.into_iter().reduce(f).unwrap();
2058 let q = [x0, y0, x1, y1, x2, y2, x3, y3].map(|v| v as f32);
2059 (
2060 fold(xs, f64::min),
2061 fold(ys, f64::min),
2062 fold(xs, f64::max),
2063 fold(ys, f64::max),
2064 Some(q),
2065 )
2066 };
2067 if let Some(s) = text {
2068 for ch in s.chars() {
2070 if ch != '\u{0}' {
2071 out.push(Glyph {
2072 ch,
2073 l: left as f32,
2074 b: bot as f32,
2075 r: right as f32,
2076 t: top as f32,
2077 ll: left as f32,
2078 lb: bot as f32,
2079 lr: right as f32,
2080 lt: top as f32,
2081 font: font.hash,
2082 quad,
2083 });
2084 }
2085 }
2086 }
2087 let is_space = !font.two_byte && code == 32;
2089 let tx = (w0 * fsize + tc + if is_space { tw } else { 0.0 }) * th;
2090 *tm = Mat {
2091 a: 1.0,
2092 b: 0.0,
2093 c: 0.0,
2094 d: 1.0,
2095 e: tx,
2096 f: 0.0,
2097 }
2098 .then(*tm);
2099 }
2100}
2101
2102fn simple_encoding_table(doc: &Document, fdict: &Dictionary) -> HashMap<u8, char> {
2106 let enc = fdict.get(b"Encoding").ok().and_then(|o| deref(doc, o));
2107 let base_name = match enc {
2108 Some(Object::Name(n)) => n.clone(),
2109 Some(Object::Dictionary(d)) => d
2110 .get(b"BaseEncoding")
2111 .ok()
2112 .and_then(|o| o.as_name().ok())
2113 .map(|n| n.to_vec())
2114 .unwrap_or_default(),
2115 _ => Vec::new(),
2116 };
2117 let mut m = if base_name == b"MacRomanEncoding" {
2118 macroman_table()
2119 } else if base_name.is_empty() {
2120 tex_math_builtin(fdict).unwrap_or_else(winansi_table)
2127 } else {
2128 winansi_table()
2129 };
2130 if let Some(Object::Dictionary(d)) = enc {
2132 if let Some(Object::Array(diffs)) = d.get(b"Differences").ok().and_then(|o| deref(doc, o)) {
2133 let mut code = 0u8;
2134 for el in diffs {
2135 match el {
2136 Object::Integer(i) => code = *i as u8,
2137 Object::Name(name) => {
2138 if let Some(ch) = glyph_name_to_char(name) {
2139 m.insert(code, ch);
2140 }
2141 code = code.wrapping_add(1);
2142 }
2143 _ => {}
2144 }
2145 }
2146 }
2147 }
2148 m
2149}
2150
2151fn tex_math_builtin(fdict: &Dictionary) -> Option<HashMap<u8, char>> {
2157 const CMSY: [char; 128] = [
2158 '−', '·', '×', '∗', '÷', '⋄', '±', '∓', '⊕', '⊖', '⊗', '⊘', '⊙', '◯', '∘', '•', '≍', '≡',
2159 '⊆', '⊇', '≤', '≥', '≼', '≽', '∼', '≈', '⊂', '⊃', '≪', '≫', '≺', '≻', '←', '→', '↑', '↓',
2160 '↔', '↗', '↘', '≃', '⇐', '⇒', '⇑', '⇓', '⇔', '↖', '↙', '∝', '′', '∞', '∈', '∋', '△', '▽',
2161 '\u{338}', '↦', '∀', '∃', '¬', '∅', 'ℜ', 'ℑ', '⊤', '⊥', 'ℵ', 'A', 'B', 'C', 'D', 'E', 'F',
2162 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
2163 'Y', 'Z', '∪', '∩', '⊎', '∧', '∨', '⊢', '⊣', '⌊', '⌋', '⌈', '⌉', '{', '}', '⟨', '⟩', '|',
2164 '∥', '↕', '⇕', '\\', '≀', '√', '∐', '∇', '∫', '⊔', '⊓', '⊑', '⊒', '§', '†', '‡', '¶', '♣',
2165 '♢', '♡', '♠',
2166 ];
2167 const CMMI: [char; 128] = [
2168 'Γ', 'Δ', 'Θ', 'Λ', 'Ξ', 'Π', 'Σ', 'Υ', 'Φ', 'Ψ', 'Ω', 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η',
2169 'θ', 'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'π', 'ρ', 'σ', 'τ', 'υ', 'φ', 'χ', 'ψ', 'ω', 'ϵ', 'ϑ',
2170 'ϖ', 'ϱ', 'ς', 'ϕ', '↼', '↽', '⇀', '⇁', '↩', '↪', '▷', '◁', '0', '1', '2', '3', '4', '5',
2171 '6', '7', '8', '9', '.', ',', '<', '/', '>', '⋆', '∂', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
2172 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y',
2173 'Z', '♭', '♮', '♯', '⌣', '⌢', 'ℓ', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
2174 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'ı', 'ȷ', '℘',
2175 '\u{20d7}', '⁀',
2176 ];
2177 let name = base_font_name(fdict)?;
2178 let up = name.to_ascii_uppercase();
2179 let table: &[char; 128] = if up.starts_with(b"CMSY") || up.starts_with(b"CMBSY") {
2180 &CMSY
2181 } else if up.starts_with(b"CMMI") {
2182 &CMMI
2183 } else {
2184 return None;
2185 };
2186 Some(
2187 table
2188 .iter()
2189 .enumerate()
2190 .map(|(i, &c)| (i as u8, c))
2191 .collect(),
2192 )
2193}
2194
2195pub(crate) fn glyph_name_to_char(name: &[u8]) -> Option<char> {
2200 let s = std::str::from_utf8(name).ok()?;
2201 if let Some(hex) = s.strip_prefix("uni") {
2202 if let Ok(cp) = u32::from_str_radix(hex.get(0..4)?, 16) {
2203 return char::from_u32(cp);
2204 }
2205 }
2206 if s.len() == 1 {
2208 let b = s.as_bytes()[0];
2209 if b.is_ascii_alphabetic() {
2210 return Some(b as char);
2211 }
2212 }
2213 let resolved = match s {
2214 "space" => ' ',
2215 "exclam" => '!',
2216 "quotedbl" => '"',
2217 "numbersign" => '#',
2218 "dollar" => '$',
2219 "percent" => '%',
2220 "ampersand" => '&',
2221 "quotesingle" => '\'',
2222 "parenleft" => '(',
2223 "parenright" => ')',
2224 "asterisk" => '*',
2225 "plus" => '+',
2226 "comma" => ',',
2227 "hyphen" => '-',
2228 "period" => '.',
2229 "slash" => '/',
2230 "zero" => '0',
2231 "one" => '1',
2232 "two" => '2',
2233 "three" => '3',
2234 "four" => '4',
2235 "five" => '5',
2236 "six" => '6',
2237 "seven" => '7',
2238 "eight" => '8',
2239 "nine" => '9',
2240 "colon" => ':',
2241 "semicolon" => ';',
2242 "less" => '<',
2243 "equal" => '=',
2244 "greater" => '>',
2245 "question" => '?',
2246 "at" => '@',
2247 "bracketleft" => '[',
2248 "backslash" => '\\',
2249 "bracketright" => ']',
2250 "asciicircum" => '^',
2251 "underscore" => '_',
2252 "grave" => '`',
2253 "braceleft" => '{',
2254 "bar" => '|',
2255 "braceright" => '}',
2256 "asciitilde" => '~',
2257 "bullet" => '\u{2022}',
2258 "periodcentered" => '\u{00B7}',
2259 "endash" => '\u{2013}',
2260 "emdash" => '\u{2014}',
2261 "quoteright" => '\u{2019}',
2262 "quoteleft" => '\u{2018}',
2263 "quotedblleft" => '\u{201C}',
2264 "quotedblright" => '\u{201D}',
2265 "quotedblbase" => '\u{201E}',
2266 "quotesinglbase" => '\u{201A}',
2267 "ff" => '\u{FB00}',
2272 "fi" => '\u{FB01}',
2273 "fl" => '\u{FB02}',
2274 "ffi" => '\u{FB03}',
2275 "ffl" => '\u{FB04}',
2276 "ft" => '\u{FB05}',
2277 "st" => '\u{FB06}',
2278 "degree" => '\u{00B0}',
2279 "trademark" => '\u{2122}',
2280 "registered" => '\u{00AE}',
2281 "copyright" => '\u{00A9}',
2282 "ellipsis" => '\u{2026}',
2283 "minus" => '\u{2212}',
2284 "fraction" => '\u{2044}',
2285 "nbspace" => '\u{00A0}',
2286 "alpha" => '\u{03B1}',
2291 "beta" => '\u{03B2}',
2292 "gamma" => '\u{03B3}',
2293 "delta" => '\u{03B4}',
2294 "epsilon" | "epsilon1" => '\u{03B5}',
2295 "zeta" => '\u{03B6}',
2296 "eta" => '\u{03B7}',
2297 "theta" | "theta1" => '\u{03B8}',
2298 "iota" => '\u{03B9}',
2299 "kappa" => '\u{03BA}',
2300 "lambda" => '\u{03BB}',
2301 "mu" => '\u{03BC}',
2302 "nu" => '\u{03BD}',
2303 "xi" => '\u{03BE}',
2304 "omicron" => '\u{03BF}',
2305 "pi" | "pi1" => '\u{03C0}',
2306 "rho" | "rho1" => '\u{03C1}',
2307 "sigma" => '\u{03C3}',
2308 "sigma1" => '\u{03C2}',
2309 "tau" => '\u{03C4}',
2310 "upsilon" => '\u{03C5}',
2311 "phi" | "phi1" => '\u{03C6}',
2312 "chi" => '\u{03C7}',
2313 "psi" => '\u{03C8}',
2314 "omega" | "omega1" => '\u{03C9}',
2315 "Gamma" => '\u{0393}',
2316 "Delta" => '\u{0394}',
2317 "Theta" => '\u{0398}',
2318 "Lambda" => '\u{039B}',
2319 "Xi" => '\u{039E}',
2320 "Pi" => '\u{03A0}',
2321 "Sigma" => '\u{03A3}',
2322 "Upsilon" => '\u{03A5}',
2323 "Phi" => '\u{03A6}',
2324 "Psi" => '\u{03A8}',
2325 "Omega" => '\u{03A9}',
2326 "lessequal" => '\u{2264}',
2327 "greaterequal" => '\u{2265}',
2328 "notequal" => '\u{2260}',
2329 "approxequal" => '\u{2248}',
2330 "equivalence" => '\u{2261}',
2331 "element" => '\u{2208}',
2332 "plusminus" => '\u{00B1}',
2333 "multiply" => '\u{00D7}',
2334 "divide" => '\u{00F7}',
2335 "infinity" => '\u{221E}',
2336 "partialdiff" => '\u{2202}',
2337 "gradient" => '\u{2207}',
2338 "summation" => '\u{2211}',
2339 "product" => '\u{220F}',
2340 "integral" => '\u{222B}',
2341 "radical" => '\u{221A}',
2342 "proportional" => '\u{221D}',
2343 "arrowright" => '\u{2192}',
2344 "arrowleft" => '\u{2190}',
2345 "arrowup" => '\u{2191}',
2346 "arrowdown" => '\u{2193}',
2347 "arrowboth" => '\u{2194}',
2348 "arrowdblright" => '\u{21D2}',
2349 "logicaland" => '\u{2227}',
2350 "logicalor" => '\u{2228}',
2351 "intersection" => '\u{2229}',
2352 "union" => '\u{222A}',
2353 "similar" => '\u{223C}',
2354 "congruent" => '\u{2245}',
2355 "dotmath" => '\u{22C5}',
2356 "asteriskmath" => '\u{2217}',
2357 _ => {
2358 if let Some((base, _)) = s.split_once('.') {
2360 if !base.is_empty() {
2361 return glyph_name_to_char(base.as_bytes());
2362 }
2363 }
2364 return None;
2365 }
2366 };
2367 Some(resolved)
2368}
2369
2370fn winansi_table() -> HashMap<u8, char> {
2372 let mut m = HashMap::new();
2373 for b in 0x20u8..=0x7e {
2374 m.insert(b, b as char);
2375 }
2376 let extra: &[(u8, char)] = &[
2378 (0x91, '\u{2018}'),
2379 (0x92, '\u{2019}'),
2380 (0x93, '\u{201C}'),
2381 (0x94, '\u{201D}'),
2382 (0x95, '\u{2022}'),
2383 (0x96, '\u{2013}'),
2384 (0x97, '\u{2014}'),
2385 (0x85, '\u{2026}'),
2386 (0xA0, '\u{00A0}'),
2387 ];
2388 for &(b, c) in extra {
2389 m.insert(b, c);
2390 }
2391 for b in 0xA1u8..=0xFF {
2392 m.entry(b).or_insert(b as char);
2393 }
2394 m
2395}
2396
2397fn macroman_table() -> HashMap<u8, char> {
2400 let mut m = HashMap::new();
2401 for b in 0x20u8..=0x7e {
2402 m.insert(b, b as char);
2403 }
2404 let high: &[(u8, char)] = &[
2405 (0xA5, '\u{2022}'), (0xD0, '\u{2013}'), (0xD1, '\u{2014}'), (0xD2, '\u{201C}'),
2409 (0xD3, '\u{201D}'),
2410 (0xD4, '\u{2018}'),
2411 (0xD5, '\u{2019}'),
2412 (0xCA, '\u{00A0}'),
2413 (0xC9, '\u{2026}'),
2414 (0xDE, '\u{FB01}'),
2415 (0xDF, '\u{FB02}'),
2416 ];
2417 for &(b, c) in high {
2418 m.insert(b, c);
2419 }
2420 m
2421}
2422
2423#[cfg(test)]
2424mod page_box_frame {
2425 use super::*;
2426
2427 fn pdf(boxes: &str, x: f32, y: f32) -> Vec<u8> {
2430 pdf_content(
2431 boxes,
2432 &format!("BT /F1 12 Tf {x} {y} Td (First printing) Tj ET\n"),
2433 )
2434 }
2435
2436 fn pdf_content(boxes: &str, content: &str) -> Vec<u8> {
2439 let objs: Vec<String> = vec![
2440 "<</Type/Catalog/Pages 2 0 R>>".into(),
2441 format!("<</Type/Pages/Kids[3 0 R]/Count 1{boxes}>>"),
2442 "<</Type/Page/Parent 2 0 R/Contents 4 0 R/Resources<</Font<</F1 5 0 R>>>>>>".into(),
2443 format!("<</Length {}>>stream\n{content}endstream", content.len()),
2444 "<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>".into(),
2445 ];
2446 let mut out = b"%PDF-1.4\n".to_vec();
2447 let mut offsets = Vec::new();
2448 for (i, body) in objs.iter().enumerate() {
2449 offsets.push(out.len());
2450 out.extend_from_slice(format!("{} 0 obj{body}endobj\n", i + 1).as_bytes());
2451 }
2452 let xref_at = out.len();
2453 out.extend_from_slice(
2454 format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes(),
2455 );
2456 for off in &offsets {
2457 out.extend_from_slice(format!("{off:010} 00000 n \n").as_bytes());
2458 }
2459 out.extend_from_slice(
2460 format!(
2461 "trailer<</Size {}/Root 1 0 R>>\nstartxref\n{xref_at}\n%%EOF\n",
2462 objs.len() + 1
2463 )
2464 .as_bytes(),
2465 );
2466 out
2467 }
2468
2469 fn only_page(bytes: &[u8]) -> (PageBox, Vec<Glyph>) {
2470 let doc = load_document(bytes).expect("loads");
2471 let pid = *doc.get_pages().values().next().expect("one page");
2472 (page_box(&doc, pid), page_glyphs(&doc, pid))
2473 }
2474
2475 #[test]
2480 fn glyphs_count_from_the_cropbox_corner_like_pdfium() {
2481 let (pb, shifted) = only_page(&pdf(
2482 "/MediaBox[-56.505 -58.25 576.303 723.31]/CropBox[1.095 -0.65 518.703 665.71]",
2483 37.0 + 1.095,
2484 58.0 - 0.65,
2485 ));
2486 assert!((pb.l - 1.095).abs() < 1e-3 && (pb.b + 0.65).abs() < 1e-3);
2487 assert!(
2488 (pb.w - 517.608).abs() < 1e-3 && (pb.h - 666.36).abs() < 1e-3,
2489 "{pb:?}"
2490 );
2491 let (pb0, plain) = only_page(&pdf("/MediaBox[0 0 517.608 666.36]", 37.0, 58.0));
2492 assert!((pb0.w - pb.w).abs() < 1e-3 && (pb0.h - pb.h).abs() < 1e-3);
2493 assert_eq!(shifted.len(), plain.len());
2494 assert!(!plain.is_empty());
2495 for (a, b) in shifted.iter().zip(&plain) {
2496 assert!(
2497 (a.l - b.l).abs() < 1e-3 && (a.b - b.b).abs() < 1e-3,
2498 "{:?} vs {:?}",
2499 (a.l, a.b),
2500 (b.l, b.b)
2501 );
2502 }
2503 assert!((plain[0].l - 37.0).abs() < 1e-3, "{}", plain[0].l);
2504 }
2505
2506 fn text(glyphs: &[Glyph]) -> String {
2507 glyphs.iter().map(|g| g.ch).collect()
2508 }
2509
2510 #[test]
2515 fn glyphs_outside_the_display_box_are_dropped() {
2516 let (_, g) = only_page(&pdf_content(
2517 "/MediaBox[0 0 400 400]/CropBox[100 100 400 400]",
2518 "BT /F1 14 Tf 120 300 Td (Visible.) Tj ET\nBT /F1 8 Tf 5 40 Td (Slug) Tj ET\n",
2519 ));
2520 assert_eq!(text(&g), "Visible.");
2521 let (_, g) = only_page(&pdf_content(
2522 "/MediaBox[0 0 300 400]",
2523 "BT /F1 14 Tf 40 300 Td (On page) Tj ET\nBT /F1 14 Tf 400 250 Td (Beyond) Tj ET\n",
2524 ));
2525 assert_eq!(text(&g), "On page");
2526 let (_, g) = only_page(&pdf_content(
2528 "/MediaBox[0 0 300 400]",
2529 "BT /F1 14 Tf 200 250 Td (Crossing the right edge) Tj ET\n",
2530 ));
2531 assert_eq!(text(&g), "Crossing the rig");
2532 }
2533
2534 #[test]
2539 fn a_glyph_on_the_edge_stays_one_over_it_goes() {
2540 let w_at = |x: f32, y: f32| {
2541 let (_, g) = only_page(&pdf_content(
2542 "/MediaBox[0 0 300 400]",
2543 &format!("BT /F1 50 Tf {x} {y} Td (W) Tj ET\n"),
2544 ));
2545 text(&g)
2546 };
2547 assert_eq!(w_at(252.8, 200.0), "W");
2548 assert_eq!(w_at(252.81, 200.0), "");
2549 assert_eq!(w_at(0.0, 200.0), "W");
2550 assert_eq!(w_at(-0.01, 200.0), "");
2551 assert_eq!(w_at(100.0, 20.0), "W");
2554 assert_eq!(w_at(100.0, 0.0), "");
2555 assert_eq!(w_at(100.0, 380.0), "");
2556 }
2557
2558 #[test]
2561 fn page_box_follows_pdfium_fallbacks() {
2562 let (pb, _) = only_page(&pdf("", 10.0, 10.0));
2563 assert_eq!((pb.l, pb.b, pb.w, pb.h), (0.0, 0.0, 612.0, 792.0));
2564 let (pb, _) = only_page(&pdf(
2565 "/MediaBox[0 0 500 700]/CropBox[-100 100 600 900]",
2566 10.0,
2567 10.0,
2568 ));
2569 assert_eq!((pb.l, pb.b, pb.w, pb.h), (0.0, 100.0, 500.0, 600.0));
2570 let (pb, _) = only_page(&pdf(
2571 "/MediaBox[0 0 500 700]/CropBox[800 800 900 900]",
2572 10.0,
2573 10.0,
2574 ));
2575 assert_eq!((pb.l, pb.b, pb.w, pb.h), (0.0, 0.0, 500.0, 700.0));
2576 let (pb, _) = only_page(&pdf("/MediaBox[500 700 0 0]", 10.0, 10.0));
2578 assert_eq!((pb.l, pb.b, pb.w, pb.h), (0.0, 0.0, 500.0, 700.0));
2579 }
2580}
2581
2582#[cfg(test)]
2583mod xref_repair {
2584 fn pdf_with_xref(two_byte_eol: bool) -> Vec<u8> {
2588 let content = b"BT /F1 12 Tf 72 700 Td (Invoice 922769430725) Tj ET\n";
2589 let stream = format!("<</Length {}>>stream\n", content.len()).into_bytes();
2590 let objs: Vec<Vec<u8>> = vec![
2591 b"<</Type/Catalog/Pages 2 0 R>>".to_vec(),
2592 b"<</Type/Pages/Kids[3 0 R]/Count 1>>".to_vec(),
2593 b"<</Type/Page/Parent 2 0 R/MediaBox[0 0 595 842]/Contents 4 0 R\
2594 /Resources<</Font<</F1 5 0 R>>>>>>"
2595 .to_vec(),
2596 [stream.as_slice(), content.as_slice(), b"endstream"].concat(),
2597 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>".to_vec(),
2598 ];
2599
2600 let mut out = b"%PDF-1.4\n".to_vec();
2601 let mut offsets = Vec::new();
2602 for (i, body) in objs.iter().enumerate() {
2603 offsets.push(out.len());
2604 out.extend_from_slice(format!("{} 0 obj", i + 1).as_bytes());
2605 out.extend_from_slice(body);
2606 out.extend_from_slice(b"endobj\n");
2607 }
2608 let xref_at = out.len();
2609 let eol: &[u8] = if two_byte_eol { b" \n" } else { b"\n" };
2610 out.extend_from_slice(format!("xref\n0 {}\n", objs.len() + 1).as_bytes());
2611 out.extend_from_slice(b"0000000000 65535 f");
2612 out.extend_from_slice(eol);
2613 for off in &offsets {
2614 out.extend_from_slice(format!("{off:010} 00000 n").as_bytes());
2615 out.extend_from_slice(eol);
2616 }
2617 out.extend_from_slice(
2618 format!("trailer<</Size {}/Root 1 0 R>>\n", objs.len() + 1).as_bytes(),
2619 );
2620 out.extend_from_slice(format!("startxref\n{xref_at}\n%%EOF\n").as_bytes());
2621 out
2622 }
2623
2624 #[test]
2630 fn short_xref_entries_still_parse() {
2631 let good = pdf_with_xref(true);
2632 let broken = pdf_with_xref(false);
2633 assert!(
2634 broken.len() < good.len(),
2635 "the broken file is the shorter one"
2636 );
2637 assert!(
2638 lopdf::Document::load_mem(&good).is_ok(),
2639 "the control file must load unaided"
2640 );
2641 assert!(
2642 lopdf::Document::load_mem(&broken).is_err(),
2643 "lopdf rejects 19-byte entries — if this ever passes, drop the repair"
2644 );
2645
2646 let cells = |b: &[u8]| -> Vec<String> {
2647 super::pdf_textlines(b)
2648 .into_iter()
2649 .flat_map(|(_, _, c)| c.into_iter().map(|c| c.text))
2650 .collect()
2651 };
2652 let from_good = cells(&good);
2653 assert!(
2654 from_good.iter().any(|t| t.contains("922769430725")),
2655 "control text: {from_good:?}"
2656 );
2657 assert_eq!(
2658 cells(&broken),
2659 from_good,
2660 "repair must match the good parse"
2661 );
2662 }
2663
2664 #[test]
2669 fn overstated_stream_length_still_yields_content() {
2670 let good = pdf_with_xref(true);
2671 let broken = {
2674 let at = good
2675 .windows(8)
2676 .position(|w| w == b"/Length ")
2677 .expect("a /Length")
2678 + 8;
2679 let digits = good[at..].iter().take_while(|c| c.is_ascii_digit()).count();
2680 let n: usize = std::str::from_utf8(&good[at..at + digits])
2681 .unwrap()
2682 .parse()
2683 .unwrap();
2684 let inflated = (n + 1).to_string();
2685 assert_eq!(inflated.len(), digits, "keep the digit count");
2686 let mut b = good.clone();
2687 b[at..at + digits].copy_from_slice(inflated.as_bytes());
2688 b
2689 };
2690 assert_eq!(broken.len(), good.len(), "the defect must not move bytes");
2691 let raw = lopdf::Document::load_mem(&broken).expect("still loads");
2693 assert!(
2694 raw.get_pages()
2695 .into_values()
2696 .all(|p| raw.get_page_content(p).is_empty()),
2697 "lopdf should drop the stream — if it stops, drop this repair"
2698 );
2699 let text = |b: &[u8]| -> Vec<String> {
2701 super::pdf_textlines(b)
2702 .into_iter()
2703 .flat_map(|(_, _, c)| c.into_iter().map(|c| c.text))
2704 .collect()
2705 };
2706 let expected = text(&good);
2707 assert!(!expected.is_empty(), "control must produce text");
2708 assert_eq!(text(&broken), expected);
2709 }
2710
2711 #[test]
2715 fn repair_declines_when_padding_would_move_objects() {
2716 let mut incremental = pdf_with_xref(false);
2717 incremental.extend_from_slice(b"6 0 obj<</Type/Whatever>>endobj\n");
2718 let declined = super::pad_short_xref_entries(&incremental).unwrap_err();
2719 assert!(
2720 declined.contains("object follows the xref"),
2721 "reason: {declined}"
2722 );
2723 }
2724}
2725
2726#[cfg(test)]
2732mod base14_fonts {
2733 fn pdf_with_font(fontdict: &[u8], text: &[u8]) -> Vec<u8> {
2735 let content = [b"BT /F1 12 Tf 72 700 Td (".as_slice(), text, b") Tj ET\n"].concat();
2736 pdf_with_content(fontdict, &content)
2737 }
2738
2739 fn pdf_with_content(fontdict: &[u8], content: &[u8]) -> Vec<u8> {
2741 let stream = format!("<</Length {}>>stream\n", content.len()).into_bytes();
2742 let objs: Vec<Vec<u8>> = vec![
2743 b"<</Type/Catalog/Pages 2 0 R>>".to_vec(),
2744 b"<</Type/Pages/Kids[3 0 R]/Count 1>>".to_vec(),
2745 b"<</Type/Page/Parent 2 0 R/MediaBox[0 0 595 842]/Contents 4 0 R\
2746 /Resources<</Font<</F1 5 0 R>>>>>>"
2747 .to_vec(),
2748 [stream.as_slice(), content, b"endstream"].concat(),
2749 fontdict.to_vec(),
2750 ];
2751 let mut out = b"%PDF-1.4\n".to_vec();
2752 let mut offsets = Vec::new();
2753 for (i, body) in objs.iter().enumerate() {
2754 offsets.push(out.len());
2755 out.extend_from_slice(format!("{} 0 obj", i + 1).as_bytes());
2756 out.extend_from_slice(body);
2757 out.extend_from_slice(b"endobj\n");
2758 }
2759 let xref_at = out.len();
2760 out.extend_from_slice(format!("xref\n0 {}\n", objs.len() + 1).as_bytes());
2761 out.extend_from_slice(b"0000000000 65535 f \n");
2762 for off in &offsets {
2763 out.extend_from_slice(format!("{off:010} 00000 n \n").as_bytes());
2764 }
2765 out.extend_from_slice(
2766 format!("trailer<</Size {}/Root 1 0 R>>\n", objs.len() + 1).as_bytes(),
2767 );
2768 out.extend_from_slice(format!("startxref\n{xref_at}\n%%EOF\n").as_bytes());
2769 out
2770 }
2771
2772 fn cells(pdf: &[u8]) -> Vec<crate::pdfium_backend::TextCell> {
2774 super::pdf_textlines(pdf)
2775 .into_iter()
2776 .flat_map(|(_, _, c)| c)
2777 .collect()
2778 }
2779
2780 #[test]
2787 fn rotated_text_matrix_reads_in_order() {
2788 const HELV: &[u8] = b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>";
2789 const TEXT: &str = "Upright text on a rotated page.";
2790 for (tm, [l, b, r, t]) in [
2791 ("0 14 -14 0 200 100", [189.9, 100.0, 202.9, 289.1]), ("-14 0 0 -14 350 300", [160.9, 289.9, 350.0, 302.9]), ("0 -14 14 0 200 500", [197.1, 310.9, 210.1, 500.0]), (
2795 "9.8995 9.8995 -9.8995 9.8995 100 100",
2796 [92.9, 98.0, 235.8, 240.8],
2797 ), ] {
2799 let content = format!("BT /F1 1 Tf {tm} Tm ({TEXT}) Tj ET\n");
2800 let pdf = pdf_with_content(HELV, content.as_bytes());
2801 let cs = cells(&pdf);
2802 assert_eq!(cs.len(), 1, "{tm}: {cs:?}");
2803 let c = &cs[0];
2804 assert_eq!(c.text, TEXT, "{tm}");
2805 let got = [c.l, 842.0 - c.b, c.r, 842.0 - c.t];
2810 for (g, w) in got.iter().zip([l, b, r, t]) {
2811 assert!(
2812 (g - w).abs() < 1.0,
2813 "{tm}: box {got:?} vs docling-parse {:?}",
2814 [l, b, r, t]
2815 );
2816 }
2817 let words: Vec<String> = super::pdf_words(&pdf)
2818 .into_iter()
2819 .flat_map(|(_, _, c)| c)
2820 .map(|c| c.text)
2821 .collect();
2822 assert_eq!(words, TEXT.split(' ').collect::<Vec<_>>(), "{tm}");
2823 }
2824 }
2825
2826 #[test]
2831 fn rotated_text_off_the_page_is_dropped() {
2832 const HELV: &[u8] = b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>";
2833 let off = pdf_with_content(
2834 HELV,
2835 b"BT /F1 1 Tf 0 14 -14 0 -4 200 Tm (Spine title) Tj ET\n",
2836 );
2837 assert!(cells(&off).is_empty(), "{:?}", cells(&off));
2838 let on = pdf_with_content(
2839 HELV,
2840 b"BT /F1 1 Tf 0 14 -14 0 30 200 Tm (Margin note) Tj ET\n",
2841 );
2842 assert_eq!(cells(&on).len(), 1);
2843 }
2844
2845 #[test]
2848 fn upright_glyphs_carry_no_quad() {
2849 let only_page = |pdf: &[u8]| {
2850 let doc = super::load_document(pdf).expect("loads");
2851 let pid = *doc.get_pages().values().next().expect("one page");
2852 super::page_glyphs(&doc, pid)
2853 };
2854 let pdf = pdf_with_font(b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>", b"Plain");
2855 let glyphs = only_page(&pdf);
2856 assert!(!glyphs.is_empty());
2857 assert!(glyphs.iter().all(|g| g.quad.is_none() && g.lr > g.ll));
2858 let pdf = pdf_with_content(
2860 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>",
2861 b"BT /F1 1 Tf 0 14 -14 0 200 100 Tm (W) Tj ET\n",
2862 );
2863 let glyphs = only_page(&pdf);
2864 let g = &glyphs[0];
2865 assert!(g.quad.is_some());
2866 assert!((g.height() - 14.0).abs() < 0.05, "{}", g.height());
2868 }
2869
2870 #[test]
2875 fn ticks_stacked_by_cm_are_separate_line_cells() {
2876 let mut content = String::new();
2877 for (k, tick) in "345678".chars().enumerate() {
2878 let y = 300.0 + 25.92 * k as f64;
2879 content.push_str(&format!(
2880 "q 1 0 0 1 125 {y} cm BT /F1 10 Tf 1 0 0 1 -5 -4 Tm ({tick}) Tj ET Q\n"
2881 ));
2882 }
2883 let pdf = pdf_with_content(
2884 b"<</Type/Font/Subtype/Type1/BaseFont/Times-Roman>>",
2885 content.as_bytes(),
2886 );
2887 let cells = cells(&pdf);
2888 let texts: Vec<&str> = cells.iter().map(|c| c.text.as_str()).collect();
2889 assert_eq!(texts, ["3", "4", "5", "6", "7", "8"]);
2890 for (k, c) in cells.iter().enumerate() {
2891 let base = 842.0 - (300.0 + 25.92 * k as f32 - 4.0);
2893 assert!((c.l - 120.0).abs() < 0.1, "{k}: l {}", c.l);
2894 assert!(
2895 c.t < base && base - c.t < 10.0,
2896 "{k}: t {} base {base}",
2897 c.t
2898 );
2899 }
2900 }
2901
2902 #[test]
2908 fn drawn_checkbox_squares_are_found() {
2909 let content = b"q 1 0 0 1 119.52 449.04 cm \
2910 n 0 12.96 m 12.96 12.96 l S n 0 0 m 12.96 0 l S \
2911 n 0 0 m 0 12.96 l S n 12.96 0 m 12.96 12.96 l S Q\n\
2912 200 400 10 10 re S 202 405 m 204.5 402 l 208.5 408.5 l S\n\
2913 300 400 12 12 re f 50 50 100 100 re S\n\
2914 q 0.2 0.4 0.8 rg 400 400 12 12 re B Q q 1 g 500 400 12 12 re B Q\n";
2915 let pdf = pdf_with_content(b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>", content);
2916 let mut parser = super::PageTextParser::open(&pdf).expect("parses");
2917 let mut boxes = parser.cells(0).checkboxes;
2918 boxes.sort_by(|a, b| a.l.total_cmp(&b.l));
2919 let got: Vec<([i32; 4], bool)> = boxes
2920 .iter()
2921 .map(|c| {
2922 let r = |v: f32| (v * 100.0).round() as i32;
2923 ([r(c.l), r(c.t), r(c.r), r(c.b)], c.checked)
2924 })
2925 .collect();
2926 assert_eq!(
2928 got,
2929 [
2930 ([11952, 38000, 13248, 39296], false),
2931 ([20000, 43200, 21000, 44200], true),
2932 ([50000, 43000, 51200, 44200], false),
2933 ]
2934 );
2935 }
2936
2937 #[test]
2939 fn standard14_faces_get_builtin_widths() {
2940 for fontdict in [
2941 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/WinAnsiEncoding>>".as_slice(),
2943 b"<</Type/Font/Subtype/Type1/BaseFont/Times-BoldItalic>>",
2945 b"<</Type/Font/Subtype/TrueType/BaseFont/Arial,Bold>>",
2947 b"<</Type/Font/Subtype/Type1/BaseFont/ABCDEF+Courier-Oblique>>",
2948 ] {
2949 let pdf = pdf_with_font(fontdict, b"Words have width now");
2950 let cs = cells(&pdf);
2951 let text: String = cs
2952 .iter()
2953 .map(|c| c.text.as_str())
2954 .collect::<Vec<_>>()
2955 .join(" ");
2956 assert!(
2957 text.contains("Words have width now"),
2958 "{}: text lost: {text:?}",
2959 String::from_utf8_lossy(fontdict)
2960 );
2961 assert!(
2962 cs.iter().all(|c| c.r > c.l),
2963 "{}: zero-width cells: {cs:?}",
2964 String::from_utf8_lossy(fontdict)
2965 );
2966 }
2967 }
2968
2969 #[test]
2972 fn explicit_widths_win_and_unknown_faces_are_untouched() {
2973 let explicit = pdf_with_font(
2977 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/FirstChar 65\
2978 /Widths[100 100 100 100]/Encoding/WinAnsiEncoding>>",
2979 b"ABBA",
2980 );
2981 let builtin = pdf_with_font(
2982 b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/WinAnsiEncoding>>",
2983 b"ABBA",
2984 );
2985 let w = |pdf: &[u8]| {
2986 let cs = cells(pdf);
2987 assert_eq!(cs.len(), 1, "one word cell: {cs:?}");
2988 cs[0].r - cs[0].l
2989 };
2990 let (we, wb) = (w(&explicit), w(&builtin));
2991 assert!(
2992 (we - 4.8).abs() < 0.1,
2993 "explicit widths must win: got {we}, want 4×100×12/1000"
2994 );
2995 assert!(
2996 wb > 2.0 * we,
2997 "built-in Helvetica is much wider: {wb} vs {we}"
2998 );
2999
3000 let unknown = pdf_with_font(
3004 b"<</Type/Font/Subtype/Type1/BaseFont/FancyCorp-Display>>",
3005 b"Mystery",
3006 );
3007 let cs = cells(&unknown);
3008 let text: String = cs.iter().map(|c| c.text.as_str()).collect();
3009 assert!(text.contains("Mystery"), "text still decodes: {cs:?}");
3010 }
3011}
3012
3013#[cfg(test)]
3014mod overpainted {
3015 use crate::pdfium_backend::TextCell;
3016
3017 fn cell(text: &str, l: f32, t: f32, r: f32, b: f32) -> TextCell {
3018 TextCell {
3019 text: text.into(),
3020 l,
3021 t,
3022 r,
3023 b,
3024 }
3025 }
3026
3027 #[test]
3031 fn stacked_logo_glyphs_are_dropped() {
3032 let mut cells = vec![
3033 cell("\"", 72.7, 21.5, 86.4, 31.5),
3034 cell("==", 59.4, 21.5, 99.6, 31.5),
3035 cell("Herr", 65.2, 151.3, 81.7, 161.3),
3036 ];
3037 super::drop_overpainted_cells(&mut cells);
3038 assert_eq!(cells.len(), 1, "cells: {cells:?}");
3039 assert_eq!(cells[0].text, "Herr");
3040 }
3041
3042 #[test]
3046 fn prose_and_double_draw_are_kept() {
3047 let mut cells = vec![
3048 cell("Telefon", 354.3, 133.2, 381.5, 143.2),
3049 cell("0676/2000", 387.3, 133.2, 428.7, 143.2),
3050 cell("Bold", 100.0, 50.0, 130.0, 60.0),
3051 cell("Bold", 100.3, 50.0, 130.3, 60.0),
3052 ];
3053 super::drop_overpainted_cells(&mut cells);
3054 assert_eq!(cells.len(), 4);
3055 }
3056}
3057
3058#[cfg(test)]
3059mod vestigial_layer {
3060 use crate::pdfium_backend::{PdfPage, TextCell};
3061
3062 fn page_with(texts: &[&str]) -> PdfPage {
3063 let cells = texts
3064 .iter()
3065 .enumerate()
3066 .map(|(i, t)| TextCell {
3067 text: t.to_string(),
3068 l: 10.0,
3069 t: 10.0 + 12.0 * i as f32,
3070 r: 90.0,
3071 b: 20.0 + 12.0 * i as f32,
3072 })
3073 .collect();
3074 PdfPage::from_cells(595.0, 842.0, 1.0, cells)
3075 }
3076
3077 #[test]
3082 fn typed_in_form_fields_are_not_a_text_layer() {
3083 let pages = vec![
3084 page_with(&["03", "05", "2025"]),
3085 page_with(&[]),
3086 page_with(&[]),
3087 ];
3088 assert!(super::text_layer_is_vestigial(&pages));
3089 assert!(super::text_layer_is_vestigial(&[page_with(&[])]));
3090 }
3091
3092 #[test]
3095 fn sparse_but_real_documents_pass() {
3096 let one_pager = vec![page_with(&[
3097 "Confidential briefing",
3098 "Prepared for the board meeting",
3099 "Do not distribute",
3100 ])];
3101 assert!(!super::text_layer_is_vestigial(&one_pager));
3102 }
3103}