use std::path::PathBuf;
use lopdf::{Document, Error as LopdfError};
use thiserror::Error;
use super::file::FileLoaderError;
#[derive(Error, Debug)]
pub enum PdfLoaderError {
#[error("{0}")]
FileLoaderError(#[from] FileLoaderError),
#[error("UTF-8 conversion error: {0}")]
FromUtf8Error(#[from] std::string::FromUtf8Error),
#[error("IO error: {0}")]
PdfError(#[from] LopdfError),
}
loadable_trait!(Loadable, PdfLoaderError, Document, load, load_with_path);
impl Loadable for PathBuf {
fn load(self) -> Result<Document, PdfLoaderError> {
Document::load(self).map_err(PdfLoaderError::PdfError)
}
fn load_with_path(self) -> Result<(PathBuf, Document), PdfLoaderError> {
let contents = Document::load(&self);
Ok((self, contents?))
}
}
impl Loadable for Vec<u8> {
fn load(self) -> Result<Document, PdfLoaderError> {
Document::load_mem(&self).map_err(PdfLoaderError::PdfError)
}
fn load_with_path(self) -> Result<(PathBuf, Document), PdfLoaderError> {
let doc = Document::load_mem(&self).map_err(PdfLoaderError::PdfError)?;
Ok((PathBuf::from("<memory>"), doc))
}
}
pub struct PdfFileLoader<'a, T> {
iterator: Box<dyn Iterator<Item = T> + 'a>,
}
#[allow(private_bounds)] impl<'a, T: Loadable + 'a> PdfFileLoader<'a, T> {
pub fn load(self) -> PdfFileLoader<'a, Result<Document, PdfLoaderError>> {
PdfFileLoader {
iterator: Box::new(self.iterator.map(|res| res.load())),
}
}
pub fn load_with_path(self) -> PdfFileLoader<'a, Result<(PathBuf, Document), PdfLoaderError>> {
PdfFileLoader {
iterator: Box::new(self.iterator.map(|res| res.load_with_path())),
}
}
}
fn page_texts(doc: &Document) -> Vec<(usize, Result<String, PdfLoaderError>)> {
doc.page_iter()
.enumerate()
.map(|(page_no, _)| {
(
page_no,
doc.extract_text(&[page_no as u32 + 1])
.map_err(PdfLoaderError::PdfError),
)
})
.collect()
}
fn all_text(doc: &Document) -> Result<String, PdfLoaderError> {
page_texts(doc).into_iter().map(|(_, text)| text).collect()
}
#[allow(private_bounds)] impl<'a, T: Loadable + 'a> PdfFileLoader<'a, T> {
pub fn read(self) -> PdfFileLoader<'a, Result<String, PdfLoaderError>> {
PdfFileLoader {
iterator: Box::new(self.iterator.map(|res| all_text(&res.load()?))),
}
}
pub fn read_with_path(self) -> PdfFileLoader<'a, Result<(PathBuf, String), PdfLoaderError>> {
PdfFileLoader {
iterator: Box::new(self.iterator.map(|res| {
let (path, doc) = res.load_with_path()?;
let content = all_text(&doc)?;
Ok((path, content))
})),
}
}
}
impl<'a> PdfFileLoader<'a, Document> {
pub fn by_page(self) -> PdfFileLoader<'a, Result<String, PdfLoaderError>> {
PdfFileLoader {
iterator: Box::new(
self.iterator
.flat_map(|doc| page_texts(&doc).into_iter().map(|(_, text)| text)),
),
}
}
}
type ByPage = (PathBuf, Vec<(usize, Result<String, PdfLoaderError>)>);
impl<'a> PdfFileLoader<'a, (PathBuf, Document)> {
pub fn by_page(self) -> PdfFileLoader<'a, ByPage> {
PdfFileLoader {
iterator: Box::new(self.iterator.map(|(path, doc)| (path, page_texts(&doc)))),
}
}
}
impl<'a> PdfFileLoader<'a, ByPage> {
pub fn ignore_errors(self) -> PdfFileLoader<'a, (PathBuf, Vec<(usize, String)>)> {
PdfFileLoader {
iterator: Box::new(self.iterator.map(|(path, pages)| {
let pages = pages
.into_iter()
.filter_map(|(page_no, res)| res.ok().map(|content| (page_no, content)))
.collect::<Vec<_>>();
(path, pages)
})),
}
}
}
loader_scaffold!(PdfFileLoader, PdfLoaderError, dir: all_entries);
loader_from_bytes!(PdfFileLoader);
#[cfg(test)]
mod tests {
use crate::loaders::test_fixtures::{fixture_glob, fixture_path};
use super::PdfFileLoader;
#[test]
fn test_pdf_loader() {
let glob = fixture_glob("*.pdf");
let loader = PdfFileLoader::with_glob(&glob).unwrap();
let actual = loader
.load_with_path()
.ignore_errors()
.by_page()
.ignore_errors()
.into_iter()
.collect::<Vec<_>>();
let mut actual = actual
.into_iter()
.map(|result| {
let (path, pages) = result;
pages.iter().for_each(|(page_no, content)| {
println!("{path:?} Page {page_no}: {content:?}");
});
(path, pages)
})
.collect::<Vec<_>>();
let mut expected = vec![
(
fixture_path("dummy.pdf"),
vec![(0, "Test\nPDF\nDocument\n".to_string())],
),
(
fixture_path("file-id-verifiers.pdf"),
vec![
(0, "rig-file-id-page-one-verifier-3a91\n".to_string()),
(1, "rig-file-id-page-two-verifier-8c27\n".to_string()),
(2, "rig-file-id-page-three-verifier-f54e\n".to_string()),
],
),
(
fixture_path("pages.pdf"),
vec![
(0, "Page\n1\n".to_string()),
(1, "Page\n2\n".to_string()),
(2, "Page\n3\n".to_string()),
],
),
];
actual.sort();
expected.sort();
assert!(!actual.is_empty());
assert!(expected == actual)
}
#[test]
fn test_pdf_loader_bytes() {
let bytes = std::fs::read(fixture_path("dummy.pdf")).unwrap();
let loader = PdfFileLoader::from_bytes(bytes);
let actual = loader
.load()
.ignore_errors()
.by_page()
.ignore_errors()
.into_iter()
.collect::<Vec<_>>();
assert_eq!(actual.len(), 1);
assert_eq!(actual, vec!["Test\nPDF\nDocument\n".to_string()]);
let bytes = std::fs::read(fixture_path("pages.pdf")).unwrap();
let loader = PdfFileLoader::from_bytes(bytes);
let actual = loader
.load()
.ignore_errors()
.by_page()
.ignore_errors()
.into_iter()
.collect::<Vec<_>>();
assert_eq!(actual.len(), 3);
assert_eq!(
actual,
vec![
"Page\n1\n".to_string(),
"Page\n2\n".to_string(),
"Page\n3\n".to_string(),
]
);
}
#[test]
fn test_pdf_loader_bytes_multi() {
let dummy = std::fs::read(fixture_path("dummy.pdf")).unwrap();
let pages = std::fs::read(fixture_path("pages.pdf")).unwrap();
let loader = PdfFileLoader::from_bytes_multi(vec![dummy, pages]);
let actual = loader
.load()
.ignore_errors()
.by_page()
.ignore_errors()
.into_iter()
.collect::<Vec<_>>();
assert_eq!(
actual,
vec![
"Test\nPDF\nDocument\n".to_string(),
"Page\n1\n".to_string(),
"Page\n2\n".to_string(),
"Page\n3\n".to_string(),
]
);
}
}