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