Skip to main content

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}