mod cmap;
mod extract;
mod font;
mod sfnt;
use pdfboss_core::{block_on, AsyncObjectSource, Document, Immediate, Page, Result};
#[derive(Debug, Clone, PartialEq)]
pub struct TextSpan {
pub text: String,
pub x: f32,
pub y: f32,
pub size: f32,
pub font: String,
}
pub fn extract_text(doc: &Document, page: &Page) -> Result<String> {
block_on(extract_text_with(Immediate(doc), page))
}
pub async fn extract_text_with<S: AsyncObjectSource>(src: S, page: &Page) -> Result<String> {
let spans = extract::page_spans_with(src, page).await?;
Ok(extract::layout(&spans))
}
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>> {
Ok(extract::page_spans_with(src, page)
.await?
.into_iter()
.map(|s| TextSpan {
text: s.text,
x: s.x,
y: s.y,
size: s.size,
font: s.font,
})
.collect())
}
#[cfg(test)]
mod tests {
use super::*;
use pdfboss_core::{resolve_with, BoxFuture, ObjRef, Object, Stream};
use pdfboss_testkit::{multi_page_doc, simple_doc, PdfBuilder};
use std::future::Future;
fn page_text(doc: &Document, index: usize) -> String {
let page = doc.page(index).unwrap();
extract_text(doc, &page).unwrap()
}
#[test]
fn simple_doc_exact_text() {
let doc = Document::load(simple_doc("Hello, world!")).unwrap();
assert_eq!(page_text(&doc, 0), "Hello, world!");
}
#[test]
fn multi_page_doc_per_page() {
let doc = Document::load(multi_page_doc(&["Page one", "Page two", "Page three"])).unwrap();
assert_eq!(doc.page_count(), 3);
assert_eq!(page_text(&doc, 0), "Page one");
assert_eq!(page_text(&doc, 1), "Page two");
assert_eq!(page_text(&doc, 2), "Page three");
}
#[test]
fn differences_remap_in_extraction() {
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 (AB) Tj ET");
b.object(
5,
"<< /Type /Font /Subtype /Type1 /BaseFont /Custom \
/Encoding << /BaseEncoding /WinAnsiEncoding \
/Differences [65 /alpha] >> >>",
);
let doc = Document::load(b.build(1)).unwrap();
assert_eq!(page_text(&doc, 0), "\u{3B1}B");
}
#[test]
fn type0_font_with_tounicode_stream() {
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 <00010001> Tj ET");
b.object(
5,
"<< /Type /Font /Subtype /Type0 /BaseFont /X /Encoding /Identity-H \
/DescendantFonts [6 0 R] /ToUnicode 7 0 R >>",
);
b.object(
6,
"<< /Type /Font /Subtype /CIDFontType2 /BaseFont /X /DW 600 >>",
);
b.stream(
7,
"",
b"1 begincodespacerange <0000> <FFFF> endcodespacerange\n\
1 beginbfchar <0001> <03A9> endbfchar",
);
let doc = Document::load(b.build(1)).unwrap();
assert_eq!(page_text(&doc, 0), "\u{3A9}\u{3A9}");
}
#[test]
fn form_xobject_recursion() {
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();
assert_eq!(page_text(&doc, 0), "out\nin");
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 form_with_partial_resources_still_sees_the_page_font() {
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"/Fx Do");
b.object(
5,
"<< /Type /Font /Subtype /Type1 /BaseFont /Custom \
/Encoding << /BaseEncoding /WinAnsiEncoding \
/Differences [65 /alpha] >> >>",
);
b.stream(
6,
"/Type /XObject /Subtype /Form /BBox [0 0 612 792] \
/Resources << /ProcSet [/PDF /Text] >>",
b"BT /F1 12 Tf 72 720 Td (A) Tj ET",
);
let doc = Document::load(b.build(1)).unwrap();
assert_eq!(page_text(&doc, 0), "\u{3B1}");
}
#[test]
fn form_with_partial_resources_still_sees_the_page_xobject() {
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 << /Outer 6 0 R /Inner 7 0 R >> >> /Contents 4 0 R >>",
);
b.stream(4, "", b"/Outer Do");
b.object(
5,
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica \
/Encoding /WinAnsiEncoding >>",
);
b.stream(
6,
"/Type /XObject /Subtype /Form /BBox [0 0 612 792] \
/Resources << /ProcSet [/PDF /Text] >>",
b"/Inner Do",
);
b.stream(
7,
"/Type /XObject /Subtype /Form /BBox [0 0 612 792]",
b"BT /F1 12 Tf 72 720 Td (deep) Tj ET",
);
let doc = Document::load(b.build(1)).unwrap();
assert_eq!(page_text(&doc, 0), "deep");
}
#[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_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));
}
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_points_yield_spawnable_futures() {
fn assert_send_static<F: Future + Send + 'static>(_: &F) {}
let doc = Document::load(simple_doc("Hello")).unwrap();
let text_page = doc.page(0).unwrap();
let spans_page = doc.page(0).unwrap();
drop(doc);
let text = async move {
extract_text_with(
NullSource {
payload: Vec::new(),
},
&text_page,
)
.await
};
let spans = async move {
extract_spans_with(
NullSource {
payload: Vec::new(),
},
&spans_page,
)
.await
};
assert_send_static(&text);
assert_send_static(&spans);
assert_eq!(block_on(text).unwrap(), "");
assert!(block_on(spans).unwrap().is_empty());
}
#[test]
fn committed_fixture_files() {
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/../../tests/fixtures");
let hello = std::fs::read(format!("{dir}/hello.pdf")).unwrap();
let doc = Document::load(hello).unwrap();
assert_eq!(page_text(&doc, 0), "Hello, world!");
let three = std::fs::read(format!("{dir}/three-pages.pdf")).unwrap();
let doc = Document::load(three).unwrap();
assert_eq!(doc.page_count(), 3);
assert_eq!(page_text(&doc, 0), "Page one");
assert_eq!(page_text(&doc, 1), "Page two");
assert_eq!(page_text(&doc, 2), "Page three");
let xs = std::fs::read(format!("{dir}/xref-stream.pdf")).unwrap();
let doc = Document::load(xs).unwrap();
assert_eq!(page_text(&doc, 0), "Hello, world!");
}
}