1use crate::error::{PptError, Result};
4use crate::parts::presentation::PresentationPart;
5use crate::slide::{Slide, Slides};
6use std::io::{Read, Seek, Write};
7
8pub struct Presentation {
13 part: PresentationPart,
14}
15
16impl Presentation {
17 pub fn new() -> Result<Self> {
19 let part = PresentationPart::new()?;
20 Ok(Self { part })
21 }
22
23 pub fn open<R: Read + Seek>(reader: R) -> Result<Self> {
25 use crate::opc::package::Package;
26 use crate::opc::constants::RELATIONSHIP_TYPE;
27 use crate::opc::part::Part;
28
29 let package = Package::open(reader)?;
31
32 let pkg_rels = package.relationships();
34 if let Some(rel) = pkg_rels.iter().find(|(_, r)| r.rel_type == RELATIONSHIP_TYPE::OFFICE_DOCUMENT) {
35 let target = &rel.1.target;
36 let partname = if target.starts_with('/') {
37 crate::opc::packuri::PackURI::new(target)?
38 } else {
39 crate::opc::packuri::PackURI::new(&format!("/{}", target))?
40 };
41
42 if let Some(part) = package.get_part(&partname) {
43 let blob = Part::blob(part)?;
45 let xml = String::from_utf8(blob)
46 .map_err(|e| PptError::ValueError(format!("Invalid UTF-8: {}", e)))?;
47
48 let part = PresentationPart::from_xml(std::io::Cursor::new(xml.as_bytes()))?;
49 Ok(Self { part })
50 } else {
51 Self::new()
53 }
54 } else {
55 Self::new()
57 }
58 }
59
60 pub fn save<W: Write + Seek>(&self, writer: W) -> Result<()> {
62 use crate::opc::constants::RELATIONSHIP_TYPE;
63 use crate::opc::serialized::PackageWriter;
64 use crate::opc::relationships::Relationships;
65
66 let mut pkg_rels = Relationships::new();
68 pkg_rels.add(
69 "rId1".to_string(),
70 RELATIONSHIP_TYPE::OFFICE_DOCUMENT.to_string(),
71 "ppt/presentation.xml".to_string(),
72 false,
73 );
74
75 if let Ok(core_props) = self.core_properties() {
77 use crate::opc::part::Part;
78 let core_props_uri = Part::uri(&core_props);
79 pkg_rels.add(
80 "rId2".to_string(),
81 RELATIONSHIP_TYPE::CORE_PROPERTIES.to_string(),
82 core_props_uri.membername().to_string(),
83 false,
84 );
85 }
86
87 use crate::opc::part::Part;
89 let blob = Part::blob(&self.part)?;
90 let uri = Part::uri(&self.part).clone();
91 let content_type = Part::content_type(&self.part);
92 let relationships = self.part.relationships().clone();
93
94 struct OwnedPart {
96 content_type: String,
97 uri: crate::opc::packuri::PackURI,
98 blob: Vec<u8>,
99 relationships: Relationships,
100 }
101
102 impl crate::opc::part::Part for OwnedPart {
103 fn content_type(&self) -> &str {
104 &self.content_type
105 }
106 fn uri(&self) -> &crate::opc::packuri::PackURI {
107 &self.uri
108 }
109 fn relationships(&self) -> &Relationships {
110 &self.relationships
111 }
112 fn relationships_mut(&mut self) -> &mut Relationships {
113 &mut self.relationships
114 }
115 fn blob(&self) -> Result<Vec<u8>> {
116 Ok(self.blob.clone())
117 }
118 fn to_xml(&self) -> Result<String> {
119 String::from_utf8(self.blob.clone())
120 .map_err(|e| crate::error::PptError::ValueError(format!("Invalid UTF-8: {}", e)))
121 }
122 fn from_xml<R: std::io::Read>(_reader: R) -> Result<Self> {
123 Err(crate::error::PptError::NotImplemented("OwnedPart::from_xml".to_string()))
124 }
125 }
126
127 let mut parts: Vec<Box<dyn crate::opc::part::Part>> = vec![Box::new(OwnedPart {
128 content_type: content_type.to_string(),
129 uri,
130 blob,
131 relationships,
132 })];
133
134 if let Ok(core_props) = self.core_properties() {
136 use crate::opc::part::Part;
137 let core_blob = Part::blob(&core_props)?;
138 let core_uri = Part::uri(&core_props).clone();
139 let core_content_type = Part::content_type(&core_props);
140 parts.push(Box::new(OwnedPart {
141 content_type: core_content_type.to_string(),
142 uri: core_uri,
143 blob: core_blob,
144 relationships: Relationships::new(),
145 }));
146 }
147
148 PackageWriter::write(writer, &pkg_rels, &parts)
150 }
151
152 pub fn save_to_file<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
154 use std::io::Cursor;
155 let mut cursor = Cursor::new(Vec::new());
156 self.save(&mut cursor)?;
157 let data = cursor.into_inner();
158 std::fs::write(path, data)?;
159 Ok(())
160 }
161
162 pub fn slides(&mut self) -> Slides {
164 Slides::new(self.part_mut())
165 }
166
167 pub fn part(&self) -> &PresentationPart {
169 &self.part
170 }
171
172 pub fn part_mut(&mut self) -> &mut PresentationPart {
174 &mut self.part
175 }
176
177 pub fn core_properties(&self) -> Result<crate::parts::coreprops::CorePropertiesPart> {
179 self.part.core_properties()
180 }
181
182 pub fn slide_width(&self) -> Option<u32> {
184 use crate::opc::part::Part;
185 if let Ok(blob) = Part::blob(&self.part) {
187 if let Ok(xml) = String::from_utf8(blob) {
188 if let Some(start) = xml.find("sldSz cx=\"") {
190 let start = start + 10;
191 if let Some(end) = xml[start..].find('"') {
192 if let Ok(width) = xml[start..start+end].parse::<u32>() {
193 return Some(width);
194 }
195 }
196 }
197 }
198 }
199 Some(9144000) }
201
202 pub fn set_slide_width(&mut self, width: u32) -> Result<()> {
204 use crate::opc::part::Part;
205 let mut xml = Part::to_xml(&self.part)?;
207 let pattern = r#"sldSz cx="[0-9]+""#;
209 let replacement = format!(r#"sldSz cx="{}""#, width);
210 xml = regex::Regex::new(pattern)
211 .map_err(|e| PptError::ValueError(format!("Invalid regex: {}", e)))?
212 .replace_all(&xml, replacement.as_str())
213 .to_string();
214
215 if !xml.contains("sldSz") {
217 let sld_sz = format!(r#"<p:sldSz cx="{}" cy="6858000"/>"#, width);
218 xml = xml.replace("<p:sldIdLst/>", &format!("<p:sldIdLst/>\n {}", sld_sz));
219 }
220
221 let uri = Part::uri(&self.part).clone();
223 *self.part_mut() = PresentationPart::with_xml(uri, xml)?;
224 Ok(())
225 }
226
227 pub fn slide_height(&self) -> Option<u32> {
229 use crate::opc::part::Part;
230 if let Ok(blob) = Part::blob(&self.part) {
232 if let Ok(xml) = String::from_utf8(blob) {
233 if let Some(start) = xml.find("sldSz cy=\"") {
235 let start = start + 10;
236 if let Some(end) = xml[start..].find('"') {
237 if let Ok(height) = xml[start..start+end].parse::<u32>() {
238 return Some(height);
239 }
240 }
241 }
242 }
243 }
244 Some(6858000) }
246
247 pub fn set_slide_height(&mut self, height: u32) -> Result<()> {
249 use crate::opc::part::Part;
250 let mut xml = Part::to_xml(&self.part)?;
252 let pattern = r#"sldSz cx="[0-9]+" cy="[0-9]+""#;
254 let width = self.slide_width().unwrap_or(9144000);
255 let replacement = format!(r#"sldSz cx="{}" cy="{}""#, width, height);
256 xml = regex::Regex::new(pattern)
257 .map_err(|e| PptError::ValueError(format!("Invalid regex: {}", e)))?
258 .replace_all(&xml, replacement.as_str())
259 .to_string();
260
261 if !xml.contains("sldSz") {
263 let sld_sz = format!(r#"<p:sldSz cx="9144000" cy="{}"/>"#, height);
264 xml = xml.replace("<p:sldIdLst/>", &format!("<p:sldIdLst/>\n {}", sld_sz));
265 }
266
267 let uri = Part::uri(&self.part).clone();
269 *self.part_mut() = PresentationPart::with_xml(uri, xml)?;
270 Ok(())
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277 use std::io::Cursor;
278
279 #[test]
280 fn test_presentation_new() {
281 let prs = Presentation::new();
282 assert!(prs.is_ok());
283 let prs = prs.unwrap();
284 assert_eq!(prs.slide_width(), Some(9144000));
285 assert_eq!(prs.slide_height(), Some(6858000));
286 }
287
288 #[test]
289 fn test_presentation_save_to_writer() {
290 let prs = Presentation::new().unwrap();
291 let mut cursor = Cursor::new(Vec::new());
292 let result = prs.save(&mut cursor);
293 assert!(result.is_ok());
294
295 let data = cursor.into_inner();
297 assert!(!data.is_empty());
298
299 let cursor = Cursor::new(&data);
301 let archive = zip::ZipArchive::new(cursor);
302 assert!(archive.is_ok());
303 }
304
305 #[test]
306 fn test_presentation_save_to_file() {
307 let prs = Presentation::new().unwrap();
308 let test_path = "test_output/test_save.pptx";
309
310 std::fs::create_dir_all("test_output").ok();
312
313 let result = prs.save_to_file(test_path);
314 assert!(result.is_ok());
315
316 assert!(std::path::Path::new(test_path).exists());
318
319 let file = std::fs::File::open(test_path);
321 assert!(file.is_ok());
322 let archive = zip::ZipArchive::new(file.unwrap());
323 assert!(archive.is_ok());
324
325 std::fs::remove_file(test_path).ok();
327 }
328
329 #[test]
330 fn test_presentation_save_contains_content_types() {
331 let prs = Presentation::new().unwrap();
332 let mut cursor = Cursor::new(Vec::new());
333 prs.save(&mut cursor).unwrap();
334
335 let data = cursor.into_inner();
336 let cursor = Cursor::new(&data);
337 let mut archive = zip::ZipArchive::new(cursor).unwrap();
338
339 let content_types = archive.by_name("[Content_Types].xml");
341 assert!(content_types.is_ok());
342
343 let mut content_types_file = content_types.unwrap();
344 let mut content = String::new();
345 std::io::Read::read_to_string(&mut content_types_file, &mut content).unwrap();
346 assert!(content.contains("Types"));
347 assert!(content.contains("application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"));
348 }
349
350 #[test]
351 fn test_presentation_save_contains_presentation_xml() {
352 let prs = Presentation::new().unwrap();
353 let mut cursor = Cursor::new(Vec::new());
354 prs.save(&mut cursor).unwrap();
355
356 let data = cursor.into_inner();
357 let cursor = Cursor::new(&data);
358 let mut archive = zip::ZipArchive::new(cursor).unwrap();
359
360 let presentation_xml = archive.by_name("ppt/presentation.xml");
362 assert!(presentation_xml.is_ok());
363
364 let mut presentation_file = presentation_xml.unwrap();
365 let mut content = String::new();
366 std::io::Read::read_to_string(&mut presentation_file, &mut content).unwrap();
367 assert!(content.contains("presentation"));
368 assert!(content.contains("sldIdLst"));
369 assert!(content.contains("sldSz"));
370 }
371
372 #[test]
373 fn test_presentation_save_contains_relationships() {
374 let prs = Presentation::new().unwrap();
375 let mut cursor = Cursor::new(Vec::new());
376 prs.save(&mut cursor).unwrap();
377
378 let data = cursor.into_inner();
379 let cursor = Cursor::new(&data);
380 let mut archive = zip::ZipArchive::new(cursor).unwrap();
381
382 let rels = archive.by_name("_rels/.rels");
384 assert!(rels.is_ok());
385
386 let mut rels_file = rels.unwrap();
387 let mut content = String::new();
388 std::io::Read::read_to_string(&mut rels_file, &mut content).unwrap();
389 assert!(content.contains("Relationships"));
390 assert!(content.contains("ppt/presentation.xml"));
391 }
392
393 #[test]
394 fn test_presentation_slide_dimensions() {
395 let prs = Presentation::new().unwrap();
396 assert_eq!(prs.slide_width(), Some(9144000));
397 assert_eq!(prs.slide_height(), Some(6858000));
398
399 let mut prs = Presentation::new().unwrap();
401 assert!(prs.set_slide_width(10000000).is_ok());
402 assert!(prs.set_slide_height(8000000).is_ok());
403 }
404
405 #[test]
406 fn test_presentation_slides() {
407 let mut prs = Presentation::new().unwrap();
408 let slides = prs.slides();
409 assert_eq!(slides.len(), 0);
411 }
412
413 #[test]
414 fn test_presentation_part_access() {
415 use crate::opc::part::Part;
416 let prs = Presentation::new().unwrap();
417 let part = prs.part();
418 assert_eq!(Part::uri(part).as_str(), "/ppt/presentation.xml");
419
420 let mut prs = Presentation::new().unwrap();
421 let part_mut = prs.part_mut();
422 assert_eq!(Part::uri(part_mut).as_str(), "/ppt/presentation.xml");
423 }
424}
425