1#[derive(Debug, Clone, Default)]
7pub struct PdfMetadata {
8 pub title: Option<String>,
10 pub author: Option<String>,
12 pub subject: Option<String>,
14 pub keywords: Option<String>,
16 pub creator: Option<String>,
18 pub producer: Option<String>,
20}
21
22impl PdfMetadata {
23 #[must_use]
25 pub fn new() -> Self {
26 Self::default()
27 }
28
29 #[must_use]
31 pub fn title(mut self, title: impl Into<String>) -> Self {
32 self.title = Some(title.into());
33 self
34 }
35
36 #[must_use]
38 pub fn author(mut self, author: impl Into<String>) -> Self {
39 self.author = Some(author.into());
40 self
41 }
42
43 #[must_use]
45 pub fn subject(mut self, subject: impl Into<String>) -> Self {
46 self.subject = Some(subject.into());
47 self
48 }
49
50 #[must_use]
52 pub fn keywords(mut self, keywords: impl Into<String>) -> Self {
53 self.keywords = Some(keywords.into());
54 self
55 }
56
57 #[must_use]
59 pub fn to_xmp(&self) -> String {
60 let title = self.title.as_deref().unwrap_or("");
61 let author = self.author.as_deref().unwrap_or("");
62 let subject = self.subject.as_deref().unwrap_or("");
63 let keywords = self.keywords.as_deref().unwrap_or("");
64 format!(
65 r#"<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
66<x:xmpmeta xmlns:x="adobe:ns:meta/">
67 <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
68 <rdf:Description rdf:about=""
69 xmlns:dc="http://purl.org/dc/elements/1.1/">
70 <dc:title><rdf:Alt><rdf:li xml:lang="x-default">{title}</rdf:li></rdf:Alt></dc:title>
71 <dc:creator><rdf:Seq><rdf:li>{author}</rdf:li></rdf:Seq></dc:creator>
72 <dc:description><rdf:Alt><rdf:li xml:lang="x-default">{subject}</rdf:li></rdf:Alt></dc:description>
73 <dc:subject><rdf:Bag><rdf:li>{keywords}</rdf:li></rdf:Bag></dc:subject>
74 </rdf:Description>
75 </rdf:RDF>
76</x:xmpmeta>
77<?xpacket end="w"?>"#
78 )
79 }
80}
81
82#[derive(Debug, Clone)]
84pub struct PdfBookmark {
85 pub title: String,
87 pub page: usize,
89 pub children: Vec<PdfBookmark>,
91}
92
93impl PdfBookmark {
94 #[must_use]
96 pub fn new(title: impl Into<String>, page: usize) -> Self {
97 Self {
98 title: title.into(),
99 page,
100 children: Vec::new(),
101 }
102 }
103
104 #[must_use]
106 pub fn child(mut self, child: PdfBookmark) -> Self {
107 self.children.push(child);
108 self
109 }
110}
111
112#[cfg(test)]
113#[allow(clippy::uninlined_format_args, clippy::float_cmp)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn default_metadata() {
119 let meta = PdfMetadata::default();
120 assert!(meta.title.is_none());
121 assert!(meta.author.is_none());
122 assert!(meta.subject.is_none());
123 assert!(meta.keywords.is_none());
124 assert!(meta.creator.is_none());
125 assert!(meta.producer.is_none());
126 }
127
128 #[test]
129 fn new_is_default() {
130 let meta = PdfMetadata::new();
131 assert!(meta.title.is_none());
132 }
133
134 #[test]
135 fn title_builder() {
136 let meta = PdfMetadata::default().title("My Doc");
137 assert_eq!(meta.title.as_deref(), Some("My Doc"));
138 }
139
140 #[test]
141 fn author_builder() {
142 let meta = PdfMetadata::default().author("John");
143 assert_eq!(meta.author.as_deref(), Some("John"));
144 }
145
146 #[test]
147 fn subject_builder() {
148 let meta = PdfMetadata::default().subject("Test");
149 assert_eq!(meta.subject.as_deref(), Some("Test"));
150 }
151
152 #[test]
153 fn keywords_builder() {
154 let meta = PdfMetadata::default().keywords("a,b,c");
155 assert_eq!(meta.keywords.as_deref(), Some("a,b,c"));
156 }
157
158 #[test]
159 fn to_xmp_includes_fields() {
160 let meta = PdfMetadata::default()
161 .title("Title")
162 .author("Author")
163 .subject("Subject")
164 .keywords("kw");
165 let xmp = meta.to_xmp();
166 assert!(xmp.contains("Title"));
167 assert!(xmp.contains("Author"));
168 assert!(xmp.contains("Subject"));
169 assert!(xmp.contains("kw"));
170 }
171
172 #[test]
173 fn to_xmp_empty_fields() {
174 let meta = PdfMetadata::default();
175 let xmp = meta.to_xmp();
176 assert!(xmp.contains("xmpmeta"));
177 }
178
179 #[test]
180 fn debug_format() {
181 let meta = PdfMetadata::default();
182 let dbg = format!("{:?}", meta);
183 assert!(dbg.contains("PdfMetadata"));
184 }
185
186 #[test]
187 fn clone_preserves() {
188 let meta = PdfMetadata::default().title("Test");
189 let cloned = meta.clone();
190 assert_eq!(meta.title, cloned.title);
191 }
192
193 #[test]
194 fn bookmark_new() {
195 let bm = PdfBookmark::new("Chapter 1", 1);
196 assert_eq!(bm.title, "Chapter 1");
197 assert_eq!(bm.page, 1);
198 assert!(bm.children.is_empty());
199 }
200
201 #[test]
202 fn bookmark_with_child() {
203 let bm = PdfBookmark::new("Root", 1).child(PdfBookmark::new("Child", 2));
204 assert_eq!(bm.children.len(), 1);
205 assert_eq!(bm.children[0].title, "Child");
206 }
207
208 #[test]
209 fn bookmark_nested_children() {
210 let bm = PdfBookmark::new("Root", 1)
211 .child(PdfBookmark::new("L1", 2).child(PdfBookmark::new("L2", 3)));
212 assert_eq!(bm.children.len(), 1);
213 assert_eq!(bm.children[0].children.len(), 1);
214 }
215
216 #[test]
217 fn bookmark_debug() {
218 let bm = PdfBookmark::new("Test", 1);
219 let dbg = format!("{:?}", bm);
220 assert!(dbg.contains("PdfBookmark"));
221 }
222}