mod cmap;
mod extract;
mod font;
mod sfnt;
use pdfboss_core::{block_on, AsyncObjectSource, Document, Immediate, Page, Result};
pub use extract::{ExtractReport, SkipCause, SkippedText, SkippedTextKind};
pub use pdfboss_core::Point;
#[derive(Debug, Clone, PartialEq)]
pub struct TextSpan {
pub text: String,
pub x: f32,
pub y: f32,
pub end_x: f32,
pub size: f32,
pub font: String,
pub bold: bool,
pub italic: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Ruling {
pub start: Point,
pub end: Point,
pub width: f32,
}
pub fn extract_spans(doc: &Document, page: &Page) -> Result<Vec<TextSpan>> {
block_on(extract_spans_with(Immediate(doc), page))
}
pub async fn extract_spans_with<S: AsyncObjectSource>(
src: S,
page: &Page,
) -> Result<Vec<TextSpan>> {
let (spans, _, _) = extract::page_spans_and_rulings_with(src, page).await;
Ok(spans)
}
pub fn extract_spans_reporting(
doc: &Document,
page: &Page,
) -> Result<(Vec<TextSpan>, ExtractReport)> {
block_on(extract_spans_reporting_with(Immediate(doc), page))
}
pub async fn extract_spans_reporting_with<S: AsyncObjectSource>(
src: S,
page: &Page,
) -> Result<(Vec<TextSpan>, ExtractReport)> {
let (spans, _, report) = extract::page_spans_and_rulings_with(src, page).await;
Ok((spans, report))
}
pub fn extract_spans_and_rulings_reporting(
doc: &Document,
page: &Page,
) -> Result<(Vec<TextSpan>, Vec<Ruling>, ExtractReport)> {
block_on(extract_spans_and_rulings_reporting_with(
Immediate(doc),
page,
))
}
pub async fn extract_spans_and_rulings_reporting_with<S: AsyncObjectSource>(
src: S,
page: &Page,
) -> Result<(Vec<TextSpan>, Vec<Ruling>, ExtractReport)> {
let (spans, rulings, report) = extract::page_spans_and_rulings_with(src, page).await;
Ok((spans, rulings, report))
}
#[cfg(test)]
mod tests {
use super::*;
use pdfboss_core::{resolve_with, BoxFuture, ObjRef, Object, Stream};
use pdfboss_testkit::{simple_doc, PdfBuilder};
use std::future::Future;
#[test]
fn form_matrix_translates_the_nested_span() {
let mut b = PdfBuilder::new();
b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
b.object(
3,
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
/Resources << /Font << /F1 5 0 R >> /XObject << /Fx 6 0 R >> >> \
/Contents 4 0 R >>",
);
b.stream(4, "", b"BT /F1 12 Tf 72 720 Td (out) Tj ET /Fx Do");
b.object(
5,
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica \
/Encoding /WinAnsiEncoding >>",
);
b.stream(
6,
"/Type /XObject /Subtype /Form /BBox [0 0 612 792] \
/Matrix [1 0 0 1 0 -20]",
b"BT /F1 12 Tf 72 720 Td (in) Tj ET",
);
let doc = Document::load(b.build(1)).unwrap();
let page = doc.page(0).unwrap();
let spans = extract_spans(&doc, &page).unwrap();
assert_eq!(spans.len(), 2);
assert!((spans[1].y - 700.0).abs() < 1e-3); }
#[test]
fn extract_spans_sane_positions() {
let doc = Document::load(simple_doc("Hi")).unwrap();
let page = doc.page(0).unwrap();
let spans = extract_spans(&doc, &page).unwrap();
assert_eq!(spans.len(), 1);
let s = &spans[0];
assert_eq!(s.text, "Hi");
assert!((s.x - 72.0).abs() < 1e-3);
assert!((s.y - 720.0).abs() < 1e-3);
assert!((s.size - 12.0).abs() < 1e-3);
assert_eq!(s.font, "F1");
}
#[test]
fn extract_spans_and_rulings_reports_both() {
let doc = Document::load(pdfboss_testkit::doc_with_graphics(
"BT /F1 12 Tf 72 720 Td (Hi) Tj ET 72 700 m 272 700 l S",
))
.unwrap();
let page = doc.page(0).unwrap();
let (spans, rulings, report) = extract_spans_and_rulings_reporting(&doc, &page).unwrap();
assert_eq!(spans.len(), 1);
assert_eq!(spans[0].text, "Hi");
assert_eq!(rulings.len(), 1);
assert!((rulings[0].start.y - 700.0).abs() < 1e-3);
assert!(report.is_complete());
}
#[test]
fn extract_spans_ordering_multi_line() {
let doc = Document::load(pdfboss_testkit::doc_with_graphics(
"BT /F1 12 Tf 72 720 Td (top) Tj 0 -40 Td (bottom) Tj ET",
))
.unwrap();
let page = doc.page(0).unwrap();
let spans = extract_spans(&doc, &page).unwrap();
assert_eq!(spans.len(), 2);
assert!(spans[0].y > spans[1].y);
assert_eq!(spans[0].text, "top");
assert_eq!(spans[1].text, "bottom");
assert!(spans.iter().all(|s| s.size > 0.0 && s.x >= 0.0));
}
#[test]
fn descriptor_flags_set_span_style() {
let mut b = PdfBuilder::new();
b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
b.object(
3,
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
/Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
);
b.stream(4, "", b"BT /F1 12 Tf 72 720 Td (x) Tj ET");
b.object(
5,
"<< /Type /Font /Subtype /Type1 /BaseFont /Custom \
/Encoding /WinAnsiEncoding /FontDescriptor 6 0 R >>",
);
b.object(
6,
"<< /Type /FontDescriptor /FontName /Custom /Flags 64 /FontWeight 700 >>",
);
let doc = Document::load(b.build(1)).unwrap();
let page = doc.page(0).unwrap();
let spans = extract_spans(&doc, &page).unwrap();
assert!(spans[0].italic, "Flags bit 7 (mask 64) is Italic");
assert!(spans[0].bold, "FontWeight 700 >= 600 is bold");
}
#[test]
fn basefont_name_and_italic_angle_fallbacks() {
let mut b = PdfBuilder::new();
b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
b.object(
3,
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
/Resources << /Font << /F1 5 0 R /F2 6 0 R >> >> /Contents 4 0 R >>",
);
b.stream(4, "", b"BT /F1 12 Tf 72 720 Td (a) Tj /F2 12 Tf (b) Tj ET");
b.object(
5,
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-BoldOblique \
/Encoding /WinAnsiEncoding >>",
);
b.object(
6,
"<< /Type /Font /Subtype /Type1 /BaseFont /Times-Roman \
/Encoding /WinAnsiEncoding /FontDescriptor 7 0 R >>",
);
b.object(
7,
"<< /Type /FontDescriptor /FontName /Times-Roman /ItalicAngle -12 >>",
);
let doc = Document::load(b.build(1)).unwrap();
let page = doc.page(0).unwrap();
let spans = extract_spans(&doc, &page).unwrap();
assert!(
spans[0].bold && spans[0].italic,
"BaseFont substrings Bold+Oblique"
);
assert!(!spans[1].bold && spans[1].italic, "ItalicAngle != 0 alone");
}
#[test]
fn type0_descendant_descriptor_sets_style() {
let mut b = PdfBuilder::new();
b.object(1, "<< /Type /Catalog /Pages 2 0 R >>");
b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
b.object(
3,
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
/Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>",
);
b.stream(4, "", b"BT /F1 12 Tf 72 720 Td <0001> Tj ET");
b.object(
5,
"<< /Type /Font /Subtype /Type0 /BaseFont /X /Encoding /Identity-H \
/DescendantFonts [6 0 R] /ToUnicode 8 0 R >>",
);
b.object(
6,
"<< /Type /Font /Subtype /CIDFontType2 /BaseFont /X /DW 600 \
/FontDescriptor 7 0 R >>",
);
b.object(
7,
"<< /Type /FontDescriptor /FontName /X /Flags 64 /FontWeight 600 >>",
);
b.stream(
8,
"",
b"1 begincodespacerange <0000> <FFFF> endcodespacerange\n\
1 beginbfchar <0001> <0041> endbfchar",
);
let doc = Document::load(b.build(1)).unwrap();
let page = doc.page(0).unwrap();
let spans = extract_spans(&doc, &page).unwrap();
assert!(spans[0].bold && spans[0].italic);
}
struct NullSource {
payload: Vec<u8>,
}
impl AsyncObjectSource for NullSource {
fn get(&self, _r: ObjRef) -> BoxFuture<'_, Result<Object>> {
Box::pin(std::future::ready(Ok(Object::Null)))
}
fn stream_data<'a>(&'a self, _s: &'a Stream) -> BoxFuture<'a, Result<Vec<u8>>> {
Box::pin(std::future::ready(Ok(self.payload.clone())))
}
fn resolve<'a>(&'a self, o: &'a Object) -> BoxFuture<'a, Result<Object>> {
Box::pin(resolve_with(self, o))
}
}
#[test]
fn the_async_entry_point_yields_a_spawnable_future() {
fn assert_send_static<F: Future + Send + 'static>(_: &F) {}
let doc = Document::load(simple_doc("Hello")).unwrap();
let spans_page = doc.page(0).unwrap();
drop(doc);
let spans = async move {
extract_spans_with(
NullSource {
payload: Vec::new(),
},
&spans_page,
)
.await
};
assert_send_static(&spans);
assert!(block_on(spans).unwrap().is_empty());
}
}