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;