ppt_rs/presentation/
presentation.rs

1//! Main presentation object.
2
3use crate::error::Result;
4use crate::parts::presentation::PresentationPart;
5use crate::slide::Slides;
6use std::io::{Read, Seek, Write};
7use super::{dimensions, open, save};
8
9/// PresentationML (PML) presentation.
10///
11/// Not intended to be constructed directly. Use `ppt_rs::Presentation` to open or
12/// create a presentation.
13pub struct Presentation {
14    part: PresentationPart,
15    /// Internal package to store all parts (slides, images, etc.)
16    package: crate::opc::package::Package,
17}
18
19impl Presentation {
20    /// Create a new empty presentation
21    pub fn new() -> Result<Self> {
22        let part = PresentationPart::new()?;
23        let package = crate::opc::package::Package::new();
24        Ok(Self { part, package })
25    }
26
27    /// Open a presentation from a reader
28    pub fn open<R: Read + Seek>(reader: R) -> Result<Self> {
29        use crate::opc::package::Package;
30        
31        // Open package
32        let package = Package::open(reader)?;
33        
34        // Get main presentation part from package
35        let part = open::open_from_package(&package)?;
36        
37        // TODO: Load all parts from package into internal package structure
38        let package = Package::new(); // For now, create empty package
39        Ok(Self { part, package })
40    }
41
42    /// Save the presentation to a writer
43    pub fn save<W: Write + Seek>(&mut self, writer: W) -> Result<()> {
44        save::save(&mut self.part, &mut self.package, writer)
45    }
46
47    /// Save the presentation to a file path
48    pub fn save_to_file<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<()> {
49        use std::io::Cursor;
50        let mut cursor = Cursor::new(Vec::new());
51        self.save(&mut cursor)?;
52        let data = cursor.into_inner();
53        std::fs::write(path, data)?;
54        Ok(())
55    }
56
57    /// Get the slides collection
58    pub fn slides(&mut self) -> Slides {
59        Slides::new(&mut self.part)
60    }
61    
62    /// Get the internal package (for adding parts like images)
63    /// Note: This cannot be called while slides() is in use due to borrowing restrictions
64    pub fn package_mut(&mut self) -> &mut crate::opc::package::Package {
65        &mut self.package
66    }
67
68    /// Get the presentation part
69    pub fn part(&self) -> &PresentationPart {
70        &self.part
71    }
72
73    /// Get mutable presentation part
74    pub fn part_mut(&mut self) -> &mut PresentationPart {
75        &mut self.part
76    }
77
78    /// Get core properties
79    pub fn core_properties(&self) -> Result<crate::parts::coreprops::CorePropertiesPart> {
80        self.part.core_properties()
81    }
82
83    /// Get slide width in EMU (English Metric Units)
84    pub fn slide_width(&self) -> Option<u32> {
85        dimensions::slide_width(&self.part)
86    }
87
88    /// Set slide width in EMU
89    pub fn set_slide_width(&mut self, width: u32) -> Result<()> {
90        dimensions::set_slide_width(&mut self.part, width)
91    }
92
93    /// Get slide height in EMU
94    pub fn slide_height(&self) -> Option<u32> {
95        dimensions::slide_height(&self.part)
96    }
97
98    /// Set slide height in EMU
99    pub fn set_slide_height(&mut self, height: u32) -> Result<()> {
100        dimensions::set_slide_height(&mut self.part, height)
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use std::io::Cursor;
108
109    #[test]
110    fn test_presentation_new() {
111        let prs = Presentation::new();
112        assert!(prs.is_ok());
113        let prs = prs.unwrap();
114        assert_eq!(prs.slide_width(), Some(9144000));
115        assert_eq!(prs.slide_height(), Some(6858000));
116    }
117
118    #[test]
119    fn test_presentation_save_to_writer() {
120        let mut prs = Presentation::new().unwrap();
121        let mut cursor = Cursor::new(Vec::new());
122        let result = prs.save(&mut cursor);
123        assert!(result.is_ok());
124        
125        // Verify we wrote some data
126        let data = cursor.into_inner();
127        assert!(!data.is_empty());
128        
129        // Verify it's a valid ZIP file (PPTX files are ZIP archives)
130        let cursor = Cursor::new(&data);
131        let archive = zip::ZipArchive::new(cursor);
132        assert!(archive.is_ok());
133    }
134
135    #[test]
136    fn test_presentation_save_to_file() {
137        let mut prs = Presentation::new().unwrap();
138        let test_path = "test_output/test_save.pptx";
139        
140        // Create test_output directory if it doesn't exist
141        std::fs::create_dir_all("test_output").ok();
142        
143        let result = prs.save_to_file(test_path);
144        assert!(result.is_ok());
145        
146        // Verify file exists
147        assert!(std::path::Path::new(test_path).exists());
148        
149        // Verify it's a valid ZIP file
150        let file = std::fs::File::open(test_path);
151        assert!(file.is_ok());
152        let archive = zip::ZipArchive::new(file.unwrap());
153        assert!(archive.is_ok());
154        
155        // Clean up
156        std::fs::remove_file(test_path).ok();
157    }
158
159    #[test]
160    fn test_presentation_save_contains_content_types() {
161        let mut prs = Presentation::new().unwrap();
162        let mut cursor = Cursor::new(Vec::new());
163        prs.save(&mut cursor).unwrap();
164        
165        let data = cursor.into_inner();
166        let cursor = Cursor::new(&data);
167        let mut archive = zip::ZipArchive::new(cursor).unwrap();
168        
169        // Check for [Content_Types].xml
170        let content_types = archive.by_name("[Content_Types].xml");
171        assert!(content_types.is_ok());
172        
173        let mut content_types_file = content_types.unwrap();
174        let mut content = String::new();
175        std::io::Read::read_to_string(&mut content_types_file, &mut content).unwrap();
176        assert!(content.contains("Types"));
177        assert!(content.contains("application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"));
178    }
179
180    #[test]
181    fn test_presentation_save_contains_presentation_xml() {
182        let mut prs = Presentation::new().unwrap();
183        let mut cursor = Cursor::new(Vec::new());
184        prs.save(&mut cursor).unwrap();
185        
186        let data = cursor.into_inner();
187        let cursor = Cursor::new(&data);
188        let mut archive = zip::ZipArchive::new(cursor).unwrap();
189        
190        // Check for ppt/presentation.xml
191        let presentation_xml = archive.by_name("ppt/presentation.xml");
192        assert!(presentation_xml.is_ok());
193        
194        let mut presentation_file = presentation_xml.unwrap();
195        let mut content = String::new();
196        std::io::Read::read_to_string(&mut presentation_file, &mut content).unwrap();
197        assert!(content.contains("presentation"));
198        assert!(content.contains("sldIdLst"));
199        assert!(content.contains("sldSz"));
200    }
201
202    #[test]
203    fn test_presentation_save_contains_relationships() {
204        let mut prs = Presentation::new().unwrap();
205        let mut cursor = Cursor::new(Vec::new());
206        prs.save(&mut cursor).unwrap();
207        
208        let data = cursor.into_inner();
209        let cursor = Cursor::new(&data);
210        let mut archive = zip::ZipArchive::new(cursor).unwrap();
211        
212        // Check for _rels/.rels
213        let rels = archive.by_name("_rels/.rels");
214        assert!(rels.is_ok());
215        
216        let mut rels_file = rels.unwrap();
217        let mut content = String::new();
218        std::io::Read::read_to_string(&mut rels_file, &mut content).unwrap();
219        assert!(content.contains("Relationships"));
220        assert!(content.contains("ppt/presentation.xml"));
221    }
222
223    #[test]
224    fn test_presentation_slide_dimensions() {
225        let prs = Presentation::new().unwrap();
226        assert_eq!(prs.slide_width(), Some(9144000));
227        assert_eq!(prs.slide_height(), Some(6858000));
228        
229        // Test setting dimensions (even though not fully implemented)
230        let mut prs = Presentation::new().unwrap();
231        assert!(prs.set_slide_width(10000000).is_ok());
232        assert!(prs.set_slide_height(8000000).is_ok());
233    }
234
235    #[test]
236    fn test_presentation_slides() {
237        let mut prs = Presentation::new().unwrap();
238        let mut slides = prs.slides();
239        // Empty presentation should have no slides
240        assert_eq!(slides.len(), 0);
241    }
242
243    #[test]
244    fn test_presentation_save_with_slides() {
245        use crate::parts::slide::SlidePart;
246        use crate::opc::packuri::PackURI;
247        use crate::opc::part::Part;
248        
249        let mut prs = Presentation::new().unwrap();
250        // Create a slide part (not layout part) for testing
251        let slide_part = SlidePart::new(PackURI::new("/ppt/slides/slide1.xml").unwrap(), prs.part() as &dyn Part).unwrap();
252        
253        // Helper function to add slide without simultaneous borrows
254        fn add_slide_helper(
255            prs: &mut Presentation,
256            slide_part: &SlidePart,
257        ) -> Result<()> {
258            // Split borrows using unsafe (safe because we know they don't overlap)
259            unsafe {
260                let prs_ptr = prs as *mut Presentation;
261                let slides = &mut (*prs_ptr).part;
262                let package = &mut (*prs_ptr).package;
263                let mut slides_collection = crate::slide::Slides::new(slides);
264                slides_collection.add_slide(slide_part, package)?;
265            }
266            Ok(())
267        }
268        
269        add_slide_helper(&mut prs, &slide_part).unwrap();
270        
271        // Verify slide count
272        {
273            let slides = prs.slides();
274            assert_eq!(slides.len(), 1);
275        }
276        
277        // Save the presentation
278        let test_path = "test_output/test_save_with_slides.pptx";
279        std::fs::create_dir_all("test_output").ok();
280        let result = prs.save_to_file(test_path);
281        assert!(result.is_ok());
282        
283        // Verify file exists and is valid
284        assert!(std::path::Path::new(test_path).exists());
285        let file = std::fs::File::open(test_path).unwrap();
286        let mut archive = zip::ZipArchive::new(file).unwrap();
287        
288        // Verify slide part is in the archive
289        let slide_xml = archive.by_name("ppt/slides/slide1.xml");
290        assert!(slide_xml.is_ok());
291        
292        // Clean up
293        std::fs::remove_file(test_path).ok();
294    }
295
296    #[test]
297    fn test_presentation_save_with_images() {
298        use crate::parts::slide::SlidePart;
299        use crate::opc::packuri::PackURI;
300        use crate::opc::part::Part;
301        
302        let mut prs = Presentation::new().unwrap();
303        // Create a slide part (not layout part) for testing
304        let slide_part = SlidePart::new(PackURI::new("/ppt/slides/slide1.xml").unwrap(), prs.part() as &dyn Part).unwrap();
305        
306        // Create a minimal PNG image
307        let png_data = vec![
308            0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
309            0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52,
310            0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
311            0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xDE,
312            0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54,
313            0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01,
314            0x0D, 0x0A, 0x2D, 0xB4,
315            0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44,
316            0xAE, 0x42, 0x60, 0x82,
317        ];
318        
319        // Helper function to add slide and image without simultaneous borrows
320        fn add_slide_and_image_helper(
321            prs: &mut Presentation,
322            slide_part: &SlidePart,
323            png_data: Vec<u8>,
324        ) -> Result<()> {
325            // Split borrows using unsafe (safe because we know they don't overlap)
326            unsafe {
327                let prs_ptr = prs as *mut Presentation;
328                let slides = &mut (*prs_ptr).part;
329                let package = &mut (*prs_ptr).package;
330                let mut slides_collection = crate::slide::Slides::new(slides);
331                let mut slide = slides_collection.add_slide(slide_part, package)?;
332                slide.add_image(png_data, "png", package)?;
333                // Slide is dropped here, but its part is already in the package
334            }
335            Ok(())
336        }
337        
338        add_slide_and_image_helper(&mut prs, &slide_part, png_data).unwrap();
339        
340        // Save the presentation
341        let test_path = "test_output/test_save_with_images.pptx";
342        std::fs::create_dir_all("test_output").ok();
343        let result = prs.save_to_file(test_path);
344        assert!(result.is_ok());
345        
346        // Verify file exists and is valid
347        assert!(std::path::Path::new(test_path).exists());
348        let file = std::fs::File::open(test_path).unwrap();
349        let mut archive = zip::ZipArchive::new(file).unwrap();
350        
351        // Verify image part is in the archive
352        {
353            let image_file = archive.by_name("ppt/media/image1.png");
354            assert!(image_file.is_ok());
355        }
356        
357        // Verify slide part has relationship to image
358        {
359            let slide_xml = archive.by_name("ppt/slides/slide1.xml");
360            assert!(slide_xml.is_ok());
361            let mut slide_content = String::new();
362            std::io::Read::read_to_string(&mut slide_xml.unwrap(), &mut slide_content).unwrap();
363            assert!(slide_content.contains("image1.png"));
364        }
365        
366        // Clean up
367        std::fs::remove_file(test_path).ok();
368    }
369
370    #[test]
371    fn test_presentation_package_mut() {
372        let mut prs = Presentation::new().unwrap();
373        let package = prs.package_mut();
374        assert_eq!(package.iter_parts().count(), 0);
375    }
376
377    #[test]
378    fn test_presentation_save_collects_all_parts() {
379        use crate::parts::slide::SlidePart;
380        use crate::opc::packuri::PackURI;
381        use crate::opc::part::Part;
382        
383        let mut prs = Presentation::new().unwrap();
384        // Create a slide part (not layout part) for testing
385        let slide_part = SlidePart::new(PackURI::new("/ppt/slides/slide1.xml").unwrap(), prs.part() as &dyn Part).unwrap();
386        
387        // Helper function to add slide and image without simultaneous borrows
388        fn add_slide_and_image_helper(
389            prs: &mut Presentation,
390            slide_part: &SlidePart,
391            png_data: Vec<u8>,
392        ) -> Result<()> {
393            // Split borrows using unsafe (safe because we know they don't overlap)
394            unsafe {
395                let prs_ptr = prs as *mut Presentation;
396                let slides = &mut (*prs_ptr).part;
397                let package = &mut (*prs_ptr).package;
398                let mut slides_collection = crate::slide::Slides::new(slides);
399                let mut slide = slides_collection.add_slide(slide_part, package)?;
400                slide.add_image(png_data, "png", package)?;
401                // Slide is dropped here, but its part is already in the package
402            }
403            Ok(())
404        }
405        
406        let png_data = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
407        add_slide_and_image_helper(&mut prs, &slide_part, png_data).unwrap();
408        
409        // Save and verify all parts are included
410        let mut cursor = std::io::Cursor::new(Vec::new());
411        prs.save(&mut cursor).unwrap();
412        
413        let data = cursor.into_inner();
414        let cursor = std::io::Cursor::new(&data);
415        let mut archive = zip::ZipArchive::new(cursor).unwrap();
416        
417        // Verify presentation part
418        assert!(archive.by_name("ppt/presentation.xml").is_ok());
419        
420        // Verify slide part
421        assert!(archive.by_name("ppt/slides/slide1.xml").is_ok());
422        
423        // Verify image part
424        assert!(archive.by_name("ppt/media/image1.png").is_ok());
425        
426        // Verify relationships
427        assert!(archive.by_name("ppt/_rels/presentation.xml.rels").is_ok());
428        assert!(archive.by_name("ppt/slides/_rels/slide1.xml.rels").is_ok());
429    }
430}
431