Skip to main content

rig_core/loaders/
pdf.rs

1//! Lazy PDF loading and text extraction from paths or bytes.
2//!
3//! ```no_run
4//! use rig_core::loaders::PdfFileLoader;
5//!
6//! let documents = PdfFileLoader::with_glob("documents/*.pdf")?
7//!     .read().into_iter().collect::<Result<Vec<_>, _>>()?;
8//! # let _ = documents;
9//! # Ok::<(), rig_core::loaders::pdf::PdfLoaderError>(())
10//! ```
11
12use std::path::PathBuf;
13
14use lopdf::{Document, Error as LopdfError};
15use thiserror::Error;
16
17use super::file::FileLoaderError;
18
19#[derive(Error, Debug)]
20pub enum PdfLoaderError {
21    #[error("{0}")]
22    FileLoaderError(#[from] FileLoaderError),
23
24    #[error("UTF-8 conversion error: {0}")]
25    FromUtf8Error(#[from] std::string::FromUtf8Error),
26
27    #[error("IO error: {0}")]
28    PdfError(#[from] LopdfError),
29}
30
31loadable_trait!(Loadable, PdfLoaderError, Document, load, load_with_path);
32
33impl Loadable for PathBuf {
34    fn load(self) -> Result<Document, PdfLoaderError> {
35        Document::load(self).map_err(PdfLoaderError::PdfError)
36    }
37    fn load_with_path(self) -> Result<(PathBuf, Document), PdfLoaderError> {
38        let contents = Document::load(&self);
39        Ok((self, contents?))
40    }
41}
42
43impl Loadable for Vec<u8> {
44    fn load(self) -> Result<Document, PdfLoaderError> {
45        Document::load_mem(&self).map_err(PdfLoaderError::PdfError)
46    }
47
48    fn load_with_path(self) -> Result<(PathBuf, Document), PdfLoaderError> {
49        let doc = Document::load_mem(&self).map_err(PdfLoaderError::PdfError)?;
50        Ok((PathBuf::from("<memory>"), doc))
51    }
52}
53
54/// Iterator pipeline for loading PDF documents and extracting text synchronously.
55/// Loading and extraction errors are yielded per item unless filtered.
56pub struct PdfFileLoader<'a, T> {
57    iterator: Box<dyn Iterator<Item = T> + 'a>,
58}
59
60#[allow(private_bounds)] // `Loadable` deliberately seals which states expose these methods
61impl<'a, T: Loadable + 'a> PdfFileLoader<'a, T> {
62    /// Parses each input as a PDF during iteration, yielding document-loading errors.
63    ///
64    /// ```no_run
65    /// # use rig_core::loaders::PdfFileLoader;
66    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
67    /// let content = PdfFileLoader::with_glob("tests/data/*.pdf")?.load().into_iter();
68    /// for result in content {
69    ///     match result {
70    ///         Ok(doc) => println!("{doc:?}"),
71    ///         Err(e) => eprintln!("Error reading pdf: {e}"),
72    ///     }
73    /// }
74    /// # Ok(())
75    /// # }
76    /// ```
77    pub fn load(self) -> PdfFileLoader<'a, Result<Document, PdfLoaderError>> {
78        PdfFileLoader {
79            iterator: Box::new(self.iterator.map(Loadable::load)),
80        }
81    }
82
83    /// Parses each PDF and pairs it with its path, yielding loading errors.
84    /// In-memory inputs use the path `<memory>`.
85    ///
86    /// ```no_run
87    /// # use rig_core::loaders::PdfFileLoader;
88    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
89    /// let content = PdfFileLoader::with_glob("tests/data/*.pdf")?.load_with_path().into_iter();
90    /// for result in content {
91    ///     match result {
92    ///         Ok((path, doc)) => println!("{path:?} {doc:?}"),
93    ///         Err(e) => eprintln!("Error reading pdf: {e}"),
94    ///     }
95    /// }
96    /// # Ok(())
97    /// # }
98    /// ```
99    pub fn load_with_path(self) -> PdfFileLoader<'a, Result<(PathBuf, Document), PdfLoaderError>> {
100        PdfFileLoader {
101            iterator: Box::new(self.iterator.map(Loadable::load_with_path)),
102        }
103    }
104}
105
106/// Extract each page's text, paired with its zero-based page number.
107fn page_texts(doc: &Document) -> Vec<(usize, Result<String, PdfLoaderError>)> {
108    doc.page_iter()
109        .enumerate()
110        .map(|(page_no, _)| {
111            (
112                page_no,
113                doc.extract_text(&[page_no as u32 + 1])
114                    .map_err(PdfLoaderError::PdfError),
115            )
116        })
117        .collect()
118}
119
120/// Concatenate the text of every page, failing on the first unreadable page.
121fn all_text(doc: &Document) -> Result<String, PdfLoaderError> {
122    page_texts(doc).into_iter().map(|(_, text)| text).collect()
123}
124
125#[allow(private_bounds)] // `Loadable` deliberately seals which states expose these methods
126impl<'a, T: Loadable + 'a> PdfFileLoader<'a, T> {
127    /// Loads each PDF and concatenates its page text without separators.
128    /// Yields a loading error or the first page-extraction error for each document.
129    ///
130    /// ```no_run
131    /// # use rig_core::loaders::PdfFileLoader;
132    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
133    /// let content = PdfFileLoader::with_glob("tests/data/*.pdf")?.read().into_iter();
134    /// for result in content {
135    ///     match result {
136    ///         Ok(content) => println!("{content}"),
137    ///         Err(e) => eprintln!("Error reading pdf: {e}"),
138    ///     }
139    /// }
140    /// # Ok(())
141    /// # }
142    /// ```
143    pub fn read(self) -> PdfFileLoader<'a, Result<String, PdfLoaderError>> {
144        PdfFileLoader {
145            iterator: Box::new(self.iterator.map(|res| all_text(&res.load()?))),
146        }
147    }
148
149    /// Loads each PDF and pairs its path with concatenated page text.
150    /// Yields loading or extraction errors; in-memory inputs use `<memory>`.
151    ///
152    /// ```no_run
153    /// # use rig_core::loaders::PdfFileLoader;
154    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
155    /// let content = PdfFileLoader::with_glob("tests/data/*.pdf")?.read_with_path().into_iter();
156    /// for result in content {
157    ///     match result {
158    ///         Ok((path, content)) => println!("{path:?} {content}"),
159    ///         Err(e) => eprintln!("Error reading pdf: {e}"),
160    ///     }
161    /// }
162    /// # Ok(())
163    /// # }
164    /// ```
165    pub fn read_with_path(self) -> PdfFileLoader<'a, Result<(PathBuf, String), PdfLoaderError>> {
166        PdfFileLoader {
167            iterator: Box::new(self.iterator.map(|res| {
168                let (path, doc) = res.load_with_path()?;
169                let content = all_text(&doc)?;
170                Ok((path, content))
171            })),
172        }
173    }
174}
175
176impl<'a> PdfFileLoader<'a, Document> {
177    /// Yields page text in document order, flattening all documents into one
178    /// sequence of per-page extraction results.
179    ///
180    /// ```no_run
181    /// # use rig_core::loaders::PdfFileLoader;
182    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
183    /// let content = PdfFileLoader::with_glob("tests/data/*.pdf")?
184    ///     .load()
185    ///     .ignore_errors()
186    ///     .by_page()
187    ///     .into_iter();
188    /// for result in content {
189    ///     match result {
190    ///         Ok(page) => println!("{page}"),
191    ///         Err(e) => eprintln!("Error reading pdf: {e}"),
192    ///     }
193    /// }
194    /// # Ok(())
195    /// # }
196    /// ```
197    pub fn by_page(self) -> PdfFileLoader<'a, Result<String, PdfLoaderError>> {
198        PdfFileLoader {
199            iterator: Box::new(
200                self.iterator
201                    .flat_map(|doc| page_texts(&doc).into_iter().map(|(_, text)| text)),
202            ),
203        }
204    }
205}
206
207type ByPage = (PathBuf, Vec<(usize, Result<String, PdfLoaderError>)>);
208impl<'a> PdfFileLoader<'a, (PathBuf, Document)> {
209    /// Pairs each source path with zero-based page numbers and extraction results.
210    ///
211    /// ```no_run
212    /// # use rig_core::loaders::PdfFileLoader;
213    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
214    /// let content = PdfFileLoader::with_glob("tests/data/*.pdf")?
215    ///     .load_with_path()
216    ///     .ignore_errors()
217    ///     .by_page()
218    ///     .into_iter();
219    ///
220    /// for (path, pages) in content {
221    ///     println!("{}", path.display());
222    ///     for (pageno, result) in pages {
223    ///         match result {
224    ///             Ok(content) => println!("Page {pageno}: {content}"),
225    ///             Err(e) => eprintln!("Error reading page: {e}"),
226    ///         }
227    ///     }
228    /// }
229    /// # Ok(())
230    /// # }
231    /// ```
232    pub fn by_page(self) -> PdfFileLoader<'a, ByPage> {
233        PdfFileLoader {
234            iterator: Box::new(self.iterator.map(|(path, doc)| (path, page_texts(&doc)))),
235        }
236    }
237}
238
239impl<'a> PdfFileLoader<'a, ByPage> {
240    /// Drops failed pages while retaining each document's path and original
241    /// zero-based page numbers. Documents with no successful pages remain.
242    ///
243    /// ```no_run
244    /// # use rig_core::loaders::PdfFileLoader;
245    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
246    /// let content = PdfFileLoader::with_glob("tests/data/*.pdf")?
247    ///     .load_with_path()
248    ///     .ignore_errors()
249    ///     .by_page()
250    ///     .ignore_errors();
251    /// for (_path, pages) in content {
252    ///     println!("{}", pages.len())
253    /// }
254    /// # Ok(())
255    /// # }
256    /// ```
257    pub fn ignore_errors(self) -> PdfFileLoader<'a, (PathBuf, Vec<(usize, String)>)> {
258        PdfFileLoader {
259            iterator: Box::new(self.iterator.map(|(path, pages)| {
260                let pages = pages
261                    .into_iter()
262                    .filter_map(|(page_no, res)| res.ok().map(|content| (page_no, content)))
263                    .collect::<Vec<_>>();
264                (path, pages)
265            })),
266        }
267    }
268}
269
270loader_scaffold!(PdfFileLoader, PdfLoaderError, dir: all_entries);
271loader_from_bytes!(PdfFileLoader);
272
273#[cfg(test)]
274mod tests;