rig_core/loaders/pdf.rs
1use std::path::PathBuf;
2
3use lopdf::{Document, Error as LopdfError};
4use thiserror::Error;
5
6use super::file::FileLoaderError;
7
8#[derive(Error, Debug)]
9pub enum PdfLoaderError {
10 #[error("{0}")]
11 FileLoaderError(#[from] FileLoaderError),
12
13 #[error("UTF-8 conversion error: {0}")]
14 FromUtf8Error(#[from] std::string::FromUtf8Error),
15
16 #[error("IO error: {0}")]
17 PdfError(#[from] LopdfError),
18}
19
20// ================================================================
21// Implementing Loadable trait for loading pdfs
22// ================================================================
23
24loadable_trait!(Loadable, PdfLoaderError, Document, load, load_with_path);
25
26impl Loadable for PathBuf {
27 fn load(self) -> Result<Document, PdfLoaderError> {
28 Document::load(self).map_err(PdfLoaderError::PdfError)
29 }
30 fn load_with_path(self) -> Result<(PathBuf, Document), PdfLoaderError> {
31 let contents = Document::load(&self);
32 Ok((self, contents?))
33 }
34}
35
36impl Loadable for Vec<u8> {
37 fn load(self) -> Result<Document, PdfLoaderError> {
38 Document::load_mem(&self).map_err(PdfLoaderError::PdfError)
39 }
40
41 fn load_with_path(self) -> Result<(PathBuf, Document), PdfLoaderError> {
42 let doc = Document::load_mem(&self).map_err(PdfLoaderError::PdfError)?;
43 Ok((PathBuf::from("<memory>"), doc))
44 }
45}
46
47// ================================================================
48// PdfFileLoader definitions and implementations
49// ================================================================
50
51/// [PdfFileLoader] is a utility for loading pdf files from the filesystem using glob patterns or
52/// directory paths. It provides methods to read file contents and handle errors gracefully.
53///
54/// # Errors
55///
56/// This module defines a custom error type [PdfLoaderError] which can represent various errors
57/// that might occur during file loading operations, such as any [FileLoaderError] alongside
58/// specific PDF-related errors.
59///
60/// # Example Usage
61///
62/// ```no_run
63/// use rig_core::loaders::PdfFileLoader;
64///
65/// fn main() -> Result<(), Box<dyn std::error::Error>> {
66/// // Create a FileLoader using a glob pattern
67/// let loader = PdfFileLoader::with_glob("tests/data/*.pdf")?;
68///
69/// // Load pdf file contents by page, ignoring any errors
70/// let contents: Vec<String> = loader
71/// .load()
72/// .ignore_errors()
73/// .by_page()
74/// .ignore_errors()
75/// .into_iter()
76/// .collect();
77///
78/// for content in contents {
79/// println!("{}", content);
80/// }
81///
82/// Ok(())
83/// }
84/// ```
85///
86/// [PdfFileLoader] uses strict typing between the iterator methods to ensure that transitions
87/// between different implementations of the loaders and it's methods are handled properly by
88/// the compiler.
89pub struct PdfFileLoader<'a, T> {
90 iterator: Box<dyn Iterator<Item = T> + 'a>,
91}
92
93#[allow(private_bounds)] // `Loadable` deliberately seals which states expose these methods
94impl<'a, T: Loadable + 'a> PdfFileLoader<'a, T> {
95 /// Loads the contents of the pdfs within the iterator returned by [PdfFileLoader::with_glob]
96 /// or [PdfFileLoader::with_dir]. Loaded PDF documents are raw PDF instances that can be
97 /// further processed (by page, etc).
98 ///
99 /// # Example
100 /// Load pdfs in directory "tests/data/*.pdf" and return the loaded documents
101 ///
102 /// ```no_run
103 /// # use rig_core::loaders::PdfFileLoader;
104 /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
105 /// let content = PdfFileLoader::with_glob("tests/data/*.pdf")?.load().into_iter();
106 /// for result in content {
107 /// match result {
108 /// Ok(doc) => println!("{:?}", doc),
109 /// Err(e) => eprintln!("Error reading pdf: {}", e),
110 /// }
111 /// }
112 /// # Ok(())
113 /// # }
114 /// ```
115 pub fn load(self) -> PdfFileLoader<'a, Result<Document, PdfLoaderError>> {
116 PdfFileLoader {
117 iterator: Box::new(self.iterator.map(|res| res.load())),
118 }
119 }
120
121 /// Loads the contents of the pdfs within the iterator returned by [PdfFileLoader::with_glob]
122 /// or [PdfFileLoader::with_dir]. Loaded PDF documents are raw PDF instances with their path
123 /// that can be further processed.
124 ///
125 /// # Example
126 /// Load pdfs in directory "tests/data/*.pdf" and return the loaded documents
127 ///
128 /// ```no_run
129 /// # use rig_core::loaders::PdfFileLoader;
130 /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
131 /// let content = PdfFileLoader::with_glob("tests/data/*.pdf")?.load_with_path().into_iter();
132 /// for result in content {
133 /// match result {
134 /// Ok((path, doc)) => println!("{:?} {:?}", path, doc),
135 /// Err(e) => eprintln!("Error reading pdf: {}", e),
136 /// }
137 /// }
138 /// # Ok(())
139 /// # }
140 /// ```
141 pub fn load_with_path(self) -> PdfFileLoader<'a, Result<(PathBuf, Document), PdfLoaderError>> {
142 PdfFileLoader {
143 iterator: Box::new(self.iterator.map(|res| res.load_with_path())),
144 }
145 }
146}
147
148/// Extract each page's text, paired with its zero-based page number.
149fn page_texts(doc: &Document) -> Vec<(usize, Result<String, PdfLoaderError>)> {
150 doc.page_iter()
151 .enumerate()
152 .map(|(page_no, _)| {
153 (
154 page_no,
155 doc.extract_text(&[page_no as u32 + 1])
156 .map_err(PdfLoaderError::PdfError),
157 )
158 })
159 .collect()
160}
161
162/// Concatenate the text of every page, failing on the first unreadable page.
163fn all_text(doc: &Document) -> Result<String, PdfLoaderError> {
164 page_texts(doc).into_iter().map(|(_, text)| text).collect()
165}
166
167#[allow(private_bounds)] // `Loadable` deliberately seals which states expose these methods
168impl<'a, T: Loadable + 'a> PdfFileLoader<'a, T> {
169 /// Directly reads the contents of the pdfs within the iterator returned by
170 /// [PdfFileLoader::with_glob] or [PdfFileLoader::with_dir].
171 ///
172 /// # Example
173 /// Read pdfs in directory "tests/data/*.pdf" and return the contents of the documents.
174 ///
175 /// ```no_run
176 /// # use rig_core::loaders::PdfFileLoader;
177 /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
178 /// let content = PdfFileLoader::with_glob("tests/data/*.pdf")?.read().into_iter();
179 /// for result in content {
180 /// match result {
181 /// Ok(content) => println!("{}", content),
182 /// Err(e) => eprintln!("Error reading pdf: {}", e),
183 /// }
184 /// }
185 /// # Ok(())
186 /// # }
187 /// ```
188 pub fn read(self) -> PdfFileLoader<'a, Result<String, PdfLoaderError>> {
189 PdfFileLoader {
190 iterator: Box::new(self.iterator.map(|res| all_text(&res.load()?))),
191 }
192 }
193
194 /// Directly reads the contents of the pdfs within the iterator returned by
195 /// [PdfFileLoader::with_glob] or [PdfFileLoader::with_dir] and returns the path along with
196 /// the content.
197 ///
198 /// # Example
199 /// Read pdfs in directory "tests/data/*.pdf" and return the content and paths of the documents.
200 ///
201 /// ```no_run
202 /// # use rig_core::loaders::PdfFileLoader;
203 /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
204 /// let content = PdfFileLoader::with_glob("tests/data/*.pdf")?.read_with_path().into_iter();
205 /// for result in content {
206 /// match result {
207 /// Ok((path, content)) => println!("{:?} {}", path, content),
208 /// Err(e) => eprintln!("Error reading pdf: {}", e),
209 /// }
210 /// }
211 /// # Ok(())
212 /// # }
213 /// ```
214 pub fn read_with_path(self) -> PdfFileLoader<'a, Result<(PathBuf, String), PdfLoaderError>> {
215 PdfFileLoader {
216 iterator: Box::new(self.iterator.map(|res| {
217 let (path, doc) = res.load_with_path()?;
218 let content = all_text(&doc)?;
219 Ok((path, content))
220 })),
221 }
222 }
223}
224
225impl<'a> PdfFileLoader<'a, Document> {
226 /// Chunks the pages of a loaded document by page, flattened as a single vector.
227 ///
228 /// # Example
229 /// Load pdfs in directory "tests/data/*.pdf" and chunk all document into it's pages.
230 ///
231 /// ```no_run
232 /// # use rig_core::loaders::PdfFileLoader;
233 /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
234 /// let content = PdfFileLoader::with_glob("tests/data/*.pdf")?
235 /// .load()
236 /// .ignore_errors()
237 /// .by_page()
238 /// .into_iter();
239 /// for result in content {
240 /// match result {
241 /// Ok(page) => println!("{}", page),
242 /// Err(e) => eprintln!("Error reading pdf: {}", e),
243 /// }
244 /// }
245 /// # Ok(())
246 /// # }
247 /// ```
248 pub fn by_page(self) -> PdfFileLoader<'a, Result<String, PdfLoaderError>> {
249 PdfFileLoader {
250 iterator: Box::new(
251 self.iterator
252 .flat_map(|doc| page_texts(&doc).into_iter().map(|(_, text)| text)),
253 ),
254 }
255 }
256}
257
258type ByPage = (PathBuf, Vec<(usize, Result<String, PdfLoaderError>)>);
259impl<'a> PdfFileLoader<'a, (PathBuf, Document)> {
260 /// Chunks the pages of a loaded document by page, processed as a vector of documents by path
261 /// which each document container an inner vector of pages by page number.
262 ///
263 /// # Example
264 /// Read pdfs in directory "tests/data/*.pdf" and chunk all documents by path by it's pages.
265 ///
266 /// ```no_run
267 /// # use rig_core::loaders::PdfFileLoader;
268 /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
269 /// let content = PdfFileLoader::with_glob("tests/data/*.pdf")?
270 /// .load_with_path()
271 /// .ignore_errors()
272 /// .by_page()
273 /// .into_iter();
274 ///
275 /// for (path, pages) in content {
276 /// println!("{}", path.display());
277 /// for (pageno, result) in pages {
278 /// match result {
279 /// Ok(content) => println!("Page {}: {}", pageno, content),
280 /// Err(e) => eprintln!("Error reading page: {}", e),
281 /// }
282 /// }
283 /// }
284 /// # Ok(())
285 /// # }
286 /// ```
287 pub fn by_page(self) -> PdfFileLoader<'a, ByPage> {
288 PdfFileLoader {
289 iterator: Box::new(self.iterator.map(|(path, doc)| (path, page_texts(&doc)))),
290 }
291 }
292}
293
294impl<'a> PdfFileLoader<'a, ByPage> {
295 /// Ignores errors in the iterator, returning only successful results. This can be used on any
296 /// [PdfFileLoader] state of iterator whose items are results.
297 ///
298 /// # Example
299 /// Read files in directory "tests/data/*.pdf" and ignore errors from unreadable files.
300 ///
301 /// ```no_run
302 /// # use rig_core::loaders::PdfFileLoader;
303 /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
304 /// let content = PdfFileLoader::with_glob("tests/data/*.pdf")?
305 /// .load_with_path()
306 /// .ignore_errors()
307 /// .by_page()
308 /// .ignore_errors();
309 /// for (_path, pages) in content {
310 /// println!("{}", pages.len())
311 /// }
312 /// # Ok(())
313 /// # }
314 /// ```
315 pub fn ignore_errors(self) -> PdfFileLoader<'a, (PathBuf, Vec<(usize, String)>)> {
316 PdfFileLoader {
317 iterator: Box::new(self.iterator.map(|(path, pages)| {
318 let pages = pages
319 .into_iter()
320 .filter_map(|(page_no, res)| res.ok().map(|content| (page_no, content)))
321 .collect::<Vec<_>>();
322 (path, pages)
323 })),
324 }
325 }
326}
327
328loader_scaffold!(PdfFileLoader, PdfLoaderError, dir: all_entries);
329loader_from_bytes!(PdfFileLoader);
330
331#[cfg(test)]
332mod tests {
333 use crate::loaders::test_fixtures::{fixture_glob, fixture_path};
334
335 use super::PdfFileLoader;
336
337 #[test]
338 fn test_pdf_loader() {
339 let glob = fixture_glob("*.pdf");
340 let loader = PdfFileLoader::with_glob(&glob).unwrap();
341 let actual = loader
342 .load_with_path()
343 .ignore_errors()
344 .by_page()
345 .ignore_errors()
346 .into_iter()
347 .collect::<Vec<_>>();
348
349 let mut actual = actual
350 .into_iter()
351 .map(|result| {
352 let (path, pages) = result;
353 pages.iter().for_each(|(page_no, content)| {
354 println!("{path:?} Page {page_no}: {content:?}");
355 });
356 (path, pages)
357 })
358 .collect::<Vec<_>>();
359
360 let mut expected = vec![
361 (
362 fixture_path("dummy.pdf"),
363 vec![(0, "Test\nPDF\nDocument\n".to_string())],
364 ),
365 (
366 fixture_path("file-id-verifiers.pdf"),
367 vec![
368 (0, "rig-file-id-page-one-verifier-3a91\n".to_string()),
369 (1, "rig-file-id-page-two-verifier-8c27\n".to_string()),
370 (2, "rig-file-id-page-three-verifier-f54e\n".to_string()),
371 ],
372 ),
373 (
374 fixture_path("pages.pdf"),
375 vec![
376 (0, "Page\n1\n".to_string()),
377 (1, "Page\n2\n".to_string()),
378 (2, "Page\n3\n".to_string()),
379 ],
380 ),
381 ];
382
383 actual.sort();
384 expected.sort();
385
386 assert!(!actual.is_empty());
387 assert!(expected == actual)
388 }
389
390 #[test]
391 fn test_pdf_loader_bytes() {
392 // this should never fail!
393 let bytes = std::fs::read(fixture_path("dummy.pdf")).unwrap();
394
395 let loader = PdfFileLoader::from_bytes(bytes);
396
397 let actual = loader
398 .load()
399 .ignore_errors()
400 .by_page()
401 .ignore_errors()
402 .into_iter()
403 .collect::<Vec<_>>();
404
405 assert_eq!(actual.len(), 1);
406 assert_eq!(actual, vec!["Test\nPDF\nDocument\n".to_string()]);
407
408 // this should never fail!
409 let bytes = std::fs::read(fixture_path("pages.pdf")).unwrap();
410
411 let loader = PdfFileLoader::from_bytes(bytes);
412
413 let actual = loader
414 .load()
415 .ignore_errors()
416 .by_page()
417 .ignore_errors()
418 .into_iter()
419 .collect::<Vec<_>>();
420
421 assert_eq!(actual.len(), 3);
422 assert_eq!(
423 actual,
424 vec![
425 "Page\n1\n".to_string(),
426 "Page\n2\n".to_string(),
427 "Page\n3\n".to_string(),
428 ]
429 );
430 }
431
432 #[test]
433 fn test_pdf_loader_bytes_multi() {
434 let dummy = std::fs::read(fixture_path("dummy.pdf")).unwrap();
435 let pages = std::fs::read(fixture_path("pages.pdf")).unwrap();
436
437 let loader = PdfFileLoader::from_bytes_multi(vec![dummy, pages]);
438
439 let actual = loader
440 .load()
441 .ignore_errors()
442 .by_page()
443 .ignore_errors()
444 .into_iter()
445 .collect::<Vec<_>>();
446
447 assert_eq!(
448 actual,
449 vec![
450 "Test\nPDF\nDocument\n".to_string(),
451 "Page\n1\n".to_string(),
452 "Page\n2\n".to_string(),
453 "Page\n3\n".to_string(),
454 ]
455 );
456 }
457}