1mod cmap;
5mod extract;
6mod font;
7mod sfnt;
8
9use pdfboss_core::{block_on, AsyncObjectSource, Document, Immediate, OcState, Page, Result};
10
11pub use extract::{ExtractReport, FontCache, SkipCause, SkippedText, SkippedTextKind};
12pub use pdfboss_core::{Point, Rect};
13
14#[derive(Debug, Clone, PartialEq)]
16pub struct TextSpan {
17 pub text: String,
19 pub x: f32,
21 pub y: f32,
23 pub end_x: f32,
25 pub size: f32,
27 pub font: String,
29 pub font_name: String,
33 pub page: usize,
35 pub bbox: Rect,
41 pub bold: bool,
46 pub italic: bool,
50 pub monospace: bool,
52 pub serif: bool,
54 pub rise: f32,
58 pub vertical: bool,
61 pub invisible: bool,
64 pub color: Option<(f32, f32, f32)>,
69 pub underline: bool,
73 pub strikethrough: bool,
76}
77
78#[derive(Debug, Clone, PartialEq)]
85pub struct Ruling {
86 pub start: Point,
87 pub end: Point,
88 pub width: f32,
92}
93
94pub fn extract_spans(doc: &Document, page: &Page) -> Result<Vec<TextSpan>> {
107 let oc = doc.oc_state();
108 let (spans, _, _) = block_on(extract::page_spans_and_rulings_with(
109 Immediate(doc),
110 page,
111 None,
112 oc.as_ref(),
113 ));
114 Ok(spans)
115}
116
117pub async fn extract_spans_with<S: AsyncObjectSource>(
133 src: S,
134 page: &Page,
135 oc: Option<&OcState>,
136) -> Result<Vec<TextSpan>> {
137 let (spans, _, _) = extract::page_spans_and_rulings_with(src, page, None, oc).await;
138 Ok(spans)
139}
140
141pub fn extract_spans_reporting(
147 doc: &Document,
148 page: &Page,
149) -> Result<(Vec<TextSpan>, ExtractReport)> {
150 let oc = doc.oc_state();
151 let (spans, _, report) = block_on(extract::page_spans_and_rulings_with(
152 Immediate(doc),
153 page,
154 None,
155 oc.as_ref(),
156 ));
157 Ok((spans, report))
158}
159
160pub async fn extract_spans_reporting_with<S: AsyncObjectSource>(
163 src: S,
164 page: &Page,
165 oc: Option<&OcState>,
166) -> Result<(Vec<TextSpan>, ExtractReport)> {
167 let (spans, _, report) = extract::page_spans_and_rulings_with(src, page, None, oc).await;
168 Ok((spans, report))
169}
170
171pub fn extract_spans_reporting_cached(
182 doc: &Document,
183 page: &Page,
184 fonts: &FontCache,
185) -> Result<(Vec<TextSpan>, ExtractReport)> {
186 let oc = doc.oc_state();
187 let (spans, _, report) = block_on(extract::page_spans_and_rulings_with(
188 Immediate(doc),
189 page,
190 Some(fonts),
191 oc.as_ref(),
192 ));
193 Ok((spans, report))
194}
195
196pub async fn extract_spans_reporting_cached_with<S: AsyncObjectSource>(
199 src: S,
200 page: &Page,
201 fonts: &FontCache,
202 oc: Option<&OcState>,
203) -> Result<(Vec<TextSpan>, ExtractReport)> {
204 let (spans, _, report) = extract::page_spans_and_rulings_with(src, page, Some(fonts), oc).await;
205 Ok((spans, report))
206}
207
208pub fn extract_spans_and_rulings_reporting(
213 doc: &Document,
214 page: &Page,
215) -> Result<(Vec<TextSpan>, Vec<Ruling>, ExtractReport)> {
216 let oc = doc.oc_state();
217 let (spans, rulings, report) = block_on(extract::page_spans_and_rulings_with(
218 Immediate(doc),
219 page,
220 None,
221 oc.as_ref(),
222 ));
223 Ok((spans, rulings, report))
224}
225
226pub async fn extract_spans_and_rulings_reporting_with<S: AsyncObjectSource>(
229 src: S,
230 page: &Page,
231 oc: Option<&OcState>,
232) -> Result<(Vec<TextSpan>, Vec<Ruling>, ExtractReport)> {
233 let (spans, rulings, report) = extract::page_spans_and_rulings_with(src, page, None, oc).await;
234 Ok((spans, rulings, report))
235}
236
237pub fn extract_spans_and_rulings_reporting_cached(
244 doc: &Document,
245 page: &Page,
246 fonts: &FontCache,
247) -> Result<(Vec<TextSpan>, Vec<Ruling>, ExtractReport)> {
248 let oc = doc.oc_state();
249 let (spans, rulings, report) = block_on(extract::page_spans_and_rulings_with(
250 Immediate(doc),
251 page,
252 Some(fonts),
253 oc.as_ref(),
254 ));
255 Ok((spans, rulings, report))
256}
257
258pub async fn extract_spans_and_rulings_reporting_cached_with<S: AsyncObjectSource>(
262 src: S,
263 page: &Page,
264 fonts: &FontCache,
265 oc: Option<&OcState>,
266) -> Result<(Vec<TextSpan>, Vec<Ruling>, ExtractReport)> {
267 let (spans, rulings, report) =
268 extract::page_spans_and_rulings_with(src, page, Some(fonts), oc).await;
269 Ok((spans, rulings, report))
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275 use pdfboss_core::{resolve_with, BoxFuture, ObjRef, Object, Stream};
276 use pdfboss_testkit::{simple_doc, PdfBuilder};
277 use std::future::Future;
278
279 #[test]
284 fn form_matrix_translates_the_nested_span() {
285 let mut b = PdfBuilder::new();
286 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
287 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
288 b.object(
289 3,
290 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
291 /Resources << /Font << /F1 5 0 R >> /XObject << /Fx 6 0 R >> >> \
292 /Contents 4 0 R >>",
293 );
294 b.stream(4, "", b"BT /F1 12 Tf 72 720 Td (out) Tj ET /Fx Do");
295 b.object(
296 5,
297 "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica \
298 /Encoding /WinAnsiEncoding >>",
299 );
300 b.stream(
302 6,
303 "/Type /XObject /Subtype /Form /BBox [0 0 612 792] \
304 /Matrix [1 0 0 1 0 -20]",
305 b"BT /F1 12 Tf 72 720 Td (in) Tj ET",
306 );
307 let doc = Document::load(b.build(1)).unwrap();
308 let page = doc.page(0).unwrap();
309 let spans = extract_spans(&doc, &page).unwrap();
310 assert_eq!(spans.len(), 2);
311 assert!((spans[1].y - 700.0).abs() < 1e-3); }
313
314 #[test]
315 fn extract_spans_sane_positions() {
316 let doc = Document::load(simple_doc("Hi")).unwrap();
317 let page = doc.page(0).unwrap();
318 let spans = extract_spans(&doc, &page).unwrap();
319 assert_eq!(spans.len(), 1);
320 let s = &spans[0];
321 assert_eq!(s.text, "Hi");
322 assert!((s.x - 72.0).abs() < 1e-3);
323 assert!((s.y - 720.0).abs() < 1e-3);
324 assert!((s.size - 12.0).abs() < 1e-3);
325 assert_eq!(s.font, "F1");
326 }
327
328 #[test]
331 fn extract_spans_and_rulings_reports_both() {
332 let doc = Document::load(pdfboss_testkit::doc_with_graphics(
333 "BT /F1 12 Tf 72 720 Td (Hi) Tj ET 72 700 m 272 700 l S",
334 ))
335 .unwrap();
336 let page = doc.page(0).unwrap();
337 let (spans, rulings, report) = extract_spans_and_rulings_reporting(&doc, &page).unwrap();
338 assert_eq!(spans.len(), 1);
339 assert_eq!(spans[0].text, "Hi");
340 assert_eq!(rulings.len(), 1);
341 assert!((rulings[0].start.y - 700.0).abs() < 1e-3);
342 assert!(report.is_complete());
343 }
344
345 #[test]
346 fn extract_spans_ordering_multi_line() {
347 let doc = Document::load(pdfboss_testkit::doc_with_graphics(
348 "BT /F1 12 Tf 72 720 Td (top) Tj 0 -40 Td (bottom) Tj ET",
349 ))
350 .unwrap();
351 let page = doc.page(0).unwrap();
352 let spans = extract_spans(&doc, &page).unwrap();
353 assert_eq!(spans.len(), 2);
354 assert!(spans[0].y > spans[1].y);
355 assert_eq!(spans[0].text, "top");
356 assert_eq!(spans[1].text, "bottom");
357 assert!(spans.iter().all(|s| s.size > 0.0 && s.x >= 0.0));
358 }
359
360 #[test]
363 fn span_font_name_is_the_base_font_verbatim() {
364 let mut b = PdfBuilder::new();
365 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
366 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
367 b.object(
368 3,
369 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
370 /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
371 );
372 b.stream(4, "", b"BT /F1 12 Tf 72 720 Td (x) Tj ET");
373 b.object(
374 5,
375 "<< /Type /Font /Subtype /Type1 /BaseFont /ABCDEF+Times-Roman \
376 /Encoding /WinAnsiEncoding >>",
377 );
378 let doc = Document::load(b.build(1)).unwrap();
379 let page = doc.page(0).unwrap();
380 let spans = extract_spans(&doc, &page).unwrap();
381 assert_eq!(spans[0].font_name, "ABCDEF+Times-Roman");
382 assert_eq!(spans[0].font, "F1");
383 }
384
385 #[test]
389 fn span_font_name_falls_back_to_descriptor_font_name() {
390 let mut b = PdfBuilder::new();
391 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
392 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
393 b.object(
394 3,
395 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
396 /Resources << /Font << /F1 5 0 R /F2 7 0 R >> >> /Contents 4 0 R >>",
397 );
398 b.stream(4, "", b"BT /F1 12 Tf 72 720 Td (a) Tj /F2 12 Tf (b) Tj ET");
399 b.object(
400 5,
401 "<< /Type /Font /Subtype /Type1 /Encoding /WinAnsiEncoding \
402 /FontDescriptor 6 0 R >>",
403 );
404 b.object(6, "<< /Type /FontDescriptor /FontName /Nameless-Face >>");
405 b.object(
406 7,
407 "<< /Type /Font /Subtype /Type1 /Encoding /WinAnsiEncoding >>",
408 );
409 let doc = Document::load(b.build(1)).unwrap();
410 let page = doc.page(0).unwrap();
411 let spans = extract_spans(&doc, &page).unwrap();
412 assert_eq!(spans[0].font_name, "Nameless-Face");
413 assert_eq!(spans[1].font_name, "");
414 }
415
416 #[test]
418 fn span_carries_its_page_index() {
419 let mut b = PdfBuilder::new();
420 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
421 b.object(2, "<< /Type /Pages /Kids [3 0 R 5 0 R] /Count 2 >>");
422 b.object(
423 3,
424 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
425 /Resources << /Font << /F1 7 0 R >> >> /Contents 4 0 R >>",
426 );
427 b.stream(4, "", b"BT /F1 12 Tf 72 720 Td (one) Tj ET");
428 b.object(
429 5,
430 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
431 /Resources << /Font << /F1 7 0 R >> >> /Contents 6 0 R >>",
432 );
433 b.stream(6, "", b"BT /F1 12 Tf 72 720 Td (two) Tj ET");
434 b.object(
435 7,
436 "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica \
437 /Encoding /WinAnsiEncoding >>",
438 );
439 let doc = Document::load(b.build(1)).unwrap();
440 for index in 0..2 {
441 let page = doc.page(index).unwrap();
442 let spans = extract_spans(&doc, &page).unwrap();
443 assert_eq!(spans[0].page, index, "page {index}");
444 }
445 }
446
447 #[test]
450 fn span_bbox_uses_descriptor_metrics() {
451 let mut b = PdfBuilder::new();
452 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
453 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
454 b.object(
455 3,
456 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
457 /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
458 );
459 b.stream(4, "", b"BT /F1 12 Tf 72 720 Td (Hi) Tj ET");
460 b.object(
461 5,
462 "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica \
463 /Encoding /WinAnsiEncoding /FontDescriptor 6 0 R >>",
464 );
465 b.object(
466 6,
467 "<< /Type /FontDescriptor /FontName /Helvetica \
468 /Ascent 718 /Descent -207 >>",
469 );
470 let doc = Document::load(b.build(1)).unwrap();
471 let page = doc.page(0).unwrap();
472 let spans = extract_spans(&doc, &page).unwrap();
473 let s = &spans[0];
474 assert!((s.bbox.x0 - s.x).abs() < 1e-3);
475 assert!((s.bbox.x1 - s.end_x).abs() < 1e-3);
476 assert!((s.bbox.y0 - (720.0 - 0.207 * 12.0)).abs() < 1e-3);
477 assert!((s.bbox.y1 - (720.0 + 0.718 * 12.0)).abs() < 1e-3);
478 }
479
480 #[test]
483 fn span_bbox_defaults_to_em_fractions() {
484 let doc = Document::load(simple_doc("Hi")).unwrap();
485 let page = doc.page(0).unwrap();
486 let spans = extract_spans(&doc, &page).unwrap();
487 let s = &spans[0];
488 assert!((s.bbox.y0 - (720.0 - 0.2 * 12.0)).abs() < 1e-3);
489 assert!((s.bbox.y1 - (720.0 + 0.8 * 12.0)).abs() < 1e-3);
490 }
491
492 #[test]
495 fn span_bbox_falls_back_to_cap_height() {
496 let mut b = PdfBuilder::new();
497 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
498 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
499 b.object(
500 3,
501 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
502 /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
503 );
504 b.stream(4, "", b"BT /F1 12 Tf 72 720 Td (Hi) Tj ET");
505 b.object(
506 5,
507 "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica \
508 /Encoding /WinAnsiEncoding /FontDescriptor 6 0 R >>",
509 );
510 b.object(
511 6,
512 "<< /Type /FontDescriptor /FontName /Helvetica /CapHeight 700 >>",
513 );
514 let doc = Document::load(b.build(1)).unwrap();
515 let page = doc.page(0).unwrap();
516 let spans = extract_spans(&doc, &page).unwrap();
517 assert!((spans[0].bbox.y1 - (720.0 + 0.7 * 12.0)).abs() < 1e-3);
518 assert!((spans[0].bbox.y0 - (720.0 - 0.2 * 12.0)).abs() < 1e-3);
519 }
520
521 #[test]
523 fn fixed_pitch_flag_marks_monospace() {
524 let spans = flag_spans(1);
525 assert!(spans[0].monospace);
526 assert!(!spans[0].serif);
527 }
528
529 #[test]
531 fn serif_flag_marks_serif() {
532 let spans = flag_spans(2);
533 assert!(spans[0].serif);
534 assert!(!spans[0].monospace);
535 }
536
537 fn flag_spans(flags: u32) -> Vec<TextSpan> {
539 let mut b = PdfBuilder::new();
540 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
541 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
542 b.object(
543 3,
544 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
545 /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
546 );
547 b.stream(4, "", b"BT /F1 12 Tf 72 720 Td (x) Tj ET");
548 b.object(
549 5,
550 "<< /Type /Font /Subtype /Type1 /BaseFont /Custom \
551 /Encoding /WinAnsiEncoding /FontDescriptor 6 0 R >>",
552 );
553 b.object(
554 6,
555 &format!("<< /Type /FontDescriptor /FontName /Custom /Flags {flags} >>"),
556 );
557 let doc = Document::load(b.build(1)).unwrap();
558 let page = doc.page(0).unwrap();
559 extract_spans(&doc, &page).unwrap()
560 }
561
562 #[test]
564 fn span_carries_text_rise() {
565 let doc = Document::load(pdfboss_testkit::doc_with_graphics(
566 "BT /F1 12 Tf 72 720 Td (flat) Tj 5 Ts (up) Tj ET",
567 ))
568 .unwrap();
569 let page = doc.page(0).unwrap();
570 let spans = extract_spans(&doc, &page).unwrap();
571 assert_eq!(spans[0].rise, 0.0);
572 assert_eq!(spans[1].rise, 5.0);
573 }
574
575 #[test]
577 fn span_marks_vertical_writing() {
578 let mut b = PdfBuilder::new();
579 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
580 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
581 b.object(
582 3,
583 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
584 /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
585 );
586 b.stream(4, "", b"BT /F1 12 Tf 72 720 Td <0001> Tj ET");
587 b.object(
588 5,
589 "<< /Type /Font /Subtype /Type0 /BaseFont /X /Encoding /Identity-V \
590 /DescendantFonts [6 0 R] /ToUnicode 7 0 R >>",
591 );
592 b.object(
593 6,
594 "<< /Type /Font /Subtype /CIDFontType2 /BaseFont /X /DW 600 >>",
595 );
596 b.stream(
597 7,
598 "",
599 b"1 begincodespacerange <0000> <FFFF> endcodespacerange\n\
600 1 beginbfchar <0001> <0041> endbfchar",
601 );
602 let doc = Document::load(b.build(1)).unwrap();
603 let page = doc.page(0).unwrap();
604 let spans = extract_spans(&doc, &page).unwrap();
605 assert!(spans[0].vertical);
606 }
607
608 #[test]
612 fn invisible_render_modes_mark_the_span() {
613 let doc = Document::load(pdfboss_testkit::doc_with_graphics(
614 "BT /F1 12 Tf 72 720 Td (seen) Tj 3 Tr (ocr) Tj 7 Tr (clip) Tj 0 Tr (back) Tj ET",
615 ))
616 .unwrap();
617 let page = doc.page(0).unwrap();
618 let spans = extract_spans(&doc, &page).unwrap();
619 let invisible: Vec<bool> = spans.iter().map(|s| s.invisible).collect();
620 assert_eq!(invisible, [false, true, true, false]);
621 }
622
623 #[test]
625 fn span_color_defaults_to_black() {
626 let doc = Document::load(simple_doc("Hi")).unwrap();
627 let page = doc.page(0).unwrap();
628 let spans = extract_spans(&doc, &page).unwrap();
629 assert_eq!(spans[0].color, Some((0.0, 0.0, 0.0)));
630 }
631
632 #[test]
634 fn device_fill_colors_set_the_span_color() {
635 let doc = Document::load(pdfboss_testkit::doc_with_graphics(
636 "BT /F1 12 Tf 72 720 Td 1 0 0 rg (red) Tj 0.5 g (gray) Tj \
637 1 0 0 0 k (cyan) Tj ET",
638 ))
639 .unwrap();
640 let page = doc.page(0).unwrap();
641 let spans = extract_spans(&doc, &page).unwrap();
642 assert_eq!(spans[0].color, Some((1.0, 0.0, 0.0)));
643 assert_eq!(spans[1].color, Some((0.5, 0.5, 0.5)));
644 assert_eq!(spans[2].color, Some((0.0, 1.0, 1.0)));
645 }
646
647 #[test]
651 fn sc_components_set_the_span_color_by_count() {
652 let doc = Document::load(pdfboss_testkit::doc_with_graphics(
653 "BT /F1 12 Tf 72 720 Td /DeviceRGB cs 0 1 0 sc (green) Tj \
654 0.25 sc (dark) Tj ET",
655 ))
656 .unwrap();
657 let page = doc.page(0).unwrap();
658 let spans = extract_spans(&doc, &page).unwrap();
659 assert_eq!(spans[0].color, Some((0.0, 1.0, 0.0)));
660 assert_eq!(spans[1].color, Some((0.25, 0.25, 0.25)));
661 }
662
663 #[test]
666 fn pattern_fill_leaves_color_unknown() {
667 let doc = Document::load(pdfboss_testkit::doc_with_graphics(
668 "BT /F1 12 Tf 72 720 Td /Pattern cs /P1 scn (patterned) Tj ET",
669 ))
670 .unwrap();
671 let page = doc.page(0).unwrap();
672 let spans = extract_spans(&doc, &page).unwrap();
673 assert_eq!(spans[0].color, None);
674 }
675
676 #[test]
679 fn an_underline_ruling_marks_the_span() {
680 let doc = Document::load(pdfboss_testkit::doc_with_graphics(
681 "BT /F1 12 Tf 72 720 Td (Hello) Tj ET 72 718.5 m 105 718.5 l S",
682 ))
683 .unwrap();
684 let page = doc.page(0).unwrap();
685 let spans = extract_spans(&doc, &page).unwrap();
686 assert!(spans[0].underline);
687 assert!(!spans[0].strikethrough);
688 }
689
690 #[test]
692 fn a_strikethrough_ruling_marks_the_span() {
693 let doc = Document::load(pdfboss_testkit::doc_with_graphics(
694 "BT /F1 12 Tf 72 720 Td (Hello) Tj ET 72 723.6 m 105 723.6 l S",
695 ))
696 .unwrap();
697 let page = doc.page(0).unwrap();
698 let spans = extract_spans(&doc, &page).unwrap();
699 assert!(spans[0].strikethrough);
700 assert!(!spans[0].underline);
701 }
702
703 #[test]
706 fn a_distant_ruling_marks_nothing() {
707 let doc = Document::load(pdfboss_testkit::doc_with_graphics(
708 "BT /F1 12 Tf 72 720 Td (Hello) Tj ET 72 700 m 105 700 l S",
709 ))
710 .unwrap();
711 let page = doc.page(0).unwrap();
712 let spans = extract_spans(&doc, &page).unwrap();
713 assert!(!spans[0].underline);
714 assert!(!spans[0].strikethrough);
715 }
716
717 #[test]
721 fn an_underline_needs_most_of_the_span_covered() {
722 let doc = Document::load(pdfboss_testkit::doc_with_graphics(
723 "BT /F1 12 Tf 72 720 Td (Hello) Tj ET 100 718.5 m 130 718.5 l S",
724 ))
725 .unwrap();
726 let page = doc.page(0).unwrap();
727 let spans = extract_spans(&doc, &page).unwrap();
728 assert!(!spans[0].underline);
729 }
730
731 #[test]
737 fn the_source_generic_entry_points_honor_optional_content() {
738 let mut b = PdfBuilder::new();
739 b.object(
740 1,
741 "<< /Type /Catalog /Pages 2 0 R /OCProperties \
742 << /OCGs [6 0 R] /D << /OFF [6 0 R] >> >> >>",
743 );
744 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
745 b.object(
746 3,
747 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
748 /Resources << /Font << /F1 5 0 R >> \
749 /Properties << /H 6 0 R >> >> /Contents 4 0 R >>",
750 );
751 b.stream(
752 4,
753 "",
754 b"BT /F1 12 Tf 72 720 Td /OC /H BDC (hidden) Tj EMC (kept) Tj ET",
755 );
756 b.object(
757 5,
758 "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica \
759 /Encoding /WinAnsiEncoding >>",
760 );
761 b.object(6, "<< /Type /OCG /Name (hidden) >>");
762 let doc = Document::load(b.build(1)).unwrap();
763 let page = doc.page(0).unwrap();
764 let oc = doc.oc_state();
765 let gated = block_on(extract_spans_with(Immediate(&doc), &page, oc.as_ref())).unwrap();
766 let texts: Vec<&str> = gated.iter().map(|s| s.text.as_str()).collect();
767 assert_eq!(texts, ["kept"]);
768 let all = block_on(extract_spans_with(Immediate(&doc), &page, None)).unwrap();
769 let texts: Vec<&str> = all.iter().map(|s| s.text.as_str()).collect();
770 assert_eq!(texts, ["hidden", "kept"]);
771 }
772
773 #[test]
778 fn descriptor_flags_set_span_style() {
779 let mut b = PdfBuilder::new();
780 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
781 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
782 b.object(
783 3,
784 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
785 /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
786 );
787 b.stream(4, "", b"BT /F1 12 Tf 72 720 Td (x) Tj ET");
788 b.object(
789 5,
790 "<< /Type /Font /Subtype /Type1 /BaseFont /Custom \
791 /Encoding /WinAnsiEncoding /FontDescriptor 6 0 R >>",
792 );
793 b.object(
794 6,
795 "<< /Type /FontDescriptor /FontName /Custom /Flags 64 /FontWeight 700 >>",
796 );
797 let doc = Document::load(b.build(1)).unwrap();
798 let page = doc.page(0).unwrap();
799 let spans = extract_spans(&doc, &page).unwrap();
800 assert!(spans[0].italic, "Flags bit 7 (mask 64) is Italic");
801 assert!(spans[0].bold, "FontWeight 700 >= 600 is bold");
802 }
803
804 #[test]
808 fn thick_stemv_reads_as_bold() {
809 let mut b = PdfBuilder::new();
810 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
811 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
812 b.object(
813 3,
814 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
815 /Resources << /Font << /F1 5 0 R /F2 7 0 R >> >> /Contents 4 0 R >>",
816 );
817 b.stream(4, "", b"BT /F1 12 Tf 72 720 Td (a) Tj /F2 12 Tf (b) Tj ET");
818 b.object(
819 5,
820 "<< /Type /Font /Subtype /Type1 /BaseFont /NimbusRomNo9L-Medi \
821 /Encoding /WinAnsiEncoding /FontDescriptor 6 0 R >>",
822 );
823 b.object(
824 6,
825 "<< /Type /FontDescriptor /FontName /NimbusRomNo9L-Medi /Flags 4 /StemV 140 >>",
826 );
827 b.object(
828 7,
829 "<< /Type /Font /Subtype /Type1 /BaseFont /NimbusRomNo9L-Regu \
830 /Encoding /WinAnsiEncoding /FontDescriptor 8 0 R >>",
831 );
832 b.object(
833 8,
834 "<< /Type /FontDescriptor /FontName /NimbusRomNo9L-Regu /Flags 4 /StemV 85 >>",
835 );
836 let doc = Document::load(b.build(1)).unwrap();
837 let page = doc.page(0).unwrap();
838 let spans = extract_spans(&doc, &page).unwrap();
839 assert!(spans[0].bold, "StemV 140 is a bold stem");
840 assert!(!spans[1].bold, "StemV 85 is a regular stem");
841 }
842
843 #[test]
845 fn basefont_name_and_italic_angle_fallbacks() {
846 let mut b = PdfBuilder::new();
847 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
848 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
849 b.object(
850 3,
851 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
852 /Resources << /Font << /F1 5 0 R /F2 6 0 R >> >> /Contents 4 0 R >>",
853 );
854 b.stream(4, "", b"BT /F1 12 Tf 72 720 Td (a) Tj /F2 12 Tf (b) Tj ET");
855 b.object(
856 5,
857 "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-BoldOblique \
858 /Encoding /WinAnsiEncoding >>",
859 );
860 b.object(
861 6,
862 "<< /Type /Font /Subtype /Type1 /BaseFont /Times-Roman \
863 /Encoding /WinAnsiEncoding /FontDescriptor 7 0 R >>",
864 );
865 b.object(
866 7,
867 "<< /Type /FontDescriptor /FontName /Times-Roman /ItalicAngle -12 >>",
868 );
869 let doc = Document::load(b.build(1)).unwrap();
870 let page = doc.page(0).unwrap();
871 let spans = extract_spans(&doc, &page).unwrap();
872 assert!(
873 spans[0].bold && spans[0].italic,
874 "BaseFont substrings Bold+Oblique"
875 );
876 assert!(!spans[1].bold && spans[1].italic, "ItalicAngle != 0 alone");
877 }
878
879 #[test]
881 fn type0_descendant_descriptor_sets_style() {
882 let mut b = PdfBuilder::new();
883 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
884 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
885 b.object(
886 3,
887 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
888 /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
889 );
890 b.stream(4, "", b"BT /F1 12 Tf 72 720 Td <0001> Tj ET");
891 b.object(
892 5,
893 "<< /Type /Font /Subtype /Type0 /BaseFont /X /Encoding /Identity-H \
894 /DescendantFonts [6 0 R] /ToUnicode 8 0 R >>",
895 );
896 b.object(
897 6,
898 "<< /Type /Font /Subtype /CIDFontType2 /BaseFont /X /DW 600 \
899 /FontDescriptor 7 0 R >>",
900 );
901 b.object(
902 7,
903 "<< /Type /FontDescriptor /FontName /X /Flags 64 /FontWeight 600 >>",
904 );
905 b.stream(
906 8,
907 "",
908 b"1 begincodespacerange <0000> <FFFF> endcodespacerange\n\
909 1 beginbfchar <0001> <0041> endbfchar",
910 );
911 let doc = Document::load(b.build(1)).unwrap();
912 let page = doc.page(0).unwrap();
913 let spans = extract_spans(&doc, &page).unwrap();
914 assert!(spans[0].bold && spans[0].italic);
915 }
916
917 struct Counting<'a> {
922 inner: Immediate<&'a Document>,
923 resolutions: std::cell::RefCell<std::collections::HashMap<u32, usize>>,
924 }
925
926 impl<'a> Counting<'a> {
927 fn new(doc: &'a Document) -> Counting<'a> {
928 Counting {
929 inner: Immediate(doc),
930 resolutions: std::cell::RefCell::new(std::collections::HashMap::new()),
931 }
932 }
933
934 fn resolutions(&self, num: u32) -> usize {
935 self.resolutions.borrow().get(&num).copied().unwrap_or(0)
936 }
937 }
938
939 impl AsyncObjectSource for Counting<'_> {
940 fn get(&self, r: ObjRef) -> BoxFuture<'_, Result<Object>> {
941 self.inner.get(r)
942 }
943
944 fn stream_data<'b>(&'b self, s: &'b Stream) -> BoxFuture<'b, Result<Vec<u8>>> {
945 self.inner.stream_data(s)
946 }
947
948 fn resolve<'b>(&'b self, o: &'b Object) -> BoxFuture<'b, Result<Object>> {
949 if let Object::Ref(r) = o {
950 *self.resolutions.borrow_mut().entry(r.num).or_insert(0) += 1;
951 }
952 self.inner.resolve(o)
953 }
954 }
955
956 #[test]
960 fn a_font_reached_from_repeated_forms_loads_once_per_page() {
961 let mut b = PdfBuilder::new();
962 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
963 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
964 b.object(
965 3,
966 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
967 /Resources << /XObject << /Fx 6 0 R >> >> /Contents 4 0 R >>",
968 );
969 b.stream(4, "", b"/Fx Do /Fx Do");
970 b.object(
971 5,
972 "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica \
973 /Encoding /WinAnsiEncoding >>",
974 );
975 b.stream(
976 6,
977 "/Type /XObject /Subtype /Form /BBox [0 0 612 792] \
978 /Resources << /Font << /F1 5 0 R >> >>",
979 b"BT /F1 12 Tf 72 700 Td (x) Tj ET",
980 );
981 let doc = Document::load(b.build(1)).unwrap();
982 let page = doc.page(0).unwrap();
983 let counting = Counting::new(&doc);
984 let (spans, report) =
985 block_on(extract_spans_reporting_with(&counting, &page, None)).unwrap();
986 assert!(report.is_complete(), "unexpected skips: {report:?}");
987 assert_eq!(spans.len(), 2, "both form invocations must show text");
988 assert_eq!(
989 counting.resolutions(5),
990 1,
991 "one font dictionary resolution per page walk"
992 );
993 }
994
995 #[test]
1001 fn a_resource_category_resolves_once_per_walk() {
1002 let mut b = PdfBuilder::new();
1003 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1004 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
1005 b.object(
1006 3,
1007 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1008 /Resources << /ExtGState 5 0 R >> /Contents 4 0 R >>",
1009 );
1010 b.stream(
1011 4,
1012 "",
1013 b"/G1 gs 10 10 m 100 10 l S \
1014 /G1 gs 10 20 m 100 20 l S \
1015 /G1 gs 10 30 m 100 30 l S",
1016 );
1017 b.object(5, "<< /G1 << /LW 2 >> >>");
1018 let doc = Document::load(b.build(1)).unwrap();
1019 let page = doc.page(0).unwrap();
1020 let counting = Counting::new(&doc);
1021 let (_, rulings, report) = block_on(extract_spans_and_rulings_reporting_with(
1022 &counting, &page, None,
1023 ))
1024 .unwrap();
1025 assert!(report.is_complete(), "unexpected skips: {report:?}");
1026 assert_eq!(rulings.len(), 3, "all three strokes extract");
1027 assert_eq!(
1028 counting.resolutions(5),
1029 1,
1030 "one category dictionary resolution per page walk"
1031 );
1032 }
1033
1034 #[test]
1038 fn a_font_shared_across_pages_loads_once_per_document() {
1039 let mut b = PdfBuilder::new();
1040 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1041 b.object(2, "<< /Type /Pages /Kids [3 0 R 5 0 R] /Count 2 >>");
1042 b.object(
1043 3,
1044 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1045 /Resources << /Font << /F1 7 0 R >> >> /Contents 4 0 R >>",
1046 );
1047 b.stream(4, "", b"BT /F1 12 Tf 72 720 Td (one) Tj ET");
1048 b.object(
1049 5,
1050 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1051 /Resources << /Font << /F1 7 0 R >> >> /Contents 6 0 R >>",
1052 );
1053 b.stream(6, "", b"BT /F1 12 Tf 72 720 Td (two) Tj ET");
1054 b.object(
1055 7,
1056 "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica \
1057 /Encoding /WinAnsiEncoding >>",
1058 );
1059 let doc = Document::load(b.build(1)).unwrap();
1060 let fonts = FontCache::default();
1061 let counting = Counting::new(&doc);
1062 let mut cached = Vec::new();
1063 for index in 0..2 {
1064 let page = doc.page(index).unwrap();
1065 let (spans, report) = block_on(extract_spans_reporting_cached_with(
1066 &counting, &page, &fonts, None,
1067 ))
1068 .unwrap();
1069 assert!(report.is_complete(), "unexpected skips: {report:?}");
1070 cached.push(spans);
1071 }
1072 assert_eq!(
1073 counting.resolutions(7),
1074 1,
1075 "one font dictionary resolution per document"
1076 );
1077 for (index, spans) in cached.iter().enumerate() {
1078 let page = doc.page(index).unwrap();
1079 let plain = extract_spans_reporting(&doc, &page).unwrap().0;
1080 assert_eq!(spans, &plain, "page {index} must extract identically");
1081 }
1082 }
1083
1084 #[test]
1089 fn a_shared_cache_keeps_the_name_binding_per_page() {
1090 let mut b = PdfBuilder::new();
1091 b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
1092 b.object(2, "<< /Type /Pages /Kids [3 0 R 5 0 R] /Count 2 >>");
1093 b.object(
1094 3,
1095 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1096 /Resources << /Font << /F1 7 0 R >> >> /Contents 4 0 R >>",
1097 );
1098 b.stream(4, "", b"BT /F1 12 Tf 72 720 Td (aa) Tj ET");
1099 b.object(
1100 5,
1101 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
1102 /Resources << /Font << /F1 8 0 R >> >> /Contents 6 0 R >>",
1103 );
1104 b.stream(6, "", b"BT /F1 12 Tf 72 720 Td (aa) Tj ET");
1105 b.object(
1106 7,
1107 "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica \
1108 /Encoding /WinAnsiEncoding /FirstChar 97 /LastChar 97 /Widths [500] >>",
1109 );
1110 b.object(
1111 8,
1112 "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica \
1113 /Encoding /WinAnsiEncoding /FirstChar 97 /LastChar 97 /Widths [1000] >>",
1114 );
1115 let doc = Document::load(b.build(1)).unwrap();
1116 let fonts = FontCache::default();
1117 let mut advances = Vec::new();
1118 for index in 0..2 {
1119 let page = doc.page(index).unwrap();
1120 let (spans, _) = extract_spans_reporting_cached(&doc, &page, &fonts).unwrap();
1121 assert_eq!(spans.len(), 1);
1122 advances.push(spans[0].end_x - spans[0].x);
1123 }
1124 assert!(
1125 (advances[0] - 12.0).abs() < 1e-3,
1126 "page one: {}",
1127 advances[0]
1128 );
1129 assert!(
1130 (advances[1] - 24.0).abs() < 1e-3,
1131 "page two: {}",
1132 advances[1]
1133 );
1134 }
1135
1136 struct NullSource {
1148 payload: Vec<u8>,
1149 }
1150
1151 impl AsyncObjectSource for NullSource {
1152 fn get(&self, _r: ObjRef) -> BoxFuture<'_, Result<Object>> {
1153 Box::pin(std::future::ready(Ok(Object::Null)))
1154 }
1155
1156 fn stream_data<'a>(&'a self, _s: &'a Stream) -> BoxFuture<'a, Result<Vec<u8>>> {
1157 Box::pin(std::future::ready(Ok(self.payload.clone())))
1158 }
1159
1160 fn resolve<'a>(&'a self, o: &'a Object) -> BoxFuture<'a, Result<Object>> {
1161 Box::pin(resolve_with(self, o))
1162 }
1163 }
1164
1165 #[test]
1179 fn the_async_entry_point_yields_a_spawnable_future() {
1180 fn assert_send_static<F: Future + Send + 'static>(_: &F) {}
1181
1182 let doc = Document::load(simple_doc("Hello")).unwrap();
1183 let spans_page = doc.page(0).unwrap();
1184 drop(doc);
1185
1186 let spans = async move {
1187 extract_spans_with(
1188 NullSource {
1189 payload: Vec::new(),
1190 },
1191 &spans_page,
1192 None,
1193 )
1194 .await
1195 };
1196 assert_send_static(&spans);
1197
1198 assert!(block_on(spans).unwrap().is_empty());
1201 }
1202}