Skip to main content

ppt_rs/oxml/
editor.rs

1//! Presentation editing capabilities
2//!
3//! Provides functionality to modify existing PPTX files:
4//! - Add new slides
5//! - Update slide content
6//! - Remove slides
7//! - Modify presentation properties
8
9use super::slide::{ParsedSlide, SlideParser};
10use crate::exc::PptxError;
11use crate::generator::slide_content::SlideContent;
12use crate::generator::slide_xml::{create_slide_rels_xml, create_slide_xml_with_content};
13use crate::opc::Package;
14
15/// Presentation editor for modifying PPTX files
16pub struct PresentationEditor {
17    package: Package,
18    slide_count: usize,
19}
20
21impl PresentationEditor {
22    /// Open a PPTX file for editing
23    pub fn open(path: &str) -> Result<Self, PptxError> {
24        let package = Package::open(path)?;
25        let slide_count = Self::count_slides(&package);
26
27        Ok(PresentationEditor {
28            package,
29            slide_count,
30        })
31    }
32
33    /// Create a new presentation for editing
34    pub fn new() -> Self {
35        PresentationEditor {
36            package: Package::new(),
37            slide_count: 0,
38        }
39    }
40
41    /// Get number of slides
42    pub fn slide_count(&self) -> usize {
43        self.slide_count
44    }
45
46    /// Get a parsed slide by index (0-based)
47    pub fn get_slide(&self, index: usize) -> Result<ParsedSlide, PptxError> {
48        let slide_num = index + 1;
49        let path = format!("ppt/slides/slide{slide_num}.xml");
50        let xml = self
51            .package
52            .get_part(&path)
53            .ok_or_else(|| PptxError::NotFound(format!("Slide {index} not found")))?;
54
55        let xml_str = String::from_utf8_lossy(xml);
56        SlideParser::parse(&xml_str)
57    }
58
59    /// Add a new slide at the end
60    pub fn add_slide(&mut self, content: SlideContent) -> Result<usize, PptxError> {
61        let new_index = self.slide_count + 1;
62
63        // Generate slide XML
64        let slide_xml = create_slide_xml_with_content(new_index, &content, &[]);
65        let slide_rels_xml = create_slide_rels_xml();
66
67        // Add slide file
68        let slide_path = format!("ppt/slides/slide{new_index}.xml");
69        self.package.add_part(slide_path, slide_xml.into_bytes());
70
71        // Add slide relationships
72        let rels_path = format!("ppt/slides/_rels/slide{new_index}.xml.rels");
73        self.package
74            .add_part(rels_path, slide_rels_xml.into_bytes());
75
76        // Update presentation.xml to include new slide
77        self.update_presentation_xml(new_index)?;
78
79        // Update presentation.xml.rels
80        self.update_presentation_rels(new_index)?;
81
82        // Update [Content_Types].xml
83        self.update_content_types(new_index)?;
84
85        self.slide_count = new_index;
86        Ok(new_index - 1) // Return 0-based index
87    }
88
89    /// Update slide content at index
90    pub fn update_slide(&mut self, index: usize, content: SlideContent) -> Result<(), PptxError> {
91        if index >= self.slide_count {
92            return Err(PptxError::NotFound(format!("Slide {index} not found")));
93        }
94
95        let slide_num = index + 1;
96        let slide_xml = create_slide_xml_with_content(slide_num, &content, &[]);
97        let slide_path = format!("ppt/slides/slide{slide_num}.xml");
98
99        self.package.add_part(slide_path, slide_xml.into_bytes());
100        Ok(())
101    }
102
103    /// Remove a slide by index
104    pub fn remove_slide(&mut self, index: usize) -> Result<(), PptxError> {
105        if index >= self.slide_count {
106            return Err(PptxError::NotFound(format!("Slide {index} not found")));
107        }
108
109        let slide_num = index + 1;
110
111        // Remove slide file
112        let slide_path = format!("ppt/slides/slide{slide_num}.xml");
113        self.package.remove_part(&slide_path);
114
115        // Remove slide relationships
116        let rels_path = format!("ppt/slides/_rels/slide{slide_num}.xml.rels");
117        self.package.remove_part(&rels_path);
118
119        // Renumber remaining slides
120        for i in (slide_num + 1)..=self.slide_count {
121            self.renumber_slide(i, i - 1)?;
122        }
123
124        self.slide_count -= 1;
125
126        // Update presentation files
127        self.rebuild_presentation_xml()?;
128        self.rebuild_presentation_rels()?;
129        self.rebuild_content_types()?;
130
131        Ok(())
132    }
133
134    /// Save the modified presentation
135    pub fn save(&self, path: &str) -> Result<(), PptxError> {
136        self.package.save(path)?;
137        Ok(())
138    }
139
140    /// Get the underlying package for advanced operations
141    pub fn package(&self) -> &Package {
142        &self.package
143    }
144
145    /// Get mutable reference to package
146    pub fn package_mut(&mut self) -> &mut Package {
147        &mut self.package
148    }
149
150    // Helper methods
151
152    fn count_slides(package: &Package) -> usize {
153        package
154            .part_paths()
155            .iter()
156            .filter(|p| {
157                p.starts_with("ppt/slides/slide") && p.ends_with(".xml") && !p.contains("_rels")
158            })
159            .count()
160    }
161
162    fn update_presentation_xml(&mut self, new_slide_count: usize) -> Result<(), PptxError> {
163        if let Some(xml) = self.package.get_part_string("ppt/presentation.xml") {
164            // Parse and update the presentation XML
165            let updated = self.add_slide_to_presentation_xml(&xml, new_slide_count)?;
166            self.package
167                .add_part("ppt/presentation.xml".to_string(), updated.into_bytes());
168        }
169        Ok(())
170    }
171
172    fn add_slide_to_presentation_xml(
173        &self,
174        xml: &str,
175        slide_num: usize,
176    ) -> Result<String, PptxError> {
177        // Find </p:sldIdLst> and insert new slide reference before it
178        let slide_id = 256 + slide_num;
179        let r_id = slide_num + 2; // rId1=master, rId2=theme, rId3+=slides
180
181        let new_slide_ref = format!("\n<p:sldId id=\"{slide_id}\" r:id=\"rId{r_id}\"/>");
182
183        if let Some(pos) = xml.find("</p:sldIdLst>") {
184            let mut result = xml.to_string();
185            result.insert_str(pos, &new_slide_ref);
186            Ok(result)
187        } else {
188            // If no sldIdLst, return as-is (shouldn't happen for valid PPTX)
189            Ok(xml.to_string())
190        }
191    }
192
193    fn update_presentation_rels(&mut self, new_slide_count: usize) -> Result<(), PptxError> {
194        if let Some(xml) = self
195            .package
196            .get_part_string("ppt/_rels/presentation.xml.rels")
197        {
198            let updated = self.add_slide_to_presentation_rels(&xml, new_slide_count)?;
199            self.package.add_part(
200                "ppt/_rels/presentation.xml.rels".to_string(),
201                updated.into_bytes(),
202            );
203        }
204        Ok(())
205    }
206
207    fn add_slide_to_presentation_rels(
208        &self,
209        xml: &str,
210        slide_num: usize,
211    ) -> Result<String, PptxError> {
212        let r_id = slide_num + 2;
213        let new_rel = format!(
214            "\n    <Relationship Id=\"rId{r_id}\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide\" Target=\"slides/slide{slide_num}.xml\"/>"
215        );
216
217        if let Some(pos) = xml.find("</Relationships>") {
218            let mut result = xml.to_string();
219            result.insert_str(pos, &new_rel);
220            Ok(result)
221        } else {
222            Ok(xml.to_string())
223        }
224    }
225
226    fn update_content_types(&mut self, new_slide_count: usize) -> Result<(), PptxError> {
227        if let Some(xml) = self.package.get_part_string("[Content_Types].xml") {
228            let updated = self.add_slide_to_content_types(&xml, new_slide_count)?;
229            self.package
230                .add_part("[Content_Types].xml".to_string(), updated.into_bytes());
231        }
232        Ok(())
233    }
234
235    fn add_slide_to_content_types(&self, xml: &str, slide_num: usize) -> Result<String, PptxError> {
236        let new_override = format!(
237            "\n<Override PartName=\"/ppt/slides/slide{slide_num}.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.presentationml.slide+xml\"/>"
238        );
239
240        if let Some(pos) = xml.find("</Types>") {
241            let mut result = xml.to_string();
242            result.insert_str(pos, &new_override);
243            Ok(result)
244        } else {
245            Ok(xml.to_string())
246        }
247    }
248
249    fn renumber_slide(&mut self, old_num: usize, new_num: usize) -> Result<(), PptxError> {
250        // Move slide file
251        let old_path = format!("ppt/slides/slide{old_num}.xml");
252        let new_path = format!("ppt/slides/slide{new_num}.xml");
253
254        if let Some(content) = self.package.remove_part(&old_path) {
255            self.package.add_part(new_path, content);
256        }
257
258        // Move slide rels
259        let old_rels = format!("ppt/slides/_rels/slide{old_num}.xml.rels");
260        let new_rels = format!("ppt/slides/_rels/slide{new_num}.xml.rels");
261
262        if let Some(content) = self.package.remove_part(&old_rels) {
263            self.package.add_part(new_rels, content);
264        }
265
266        Ok(())
267    }
268
269    fn rebuild_presentation_xml(&mut self) -> Result<(), PptxError> {
270        let mut slide_refs = String::new();
271        for i in 1..=self.slide_count {
272            let slide_id = 256 + i;
273            let r_id = i + 2;
274            slide_refs.push_str(&format!(
275                "\n<p:sldId id=\"{slide_id}\" r:id=\"rId{r_id}\"/>"
276            ));
277        }
278
279        let xml = format!(
280            r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
281<p:presentation xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" saveSubsetFonts="1">
282<p:sldMasterIdLst>
283<p:sldMasterId id="2147483648" r:id="rId1"/>
284</p:sldMasterIdLst>
285<p:sldIdLst>{slide_refs}
286</p:sldIdLst>
287<p:sldSz cx="9144000" cy="6858000" type="screen4x3"/>
288<p:notesSz cx="6858000" cy="9144000"/>
289</p:presentation>"#
290        );
291
292        self.package
293            .add_part("ppt/presentation.xml".to_string(), xml.into_bytes());
294        Ok(())
295    }
296
297    fn rebuild_presentation_rels(&mut self) -> Result<(), PptxError> {
298        let mut slide_rels = String::new();
299        for i in 1..=self.slide_count {
300            let r_id = i + 2;
301            slide_rels.push_str(&format!(
302                "\n    <Relationship Id=\"rId{r_id}\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide\" Target=\"slides/slide{i}.xml\"/>"
303            ));
304        }
305
306        let xml = format!(
307            r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
308<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
309    <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMasters/slideMaster1.xml"/>
310    <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/>{slide_rels}
311</Relationships>"#
312        );
313
314        self.package.add_part(
315            "ppt/_rels/presentation.xml.rels".to_string(),
316            xml.into_bytes(),
317        );
318        Ok(())
319    }
320
321    fn rebuild_content_types(&mut self) -> Result<(), PptxError> {
322        let mut slide_overrides = String::new();
323        for i in 1..=self.slide_count {
324            slide_overrides.push_str(&format!(
325                "\n<Override PartName=\"/ppt/slides/slide{i}.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.presentationml.slide+xml\"/>"
326            ));
327        }
328
329        let xml = format!(
330            r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
331<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
332<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
333<Default Extension="xml" ContentType="application/xml"/>
334<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>{slide_overrides}
335<Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>
336<Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/>
337<Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>
338<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>
339<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>
340</Types>"#
341        );
342
343        self.package
344            .add_part("[Content_Types].xml".to_string(), xml.into_bytes());
345        Ok(())
346    }
347}
348
349impl Default for PresentationEditor {
350    fn default() -> Self {
351        Self::new()
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358    use crate::generator::create_pptx_with_content;
359    use crate::oxml::PresentationReader;
360    use std::fs;
361
362    #[test]
363    fn test_open_and_add_slide() {
364        // Create initial presentation
365        let slides = vec![SlideContent::new("Original Slide 1").add_bullet("Original content")];
366        let pptx_data = create_pptx_with_content("Test", slides).unwrap();
367        fs::write("test_edit.pptx", &pptx_data).unwrap();
368
369        // Open and add a slide
370        let mut editor = PresentationEditor::open("test_edit.pptx").unwrap();
371        assert_eq!(editor.slide_count(), 1);
372
373        let new_slide = SlideContent::new("New Slide").add_bullet("Added via editor");
374        editor.add_slide(new_slide).unwrap();
375
376        assert_eq!(editor.slide_count(), 2);
377
378        // Save and verify
379        editor.save("test_edit_modified.pptx").unwrap();
380
381        let reader = PresentationReader::open("test_edit_modified.pptx").unwrap();
382        assert_eq!(reader.slide_count(), 2);
383
384        // Cleanup
385        fs::remove_file("test_edit.pptx").ok();
386        fs::remove_file("test_edit_modified.pptx").ok();
387    }
388
389    #[test]
390    fn test_update_slide() {
391        let slides = vec![SlideContent::new("Original Title").add_bullet("Original bullet")];
392        let pptx_data = create_pptx_with_content("Test", slides).unwrap();
393        fs::write("test_update.pptx", &pptx_data).unwrap();
394
395        let mut editor = PresentationEditor::open("test_update.pptx").unwrap();
396
397        let updated = SlideContent::new("Updated Title").add_bullet("Updated bullet");
398        editor.update_slide(0, updated).unwrap();
399
400        editor.save("test_update_modified.pptx").unwrap();
401
402        let reader = PresentationReader::open("test_update_modified.pptx").unwrap();
403        let slide = reader.get_slide(0).unwrap();
404        assert_eq!(slide.title, Some("Updated Title".to_string()));
405
406        fs::remove_file("test_update.pptx").ok();
407        fs::remove_file("test_update_modified.pptx").ok();
408    }
409}