edgeparse_core/pdf/
metadata_writer.rs1use lopdf::{Document, Object};
5
6#[derive(Debug, Clone, Default)]
8pub struct PdfMetadata {
9 pub title: Option<String>,
11 pub author: Option<String>,
13 pub subject: Option<String>,
15 pub keywords: Option<String>,
17 pub creator: Option<String>,
19 pub producer: Option<String>,
21}
22
23impl PdfMetadata {
24 pub fn with_title(title: &str) -> Self {
26 Self {
27 title: Some(title.to_string()),
28 ..Default::default()
29 }
30 }
31
32 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
43pub fn write_metadata(doc: &mut Document, metadata: &PdfMetadata) {
45 if !metadata.has_any() {
46 return;
47 }
48
49 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
74pub 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 if let Ok(Object::Reference(r)) = doc.trailer.get(b"Info") {
106 return *r;
107 }
108
109 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 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}