1use super::slide::{ParsedSlide, SlideParser};
10use crate::exc::{messages, PptxError};
11use crate::generator::slide_content::SlideContent;
12use crate::generator::package_xml::{
13 create_presentation_rels_xml_full, slide_id_value, slide_rel_id,
14};
15use crate::generator::slide_xml::{create_slide_rels_xml, create_slide_xml_with_content};
16use crate::opc::Package;
17
18pub struct PresentationEditor {
20 package: Package,
21 slide_count: usize,
22}
23
24impl PresentationEditor {
25 pub fn open(path: &str) -> Result<Self, PptxError> {
27 let package = Package::open(path)?;
28 let slide_count = Self::count_slides(&package);
29
30 Ok(PresentationEditor {
31 package,
32 slide_count,
33 })
34 }
35
36 pub fn new() -> Self {
38 PresentationEditor {
39 package: Package::new(),
40 slide_count: 0,
41 }
42 }
43
44 pub fn slide_count(&self) -> usize {
46 self.slide_count
47 }
48
49 pub fn get_slide(&self, index: usize) -> Result<ParsedSlide, PptxError> {
51 let slide_num = index + 1;
52 let path = format!("ppt/slides/slide{slide_num}.xml");
53 let xml = self
54 .package
55 .get_part(&path)
56 .ok_or_else(|| PptxError::NotFound(messages::slide_not_found(index)))?;
57
58 let xml_str = String::from_utf8_lossy(xml);
59 SlideParser::parse(&xml_str)
60 }
61
62 pub fn add_slide(&mut self, content: SlideContent) -> Result<usize, PptxError> {
64 let new_index = self.slide_count + 1;
65
66 let slide_xml = create_slide_xml_with_content(new_index, &content, &[], None);
68 let slide_rels_xml = create_slide_rels_xml();
69
70 let slide_path = format!("ppt/slides/slide{new_index}.xml");
72 self.package.add_part(slide_path, slide_xml.into_bytes());
73
74 let rels_path = format!("ppt/slides/_rels/slide{new_index}.xml.rels");
76 self.package
77 .add_part(rels_path, slide_rels_xml.into_bytes());
78
79 self.update_presentation_xml(new_index)?;
81
82 self.update_presentation_rels(new_index)?;
84
85 self.update_content_types(new_index)?;
87
88 self.slide_count = new_index;
89 Ok(new_index - 1) }
91
92 pub fn update_slide(&mut self, index: usize, content: SlideContent) -> Result<(), PptxError> {
94 if index >= self.slide_count {
95 return Err(PptxError::NotFound(messages::slide_not_found(index)));
96 }
97
98 let slide_num = index + 1;
99 let slide_xml = create_slide_xml_with_content(slide_num, &content, &[], None);
100 let slide_path = format!("ppt/slides/slide{slide_num}.xml");
101
102 self.package.add_part(slide_path, slide_xml.into_bytes());
103 Ok(())
104 }
105
106 pub fn remove_slide(&mut self, index: usize) -> Result<(), PptxError> {
108 if index >= self.slide_count {
109 return Err(PptxError::NotFound(messages::slide_not_found(index)));
110 }
111
112 let slide_num = index + 1;
113
114 let slide_path = format!("ppt/slides/slide{slide_num}.xml");
116 self.package.remove_part(&slide_path);
117
118 let rels_path = format!("ppt/slides/_rels/slide{slide_num}.xml.rels");
120 self.package.remove_part(&rels_path);
121
122 for i in (slide_num + 1)..=self.slide_count {
124 self.renumber_slide(i, i - 1)?;
125 }
126
127 self.slide_count -= 1;
128
129 self.rebuild_presentation_xml()?;
131 self.rebuild_presentation_rels()?;
132 self.rebuild_content_types()?;
133
134 Ok(())
135 }
136
137 pub fn duplicate_slide(&mut self, index: usize) -> Result<usize, PptxError> {
139 if index >= self.slide_count {
140 return Err(PptxError::NotFound(messages::slide_not_found(index)));
141 }
142
143 let slide_num = index + 1;
144 let new_index = index + 1;
145 let new_num = new_index + 1;
146
147 let slide_path = format!("ppt/slides/slide{slide_num}.xml");
149 let rels_path = format!("ppt/slides/_rels/slide{slide_num}.xml.rels");
150
151 let slide_data = self
152 .package
153 .get_part(&slide_path)
154 .ok_or_else(|| PptxError::NotFound(messages::slide_not_found(index)))?
155 .to_vec();
156 let rels_data = self
157 .package
158 .get_part(&rels_path)
159 .unwrap_or_default()
160 .to_vec();
161
162 for i in ((new_num + 1)..=(self.slide_count + 1)).rev() {
164 self.renumber_slide(i - 1, i)?;
165 }
166
167 self.package
169 .add_part(format!("ppt/slides/slide{new_num}.xml"), slide_data);
170 self.package
171 .add_part(format!("ppt/slides/_rels/slide{new_num}.xml.rels"), rels_data);
172
173 self.slide_count += 1;
174
175 self.rebuild_presentation_xml()?;
176 self.rebuild_presentation_rels()?;
177 self.rebuild_content_types()?;
178
179 Ok(new_index)
180 }
181
182 pub fn insert_slide(&mut self, index: usize, content: SlideContent) -> Result<usize, PptxError> {
184 if index > self.slide_count {
185 return Err(PptxError::NotFound(messages::slide_not_found(index)));
186 }
187
188 let new_num = index + 1;
189
190 for i in ((new_num + 1)..=(self.slide_count + 1)).rev() {
192 self.renumber_slide(i - 1, i)?;
193 }
194
195 let slide_xml = create_slide_xml_with_content(new_num, &content, &[], None);
197 let slide_rels_xml = create_slide_rels_xml();
198
199 self.package
200 .add_part(format!("ppt/slides/slide{new_num}.xml"), slide_xml.into_bytes());
201 self.package.add_part(
202 format!("ppt/slides/_rels/slide{new_num}.xml.rels"),
203 slide_rels_xml.into_bytes(),
204 );
205
206 self.slide_count += 1;
207
208 self.rebuild_presentation_xml()?;
209 self.rebuild_presentation_rels()?;
210 self.rebuild_content_types()?;
211
212 Ok(index)
213 }
214
215 pub fn reorder_slide(&mut self, from_index: usize, to_index: usize) -> Result<(), PptxError> {
217 if from_index >= self.slide_count || to_index >= self.slide_count {
218 return Err(PptxError::NotFound(messages::slide_not_found(
219 from_index.max(to_index),
220 )));
221 }
222
223 if from_index == to_index {
224 return Ok(());
225 }
226
227 let from_num = from_index + 1;
228 let to_num = to_index + 1;
229
230 let temp_slide_path = "ppt/slides/_temp_slide.xml".to_string();
232 let temp_rels_path = "ppt/slides/_rels/_temp_slide.xml.rels".to_string();
233
234 if let Some(data) = self.package.remove_part(&format!("ppt/slides/slide{from_num}.xml")) {
235 self.package.add_part(temp_slide_path.clone(), data);
236 }
237 if let Some(data) =
238 self.package.remove_part(&format!("ppt/slides/_rels/slide{from_num}.xml.rels"))
239 {
240 self.package.add_part(temp_rels_path.clone(), data);
241 }
242
243 if from_index < to_index {
244 for i in from_num..to_num {
246 self.renumber_slide(i + 1, i)?;
247 }
248 } else {
249 for i in (to_num..from_num).rev() {
251 self.renumber_slide(i, i + 1)?;
252 }
253 }
254
255 if let Some(data) = self.package.remove_part(&temp_slide_path) {
257 self.package
258 .add_part(format!("ppt/slides/slide{to_num}.xml"), data);
259 }
260 if let Some(data) = self.package.remove_part(&temp_rels_path) {
261 self.package
262 .add_part(format!("ppt/slides/_rels/slide{to_num}.xml.rels"), data);
263 }
264
265 self.rebuild_presentation_xml()?;
266 self.rebuild_presentation_rels()?;
267 self.rebuild_content_types()?;
268
269 Ok(())
270 }
271
272 pub fn save(&self, path: &str) -> Result<(), PptxError> {
274 self.package.save(path)?;
275 Ok(())
276 }
277
278 pub fn package(&self) -> &Package {
280 &self.package
281 }
282
283 pub fn package_mut(&mut self) -> &mut Package {
285 &mut self.package
286 }
287
288 fn count_slides(package: &Package) -> usize {
291 package
292 .part_paths()
293 .iter()
294 .filter(|p| {
295 p.starts_with("ppt/slides/slide") && p.ends_with(".xml") && !p.contains("_rels")
296 })
297 .count()
298 }
299
300 fn presentation_has_notes(xml: &str) -> bool {
301 xml.contains("<p:notesMasterIdLst>")
302 }
303
304 fn presentation_has_handout(xml: &str) -> bool {
305 xml.contains("<p:handoutMasterIdLst>")
306 }
307
308 fn update_presentation_xml(&mut self, new_slide_count: usize) -> Result<(), PptxError> {
309 if let Some(xml) = self.package.get_part_string("ppt/presentation.xml") {
310 let updated = self.add_slide_to_presentation_xml(&xml, new_slide_count)?;
312 self.package
313 .add_part("ppt/presentation.xml".to_string(), updated.into_bytes());
314 }
315 Ok(())
316 }
317
318 fn add_slide_to_presentation_xml(
319 &self,
320 xml: &str,
321 slide_num: usize,
322 ) -> Result<String, PptxError> {
323 let has_notes = Self::presentation_has_notes(xml);
325 let has_handout = Self::presentation_has_handout(xml);
326 let slide_id = slide_id_value(slide_num);
327 let r_id = slide_rel_id(slide_num, has_notes, has_handout);
328
329 let new_slide_ref = format!("\n<p:sldId id=\"{slide_id}\" r:id=\"rId{r_id}\"/>");
330
331 if let Some(pos) = xml.find("</p:sldIdLst>") {
332 let mut result = xml.to_string();
333 result.insert_str(pos, &new_slide_ref);
334 Ok(result)
335 } else {
336 Ok(xml.to_string())
338 }
339 }
340
341 fn update_presentation_rels(&mut self, new_slide_count: usize) -> Result<(), PptxError> {
342 if let Some(xml) = self
343 .package
344 .get_part_string("ppt/_rels/presentation.xml.rels")
345 {
346 let updated = self.add_slide_to_presentation_rels(&xml, new_slide_count)?;
347 self.package.add_part(
348 "ppt/_rels/presentation.xml.rels".to_string(),
349 updated.into_bytes(),
350 );
351 }
352 Ok(())
353 }
354
355 fn add_slide_to_presentation_rels(
356 &self,
357 xml: &str,
358 slide_num: usize,
359 ) -> Result<String, PptxError> {
360 let has_notes = Self::presentation_has_notes(xml);
361 let has_handout = Self::presentation_has_handout(xml);
362 let r_id = slide_rel_id(slide_num, has_notes, has_handout);
363 let new_rel = format!(
364 "\n<Relationship Id=\"rId{r_id}\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide\" Target=\"slides/slide{slide_num}.xml\"/>"
365 );
366
367 if let Some(pos) = xml.find("</Relationships>") {
368 let mut result = xml.to_string();
369 result.insert_str(pos, &new_rel);
370 Ok(result)
371 } else {
372 Ok(xml.to_string())
373 }
374 }
375
376 fn update_content_types(&mut self, new_slide_count: usize) -> Result<(), PptxError> {
377 if let Some(xml) = self.package.get_part_string("[Content_Types].xml") {
378 let updated = self.add_slide_to_content_types(&xml, new_slide_count)?;
379 self.package
380 .add_part("[Content_Types].xml".to_string(), updated.into_bytes());
381 }
382 Ok(())
383 }
384
385 fn add_slide_to_content_types(&self, xml: &str, slide_num: usize) -> Result<String, PptxError> {
386 let new_override = format!(
387 "\n<Override PartName=\"/ppt/slides/slide{slide_num}.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.presentationml.slide+xml\"/>"
388 );
389
390 if let Some(pos) = xml.find("</Types>") {
391 let mut result = xml.to_string();
392 result.insert_str(pos, &new_override);
393 Ok(result)
394 } else {
395 Ok(xml.to_string())
396 }
397 }
398
399 fn renumber_slide(&mut self, old_num: usize, new_num: usize) -> Result<(), PptxError> {
400 let old_path = format!("ppt/slides/slide{old_num}.xml");
402 let new_path = format!("ppt/slides/slide{new_num}.xml");
403
404 if let Some(content) = self.package.remove_part(&old_path) {
405 self.package.add_part(new_path, content);
406 }
407
408 let old_rels = format!("ppt/slides/_rels/slide{old_num}.xml.rels");
410 let new_rels = format!("ppt/slides/_rels/slide{new_num}.xml.rels");
411
412 if let Some(content) = self.package.remove_part(&old_rels) {
413 self.package.add_part(new_rels, content);
414 }
415
416 Ok(())
417 }
418
419 fn rebuild_presentation_xml(&mut self) -> Result<(), PptxError> {
420 let existing = self
421 .package
422 .get_part_string("ppt/presentation.xml")
423 .unwrap_or_default();
424 let has_notes = Self::presentation_has_notes(&existing);
425 let has_handout = Self::presentation_has_handout(&existing);
426
427 let mut slide_refs = String::new();
428 for i in 1..=self.slide_count {
429 let slide_id = slide_id_value(i);
430 let r_id = slide_rel_id(i, has_notes, has_handout);
431 slide_refs.push_str(&format!(
432 "\n<p:sldId id=\"{slide_id}\" r:id=\"rId{r_id}\"/>"
433 ));
434 }
435
436 let mut xml = String::from(
437 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
438<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" autoCompressPictures="0">
439<p:sldMasterIdLst>
440<p:sldMasterId id="2147483648" r:id="rId1"/>
441</p:sldMasterIdLst>"#,
442 );
443
444 if has_notes {
445 xml.push_str(
446 r#"
447<p:notesMasterIdLst>
448<p:notesMasterId id="2147483650" r:id="rId6"/>
449</p:notesMasterIdLst>"#,
450 );
451 }
452
453 if has_handout {
454 let handout_rid = if has_notes { 7 } else { 6 };
455 xml.push_str(&format!(
456 r#"
457<p:handoutMasterIdLst>
458<p:handoutMasterId id="2147483651" r:id="rId{handout_rid}"/>
459</p:handoutMasterIdLst>"#
460 ));
461 }
462
463 xml.push_str(&format!(
464 r#"
465<p:sldIdLst>{slide_refs}
466</p:sldIdLst>
467<p:sldSz cx="9144000" cy="6858000" type="screen4x3"/>
468<p:notesSz cx="6858000" cy="9144000"/>
469</p:presentation>"#
470 ));
471
472 self.package
473 .add_part("ppt/presentation.xml".to_string(), xml.into_bytes());
474 Ok(())
475 }
476
477 fn rebuild_presentation_rels(&mut self) -> Result<(), PptxError> {
478 let existing = self
479 .package
480 .get_part_string("ppt/presentation.xml")
481 .unwrap_or_default();
482 let has_notes = Self::presentation_has_notes(&existing);
483 let has_handout = Self::presentation_has_handout(&existing);
484
485 let xml = create_presentation_rels_xml_full(self.slide_count, has_notes, has_handout);
486
487 self.package.add_part(
488 "ppt/_rels/presentation.xml.rels".to_string(),
489 xml.into_bytes(),
490 );
491 Ok(())
492 }
493
494 fn rebuild_content_types(&mut self) -> Result<(), PptxError> {
495 let mut slide_overrides = String::new();
496 for i in 1..=self.slide_count {
497 slide_overrides.push_str(&format!(
498 "\n<Override PartName=\"/ppt/slides/slide{i}.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.presentationml.slide+xml\"/>"
499 ));
500 }
501
502 let xml = format!(
503 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
504<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
505<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
506<Default Extension="xml" ContentType="application/xml"/>
507<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>{slide_overrides}
508<Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>
509<Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/>
510<Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>
511<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>
512<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>
513</Types>"#
514 );
515
516 self.package
517 .add_part("[Content_Types].xml".to_string(), xml.into_bytes());
518 Ok(())
519 }
520}
521
522impl Default for PresentationEditor {
523 fn default() -> Self {
524 Self::new()
525 }
526}
527
528#[cfg(test)]
529mod tests {
530 use super::*;
531 use crate::generator::create_pptx_with_content;
532 use crate::oxml::PresentationReader;
533 use std::fs;
534
535 #[test]
536 fn test_open_and_add_slide() {
537 let slides = vec![SlideContent::new("Original Slide 1").add_bullet("Original content")];
539 let pptx_data = create_pptx_with_content("Test", slides).unwrap();
540 fs::write("test_edit.pptx", &pptx_data).unwrap();
541
542 let mut editor = PresentationEditor::open("test_edit.pptx").unwrap();
544 assert_eq!(editor.slide_count(), 1);
545
546 let new_slide = SlideContent::new("New Slide").add_bullet("Added via editor");
547 editor.add_slide(new_slide).unwrap();
548
549 assert_eq!(editor.slide_count(), 2);
550
551 editor.save("test_edit_modified.pptx").unwrap();
553
554 let reader = PresentationReader::open("test_edit_modified.pptx").unwrap();
555 assert_eq!(reader.slide_count(), 2);
556
557 fs::remove_file("test_edit.pptx").ok();
559 fs::remove_file("test_edit_modified.pptx").ok();
560 }
561
562 #[test]
563 fn test_update_slide() {
564 let slides = vec![SlideContent::new("Original Title").add_bullet("Original bullet")];
565 let pptx_data = create_pptx_with_content("Test", slides).unwrap();
566 fs::write("test_update.pptx", &pptx_data).unwrap();
567
568 let mut editor = PresentationEditor::open("test_update.pptx").unwrap();
569
570 let updated = SlideContent::new("Updated Title").add_bullet("Updated bullet");
571 editor.update_slide(0, updated).unwrap();
572
573 editor.save("test_update_modified.pptx").unwrap();
574
575 let reader = PresentationReader::open("test_update_modified.pptx").unwrap();
576 let slide = reader.get_slide(0).unwrap();
577 assert_eq!(slide.title, Some("Updated Title".to_string()));
578
579 fs::remove_file("test_update.pptx").ok();
580 fs::remove_file("test_update_modified.pptx").ok();
581 }
582
583 #[test]
584 fn test_duplicate_slide() {
585 let slides = vec![
586 SlideContent::new("Slide 1").add_bullet("one"),
587 SlideContent::new("Slide 2").add_bullet("two"),
588 ];
589 let pptx_data = create_pptx_with_content("Test", slides).unwrap();
590 fs::write("test_duplicate.pptx", &pptx_data).unwrap();
591
592 let mut editor = PresentationEditor::open("test_duplicate.pptx").unwrap();
593 let new_index = editor.duplicate_slide(0).unwrap();
594 assert_eq!(new_index, 1);
595 assert_eq!(editor.slide_count(), 3);
596
597 editor.save("test_duplicate_modified.pptx").unwrap();
598 let reader = PresentationReader::open("test_duplicate_modified.pptx").unwrap();
599 assert_eq!(reader.slide_count(), 3);
600
601 fs::remove_file("test_duplicate.pptx").ok();
602 fs::remove_file("test_duplicate_modified.pptx").ok();
603 }
604
605 #[test]
606 fn test_insert_slide() {
607 let slides = vec![
608 SlideContent::new("Slide 1").add_bullet("one"),
609 SlideContent::new("Slide 2").add_bullet("two"),
610 ];
611 let pptx_data = create_pptx_with_content("Test", slides).unwrap();
612 fs::write("test_insert.pptx", &pptx_data).unwrap();
613
614 let mut editor = PresentationEditor::open("test_insert.pptx").unwrap();
615 let inserted = editor
616 .insert_slide(1, SlideContent::new("Inserted").add_bullet("new"))
617 .unwrap();
618 assert_eq!(inserted, 1);
619 assert_eq!(editor.slide_count(), 3);
620
621 editor.save("test_insert_modified.pptx").unwrap();
622 let reader = PresentationReader::open("test_insert_modified.pptx").unwrap();
623 assert_eq!(reader.slide_count(), 3);
624 let titles: Vec<_> = (0..reader.slide_count())
625 .map(|i| reader.get_slide(i).unwrap().title.unwrap_or_default())
626 .collect();
627 assert_eq!(titles, vec!["Slide 1", "Inserted", "Slide 2"]);
628
629 fs::remove_file("test_insert.pptx").ok();
630 fs::remove_file("test_insert_modified.pptx").ok();
631 }
632
633 #[test]
634 fn test_reorder_slide() {
635 let slides = vec![
636 SlideContent::new("Slide A").add_bullet("a"),
637 SlideContent::new("Slide B").add_bullet("b"),
638 SlideContent::new("Slide C").add_bullet("c"),
639 ];
640 let pptx_data = create_pptx_with_content("Test", slides).unwrap();
641 fs::write("test_reorder.pptx", &pptx_data).unwrap();
642
643 let mut editor = PresentationEditor::open("test_reorder.pptx").unwrap();
644 editor.reorder_slide(2, 0).unwrap();
645 assert_eq!(editor.slide_count(), 3);
646
647 editor.save("test_reorder_modified.pptx").unwrap();
648 let reader = PresentationReader::open("test_reorder_modified.pptx").unwrap();
649 let titles: Vec<_> = (0..reader.slide_count())
650 .map(|i| reader.get_slide(i).unwrap().title.unwrap_or_default())
651 .collect();
652 assert_eq!(titles, vec!["Slide C", "Slide A", "Slide B"]);
653
654 fs::remove_file("test_reorder.pptx").ok();
655 fs::remove_file("test_reorder_modified.pptx").ok();
656 }
657}