use super::{Extraction, which};
use crate::clean::clean_paged_text;
use anyhow::{Context, Result};
use std::path::Path;
use std::process::Command;
const TABLE_MARKER: &str = "<!-- layout-preserved rendering of this page's tables -->";
pub fn extract(path: &Path) -> Result<Extraction> {
let result = pdf_inspector::extract_pages_markdown(path, None)
.map_err(|e| anyhow::anyhow!("{e}"))
.with_context(|| format!("pdf-inspector could not read {}", path.display()))?;
let page_count = result.pages.len() as u32;
let have_pdftotext = which("pdftotext").is_some();
let supplement = pages_to_supplement(
&result.pages_with_tables,
&result.pages_with_columns,
page_count,
);
let mut pages: Vec<String> = Vec::with_capacity(result.pages.len());
for page in &result.pages {
let page_no = page.page + 1;
let mut body = page.markdown.clone();
if have_pdftotext
&& supplement.contains(&page_no)
&& let Some(layout) = pdftotext_page(path, page_no)
&& !layout.trim().is_empty()
{
body.push_str("\n\n");
body.push_str(TABLE_MARKER);
body.push('\n');
body.push_str(layout.trim_end());
}
pages.push(body);
}
let joined = pages.join("\u{c}");
let text = clean_paged_text(&joined);
let method = if have_pdftotext && !supplement.is_empty() {
"pdf-inspector + pdftotext(-layout) on table pages"
} else {
"pdf-inspector"
};
Ok(Extraction {
text,
method: method.to_string(),
pages_needing_ocr: result.pages_needing_ocr,
pages_with_tables: result.pages_with_tables,
pages_with_columns: result.pages_with_columns,
page_count: Some(page_count),
})
}
fn pages_to_supplement(with_tables: &[u32], with_columns: &[u32], page_count: u32) -> Vec<u32> {
let mut pages = with_tables.to_vec();
let mostly_multi_column = page_count > 0 && with_columns.len() * 2 > page_count as usize;
if !mostly_multi_column {
pages.extend_from_slice(with_columns);
}
pages.sort_unstable();
pages.dedup();
pages
}
fn pdftotext_page(path: &Path, page_no: u32) -> Option<String> {
let out = Command::new("pdftotext")
.arg("-layout")
.arg("-f")
.arg(page_no.to_string())
.arg("-l")
.arg(page_no.to_string())
.arg(path)
.arg("-")
.output()
.ok()?;
out.status
.success()
.then(|| String::from_utf8_lossy(&out.stdout).into_owned())
}
#[cfg(test)]
mod tests {
use super::pages_to_supplement;
#[test]
fn single_column_book_supplements_its_wide_table_pages() {
let pages = pages_to_supplement(&[6, 8, 10, 13], &[8, 9, 10], 15);
assert_eq!(pages, vec![6, 8, 9, 10, 13]);
}
#[test]
fn two_column_paper_leaves_its_column_pages_alone() {
let pages = pages_to_supplement(&[5, 8, 11, 12], &[1, 2, 3, 4, 6, 7, 8, 9, 10, 12], 12);
assert_eq!(pages, vec![5, 8, 11, 12]);
}
#[test]
fn exactly_half_is_not_mostly() {
assert_eq!(pages_to_supplement(&[], &[1, 2], 4), vec![1, 2]);
assert_eq!(pages_to_supplement(&[], &[1, 2, 3], 4), Vec::<u32>::new());
}
#[test]
fn no_layout_signals_means_no_supplement() {
assert_eq!(pages_to_supplement(&[], &[], 10), Vec::<u32>::new());
}
}