Skip to main content

edgeparse_core/pdf/
metadata_writer.rs

1//! PDF metadata writer — update document metadata (title, author, etc.)
2//! in a lopdf Document before saving.
3
4use lopdf::{Document, Object};
5
6/// Metadata fields that can be written to a PDF.
7#[derive(Debug, Clone, Default)]
8pub struct PdfMetadata {
9    /// Document title.
10    pub title: Option<String>,
11    /// Document author.
12    pub author: Option<String>,
13    /// Document subject.
14    pub subject: Option<String>,
15    /// Keywords associated with the document.
16    pub keywords: Option<String>,
17    /// Application that created the document.
18    pub creator: Option<String>,
19    /// Application that produced the PDF.
20    pub producer: Option<String>,
21}
22
23impl PdfMetadata {
24    /// Create metadata with just a title.
25    pub fn with_title(title: &str) -> Self {
26        Self {
27            title: Some(title.to_string()),
28            ..Default::default()
29        }
30    }
31
32    /// Whether any field is set.
33    pub fn has_any(&self) -> bool {
34        self.title.is_some()
35            || self.author.is_some()
36            || self.subject.is_some()
37            || self.keywords.is_some()
38            || self.creator.is_some()
39            || self.producer.is_some()
40    }
41}
42
43/// Write metadata into a PDF document's /Info dictionary.
44pub fn write_metadata(doc: &mut Document, metadata: &PdfMetadata) {
45    if !metadata.has_any() {
46        return;
47    }
48
49    // Get or create /Info dictionary reference from trailer
50    let info_id = get_or_create_info_dict(doc);
51
52    if let Ok(Object::Dictionary(ref mut dict)) = doc.get_object_mut(info_id) {
53        if let Some(ref title) = metadata.title {
54            dict.set("Title", Object::string_literal(title.as_bytes()));
55        }
56        if let Some(ref author) = metadata.author {
57            dict.set("Author", Object::string_literal(author.as_bytes()));
58        }
59        if let Some(ref subject) = metadata.subject {
60            dict.set("Subject", Object::string_literal(subject.as_bytes()));
61        }
62        if let Some(ref keywords) = metadata.keywords {
63            dict.set("Keywords", Object::string_literal(keywords.as_bytes()));
64        }
65        if let Some(ref creator) = metadata.creator {
66            dict.set("Creator", Object::string_literal(creator.as_bytes()));
67        }
68        if let Some(ref producer) = metadata.producer {
69            dict.set("Producer", Object::string_literal(producer.as_bytes()));
70        }
71    }
72}
73
74/// Read metadata from a PDF document's /Info dictionary.
75pub fn read_metadata(doc: &Document) -> PdfMetadata {
76    let info_ref = match doc.trailer.get(b"Info") {
77        Ok(Object::Reference(r)) => *r,
78        _ => return PdfMetadata::default(),
79    };
80
81    let dict = match doc.get_object(info_ref).and_then(|o| o.as_dict()) {
82        Ok(d) => d,
83        Err(_) => return PdfMetadata::default(),
84    };
85
86    PdfMetadata {
87        title: get_string(dict, b"Title"),
88        author: get_string(dict, b"Author"),
89        subject: get_string(dict, b"Subject"),
90        keywords: get_string(dict, b"Keywords"),
91        creator: get_string(dict, b"Creator"),
92        producer: get_string(dict, b"Producer"),
93    }
94}
95
96fn get_string(dict: &lopdf::Dictionary, key: &[u8]) -> Option<String> {
97    dict.get(key).ok().and_then(|o| match o {
98        Object::String(s, _) => Some(String::from_utf8_lossy(s).to_string()),
99        _ => None,
100    })
101}
102
103fn get_or_create_info_dict(doc: &mut Document) -> lopdf::ObjectId {
104    // Check if /Info already exists in trailer
105    if let Ok(Object::Reference(r)) = doc.trailer.get(b"Info") {
106        return *r;
107    }
108
109    // Create a new /Info dictionary
110    let info_dict = lopdf::Dictionary::new();
111    let info_id = doc.add_object(Object::Dictionary(info_dict));
112    doc.trailer.set("Info", Object::Reference(info_id));
113    info_id
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use lopdf::dictionary;
120
121    fn make_empty_pdf() -> Document {
122        let mut doc = Document::with_version("1.7");
123        let pages_id = doc.new_object_id();
124        let pages_dict = dictionary! {
125            "Type" => "Pages",
126            "Kids" => vec![],
127            "Count" => 0,
128        };
129        doc.objects.insert(pages_id, Object::Dictionary(pages_dict));
130        let catalog = dictionary! {
131            "Type" => "Catalog",
132            "Pages" => Object::Reference(pages_id),
133        };
134        let catalog_id = doc.add_object(Object::Dictionary(catalog));
135        doc.trailer.set("Root", Object::Reference(catalog_id));
136        doc
137    }
138
139    #[test]
140    fn test_write_and_read_metadata() {
141        let mut doc = make_empty_pdf();
142        let meta = PdfMetadata {
143            title: Some("Test Title".to_string()),
144            author: Some("Author".to_string()),
145            subject: None,
146            keywords: Some("pdf, test".to_string()),
147            creator: None,
148            producer: Some("EdgeParse".to_string()),
149        };
150        write_metadata(&mut doc, &meta);
151        let read = read_metadata(&doc);
152        assert_eq!(read.title.as_deref(), Some("Test Title"));
153        assert_eq!(read.author.as_deref(), Some("Author"));
154        assert_eq!(read.keywords.as_deref(), Some("pdf, test"));
155        assert_eq!(read.producer.as_deref(), Some("EdgeParse"));
156        assert!(read.subject.is_none());
157    }
158
159    #[test]
160    fn test_empty_metadata_noop() {
161        let mut doc = make_empty_pdf();
162        let meta = PdfMetadata::default();
163        assert!(!meta.has_any());
164        write_metadata(&mut doc, &meta);
165        // No /Info should be created
166        assert!(doc.trailer.get(b"Info").is_err());
167    }
168
169    #[test]
170    fn test_with_title() {
171        let meta = PdfMetadata::with_title("Hello");
172        assert!(meta.has_any());
173        assert_eq!(meta.title.as_deref(), Some("Hello"));
174    }
175
176    #[test]
177    fn test_read_nonexistent_info() {
178        let doc = make_empty_pdf();
179        let meta = read_metadata(&doc);
180        assert!(!meta.has_any());
181    }
182}