ppt_rs/
presentation.rs

1//! Main presentation object.
2
3use crate::error::{PptError, Result};
4use crate::parts::presentation::PresentationPart;
5use crate::slide::{Slide, Slides};
6use std::io::{Read, Seek, Write};
7
8/// PresentationML (PML) presentation.
9///
10/// Not intended to be constructed directly. Use `ppt_rs::Presentation` to open or
11/// create a presentation.
12pub struct Presentation {
13    part: PresentationPart,
14}
15
16impl Presentation {
17    /// Create a new empty presentation
18    pub fn new() -> Result<Self> {
19        let part = PresentationPart::new()?;
20        Ok(Self { part })
21    }
22
23    /// Open a presentation from a reader
24    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        // Open package
30        let package = Package::open(reader)?;
31        
32        // Get main presentation part from package relationships
33        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                // Get blob and create PresentationPart
44                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                // Fallback: create new presentation
52                Self::new()
53            }
54        } else {
55            // No main document found, create new presentation
56            Self::new()
57        }
58    }
59
60    /// Save the presentation to a writer
61    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        // Create package relationships
67        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        // Add core properties relationship if it exists
76        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        // Get the blob and URI directly instead of using trait objects
88        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        // Create a simple part wrapper that owns its data
95        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        // Add core properties part if it exists
135        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        // Write the package
149        PackageWriter::write(writer, &pkg_rels, &parts)
150    }
151
152    /// Save the presentation to a file path
153    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    /// Get the slides collection
163    pub fn slides(&mut self) -> Slides {
164        Slides::new(self.part_mut())
165    }
166
167    /// Get the presentation part
168    pub fn part(&self) -> &PresentationPart {
169        &self.part
170    }
171
172    /// Get mutable presentation part
173    pub fn part_mut(&mut self) -> &mut PresentationPart {
174        &mut self.part
175    }
176
177    /// Get core properties
178    pub fn core_properties(&self) -> Result<crate::parts::coreprops::CorePropertiesPart> {
179        self.part.core_properties()
180    }
181
182    /// Get slide width in EMU (English Metric Units)
183    pub fn slide_width(&self) -> Option<u32> {
184        use crate::opc::part::Part;
185        // Parse from XML blob
186        if let Ok(blob) = Part::blob(&self.part) {
187            if let Ok(xml) = String::from_utf8(blob) {
188                // Look for sldSz cx="..." pattern
189                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) // Default 10 inches
200    }
201
202    /// Set slide width in EMU
203    pub fn set_slide_width(&mut self, width: u32) -> Result<()> {
204        use crate::opc::part::Part;
205        // Parse XML, update width, and store back
206        let mut xml = Part::to_xml(&self.part)?;
207        // Replace cx value in sldSz
208        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 sldSz doesn't exist, add it
216        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        // Store updated XML
222        let uri = Part::uri(&self.part).clone();
223        *self.part_mut() = PresentationPart::with_xml(uri, xml)?;
224        Ok(())
225    }
226
227    /// Get slide height in EMU
228    pub fn slide_height(&self) -> Option<u32> {
229        use crate::opc::part::Part;
230        // Parse from XML blob
231        if let Ok(blob) = Part::blob(&self.part) {
232            if let Ok(xml) = String::from_utf8(blob) {
233                // Look for sldSz cy="..." pattern
234                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) // Default 7.5 inches
245    }
246
247    /// Set slide height in EMU
248    pub fn set_slide_height(&mut self, height: u32) -> Result<()> {
249        use crate::opc::part::Part;
250        // Parse XML, update height, and store back
251        let mut xml = Part::to_xml(&self.part)?;
252        // Replace cy value in sldSz
253        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 sldSz doesn't exist, add it
262        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        // Store updated XML
268        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        // Verify we wrote some data
296        let data = cursor.into_inner();
297        assert!(!data.is_empty());
298        
299        // Verify it's a valid ZIP file (PPTX files are ZIP archives)
300        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        // Create test_output directory if it doesn't exist
311        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        // Verify file exists
317        assert!(std::path::Path::new(test_path).exists());
318        
319        // Verify it's a valid ZIP file
320        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        // Clean up
326        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        // Check for [Content_Types].xml
340        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        // Check for ppt/presentation.xml
361        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        // Check for _rels/.rels
383        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        // Test setting dimensions (even though not fully implemented)
400        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        // Empty presentation should have no slides
410        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