1mod cmap;
5mod extract;
6mod font;
7mod sfnt;
8
9use pdfboss_core::{
10 block_on, AsyncObjectSource, Document, Error, Immediate, OcState, Page, Result, StructureTree,
11};
12
13pub use extract::{ExtractReport, FontCache, SkipCause, SkippedText, SkippedTextKind};
14pub use pdfboss_core::{MarkedContentId, Point, Rect};
15
16#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
19pub enum ReadingOrder {
20 #[default]
25 Content,
26 StructureTree,
30 Geometric,
34}
35
36impl ReadingOrder {
37 pub const ALL: [ReadingOrder; 3] = [
39 ReadingOrder::Content,
40 ReadingOrder::StructureTree,
41 ReadingOrder::Geometric,
42 ];
43
44 pub fn as_str(self) -> &'static str {
47 match self {
48 ReadingOrder::Content => "content",
49 ReadingOrder::StructureTree => "structure-tree",
50 ReadingOrder::Geometric => "geometric",
51 }
52 }
53}
54
55impl std::fmt::Display for ReadingOrder {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 f.write_str(self.as_str())
58 }
59}
60
61impl std::str::FromStr for ReadingOrder {
62 type Err = Error;
63
64 fn from_str(s: &str) -> Result<ReadingOrder> {
65 ReadingOrder::ALL
66 .into_iter()
67 .find(|order| order.as_str() == s)
68 .ok_or_else(|| {
69 Error::Other(format!(
70 "unknown reading order {s:?}: expected 'content', 'structure-tree' or 'geometric'"
71 ))
72 })
73 }
74}
75
76fn structure_for(doc: &Document, order: ReadingOrder) -> Option<StructureTree> {
78 match order {
79 ReadingOrder::StructureTree => doc.structure_tree(),
80 _ => None,
81 }
82}
83
84#[derive(Debug, Clone, PartialEq)]
86pub struct TextSpan {
87 pub text: String,
89 pub x: f32,
91 pub y: f32,
93 pub end_x: f32,
95 pub size: f32,
97 pub font: String,
99 pub font_name: String,
103 pub page: usize,
105 pub bbox: Rect,
111 pub bold: bool,
116 pub italic: bool,
120 pub monospace: bool,
122 pub serif: bool,
124 pub rise: f32,
128 pub vertical: bool,
131 pub invisible: bool,
134 pub color: Option<(f32, f32, f32)>,
139 pub underline: bool,
143 pub strikethrough: bool,
146}
147
148#[derive(Debug, Clone, PartialEq)]
155pub struct Ruling {
156 pub start: Point,
157 pub end: Point,
158 pub width: f32,
162}
163
164pub fn extract_spans(doc: &Document, page: &Page, order: ReadingOrder) -> Result<Vec<TextSpan>> {
181 let oc = doc.oc_state();
182 let structure = structure_for(doc, order);
183 let (spans, _, _) = block_on(extract::page_spans_and_rulings_with(
184 Immediate(doc),
185 page,
186 None,
187 oc.as_ref(),
188 structure.as_ref(),
189 order,
190 ));
191 Ok(spans)
192}
193
194pub async fn extract_spans_with<S: AsyncObjectSource>(
213 src: S,
214 page: &Page,
215 oc: Option<&OcState>,
216 structure: Option<&StructureTree>,
217 order: ReadingOrder,
218) -> Result<Vec<TextSpan>> {
219 let (spans, _, _) =
220 extract::page_spans_and_rulings_with(src, page, None, oc, structure, order).await;
221 Ok(spans)
222}
223
224pub fn extract_spans_reporting(
230 doc: &Document,
231 page: &Page,
232 order: ReadingOrder,
233) -> Result<(Vec<TextSpan>, ExtractReport)> {
234 let oc = doc.oc_state();
235 let structure = structure_for(doc, order);
236 let (spans, _, report) = block_on(extract::page_spans_and_rulings_with(
237 Immediate(doc),
238 page,
239 None,
240 oc.as_ref(),
241 structure.as_ref(),
242 order,
243 ));
244 Ok((spans, report))
245}
246
247pub async fn extract_spans_reporting_with<S: AsyncObjectSource>(
250 src: S,
251 page: &Page,
252 oc: Option<&OcState>,
253 structure: Option<&StructureTree>,
254 order: ReadingOrder,
255) -> Result<(Vec<TextSpan>, ExtractReport)> {
256 let (spans, _, report) =
257 extract::page_spans_and_rulings_with(src, page, None, oc, structure, order).await;
258 Ok((spans, report))
259}
260
261pub fn extract_spans_reporting_cached(
272 doc: &Document,
273 page: &Page,
274 fonts: &FontCache,
275 order: ReadingOrder,
276) -> Result<(Vec<TextSpan>, ExtractReport)> {
277 let oc = doc.oc_state();
278 let structure = structure_for(doc, order);
279 let (spans, _, report) = block_on(extract::page_spans_and_rulings_with(
280 Immediate(doc),
281 page,
282 Some(fonts),
283 oc.as_ref(),
284 structure.as_ref(),
285 order,
286 ));
287 Ok((spans, report))
288}
289
290pub async fn extract_spans_reporting_cached_with<S: AsyncObjectSource>(
293 src: S,
294 page: &Page,
295 fonts: &FontCache,
296 oc: Option<&OcState>,
297 structure: Option<&StructureTree>,
298 order: ReadingOrder,
299) -> Result<(Vec<TextSpan>, ExtractReport)> {
300 let (spans, _, report) =
301 extract::page_spans_and_rulings_with(src, page, Some(fonts), oc, structure, order).await;
302 Ok((spans, report))
303}
304
305pub fn extract_spans_and_rulings_reporting(
310 doc: &Document,
311 page: &Page,
312 order: ReadingOrder,
313) -> Result<(Vec<TextSpan>, Vec<Ruling>, ExtractReport)> {
314 let oc = doc.oc_state();
315 let structure = structure_for(doc, order);
316 let (spans, rulings, report) = block_on(extract::page_spans_and_rulings_with(
317 Immediate(doc),
318 page,
319 None,
320 oc.as_ref(),
321 structure.as_ref(),
322 order,
323 ));
324 Ok((spans, rulings, report))
325}
326
327pub async fn extract_spans_and_rulings_reporting_with<S: AsyncObjectSource>(
330 src: S,
331 page: &Page,
332 oc: Option<&OcState>,
333 structure: Option<&StructureTree>,
334 order: ReadingOrder,
335) -> Result<(Vec<TextSpan>, Vec<Ruling>, ExtractReport)> {
336 let (spans, rulings, report) =
337 extract::page_spans_and_rulings_with(src, page, None, oc, structure, order).await;
338 Ok((spans, rulings, report))
339}
340
341pub fn extract_spans_and_rulings_reporting_cached(
348 doc: &Document,
349 page: &Page,
350 fonts: &FontCache,
351 order: ReadingOrder,
352) -> Result<(Vec<TextSpan>, Vec<Ruling>, ExtractReport)> {
353 let oc = doc.oc_state();
354 let structure = structure_for(doc, order);
355 let (spans, rulings, report) = block_on(extract::page_spans_and_rulings_with(
356 Immediate(doc),
357 page,
358 Some(fonts),
359 oc.as_ref(),
360 structure.as_ref(),
361 order,
362 ));
363 Ok((spans, rulings, report))
364}
365
366pub async fn extract_spans_and_rulings_reporting_cached_with<S: AsyncObjectSource>(
370 src: S,
371 page: &Page,
372 fonts: &FontCache,
373 oc: Option<&OcState>,
374 structure: Option<&StructureTree>,
375 order: ReadingOrder,
376) -> Result<(Vec<TextSpan>, Vec<Ruling>, ExtractReport)> {
377 let (spans, rulings, report) =
378 extract::page_spans_and_rulings_with(src, page, Some(fonts), oc, structure, order).await;
379 Ok((spans, rulings, report))
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385 use pdfboss_core::{resolve_with, BoxFuture, ObjRef, Object, Stream};
386 use pdfboss_testkit::{simple_doc, PdfBuilder};
387 use std::future::Future;
388
389 #[test]
394 fn form_matrix_translates_the_nested_span() {
395 let mut b = PdfBuilder::new();
396 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
397 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
398 b.object(
399 3,
400 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
401 /Resources << /Font << /F1 5 0 R >> /XObject << /Fx 6 0 R >> >> \
402 /Contents 4 0 R >>",
403 );
404 b.stream(4, "", b"BT /F1 12 Tf 72 720 Td (out) Tj ET /Fx Do");
405 b.object(
406 5,
407 "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica \
408 /Encoding /WinAnsiEncoding >>",
409 );
410 b.stream(
412 6,
413 "/Type /XObject /Subtype /Form /BBox [0 0 612 792] \
414 /Matrix [1 0 0 1 0 -20]",
415 b"BT /F1 12 Tf 72 720 Td (in) Tj ET",
416 );
417 let doc = Document::load(b.build(1)).unwrap();
418 let page = doc.page(0).unwrap();
419 let spans = extract_spans(&doc, &page, ReadingOrder::Content).unwrap();
420 assert_eq!(spans.len(), 2);
421 assert!((spans[1].y - 700.0).abs() < 1e-3); }
423
424 #[test]
425 fn extract_spans_sane_positions() {
426 let doc = Document::load(simple_doc("Hi")).unwrap();
427 let page = doc.page(0).unwrap();
428 let spans = extract_spans(&doc, &page, ReadingOrder::Content).unwrap();
429 assert_eq!(spans.len(), 1);
430 let s = &spans[0];
431 assert_eq!(s.text, "Hi");
432 assert!((s.x - 72.0).abs() < 1e-3);
433 assert!((s.y - 720.0).abs() < 1e-3);
434 assert!((s.size - 12.0).abs() < 1e-3);
435 assert_eq!(s.font, "F1");
436 }
437
438 #[test]
441 fn extract_spans_and_rulings_reports_both() {
442 let doc = Document::load(pdfboss_testkit::doc_with_graphics(
443 "BT /F1 12 Tf 72 720 Td (Hi) Tj ET 72 700 m 272 700 l S",
444 ))
445 .unwrap();
446 let page = doc.page(0).unwrap();
447 let (spans, rulings, report) =
448 extract_spans_and_rulings_reporting(&doc, &page, ReadingOrder::Content).unwrap();
449 assert_eq!(spans.len(), 1);
450 assert_eq!(spans[0].text, "Hi");
451 assert_eq!(rulings.len(), 1);
452 assert!((rulings[0].start.y - 700.0).abs() < 1e-3);
453 assert!(report.is_complete());
454 }
455
456 #[test]
457 fn extract_spans_ordering_multi_line() {
458 let doc = Document::load(pdfboss_testkit::doc_with_graphics(
459 "BT /F1 12 Tf 72 720 Td (top) Tj 0 -40 Td (bottom) Tj ET",
460 ))
461 .unwrap();
462 let page = doc.page(0).unwrap();
463 let spans = extract_spans(&doc, &page, ReadingOrder::Content).unwrap();
464 assert_eq!(spans.len(), 2);
465 assert!(spans[0].y > spans[1].y);
466 assert_eq!(spans[0].text, "top");
467 assert_eq!(spans[1].text, "bottom");
468 assert!(spans.iter().all(|s| s.size > 0.0 && s.x >= 0.0));
469 }
470
471 #[test]
474 fn span_font_name_is_the_base_font_verbatim() {
475 let mut b = PdfBuilder::new();
476 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
477 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
478 b.object(
479 3,
480 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
481 /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
482 );
483 b.stream(4, "", b"BT /F1 12 Tf 72 720 Td (x) Tj ET");
484 b.object(
485 5,
486 "<< /Type /Font /Subtype /Type1 /BaseFont /ABCDEF+Times-Roman \
487 /Encoding /WinAnsiEncoding >>",
488 );
489 let doc = Document::load(b.build(1)).unwrap();
490 let page = doc.page(0).unwrap();
491 let spans = extract_spans(&doc, &page, ReadingOrder::Content).unwrap();
492 assert_eq!(spans[0].font_name, "ABCDEF+Times-Roman");
493 assert_eq!(spans[0].font, "F1");
494 }
495
496 #[test]
500 fn span_font_name_falls_back_to_descriptor_font_name() {
501 let mut b = PdfBuilder::new();
502 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
503 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
504 b.object(
505 3,
506 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
507 /Resources << /Font << /F1 5 0 R /F2 7 0 R >> >> /Contents 4 0 R >>",
508 );
509 b.stream(4, "", b"BT /F1 12 Tf 72 720 Td (a) Tj /F2 12 Tf (b) Tj ET");
510 b.object(
511 5,
512 "<< /Type /Font /Subtype /Type1 /Encoding /WinAnsiEncoding \
513 /FontDescriptor 6 0 R >>",
514 );
515 b.object(6, "<< /Type /FontDescriptor /FontName /Nameless-Face >>");
516 b.object(
517 7,
518 "<< /Type /Font /Subtype /Type1 /Encoding /WinAnsiEncoding >>",
519 );
520 let doc = Document::load(b.build(1)).unwrap();
521 let page = doc.page(0).unwrap();
522 let spans = extract_spans(&doc, &page, ReadingOrder::Content).unwrap();
523 assert_eq!(spans[0].font_name, "Nameless-Face");
524 assert_eq!(spans[1].font_name, "");
525 }
526
527 #[test]
529 fn span_carries_its_page_index() {
530 let mut b = PdfBuilder::new();
531 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
532 b.object(2, "<< /Type /Pages /Kids [3 0 R 5 0 R] /Count 2 >>");
533 b.object(
534 3,
535 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
536 /Resources << /Font << /F1 7 0 R >> >> /Contents 4 0 R >>",
537 );
538 b.stream(4, "", b"BT /F1 12 Tf 72 720 Td (one) Tj ET");
539 b.object(
540 5,
541 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
542 /Resources << /Font << /F1 7 0 R >> >> /Contents 6 0 R >>",
543 );
544 b.stream(6, "", b"BT /F1 12 Tf 72 720 Td (two) Tj ET");
545 b.object(
546 7,
547 "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica \
548 /Encoding /WinAnsiEncoding >>",
549 );
550 let doc = Document::load(b.build(1)).unwrap();
551 for index in 0..2 {
552 let page = doc.page(index).unwrap();
553 let spans = extract_spans(&doc, &page, ReadingOrder::Content).unwrap();
554 assert_eq!(spans[0].page, index, "page {index}");
555 }
556 }
557
558 #[test]
561 fn span_bbox_uses_descriptor_metrics() {
562 let mut b = PdfBuilder::new();
563 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
564 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
565 b.object(
566 3,
567 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
568 /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
569 );
570 b.stream(4, "", b"BT /F1 12 Tf 72 720 Td (Hi) Tj ET");
571 b.object(
572 5,
573 "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica \
574 /Encoding /WinAnsiEncoding /FontDescriptor 6 0 R >>",
575 );
576 b.object(
577 6,
578 "<< /Type /FontDescriptor /FontName /Helvetica \
579 /Ascent 718 /Descent -207 >>",
580 );
581 let doc = Document::load(b.build(1)).unwrap();
582 let page = doc.page(0).unwrap();
583 let spans = extract_spans(&doc, &page, ReadingOrder::Content).unwrap();
584 let s = &spans[0];
585 assert!((s.bbox.x0 - s.x).abs() < 1e-3);
586 assert!((s.bbox.x1 - s.end_x).abs() < 1e-3);
587 assert!((s.bbox.y0 - (720.0 - 0.207 * 12.0)).abs() < 1e-3);
588 assert!((s.bbox.y1 - (720.0 + 0.718 * 12.0)).abs() < 1e-3);
589 }
590
591 #[test]
594 fn span_bbox_defaults_to_em_fractions() {
595 let doc = Document::load(simple_doc("Hi")).unwrap();
596 let page = doc.page(0).unwrap();
597 let spans = extract_spans(&doc, &page, ReadingOrder::Content).unwrap();
598 let s = &spans[0];
599 assert!((s.bbox.y0 - (720.0 - 0.2 * 12.0)).abs() < 1e-3);
600 assert!((s.bbox.y1 - (720.0 + 0.8 * 12.0)).abs() < 1e-3);
601 }
602
603 #[test]
606 fn span_bbox_falls_back_to_cap_height() {
607 let mut b = PdfBuilder::new();
608 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
609 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
610 b.object(
611 3,
612 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
613 /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
614 );
615 b.stream(4, "", b"BT /F1 12 Tf 72 720 Td (Hi) Tj ET");
616 b.object(
617 5,
618 "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica \
619 /Encoding /WinAnsiEncoding /FontDescriptor 6 0 R >>",
620 );
621 b.object(
622 6,
623 "<< /Type /FontDescriptor /FontName /Helvetica /CapHeight 700 >>",
624 );
625 let doc = Document::load(b.build(1)).unwrap();
626 let page = doc.page(0).unwrap();
627 let spans = extract_spans(&doc, &page, ReadingOrder::Content).unwrap();
628 assert!((spans[0].bbox.y1 - (720.0 + 0.7 * 12.0)).abs() < 1e-3);
629 assert!((spans[0].bbox.y0 - (720.0 - 0.2 * 12.0)).abs() < 1e-3);
630 }
631
632 #[test]
634 fn fixed_pitch_flag_marks_monospace() {
635 let spans = flag_spans(1);
636 assert!(spans[0].monospace);
637 assert!(!spans[0].serif);
638 }
639
640 #[test]
642 fn serif_flag_marks_serif() {
643 let spans = flag_spans(2);
644 assert!(spans[0].serif);
645 assert!(!spans[0].monospace);
646 }
647
648 fn flag_spans(flags: u32) -> Vec<TextSpan> {
650 let mut b = PdfBuilder::new();
651 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
652 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
653 b.object(
654 3,
655 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
656 /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
657 );
658 b.stream(4, "", b"BT /F1 12 Tf 72 720 Td (x) Tj ET");
659 b.object(
660 5,
661 "<< /Type /Font /Subtype /Type1 /BaseFont /Custom \
662 /Encoding /WinAnsiEncoding /FontDescriptor 6 0 R >>",
663 );
664 b.object(
665 6,
666 &format!("<< /Type /FontDescriptor /FontName /Custom /Flags {flags} >>"),
667 );
668 let doc = Document::load(b.build(1)).unwrap();
669 let page = doc.page(0).unwrap();
670 extract_spans(&doc, &page, ReadingOrder::Content).unwrap()
671 }
672
673 #[test]
675 fn span_carries_text_rise() {
676 let doc = Document::load(pdfboss_testkit::doc_with_graphics(
677 "BT /F1 12 Tf 72 720 Td (flat) Tj 5 Ts (up) Tj ET",
678 ))
679 .unwrap();
680 let page = doc.page(0).unwrap();
681 let spans = extract_spans(&doc, &page, ReadingOrder::Content).unwrap();
682 assert_eq!(spans[0].rise, 0.0);
683 assert_eq!(spans[1].rise, 5.0);
684 }
685
686 #[test]
688 fn span_marks_vertical_writing() {
689 let mut b = PdfBuilder::new();
690 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
691 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
692 b.object(
693 3,
694 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
695 /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
696 );
697 b.stream(4, "", b"BT /F1 12 Tf 72 720 Td <0001> Tj ET");
698 b.object(
699 5,
700 "<< /Type /Font /Subtype /Type0 /BaseFont /X /Encoding /Identity-V \
701 /DescendantFonts [6 0 R] /ToUnicode 7 0 R >>",
702 );
703 b.object(
704 6,
705 "<< /Type /Font /Subtype /CIDFontType2 /BaseFont /X /DW 600 >>",
706 );
707 b.stream(
708 7,
709 "",
710 b"1 begincodespacerange <0000> <FFFF> endcodespacerange\n\
711 1 beginbfchar <0001> <0041> endbfchar",
712 );
713 let doc = Document::load(b.build(1)).unwrap();
714 let page = doc.page(0).unwrap();
715 let spans = extract_spans(&doc, &page, ReadingOrder::Content).unwrap();
716 assert!(spans[0].vertical);
717 }
718
719 #[test]
723 fn invisible_render_modes_mark_the_span() {
724 let doc = Document::load(pdfboss_testkit::doc_with_graphics(
725 "BT /F1 12 Tf 72 720 Td (seen) Tj 3 Tr (ocr) Tj 7 Tr (clip) Tj 0 Tr (back) Tj ET",
726 ))
727 .unwrap();
728 let page = doc.page(0).unwrap();
729 let spans = extract_spans(&doc, &page, ReadingOrder::Content).unwrap();
730 let invisible: Vec<bool> = spans.iter().map(|s| s.invisible).collect();
731 assert_eq!(invisible, [false, true, true, false]);
732 }
733
734 #[test]
736 fn span_color_defaults_to_black() {
737 let doc = Document::load(simple_doc("Hi")).unwrap();
738 let page = doc.page(0).unwrap();
739 let spans = extract_spans(&doc, &page, ReadingOrder::Content).unwrap();
740 assert_eq!(spans[0].color, Some((0.0, 0.0, 0.0)));
741 }
742
743 #[test]
745 fn device_fill_colors_set_the_span_color() {
746 let doc = Document::load(pdfboss_testkit::doc_with_graphics(
747 "BT /F1 12 Tf 72 720 Td 1 0 0 rg (red) Tj 0.5 g (gray) Tj \
748 1 0 0 0 k (cyan) Tj ET",
749 ))
750 .unwrap();
751 let page = doc.page(0).unwrap();
752 let spans = extract_spans(&doc, &page, ReadingOrder::Content).unwrap();
753 assert_eq!(spans[0].color, Some((1.0, 0.0, 0.0)));
754 assert_eq!(spans[1].color, Some((0.5, 0.5, 0.5)));
755 assert_eq!(spans[2].color, Some((0.0, 1.0, 1.0)));
756 }
757
758 #[test]
762 fn sc_components_set_the_span_color_by_count() {
763 let doc = Document::load(pdfboss_testkit::doc_with_graphics(
764 "BT /F1 12 Tf 72 720 Td /DeviceRGB cs 0 1 0 sc (green) Tj \
765 0.25 sc (dark) Tj ET",
766 ))
767 .unwrap();
768 let page = doc.page(0).unwrap();
769 let spans = extract_spans(&doc, &page, ReadingOrder::Content).unwrap();
770 assert_eq!(spans[0].color, Some((0.0, 1.0, 0.0)));
771 assert_eq!(spans[1].color, Some((0.25, 0.25, 0.25)));
772 }
773
774 #[test]
777 fn pattern_fill_leaves_color_unknown() {
778 let doc = Document::load(pdfboss_testkit::doc_with_graphics(
779 "BT /F1 12 Tf 72 720 Td /Pattern cs /P1 scn (patterned) Tj ET",
780 ))
781 .unwrap();
782 let page = doc.page(0).unwrap();
783 let spans = extract_spans(&doc, &page, ReadingOrder::Content).unwrap();
784 assert_eq!(spans[0].color, None);
785 }
786
787 #[test]
790 fn an_underline_ruling_marks_the_span() {
791 let doc = Document::load(pdfboss_testkit::doc_with_graphics(
792 "BT /F1 12 Tf 72 720 Td (Hello) Tj ET 72 718.5 m 105 718.5 l S",
793 ))
794 .unwrap();
795 let page = doc.page(0).unwrap();
796 let spans = extract_spans(&doc, &page, ReadingOrder::Content).unwrap();
797 assert!(spans[0].underline);
798 assert!(!spans[0].strikethrough);
799 }
800
801 #[test]
803 fn a_strikethrough_ruling_marks_the_span() {
804 let doc = Document::load(pdfboss_testkit::doc_with_graphics(
805 "BT /F1 12 Tf 72 720 Td (Hello) Tj ET 72 723.6 m 105 723.6 l S",
806 ))
807 .unwrap();
808 let page = doc.page(0).unwrap();
809 let spans = extract_spans(&doc, &page, ReadingOrder::Content).unwrap();
810 assert!(spans[0].strikethrough);
811 assert!(!spans[0].underline);
812 }
813
814 #[test]
817 fn a_distant_ruling_marks_nothing() {
818 let doc = Document::load(pdfboss_testkit::doc_with_graphics(
819 "BT /F1 12 Tf 72 720 Td (Hello) Tj ET 72 700 m 105 700 l S",
820 ))
821 .unwrap();
822 let page = doc.page(0).unwrap();
823 let spans = extract_spans(&doc, &page, ReadingOrder::Content).unwrap();
824 assert!(!spans[0].underline);
825 assert!(!spans[0].strikethrough);
826 }
827
828 #[test]
832 fn an_underline_needs_most_of_the_span_covered() {
833 let doc = Document::load(pdfboss_testkit::doc_with_graphics(
834 "BT /F1 12 Tf 72 720 Td (Hello) Tj ET 100 718.5 m 130 718.5 l S",
835 ))
836 .unwrap();
837 let page = doc.page(0).unwrap();
838 let spans = extract_spans(&doc, &page, ReadingOrder::Content).unwrap();
839 assert!(!spans[0].underline);
840 }
841
842 #[test]
848 fn the_source_generic_entry_points_honor_optional_content() {
849 let mut b = PdfBuilder::new();
850 b.object(
851 1,
852 "<< /Type /Catalog /Pages 2 0 R /OCProperties \
853 << /OCGs [6 0 R] /D << /OFF [6 0 R] >> >> >>",
854 );
855 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
856 b.object(
857 3,
858 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
859 /Resources << /Font << /F1 5 0 R >> \
860 /Properties << /H 6 0 R >> >> /Contents 4 0 R >>",
861 );
862 b.stream(
863 4,
864 "",
865 b"BT /F1 12 Tf 72 720 Td /OC /H BDC (hidden) Tj EMC (kept) Tj ET",
866 );
867 b.object(
868 5,
869 "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica \
870 /Encoding /WinAnsiEncoding >>",
871 );
872 b.object(6, "<< /Type /OCG /Name (hidden) >>");
873 let doc = Document::load(b.build(1)).unwrap();
874 let page = doc.page(0).unwrap();
875 let oc = doc.oc_state();
876 let gated = block_on(extract_spans_with(
877 Immediate(&doc),
878 &page,
879 oc.as_ref(),
880 None,
881 ReadingOrder::Content,
882 ))
883 .unwrap();
884 let texts: Vec<&str> = gated.iter().map(|s| s.text.as_str()).collect();
885 assert_eq!(texts, ["kept"]);
886 let all = block_on(extract_spans_with(
887 Immediate(&doc),
888 &page,
889 None,
890 None,
891 ReadingOrder::Content,
892 ))
893 .unwrap();
894 let texts: Vec<&str> = all.iter().map(|s| s.text.as_str()).collect();
895 assert_eq!(texts, ["hidden", "kept"]);
896 }
897
898 #[test]
903 fn descriptor_flags_set_span_style() {
904 let mut b = PdfBuilder::new();
905 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
906 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
907 b.object(
908 3,
909 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
910 /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
911 );
912 b.stream(4, "", b"BT /F1 12 Tf 72 720 Td (x) Tj ET");
913 b.object(
914 5,
915 "<< /Type /Font /Subtype /Type1 /BaseFont /Custom \
916 /Encoding /WinAnsiEncoding /FontDescriptor 6 0 R >>",
917 );
918 b.object(
919 6,
920 "<< /Type /FontDescriptor /FontName /Custom /Flags 64 /FontWeight 700 >>",
921 );
922 let doc = Document::load(b.build(1)).unwrap();
923 let page = doc.page(0).unwrap();
924 let spans = extract_spans(&doc, &page, ReadingOrder::Content).unwrap();
925 assert!(spans[0].italic, "Flags bit 7 (mask 64) is Italic");
926 assert!(spans[0].bold, "FontWeight 700 >= 600 is bold");
927 }
928
929 #[test]
933 fn thick_stemv_reads_as_bold() {
934 let mut b = PdfBuilder::new();
935 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
936 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
937 b.object(
938 3,
939 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
940 /Resources << /Font << /F1 5 0 R /F2 7 0 R >> >> /Contents 4 0 R >>",
941 );
942 b.stream(4, "", b"BT /F1 12 Tf 72 720 Td (a) Tj /F2 12 Tf (b) Tj ET");
943 b.object(
944 5,
945 "<< /Type /Font /Subtype /Type1 /BaseFont /NimbusRomNo9L-Medi \
946 /Encoding /WinAnsiEncoding /FontDescriptor 6 0 R >>",
947 );
948 b.object(
949 6,
950 "<< /Type /FontDescriptor /FontName /NimbusRomNo9L-Medi /Flags 4 /StemV 140 >>",
951 );
952 b.object(
953 7,
954 "<< /Type /Font /Subtype /Type1 /BaseFont /NimbusRomNo9L-Regu \
955 /Encoding /WinAnsiEncoding /FontDescriptor 8 0 R >>",
956 );
957 b.object(
958 8,
959 "<< /Type /FontDescriptor /FontName /NimbusRomNo9L-Regu /Flags 4 /StemV 85 >>",
960 );
961 let doc = Document::load(b.build(1)).unwrap();
962 let page = doc.page(0).unwrap();
963 let spans = extract_spans(&doc, &page, ReadingOrder::Content).unwrap();
964 assert!(spans[0].bold, "StemV 140 is a bold stem");
965 assert!(!spans[1].bold, "StemV 85 is a regular stem");
966 }
967
968 #[test]
970 fn basefont_name_and_italic_angle_fallbacks() {
971 let mut b = PdfBuilder::new();
972 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
973 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
974 b.object(
975 3,
976 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
977 /Resources << /Font << /F1 5 0 R /F2 6 0 R >> >> /Contents 4 0 R >>",
978 );
979 b.stream(4, "", b"BT /F1 12 Tf 72 720 Td (a) Tj /F2 12 Tf (b) Tj ET");
980 b.object(
981 5,
982 "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-BoldOblique \
983 /Encoding /WinAnsiEncoding >>",
984 );
985 b.object(
986 6,
987 "<< /Type /Font /Subtype /Type1 /BaseFont /Times-Roman \
988 /Encoding /WinAnsiEncoding /FontDescriptor 7 0 R >>",
989 );
990 b.object(
991 7,
992 "<< /Type /FontDescriptor /FontName /Times-Roman /ItalicAngle -12 >>",
993 );
994 let doc = Document::load(b.build(1)).unwrap();
995 let page = doc.page(0).unwrap();
996 let spans = extract_spans(&doc, &page, ReadingOrder::Content).unwrap();
997 assert!(
998 spans[0].bold && spans[0].italic,
999 "BaseFont substrings Bold+Oblique"
1000 );
1001 assert!(!spans[1].bold && spans[1].italic, "ItalicAngle != 0 alone");
1002 }
1003
1004 #[test]
1006 fn type0_descendant_descriptor_sets_style() {
1007 let mut b = PdfBuilder::new();
1008 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1009 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1010 b.object(
1011 3,
1012 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1013 /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
1014 );
1015 b.stream(4, "", b"BT /F1 12 Tf 72 720 Td <0001> Tj ET");
1016 b.object(
1017 5,
1018 "<< /Type /Font /Subtype /Type0 /BaseFont /X /Encoding /Identity-H \
1019 /DescendantFonts [6 0 R] /ToUnicode 8 0 R >>",
1020 );
1021 b.object(
1022 6,
1023 "<< /Type /Font /Subtype /CIDFontType2 /BaseFont /X /DW 600 \
1024 /FontDescriptor 7 0 R >>",
1025 );
1026 b.object(
1027 7,
1028 "<< /Type /FontDescriptor /FontName /X /Flags 64 /FontWeight 600 >>",
1029 );
1030 b.stream(
1031 8,
1032 "",
1033 b"1 begincodespacerange <0000> <FFFF> endcodespacerange\n\
1034 1 beginbfchar <0001> <0041> endbfchar",
1035 );
1036 let doc = Document::load(b.build(1)).unwrap();
1037 let page = doc.page(0).unwrap();
1038 let spans = extract_spans(&doc, &page, ReadingOrder::Content).unwrap();
1039 assert!(spans[0].bold && spans[0].italic);
1040 }
1041
1042 struct Counting<'a> {
1047 inner: Immediate<&'a Document>,
1048 resolutions: std::cell::RefCell<std::collections::HashMap<u32, usize>>,
1049 }
1050
1051 impl<'a> Counting<'a> {
1052 fn new(doc: &'a Document) -> Counting<'a> {
1053 Counting {
1054 inner: Immediate(doc),
1055 resolutions: std::cell::RefCell::new(std::collections::HashMap::new()),
1056 }
1057 }
1058
1059 fn resolutions(&self, num: u32) -> usize {
1060 self.resolutions.borrow().get(&num).copied().unwrap_or(0)
1061 }
1062 }
1063
1064 impl AsyncObjectSource for Counting<'_> {
1065 fn get(&self, r: ObjRef) -> BoxFuture<'_, Result<Object>> {
1066 self.inner.get(r)
1067 }
1068
1069 fn stream_data<'b>(&'b self, s: &'b Stream) -> BoxFuture<'b, Result<Vec<u8>>> {
1070 self.inner.stream_data(s)
1071 }
1072
1073 fn resolve<'b>(&'b self, o: &'b Object) -> BoxFuture<'b, Result<Object>> {
1074 if let Object::Ref(r) = o {
1075 *self.resolutions.borrow_mut().entry(r.num).or_insert(0) += 1;
1076 }
1077 self.inner.resolve(o)
1078 }
1079 }
1080
1081 #[test]
1085 fn a_font_reached_from_repeated_forms_loads_once_per_page() {
1086 let mut b = PdfBuilder::new();
1087 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1088 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1089 b.object(
1090 3,
1091 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1092 /Resources << /XObject << /Fx 6 0 R >> >> /Contents 4 0 R >>",
1093 );
1094 b.stream(4, "", b"/Fx Do /Fx Do");
1095 b.object(
1096 5,
1097 "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica \
1098 /Encoding /WinAnsiEncoding >>",
1099 );
1100 b.stream(
1101 6,
1102 "/Type /XObject /Subtype /Form /BBox [0 0 612 792] \
1103 /Resources << /Font << /F1 5 0 R >> >>",
1104 b"BT /F1 12 Tf 72 700 Td (x) Tj ET",
1105 );
1106 let doc = Document::load(b.build(1)).unwrap();
1107 let page = doc.page(0).unwrap();
1108 let counting = Counting::new(&doc);
1109 let (spans, report) = block_on(extract_spans_reporting_with(
1110 &counting,
1111 &page,
1112 None,
1113 None,
1114 ReadingOrder::Content,
1115 ))
1116 .unwrap();
1117 assert!(report.is_complete(), "unexpected skips: {report:?}");
1118 assert_eq!(spans.len(), 2, "both form invocations must show text");
1119 assert_eq!(
1120 counting.resolutions(5),
1121 1,
1122 "one font dictionary resolution per page walk"
1123 );
1124 }
1125
1126 #[test]
1132 fn a_resource_category_resolves_once_per_walk() {
1133 let mut b = PdfBuilder::new();
1134 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1135 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1136 b.object(
1137 3,
1138 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1139 /Resources << /ExtGState 5 0 R >> /Contents 4 0 R >>",
1140 );
1141 b.stream(
1142 4,
1143 "",
1144 b"/G1 gs 10 10 m 100 10 l S \
1145 /G1 gs 10 20 m 100 20 l S \
1146 /G1 gs 10 30 m 100 30 l S",
1147 );
1148 b.object(5, "<< /G1 << /LW 2 >> >>");
1149 let doc = Document::load(b.build(1)).unwrap();
1150 let page = doc.page(0).unwrap();
1151 let counting = Counting::new(&doc);
1152 let (_, rulings, report) = block_on(extract_spans_and_rulings_reporting_with(
1153 &counting,
1154 &page,
1155 None,
1156 None,
1157 ReadingOrder::Content,
1158 ))
1159 .unwrap();
1160 assert!(report.is_complete(), "unexpected skips: {report:?}");
1161 assert_eq!(rulings.len(), 3, "all three strokes extract");
1162 assert_eq!(
1163 counting.resolutions(5),
1164 1,
1165 "one category dictionary resolution per page walk"
1166 );
1167 }
1168
1169 #[test]
1173 fn a_font_shared_across_pages_loads_once_per_document() {
1174 let mut b = PdfBuilder::new();
1175 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1176 b.object(2, "<< /Type /Pages /Kids [3 0 R 5 0 R] /Count 2 >>");
1177 b.object(
1178 3,
1179 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1180 /Resources << /Font << /F1 7 0 R >> >> /Contents 4 0 R >>",
1181 );
1182 b.stream(4, "", b"BT /F1 12 Tf 72 720 Td (one) Tj ET");
1183 b.object(
1184 5,
1185 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1186 /Resources << /Font << /F1 7 0 R >> >> /Contents 6 0 R >>",
1187 );
1188 b.stream(6, "", b"BT /F1 12 Tf 72 720 Td (two) Tj ET");
1189 b.object(
1190 7,
1191 "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica \
1192 /Encoding /WinAnsiEncoding >>",
1193 );
1194 let doc = Document::load(b.build(1)).unwrap();
1195 let fonts = FontCache::default();
1196 let counting = Counting::new(&doc);
1197 let mut cached = Vec::new();
1198 for index in 0..2 {
1199 let page = doc.page(index).unwrap();
1200 let (spans, report) = block_on(extract_spans_reporting_cached_with(
1201 &counting,
1202 &page,
1203 &fonts,
1204 None,
1205 None,
1206 ReadingOrder::Content,
1207 ))
1208 .unwrap();
1209 assert!(report.is_complete(), "unexpected skips: {report:?}");
1210 cached.push(spans);
1211 }
1212 assert_eq!(
1213 counting.resolutions(7),
1214 1,
1215 "one font dictionary resolution per document"
1216 );
1217 for (index, spans) in cached.iter().enumerate() {
1218 let page = doc.page(index).unwrap();
1219 let plain = extract_spans_reporting(&doc, &page, ReadingOrder::Content)
1220 .unwrap()
1221 .0;
1222 assert_eq!(spans, &plain, "page {index} must extract identically");
1223 }
1224 }
1225
1226 #[test]
1231 fn a_shared_cache_keeps_the_name_binding_per_page() {
1232 let mut b = PdfBuilder::new();
1233 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1234 b.object(2, "<< /Type /Pages /Kids [3 0 R 5 0 R] /Count 2 >>");
1235 b.object(
1236 3,
1237 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1238 /Resources << /Font << /F1 7 0 R >> >> /Contents 4 0 R >>",
1239 );
1240 b.stream(4, "", b"BT /F1 12 Tf 72 720 Td (aa) Tj ET");
1241 b.object(
1242 5,
1243 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1244 /Resources << /Font << /F1 8 0 R >> >> /Contents 6 0 R >>",
1245 );
1246 b.stream(6, "", b"BT /F1 12 Tf 72 720 Td (aa) Tj ET");
1247 b.object(
1248 7,
1249 "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica \
1250 /Encoding /WinAnsiEncoding /FirstChar 97 /LastChar 97 /Widths [500] >>",
1251 );
1252 b.object(
1253 8,
1254 "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica \
1255 /Encoding /WinAnsiEncoding /FirstChar 97 /LastChar 97 /Widths [1000] >>",
1256 );
1257 let doc = Document::load(b.build(1)).unwrap();
1258 let fonts = FontCache::default();
1259 let mut advances = Vec::new();
1260 for index in 0..2 {
1261 let page = doc.page(index).unwrap();
1262 let (spans, _) =
1263 extract_spans_reporting_cached(&doc, &page, &fonts, ReadingOrder::Content).unwrap();
1264 assert_eq!(spans.len(), 1);
1265 advances.push(spans[0].end_x - spans[0].x);
1266 }
1267 assert!(
1268 (advances[0] - 12.0).abs() < 1e-3,
1269 "page one: {}",
1270 advances[0]
1271 );
1272 assert!(
1273 (advances[1] - 24.0).abs() < 1e-3,
1274 "page two: {}",
1275 advances[1]
1276 );
1277 }
1278
1279 struct NullSource {
1291 payload: Vec<u8>,
1292 }
1293
1294 impl AsyncObjectSource for NullSource {
1295 fn get(&self, _r: ObjRef) -> BoxFuture<'_, Result<Object>> {
1296 Box::pin(std::future::ready(Ok(Object::Null)))
1297 }
1298
1299 fn stream_data<'a>(&'a self, _s: &'a Stream) -> BoxFuture<'a, Result<Vec<u8>>> {
1300 Box::pin(std::future::ready(Ok(self.payload.clone())))
1301 }
1302
1303 fn resolve<'a>(&'a self, o: &'a Object) -> BoxFuture<'a, Result<Object>> {
1304 Box::pin(resolve_with(self, o))
1305 }
1306 }
1307
1308 #[test]
1322 fn the_async_entry_point_yields_a_spawnable_future() {
1323 fn assert_send_static<F: Future + Send + 'static>(_: &F) {}
1324
1325 let doc = Document::load(simple_doc("Hello")).unwrap();
1326 let spans_page = doc.page(0).unwrap();
1327 drop(doc);
1328
1329 let spans = async move {
1330 extract_spans_with(
1331 NullSource {
1332 payload: Vec::new(),
1333 },
1334 &spans_page,
1335 None,
1336 None,
1337 ReadingOrder::Content,
1338 )
1339 .await
1340 };
1341 assert_send_static(&spans);
1342
1343 assert!(block_on(spans).unwrap().is_empty());
1346 }
1347
1348 fn tagged_doc(
1354 content: &[u8],
1355 page_extra: &str,
1356 extra: impl FnOnce(&mut PdfBuilder),
1357 ) -> Document {
1358 let mut b = PdfBuilder::new();
1359 b.object(
1360 1,
1361 "<< /Type /Catalog /Pages 2 0 R /StructTreeRoot 10 0 R >>",
1362 );
1363 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1364 b.object(
1365 3,
1366 &format!(
1367 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /StructParents 0 \
1368 /Resources << /Font << /F1 5 0 R >> {page_extra} >> /Contents 4 0 R >>"
1369 ),
1370 );
1371 b.stream(4, "", content);
1372 b.object(
1373 5,
1374 "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>",
1375 );
1376 b.object(
1377 10,
1378 "<< /Type /StructTreeRoot /K [11 0 R] /ParentTree 12 0 R >>",
1379 );
1380 b.object(
1381 11,
1382 "<< /Type /StructElem /S /Document /P 10 0 R /K [13 0 R 14 0 R] >>",
1383 );
1384 b.object(12, "<< /Nums [0 [13 0 R 14 0 R 13 0 R 14 0 R]] >>");
1385 b.object(
1386 13,
1387 "<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [0 2] >>",
1388 );
1389 b.object(
1390 14,
1391 "<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [1 3] >>",
1392 );
1393 extra(&mut b);
1394 Document::load(b.build(1)).unwrap()
1395 }
1396
1397 const TWO_COLUMNS: &[u8] = b"BT /F1 12 Tf \
1401 /P << /MCID 2 >> BDC 1 0 0 1 72 680 Tm (L2) Tj EMC \
1402 /P << /MCID 3 >> BDC 1 0 0 1 300 680 Tm (R2) Tj EMC \
1403 /P << /MCID 0 >> BDC 1 0 0 1 72 700 Tm (L1) Tj EMC \
1404 /P << /MCID 1 >> BDC 1 0 0 1 300 700 Tm (R1) Tj EMC ET";
1405
1406 fn texts(spans: &[TextSpan]) -> Vec<&str> {
1407 spans.iter().map(|s| s.text.as_str()).collect()
1408 }
1409
1410 fn ordered(doc: &Document, order: ReadingOrder) -> (Vec<String>, ReadingOrder) {
1411 let page = doc.page(0).unwrap();
1412 let (spans, report) = extract_spans_reporting(doc, &page, order).unwrap();
1413 (spans.into_iter().map(|s| s.text).collect(), report.order)
1414 }
1415
1416 #[test]
1417 fn reading_order_names_round_trip() {
1418 for order in ReadingOrder::ALL {
1419 assert_eq!(order.as_str().parse::<ReadingOrder>().unwrap(), order);
1420 assert_eq!(order.to_string(), order.as_str());
1421 }
1422 assert_eq!(ReadingOrder::default(), ReadingOrder::Content);
1423 assert!("bogus".parse::<ReadingOrder>().is_err());
1424 }
1425
1426 #[test]
1427 fn content_order_is_the_stream_as_written() {
1428 let doc = tagged_doc(TWO_COLUMNS, "", |_| {});
1429 let (spans, order) = ordered(&doc, ReadingOrder::Content);
1430 assert_eq!(spans, ["L2", "R2", "L1", "R1"]);
1431 assert_eq!(order, ReadingOrder::Content);
1432 }
1433
1434 #[test]
1435 fn geometric_order_extracts_the_stream_and_tags_the_report() {
1436 let doc = tagged_doc(TWO_COLUMNS, "", |_| {});
1437 let (spans, order) = ordered(&doc, ReadingOrder::Geometric);
1438 assert_eq!(spans, ["L2", "R2", "L1", "R1"]);
1439 assert_eq!(order, ReadingOrder::Geometric);
1440 }
1441
1442 #[test]
1443 fn structure_tree_order_follows_the_tree() {
1444 let doc = tagged_doc(TWO_COLUMNS, "", |_| {});
1445 let (spans, order) = ordered(&doc, ReadingOrder::StructureTree);
1446 assert_eq!(spans, ["L1", "L2", "R1", "R2"]);
1447 assert_eq!(order, ReadingOrder::StructureTree);
1448 }
1449
1450 #[test]
1451 fn structure_tree_order_reads_an_untagged_document_in_content_order() {
1452 let doc = Document::load(pdfboss_testkit::multi_page_doc(&["one", "two"])).unwrap();
1453 let page = doc.page(1).unwrap();
1454 let (spans, report) =
1455 extract_spans_reporting(&doc, &page, ReadingOrder::StructureTree).unwrap();
1456 assert_eq!(texts(&spans), ["two"]);
1457 assert_eq!(report.order, ReadingOrder::Content);
1458 }
1459
1460 #[test]
1461 fn a_page_the_tree_does_not_reach_reads_in_content_order() {
1462 let doc = tagged_doc(TWO_COLUMNS, "", |b| {
1464 b.object(12, "<< /Nums [7 [13 0 R]] >>");
1465 });
1466 let (spans, order) = ordered(&doc, ReadingOrder::StructureTree);
1467 assert_eq!(spans, ["L2", "R2", "L1", "R1"]);
1468 assert_eq!(order, ReadingOrder::Content);
1469 }
1470
1471 #[test]
1472 fn untagged_content_keeps_its_place_after_the_tagged_content_before_it() {
1473 let content = b"BT /F1 12 Tf \
1474 /P << /MCID 2 >> BDC 1 0 0 1 72 680 Tm (L2) Tj EMC \
1475 /Artifact BMC 1 0 0 1 72 40 Tm (footer) Tj EMC \
1476 /P << /MCID 3 >> BDC 1 0 0 1 300 680 Tm (R2) Tj EMC \
1477 /P << /MCID 0 >> BDC 1 0 0 1 72 700 Tm (L1) Tj EMC \
1478 /P << /MCID 1 >> BDC 1 0 0 1 300 700 Tm (R1) Tj EMC ET";
1479 let doc = tagged_doc(content, "", |_| {});
1480 let (spans, _) = ordered(&doc, ReadingOrder::StructureTree);
1481 assert_eq!(spans, ["L1", "L2", "footer", "R1", "R2"]);
1482 }
1483
1484 #[test]
1485 fn named_marked_content_properties_are_read() {
1486 let content = b"BT /F1 12 Tf \
1487 /P /M2 BDC 1 0 0 1 72 680 Tm (L2) Tj EMC \
1488 /P /M3 BDC 1 0 0 1 300 680 Tm (R2) Tj EMC \
1489 /P /M0 BDC 1 0 0 1 72 700 Tm (L1) Tj EMC \
1490 /P /M1 BDC 1 0 0 1 300 700 Tm (R1) Tj EMC ET";
1491 let props = "/Properties << /M0 << /MCID 0 >> /M1 << /MCID 1 >> \
1492 /M2 << /MCID 2 >> /M3 << /MCID 3 >> >>";
1493 let doc = tagged_doc(content, props, |_| {});
1494 let (spans, order) = ordered(&doc, ReadingOrder::StructureTree);
1495 assert_eq!(spans, ["L1", "L2", "R1", "R2"]);
1496 assert_eq!(order, ReadingOrder::StructureTree);
1497 }
1498
1499 #[test]
1500 fn a_form_files_its_marked_content_under_its_own_parents_key() {
1501 let content = b"BT /F1 12 Tf \
1505 /P << /MCID 3 >> BDC 1 0 0 1 300 680 Tm (R2) Tj EMC \
1506 /P << /MCID 1 >> BDC 1 0 0 1 300 700 Tm (R1) Tj EMC ET /Fx Do";
1507 let doc = tagged_doc(content, "/XObject << /Fx 6 0 R >>", |b| {
1508 b.stream(
1509 6,
1510 "/Type /XObject /Subtype /Form /BBox [0 0 612 792] /StructParents 1",
1511 b"BT /F1 12 Tf \
1512 /P << /MCID 1 >> BDC 1 0 0 1 72 680 Tm (L2) Tj EMC \
1513 /P << /MCID 0 >> BDC 1 0 0 1 72 700 Tm (L1) Tj EMC ET",
1514 );
1515 b.object(
1516 12,
1517 "<< /Nums [0 [null 14 0 R null 14 0 R] 1 [13 0 R 13 0 R]] >>",
1518 );
1519 b.object(
1520 13,
1521 "<< /Type /StructElem /S /P /P 11 0 R /Pg 3 0 R /K [0 1] >>",
1522 );
1523 });
1524 let (spans, order) = ordered(&doc, ReadingOrder::StructureTree);
1525 assert_eq!(spans, ["L1", "L2", "R1", "R2"]);
1526 assert_eq!(order, ReadingOrder::StructureTree);
1527 }
1528}