Skip to main content

edgeparse_core/pdf/
loader.rs

1//! PDF document loading via lopdf.
2
3use std::path::Path;
4
5use lopdf::Document;
6
7use crate::EdgePdfError;
8
9/// Raw loaded PDF document with page data.
10pub struct RawPdfDocument {
11    /// The lopdf Document handle
12    pub document: Document,
13    /// Number of pages
14    pub num_pages: u32,
15    /// Document metadata
16    pub metadata: PdfMetadata,
17}
18
19impl std::fmt::Debug for RawPdfDocument {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        f.debug_struct("RawPdfDocument")
22            .field("num_pages", &self.num_pages)
23            .field("metadata", &self.metadata)
24            .finish_non_exhaustive()
25    }
26}
27
28/// Extracted PDF metadata.
29#[derive(Debug, Clone, Default)]
30pub struct PdfMetadata {
31    /// Author
32    pub author: Option<String>,
33    /// Title
34    pub title: Option<String>,
35    /// Creation date
36    pub creation_date: Option<String>,
37    /// Modification date
38    pub modification_date: Option<String>,
39    /// PDF producer application
40    pub producer: Option<String>,
41    /// Creator application
42    pub creator: Option<String>,
43}
44
45/// Load a PDF file and extract basic structure.
46///
47/// # Arguments
48/// * `path` - Path to the PDF file
49/// * `password` - Optional decryption password
50///
51/// # Errors
52/// Returns `EdgePdfError::LoadError` if the file cannot be read or parsed.
53pub fn load_pdf(path: &Path, _password: Option<&str>) -> Result<RawPdfDocument, EdgePdfError> {
54    if !path.exists() {
55        return Err(EdgePdfError::LoadError(format!(
56            "File not found: {}",
57            path.display()
58        )));
59    }
60
61    let document = Document::load(path).map_err(|e| {
62        EdgePdfError::LoadError(format!("Failed to load PDF {}: {}", path.display(), e))
63    })?;
64
65    let pages = document.get_pages();
66    let num_pages = pages.len() as u32;
67
68    let metadata = extract_metadata(&document);
69
70    Ok(RawPdfDocument {
71        document,
72        num_pages,
73        metadata,
74    })
75}
76
77/// Extract metadata from the PDF document info dictionary.
78fn extract_metadata(doc: &Document) -> PdfMetadata {
79    let mut metadata = PdfMetadata::default();
80
81    if let Ok(info_ref) = doc.trailer.get(b"Info") {
82        if let Ok(info_ref) = info_ref.as_reference() {
83            if let Ok(info) = doc.get_object(info_ref) {
84                if let Ok(dict) = info.as_dict() {
85                    metadata.author = extract_string_field(dict, b"Author");
86                    metadata.title = extract_string_field(dict, b"Title");
87                    metadata.creation_date = extract_string_field(dict, b"CreationDate");
88                    metadata.modification_date = extract_string_field(dict, b"ModDate");
89                    metadata.producer = extract_string_field(dict, b"Producer");
90                    metadata.creator = extract_string_field(dict, b"Creator");
91                }
92            }
93        }
94    }
95
96    metadata
97}
98
99/// Load a PDF from an in-memory byte slice.
100///
101/// Uses lopdf's `Document::load_mem()` which parses PDF from `&[u8]`.
102/// This is the WASM-compatible loader — no filesystem access required.
103///
104/// # Arguments
105/// * `data` — raw PDF bytes
106/// * `_password` — optional decryption password (not yet implemented)
107///
108/// # Errors
109/// Returns `EdgePdfError::LoadError` if the bytes cannot be parsed as PDF.
110pub fn load_pdf_from_bytes(
111    data: &[u8],
112    _password: Option<&str>,
113) -> Result<RawPdfDocument, EdgePdfError> {
114    if data.is_empty() {
115        return Err(EdgePdfError::LoadError("Empty PDF data".to_string()));
116    }
117
118    let document = Document::load_mem(data)
119        .map_err(|e| EdgePdfError::LoadError(format!("Failed to parse PDF from bytes: {e}")))?;
120
121    let pages = document.get_pages();
122    let num_pages = pages.len() as u32;
123    let metadata = extract_metadata(&document);
124
125    Ok(RawPdfDocument {
126        document,
127        num_pages,
128        metadata,
129    })
130}
131
132/// Extract a string field from a PDF dictionary.
133fn extract_string_field(dict: &lopdf::Dictionary, key: &[u8]) -> Option<String> {
134    dict.get(key).ok().and_then(|obj| match obj {
135        lopdf::Object::String(bytes, _) => String::from_utf8(bytes.clone()).ok(),
136        _ => None,
137    })
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn test_load_nonexistent_file() {
146        let result = load_pdf(Path::new("/nonexistent/file.pdf"), None);
147        assert!(result.is_err());
148        match result.unwrap_err() {
149            EdgePdfError::LoadError(msg) => assert!(msg.contains("File not found")),
150            other => panic!("Unexpected error: {:?}", other),
151        }
152    }
153}