1use 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
15pub struct PresentationEditor {
17 package: Package,
18 slide_count: usize,
19}
20
21impl PresentationEditor {
22 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 pub fn new() -> Self {
35 PresentationEditor {
36 package: Package::new(),
37 slide_count: 0,
38 }
39 }
40
41 pub fn slide_count(&self) -> usize {
43 self.slide_count
44 }
45
46 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 pub fn add_slide(&mut self, content: SlideContent) -> Result<usize, PptxError> {
61 let new_index = self.slide_count + 1;
62
63 let slide_xml = create_slide_xml_with_content(new_index, &content, &[]);
65 let slide_rels_xml = create_slide_rels_xml();
66
67 let slide_path = format!("ppt/slides/slide{new_index}.xml");
69 self.package.add_part(slide_path, slide_xml.into_bytes());
70
71 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 self.update_presentation_xml(new_index)?;
78
79 self.update_presentation_rels(new_index)?;
81
82 self.update_content_types(new_index)?;
84
85 self.slide_count = new_index;
86 Ok(new_index - 1) }
88
89 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 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 let slide_path = format!("ppt/slides/slide{slide_num}.xml");
113 self.package.remove_part(&slide_path);
114
115 let rels_path = format!("ppt/slides/_rels/slide{slide_num}.xml.rels");
117 self.package.remove_part(&rels_path);
118
119 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 self.rebuild_presentation_xml()?;
128 self.rebuild_presentation_rels()?;
129 self.rebuild_content_types()?;
130
131 Ok(())
132 }
133
134 pub fn save(&self, path: &str) -> Result<(), PptxError> {
136 self.package.save(path)?;
137 Ok(())
138 }
139
140 pub fn package(&self) -> &Package {
142 &self.package
143 }
144
145 pub fn package_mut(&mut self) -> &mut Package {
147 &mut self.package
148 }
149
150 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 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 let slide_id = 256 + slide_num;
179 let r_id = slide_num + 2; 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 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 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 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 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 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 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 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}