ppt_rs/parts/
coreprops.rs1use super::base::{ContentType, Part, PartType};
6use crate::exc::PptxError;
7use crate::oxml::XmlParser;
8
9fn current_timestamp() -> String {
12 let duration = std::time::SystemTime::now()
14 .duration_since(std::time::UNIX_EPOCH)
15 .unwrap_or_else(|_| std::time::Duration::from_secs(0));
16
17 let secs = duration.as_secs();
18
19 let days = secs / 86400;
22 let seconds = secs % 86400;
23
24 let year = 1970 + days / 365;
26 let remaining_days = days % 365;
27 let month = 1 + remaining_days / 30; let day = 1 + remaining_days % 30;
29
30 let hours = seconds / 3600;
31 let minutes = (seconds % 3600) / 60;
32 let secs = seconds % 60;
33
34 format!(
35 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
36 year, month, day, hours, minutes, secs
37 )
38}
39
40#[derive(Debug, Clone)]
42pub struct CorePropertiesPart {
43 path: String,
44 pub title: Option<String>,
45 pub subject: Option<String>,
46 pub creator: Option<String>,
47 pub keywords: Option<String>,
48 pub description: Option<String>,
49 pub last_modified_by: Option<String>,
50 pub revision: Option<u32>,
51 pub created: Option<String>,
52 pub modified: Option<String>,
53}
54
55impl CorePropertiesPart {
56 pub fn new() -> Self {
58 let now = current_timestamp();
59
60 CorePropertiesPart {
61 path: "docProps/core.xml".to_string(),
62 title: None,
63 subject: None,
64 creator: Some("pptx-rs".to_string()),
65 keywords: None,
66 description: None,
67 last_modified_by: Some("pptx-rs".to_string()),
68 revision: Some(1),
69 created: Some(now.clone()),
70 modified: Some(now),
71 }
72 }
73
74 pub fn set_title(&mut self, title: &str) -> &mut Self {
76 self.title = Some(title.to_string());
77 self
78 }
79
80 pub fn set_subject(&mut self, subject: &str) -> &mut Self {
82 self.subject = Some(subject.to_string());
83 self
84 }
85
86 pub fn set_creator(&mut self, creator: &str) -> &mut Self {
88 self.creator = Some(creator.to_string());
89 self
90 }
91
92 pub fn set_keywords(&mut self, keywords: &str) -> &mut Self {
94 self.keywords = Some(keywords.to_string());
95 self
96 }
97
98 pub fn set_description(&mut self, description: &str) -> &mut Self {
100 self.description = Some(description.to_string());
101 self
102 }
103
104 pub fn touch(&mut self) {
106 self.modified = Some(current_timestamp());
107 if let Some(ref mut rev) = self.revision {
108 *rev += 1;
109 }
110 }
111
112 fn escape_xml(s: &str) -> String {
113 s.replace('&', "&")
114 .replace('<', "<")
115 .replace('>', ">")
116 .replace('"', """)
117 .replace('\'', "'")
118 }
119}
120
121impl Default for CorePropertiesPart {
122 fn default() -> Self {
123 Self::new()
124 }
125}
126
127impl Part for CorePropertiesPart {
128 fn path(&self) -> &str {
129 &self.path
130 }
131
132 fn part_type(&self) -> PartType {
133 PartType::CoreProperties
134 }
135
136 fn content_type(&self) -> ContentType {
137 ContentType::CoreProperties
138 }
139
140 fn to_xml(&self) -> Result<String, PptxError> {
141 let mut elements = Vec::new();
142
143 if let Some(ref title) = self.title {
144 elements.push(format!("<dc:title>{}</dc:title>", Self::escape_xml(title)));
145 }
146 if let Some(ref subject) = self.subject {
147 elements.push(format!(
148 "<dc:subject>{}</dc:subject>",
149 Self::escape_xml(subject)
150 ));
151 }
152 if let Some(ref creator) = self.creator {
153 elements.push(format!(
154 "<dc:creator>{}</dc:creator>",
155 Self::escape_xml(creator)
156 ));
157 }
158 if let Some(ref keywords) = self.keywords {
159 elements.push(format!(
160 "<cp:keywords>{}</cp:keywords>",
161 Self::escape_xml(keywords)
162 ));
163 }
164 if let Some(ref description) = self.description {
165 elements.push(format!(
166 "<dc:description>{}</dc:description>",
167 Self::escape_xml(description)
168 ));
169 }
170 if let Some(ref last_modified_by) = self.last_modified_by {
171 elements.push(format!(
172 "<cp:lastModifiedBy>{}</cp:lastModifiedBy>",
173 Self::escape_xml(last_modified_by)
174 ));
175 }
176 if let Some(revision) = self.revision {
177 elements.push(format!("<cp:revision>{}</cp:revision>", revision));
178 }
179 if let Some(ref created) = self.created {
180 elements.push(format!(
181 r#"<dcterms:created xsi:type="dcterms:W3CDTF">{}</dcterms:created>"#,
182 created
183 ));
184 }
185 if let Some(ref modified) = self.modified {
186 elements.push(format!(
187 r#"<dcterms:modified xsi:type="dcterms:W3CDTF">{}</dcterms:modified>"#,
188 modified
189 ));
190 }
191
192 let xml = format!(
193 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
194<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
195{}
196</cp:coreProperties>"#,
197 elements.join("\n")
198 );
199
200 Ok(xml)
201 }
202
203 fn from_xml(xml: &str) -> Result<Self, PptxError> {
204 let root = XmlParser::parse_str(xml)?;
205 let mut part = CorePropertiesPart::new();
206
207 part.title = root
208 .find_descendant("title")
209 .map(|e| e.text_content())
210 .filter(|s| !s.is_empty());
211 part.subject = root
212 .find_descendant("subject")
213 .map(|e| e.text_content())
214 .filter(|s| !s.is_empty());
215 part.creator = root
216 .find_descendant("creator")
217 .map(|e| e.text_content())
218 .filter(|s| !s.is_empty());
219 part.keywords = root
220 .find_descendant("keywords")
221 .map(|e| e.text_content())
222 .filter(|s| !s.is_empty());
223 part.description = root
224 .find_descendant("description")
225 .map(|e| e.text_content())
226 .filter(|s| !s.is_empty());
227 part.last_modified_by = root
228 .find_descendant("lastModifiedBy")
229 .map(|e| e.text_content())
230 .filter(|s| !s.is_empty());
231 part.revision = root
232 .find_descendant("revision")
233 .and_then(|e| e.text_content().parse().ok());
234 part.created = root
235 .find_descendant("created")
236 .map(|e| e.text_content())
237 .filter(|s| !s.is_empty());
238 part.modified = root
239 .find_descendant("modified")
240 .map(|e| e.text_content())
241 .filter(|s| !s.is_empty());
242
243 Ok(part)
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn test_core_props_new() {
253 let part = CorePropertiesPart::new();
254 assert_eq!(part.path(), "docProps/core.xml");
255 assert!(part.creator.is_some());
256 assert!(part.created.is_some());
257 }
258
259 #[test]
260 fn test_core_props_set_title() {
261 let mut part = CorePropertiesPart::new();
262 part.set_title("My Presentation");
263
264 assert_eq!(part.title, Some("My Presentation".to_string()));
265 }
266
267 #[test]
268 fn test_core_props_to_xml() {
269 let mut part = CorePropertiesPart::new();
270 part.set_title("Test Title");
271 part.set_creator("Test Author");
272
273 let xml = part.to_xml().unwrap();
274 assert!(xml.contains("dc:title"));
275 assert!(xml.contains("Test Title"));
276 assert!(xml.contains("dc:creator"));
277 }
278
279 #[test]
280 fn test_core_props_from_xml() {
281 let xml = r#"<?xml version="1.0"?>
282 <cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
283 xmlns:dc="http://purl.org/dc/elements/1.1/"
284 xmlns:dcterms="http://purl.org/dc/terms/">
285 <dc:title>Parsed Title</dc:title>
286 <dc:creator>Parsed Author</dc:creator>
287 <cp:revision>5</cp:revision>
288 </cp:coreProperties>"#;
289
290 let part = CorePropertiesPart::from_xml(xml).unwrap();
291 assert_eq!(part.title, Some("Parsed Title".to_string()));
292 assert_eq!(part.creator, Some("Parsed Author".to_string()));
293 assert_eq!(part.revision, Some(5));
294 }
295
296 #[test]
297 fn test_core_props_touch() {
298 let mut part = CorePropertiesPart::new();
299 let original_rev = part.revision;
300
301 part.touch();
302
303 assert_eq!(part.revision, Some(original_rev.unwrap() + 1));
304 }
305}