1use std::collections::{BTreeMap, HashMap, HashSet};
6use std::ops::Range;
7
8use ooxml_drawingml::{
9 ColorValue, GradientFill, ShapeFill, ShapeOutline, Theme, resolve_color_value_to_hex_with_theme,
10};
11
12use crate::PptxError;
13use crate::comments::{CommentFlavor, CommentSlide, CommentsWrite, authors_xml, comments_xml};
14use crate::drawing::parse_run_properties;
15use crate::model::{Bullet, PptxPackage, RunProperties, ShapeElements, SlideReference};
16use crate::xml::{
17 DRAWINGML_NS, PRESENTATIONML_NS, ParseBudget, ParseLimits, XmlElement, XmlNode,
18 alternate_content_branch_index, parse_xml, serialize_xml,
19};
20
21const OFFICE_RELATIONSHIPS_NS: &str =
22 "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
23const SLIDE_RELATIONSHIP_TYPE: &str =
24 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide";
25const SLIDE_LAYOUT_RELATIONSHIP_TYPE: &str =
26 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout";
27const SLIDE_CONTENT_TYPE: &str =
28 "application/vnd.openxmlformats-officedocument.presentationml.slide+xml";
29const IMAGE_RELATIONSHIP_TYPE: &str =
30 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
31const MIN_SLIDE_ID: u32 = 256;
32const MAX_SLIDE_ID: u32 = 2_147_483_647;
33
34pub struct DeckWrite {
36 pub slides: Vec<SlideWrite>,
37 pub comments: Option<CommentsWrite>,
38 pub notes: Option<NotesWrite>,
39}
40
41pub struct NotesWrite {
43 pub per_slide: Vec<(CommentSlide, String)>,
44}
45
46const NOTES_SLIDE_RELATIONSHIP_TYPE: &str =
47 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide";
48
49pub enum SlideWrite {
50 Keep { part_path: String },
52 Patch {
54 part_path: String,
55 shapes: Vec<ShapeWrite>,
56 },
57 Add {
59 name: Option<String>,
60 layout_part_path: Option<String>,
61 shapes: Vec<ShapeAdd>,
62 },
63}
64
65pub enum ShapeWrite {
66 Keep {
69 source_index: usize,
70 },
71 Patch {
72 source_index: usize,
73 patch: Box<ShapePatch>,
74 },
75 Add(Box<ShapeAdd>),
76}
77
78#[derive(Default)]
79pub struct ShapePatch {
80 pub offset: Option<(i64, i64)>,
83 pub extent: Option<(i64, i64)>,
84 pub inherited: Option<InheritedTransform>,
86 pub materializes: bool,
89 pub fill: Option<ShapeFill>,
90 pub outline: Option<ShapeOutline>,
92 pub adjust_values: Option<BTreeMap<String, f64>>,
93 pub texts: Vec<TextWrite>,
94 pub children: Vec<ShapeWrite>,
96}
97
98#[derive(Clone, Copy)]
100pub struct InheritedTransform {
101 pub x: i64,
102 pub y: i64,
103 pub width: i64,
104 pub height: i64,
105 pub rotation_deg: f64,
106 pub flip_horizontal: bool,
107 pub flip_vertical: bool,
108}
109
110pub struct TextWrite {
111 pub target: TextTarget,
112 pub paragraphs: Vec<ParagraphWrite>,
113}
114
115pub enum TextTarget {
116 Body,
117 TableCell { row: usize, cell: usize },
118}
119
120pub struct ParagraphWrite {
121 pub source_index: Option<usize>,
123 pub rebuild: bool,
125 pub properties_changed: bool,
126 pub alignment: Option<String>,
127 pub level: u32,
128 pub bullet: Option<Bullet>,
129 pub runs: Vec<RunWrite>,
130}
131
132pub struct RunWrite {
133 pub text: String,
134 pub properties: RunProperties,
135}
136
137pub struct ShapeAdd {
138 pub name: String,
139 pub geometry: String,
140 pub x: i64,
141 pub y: i64,
142 pub width: i64,
143 pub height: i64,
144 pub adjust_values: BTreeMap<String, f64>,
145 pub fill: Option<ShapeFill>,
146 pub outline: Option<ShapeOutline>,
147 pub paragraphs: Option<Vec<ParagraphWrite>>,
148 pub picture: Option<PictureAdd>,
150}
151
152pub struct PictureAdd {
153 pub media_bytes: Vec<u8>,
154 pub content_type: String,
155}
156
157pub fn write_pptx_with_edits(
160 package: &PptxPackage,
161 deck: &DeckWrite,
162) -> Result<Vec<u8>, PptxError> {
163 let limits = ParseLimits::default();
164 let mut budget = ParseBudget::new(&limits);
165
166 let final_paths: HashSet<&str> = deck
167 .slides
168 .iter()
169 .filter_map(|slide| match slide {
170 SlideWrite::Keep { part_path } | SlideWrite::Patch { part_path, .. } => {
171 Some(part_path.as_str())
172 }
173 SlideWrite::Add { .. } => None,
174 })
175 .collect();
176 let removed: Vec<&SlideReference> = package
177 .presentation
178 .slides
179 .iter()
180 .filter(|reference| !final_paths.contains(reference.part_path.as_str()))
181 .collect();
182
183 let mut replacements: HashMap<String, Vec<u8>> = HashMap::new();
184 let mut new_parts: Vec<(String, Vec<u8>)> = Vec::new();
185
186 let mut minted = Vec::new();
187 let mut next_slide_number = next_slide_number(package);
188 let mut minted_slide_id: Option<u32> = None;
189 let mut next_relationship = next_relationship_number(package);
190 let mut structural = false;
191 let mut final_order: Vec<FinalSlide<'_>> = Vec::new();
192 let mut minted_by_slide: HashMap<usize, usize> = HashMap::new();
193 {
194 let mut sink = PartSink {
195 package,
196 replacements: &mut replacements,
197 new_parts: &mut new_parts,
198 };
199
200 for slide in &deck.slides {
201 if let SlideWrite::Patch { part_path, shapes } = slide {
202 let bytes = package
203 .part_bytes(part_path)
204 .ok_or_else(|| PptxError::MissingPart(part_path.clone()))?;
205 let mut root = parse_xml(bytes, part_path, &mut budget)?;
206 let original_root = root.clone();
207 let theme = crate::slide_theme(package, Some(part_path), None);
208 patch_slide(
209 &mut root,
210 shapes,
211 Some(&theme),
212 part_path,
213 package.shape_elements,
214 &mut sink,
215 &mut budget,
216 )?;
217 if root != original_root {
218 sink.store(part_path, serialize_xml(&root));
219 }
220 }
221 }
222
223 for (slide_index, slide) in deck.slides.iter().enumerate() {
224 match slide {
225 SlideWrite::Keep { part_path } | SlideWrite::Patch { part_path, .. } => {
226 let reference = package
227 .presentation
228 .slides
229 .iter()
230 .find(|reference| &reference.part_path == part_path)
231 .ok_or_else(|| write_error(part_path, "not a slide of this deck"))?;
232 final_order.push(FinalSlide::Existing(reference));
233 }
234 SlideWrite::Add {
235 name,
236 layout_part_path,
237 shapes,
238 } => {
239 structural = true;
240 let part_path = format!("ppt/slides/slide{next_slide_number}.xml");
241 next_slide_number += 1;
242 let relationship_id = format!("rId{next_relationship}");
243 next_relationship += 1;
244 let slide_id = match minted_slide_id {
245 None => next_slide_id(package)?,
246 Some(previous) => previous
247 .checked_add(1)
248 .filter(|id| *id <= MAX_SLIDE_ID)
249 .ok_or_else(|| {
250 write_error(
251 &package.presentation.part_path,
252 "the slide id space is exhausted",
253 )
254 })?,
255 };
256 minted_slide_id = Some(slide_id);
257 let layout = layout_part_path
258 .clone()
259 .filter(|path| {
260 package
261 .layouts
262 .iter()
263 .any(|layout| &layout.part_path == path)
264 })
265 .or_else(|| {
266 package
267 .layouts
268 .first()
269 .map(|layout| layout.part_path.clone())
270 });
271 if let Some(layout) = &layout {
272 sink.store(
273 &slide_relationships_path(&part_path),
274 slide_relationships_xml(&part_path, layout),
275 );
276 }
277 let xml =
278 slide_xml(name.as_deref(), shapes, &part_path, &mut sink, &mut budget)?;
279 sink.store(&part_path, xml);
280 minted.push(MintedSlide {
281 part_path,
282 relationship_id,
283 slide_id,
284 });
285 minted_by_slide.insert(slide_index, minted.len() - 1);
286 final_order.push(FinalSlide::Added(minted.len() - 1));
287 }
288 }
289 }
290 }
291 let original_paths: Vec<&str> = package
292 .presentation
293 .slides
294 .iter()
295 .map(|reference| reference.part_path.as_str())
296 .collect();
297 let kept_paths: Vec<&str> = final_order
298 .iter()
299 .filter_map(|slide| match slide {
300 FinalSlide::Existing(reference) => Some(reference.part_path.as_str()),
301 FinalSlide::Added(_) => None,
302 })
303 .collect();
304 structural = structural || kept_paths != original_paths;
305
306 let mut removed_paths = HashSet::new();
307 for reference in &removed {
308 removed_paths.insert(reference.part_path.clone());
309 removed_paths.insert(slide_relationships_path(&reference.part_path));
310 }
311 let orphans = orphaned_parts(package, &removed);
312 for orphan in &orphans {
313 removed_paths.insert(orphan.clone());
314 removed_paths.insert(slide_relationships_path(orphan));
315 }
316
317 if structural {
318 patch_structure(
319 package,
320 &final_order,
321 &minted,
322 &removed,
323 &orphans,
324 &mut replacements,
325 &mut budget,
326 )?;
327 }
328 if let Some(comments) = &deck.comments {
329 patch_comment_parts(
330 package,
331 comments,
332 &MintedSlides {
333 slides: &minted,
334 by_slide_index: &minted_by_slide,
335 },
336 &mut replacements,
337 &mut new_parts,
338 &mut removed_paths,
339 &mut budget,
340 )?;
341 }
342 if let Some(notes) = &deck.notes {
343 patch_notes_parts(
344 package,
345 notes,
346 &MintedSlides {
347 slides: &minted,
348 by_slide_index: &minted_by_slide,
349 },
350 &mut replacements,
351 &mut new_parts,
352 &mut removed_paths,
353 &mut budget,
354 )?;
355 }
356 let mut parts = Vec::with_capacity(package.parts.len() + new_parts.len());
357 for part in &package.parts {
358 if removed_paths.contains(&part.path) {
359 continue;
360 }
361 match replacements.remove(&part.path) {
362 Some(bytes) => parts.push((part.path.clone(), bytes)),
363 None => parts.push((part.path.clone(), part.bytes.clone())),
364 }
365 }
366 parts.extend(new_parts);
367 ooxml_opc::rezip_parts_preserving(&parts, package.source_container.as_bytes())
368 .map_err(PptxError::Container)
369}
370
371struct MintedSlide {
372 part_path: String,
373 relationship_id: String,
374 slide_id: u32,
375}
376
377enum FinalSlide<'a> {
378 Existing(&'a SlideReference),
379 Added(usize),
380}
381
382fn write_error(part: &str, message: impl Into<String>) -> PptxError {
383 PptxError::Write {
384 part: part.to_owned(),
385 message: message.into(),
386 }
387}
388
389fn next_slide_number(package: &PptxPackage) -> usize {
390 let paths: HashSet<&str> = package
391 .parts
392 .iter()
393 .map(|part| part.path.as_str())
394 .collect();
395 let mut number = package
396 .parts
397 .iter()
398 .filter_map(|part| {
399 part.path
400 .strip_prefix("ppt/slides/slide")?
401 .strip_suffix(".xml")?
402 .parse::<usize>()
403 .ok()
404 })
405 .max()
406 .unwrap_or(0)
407 + 1;
408 while paths.contains(format!("ppt/slides/slide{number}.xml").as_str())
409 || paths.contains(format!("ppt/slides/_rels/slide{number}.xml.rels").as_str())
410 {
411 number += 1;
412 }
413 number
414}
415
416fn next_slide_id(package: &PptxPackage) -> Result<u32, PptxError> {
417 let next = package
418 .presentation
419 .slides
420 .iter()
421 .map(|reference| reference.id)
422 .max()
423 .unwrap_or(0)
424 .max(MIN_SLIDE_ID - 1)
425 .checked_add(1)
426 .filter(|id| *id <= MAX_SLIDE_ID);
427 next.ok_or_else(|| {
428 write_error(
429 &package.presentation.part_path,
430 "the slide id space is exhausted",
431 )
432 })
433}
434
435fn next_relationship_number(package: &PptxPackage) -> usize {
436 package
437 .relationships
438 .get(&package.presentation.part_path)
439 .into_iter()
440 .flatten()
441 .filter_map(|relationship| relationship.id.strip_prefix("rId")?.parse::<usize>().ok())
442 .max()
443 .unwrap_or(0)
444 + 1
445}
446
447fn slide_relationships_path(part_path: &str) -> String {
448 match part_path.rsplit_once('/') {
449 Some((directory, name)) => format!("{directory}/_rels/{name}.rels"),
450 None => format!("_rels/{part_path}.rels"),
451 }
452}
453
454fn orphaned_parts(package: &PptxPackage, removed: &[&SlideReference]) -> Vec<String> {
457 if removed.is_empty() {
458 return Vec::new();
459 }
460 let excluded: HashSet<&str> = removed
461 .iter()
462 .map(|reference| reference.part_path.as_str())
463 .collect();
464 let before = reachable_parts(package, &HashSet::new());
465 let after = reachable_parts(package, &excluded);
466 before
467 .into_iter()
468 .filter(|part| !after.contains(part) && !excluded.contains(part.as_str()))
469 .collect()
470}
471
472fn reachable_parts(package: &PptxPackage, excluded: &HashSet<&str>) -> HashSet<String> {
473 let mut reachable = HashSet::new();
474 let mut queue = vec![String::new()];
475 while let Some(source) = queue.pop() {
476 for relationship in package.relationships.get(&source).into_iter().flatten() {
477 let Some(target) = &relationship.resolved_target else {
478 continue;
479 };
480 if excluded.contains(target.as_str()) {
481 continue;
482 }
483 if reachable.insert(target.clone()) {
484 queue.push(target.clone());
485 }
486 }
487 }
488 reachable
489}
490
491fn relative_target(from_part: &str, to_part: &str) -> String {
492 let from: Vec<&str> = from_part.split('/').collect();
493 let to: Vec<&str> = to_part.split('/').collect();
494 let from_directory = &from[..from.len().saturating_sub(1)];
495 let to_directory = &to[..to.len().saturating_sub(1)];
496 let shared = from_directory
497 .iter()
498 .zip(to_directory)
499 .take_while(|(a, b)| a == b)
500 .count();
501 let mut segments: Vec<String> = vec!["..".to_owned(); from_directory.len() - shared];
502 segments.extend(to[shared..].iter().map(|segment| (*segment).to_owned()));
503 segments.join("/")
504}
505
506fn patch_structure(
509 package: &PptxPackage,
510 final_order: &[FinalSlide<'_>],
511 minted: &[MintedSlide],
512 removed: &[&SlideReference],
513 orphans: &[String],
514 replacements: &mut HashMap<String, Vec<u8>>,
515 budget: &mut ParseBudget<'_>,
516) -> Result<(), PptxError> {
517 let presentation_path = package.presentation.part_path.as_str();
518 let bytes = package
519 .part_bytes(presentation_path)
520 .ok_or_else(|| PptxError::MissingPart(presentation_path.to_owned()))?;
521 let mut root = parse_xml(bytes, presentation_path, budget)?;
522 patch_slide_id_list(&mut root, final_order, minted, presentation_path)?;
523 prune_slide_references(&mut root, removed);
524 replacements.insert(presentation_path.to_owned(), serialize_xml(&root));
525
526 let relationships_path = slide_relationships_path(presentation_path);
527 let bytes = package
528 .part_bytes(&relationships_path)
529 .ok_or_else(|| PptxError::MissingPart(relationships_path.clone()))?;
530 let mut root = parse_xml(bytes, &relationships_path, budget)?;
531 let removed_ids: HashSet<&str> = removed
532 .iter()
533 .map(|reference| reference.relationship_id.as_str())
534 .collect();
535 root.children.retain(|child| match child {
536 XmlNode::Element(element) if element.local_name() == "Relationship" => element
537 .attribute("Id")
538 .is_none_or(|id| !removed_ids.contains(id)),
539 _ => true,
540 });
541 for slide in minted {
542 root.children.push(XmlNode::Element(
543 XmlElement::new("Relationship")
544 .with_attribute("Id", slide.relationship_id.clone())
545 .with_attribute("Type", SLIDE_RELATIONSHIP_TYPE)
546 .with_attribute(
547 "Target",
548 relative_target(presentation_path, &slide.part_path),
549 ),
550 ));
551 }
552 replacements.insert(relationships_path, serialize_xml(&root));
553
554 let content_types_path = "[Content_Types].xml";
555 let bytes = match replacements.get(content_types_path) {
556 Some(bytes) => bytes.clone(),
557 None => package
558 .part_bytes(content_types_path)
559 .ok_or_else(|| PptxError::MissingPart(content_types_path.to_owned()))?
560 .to_vec(),
561 };
562 let mut root = parse_xml(&bytes, content_types_path, budget)?;
563 let removed_names: HashSet<String> = removed
564 .iter()
565 .map(|reference| format!("/{}", reference.part_path))
566 .chain(orphans.iter().map(|orphan| format!("/{orphan}")))
567 .collect();
568 root.children.retain(|child| match child {
569 XmlNode::Element(element) if element.local_name() == "Override" => element
570 .attribute("PartName")
571 .is_none_or(|name| !removed_names.contains(name)),
572 _ => true,
573 });
574 for slide in minted {
575 root.children.push(XmlNode::Element(
576 XmlElement::new("Override")
577 .with_attribute("PartName", format!("/{}", slide.part_path))
578 .with_attribute("ContentType", SLIDE_CONTENT_TYPE),
579 ));
580 }
581 replacements.insert(content_types_path.to_owned(), serialize_xml(&root));
582 Ok(())
583}
584
585const PACKAGE_RELATIONSHIPS_NS: &str =
586 "http://schemas.openxmlformats.org/package/2006/relationships";
587
588struct MintedSlides<'a> {
589 slides: &'a [MintedSlide],
590 by_slide_index: &'a HashMap<usize, usize>,
591}
592
593impl MintedSlides<'_> {
594 fn get(&self, slide_index: usize) -> Option<&MintedSlide> {
595 self.by_slide_index
596 .get(&slide_index)
597 .and_then(|at| self.slides.get(*at))
598 }
599}
600
601fn patch_comment_parts(
602 package: &PptxPackage,
603 write: &CommentsWrite,
604 minted: &MintedSlides<'_>,
605 replacements: &mut HashMap<String, Vec<u8>>,
606 new_parts: &mut Vec<(String, Vec<u8>)>,
607 removed_paths: &mut HashSet<String>,
608 budget: &mut ParseBudget<'_>,
609) -> Result<(), PptxError> {
610 let mut taken: HashSet<String> = package
611 .parts
612 .iter()
613 .map(|part| part.path.clone())
614 .chain(
615 package
616 .relationships
617 .values()
618 .flatten()
619 .filter(|relationship| relationship.is_type(write.comments_relationship_type()))
620 .filter_map(|relationship| relationship.resolved_target.clone()),
621 )
622 .collect();
623
624 let mut sink = PartSink {
625 package,
626 replacements,
627 new_parts,
628 };
629 for (slide, comments) in &write.per_slide {
630 let (slide_part_path, slide_id) = match slide {
631 CommentSlide::Existing(part_path) => {
632 let reference = package
633 .presentation
634 .slides
635 .iter()
636 .find(|reference| &reference.part_path == part_path)
637 .ok_or_else(|| write_error(part_path, "not a slide of this deck"))?;
638 (part_path.clone(), reference.id)
639 }
640 CommentSlide::Added(index) => {
641 let Some(slide) = minted.get(*index) else {
642 continue;
643 };
644 (slide.part_path.clone(), slide.slide_id)
645 }
646 };
647 let relationships_path = slide_relationships_path(&slide_part_path);
648 let existing = existing_comment_part(package, &slide_part_path, write);
649
650 if comments.is_empty() {
651 if let Some(part_path) = existing {
652 removed_paths.insert(part_path.clone());
653 sink.forget(&part_path);
654 remove_content_type_override(&mut sink, &part_path, budget)?;
655 remove_relationship(
656 &mut sink,
657 &relationships_path,
658 write.comments_relationship_type(),
659 budget,
660 )?;
661 }
662 continue;
663 }
664
665 let is_existing = existing.is_some();
666 let part_path = match existing {
667 Some(part_path) => part_path,
668 None => mint_comment_part_path(write, &slide_part_path, &mut taken),
669 };
670 removed_paths.remove(&part_path);
671 let bytes = match sink.current(&part_path) {
672 Some(bytes) => crate::comment_patch::patch_comments_xml(
673 &bytes, &part_path, write, comments, slide_id, budget,
674 )?,
675 None => comments_xml(write, comments, slide_id),
676 };
677 sink.store(&part_path, bytes);
678 if !is_existing {
679 set_content_type_override(
680 &mut sink,
681 &part_path,
682 write.comments_content_type(),
683 budget,
684 )?;
685 set_relationship(
686 &mut sink,
687 &relationships_path,
688 write.comments_relationship_type(),
689 &relative_target(&slide_part_path, &part_path),
690 budget,
691 )?;
692 }
693 }
694
695 let presentation_relationships = slide_relationships_path(&package.presentation.part_path);
696 let authors_path = existing_authors_part(package, write)
697 .unwrap_or_else(|| write.authors_part_path().to_owned());
698 if write.authors.is_empty() {
699 removed_paths.insert(authors_path.clone());
700 sink.forget(&authors_path);
701 remove_content_type_override(&mut sink, &authors_path, budget)?;
702 remove_relationship(
703 &mut sink,
704 &presentation_relationships,
705 write.authors_relationship_type(),
706 budget,
707 )?;
708 } else {
709 removed_paths.remove(&authors_path);
710 let existing = sink.current(&authors_path);
711 let bytes = match &existing {
712 Some(bytes) => {
713 crate::comment_patch::patch_authors_xml(bytes, &authors_path, write, budget)?
714 }
715 None => authors_xml(write),
716 };
717 sink.store(&authors_path, bytes);
718 if existing.is_none() {
719 set_content_type_override(
720 &mut sink,
721 &authors_path,
722 write.authors_content_type(),
723 budget,
724 )?;
725 set_relationship(
726 &mut sink,
727 &presentation_relationships,
728 write.authors_relationship_type(),
729 &relative_target(&package.presentation.part_path, &authors_path),
730 budget,
731 )?;
732 }
733 }
734
735 if package.comment_flavor != Some(write.flavor) {
736 drop_other_flavor(package, write, &mut sink, removed_paths, budget)?;
737 }
738 Ok(())
739}
740
741fn patch_notes_parts(
742 package: &PptxPackage,
743 write: &NotesWrite,
744 minted: &MintedSlides<'_>,
745 replacements: &mut HashMap<String, Vec<u8>>,
746 new_parts: &mut Vec<(String, Vec<u8>)>,
747 removed_paths: &mut HashSet<String>,
748 budget: &mut ParseBudget<'_>,
749) -> Result<(), PptxError> {
750 let mut taken: HashSet<String> = package
751 .parts
752 .iter()
753 .map(|part| part.path.clone())
754 .chain(
755 package
756 .relationships
757 .values()
758 .flatten()
759 .filter(|relationship| relationship.is_type(NOTES_SLIDE_RELATIONSHIP_TYPE))
760 .filter_map(|relationship| relationship.resolved_target.clone()),
761 )
762 .collect();
763
764 let mut sink = PartSink {
765 package,
766 replacements,
767 new_parts,
768 };
769 for (slide, text) in &write.per_slide {
770 let slide_part_path = match slide {
771 CommentSlide::Existing(part_path) => part_path.clone(),
772 CommentSlide::Added(index) => {
773 let Some(slide) = minted.get(*index) else {
774 continue;
775 };
776 slide.part_path.clone()
777 }
778 };
779 let relationships_path = slide_relationships_path(&slide_part_path);
780 let existing = existing_notes_part(package, &slide_part_path);
781
782 if text.is_empty() && existing.is_none() {
783 continue;
784 }
785
786 let is_existing = existing.is_some();
787 let part_path = match existing {
788 Some(part_path) => part_path,
789 None => mint_notes_part_path(&slide_part_path, &mut taken),
790 };
791 removed_paths.remove(&part_path);
792 let bytes = match sink.current(&part_path) {
793 Some(bytes) => crate::notes::patch_notes_xml(&bytes, &part_path, text, budget)?,
794 None => crate::notes::notes_slide_xml(text),
795 };
796 sink.store(&part_path, bytes);
797 if !is_existing {
798 set_content_type_override(&mut sink, &part_path, crate::notes::CT_NOTES_SLIDE, budget)?;
799 set_relationship(
800 &mut sink,
801 &relationships_path,
802 NOTES_SLIDE_RELATIONSHIP_TYPE,
803 &relative_target(&slide_part_path, &part_path),
804 budget,
805 )?;
806 let notes_relationships = slide_relationships_path(&part_path);
807 set_relationship(
808 &mut sink,
809 ¬es_relationships,
810 SLIDE_RELATIONSHIP_TYPE,
811 &relative_target(&part_path, &slide_part_path),
812 budget,
813 )?;
814 if let Some(master) = package
815 .relationships
816 .get(&package.presentation.part_path)
817 .into_iter()
818 .flatten()
819 .find(|relationship| relationship.has_type("/notesMaster"))
820 .and_then(|relationship| relationship.resolved_target.as_deref())
821 {
822 set_relationship(
823 &mut sink,
824 ¬es_relationships,
825 &format!("{OFFICE_RELATIONSHIPS_NS}/notesMaster"),
826 &relative_target(&part_path, master),
827 budget,
828 )?;
829 }
830 }
831 }
832 Ok(())
833}
834
835fn existing_notes_part(package: &PptxPackage, slide_part_path: &str) -> Option<String> {
836 package
837 .relationships
838 .get(slide_part_path)?
839 .iter()
840 .find(|relationship| relationship.is_type(NOTES_SLIDE_RELATIONSHIP_TYPE))
841 .and_then(|relationship| relationship.resolved_target.clone())
842}
843
844fn mint_notes_part_path(slide_part_path: &str, taken: &mut HashSet<String>) -> String {
845 let preferred = slide_number(slide_part_path);
846 let mut number = preferred;
847 loop {
848 let candidate = format!("ppt/notesSlides/notesSlide{number}.xml");
849 if taken.insert(candidate.clone()) {
850 return candidate;
851 }
852 number += 1;
853 }
854}
855
856fn mint_comment_part_path(
857 write: &CommentsWrite,
858 slide_part_path: &str,
859 taken: &mut HashSet<String>,
860) -> String {
861 let preferred = slide_number(slide_part_path);
862 let mut number = preferred;
863 loop {
864 let candidate = write.comments_part_path(number);
865 if taken.insert(candidate.clone()) {
866 return candidate;
867 }
868 number += 1;
869 }
870}
871
872fn drop_other_flavor(
873 package: &PptxPackage,
874 write: &CommentsWrite,
875 sink: &mut PartSink<'_>,
876 removed_paths: &mut HashSet<String>,
877 budget: &mut ParseBudget<'_>,
878) -> Result<(), PptxError> {
879 let other = CommentsWrite {
880 flavor: match write.flavor {
881 CommentFlavor::Legacy => CommentFlavor::Modern,
882 CommentFlavor::Modern => CommentFlavor::Legacy,
883 },
884 authors: Vec::new(),
885 per_slide: Vec::new(),
886 };
887 for reference in &package.presentation.slides {
888 let Some(part_path) = existing_comment_part(package, &reference.part_path, &other) else {
889 continue;
890 };
891 removed_paths.insert(part_path.clone());
892 sink.forget(&part_path);
893 remove_content_type_override(sink, &part_path, budget)?;
894 remove_relationship(
895 sink,
896 &slide_relationships_path(&reference.part_path),
897 other.comments_relationship_type(),
898 budget,
899 )?;
900 }
901 let authors_path = existing_authors_part(package, &other)
902 .unwrap_or_else(|| other.authors_part_path().to_owned());
903 if package.part_bytes(&authors_path).is_some() {
904 removed_paths.insert(authors_path.clone());
905 sink.forget(&authors_path);
906 remove_content_type_override(sink, &authors_path, budget)?;
907 remove_relationship(
908 sink,
909 &slide_relationships_path(&package.presentation.part_path),
910 other.authors_relationship_type(),
911 budget,
912 )?;
913 }
914 Ok(())
915}
916
917fn existing_authors_part(package: &PptxPackage, write: &CommentsWrite) -> Option<String> {
918 package
919 .relationships
920 .get(&package.presentation.part_path)?
921 .iter()
922 .find(|relationship| relationship.is_type(write.authors_relationship_type()))
923 .and_then(|relationship| relationship.resolved_target.clone())
924}
925
926fn existing_comment_part(
927 package: &PptxPackage,
928 slide_part_path: &str,
929 write: &CommentsWrite,
930) -> Option<String> {
931 package
932 .relationships
933 .get(slide_part_path)?
934 .iter()
935 .find(|relationship| relationship.is_type(write.comments_relationship_type()))
936 .and_then(|relationship| relationship.resolved_target.clone())
937}
938
939fn slide_number(slide_part_path: &str) -> usize {
940 slide_part_path
941 .rsplit_once("/slide")
942 .and_then(|(_, tail)| tail.strip_suffix(".xml"))
943 .and_then(|digits| digits.parse().ok())
944 .unwrap_or(1)
945}
946
947struct PartSink<'a> {
948 package: &'a PptxPackage,
949 replacements: &'a mut HashMap<String, Vec<u8>>,
950 new_parts: &'a mut Vec<(String, Vec<u8>)>,
951}
952
953impl PartSink<'_> {
954 fn current(&self, path: &str) -> Option<Vec<u8>> {
955 if let Some(bytes) = self.replacements.get(path) {
956 return Some(bytes.clone());
957 }
958 if let Some((_, bytes)) = self.new_parts.iter().find(|(name, _)| name == path) {
959 return Some(bytes.clone());
960 }
961 self.package.part_bytes(path).map(<[u8]>::to_vec)
962 }
963
964 fn store(&mut self, path: &str, bytes: Vec<u8>) {
965 if self.package.part_bytes(path).is_some() {
966 self.replacements.insert(path.to_owned(), bytes);
967 return;
968 }
969 match self.new_parts.iter_mut().find(|(name, _)| name == path) {
970 Some((_, existing)) => *existing = bytes,
971 None => self.new_parts.push((path.to_owned(), bytes)),
972 }
973 }
974
975 fn forget(&mut self, path: &str) {
976 self.replacements.remove(path);
977 self.new_parts.retain(|(name, _)| name != path);
978 }
979}
980
981fn set_relationship(
982 sink: &mut PartSink<'_>,
983 relationships_path: &str,
984 relationship_type: &str,
985 target: &str,
986 budget: &mut ParseBudget<'_>,
987) -> Result<(), PptxError> {
988 let mut root = match sink.current(relationships_path) {
989 Some(bytes) => parse_xml(&bytes, relationships_path, budget)?,
990 None => XmlElement::new("Relationships").with_attribute("xmlns", PACKAGE_RELATIONSHIPS_NS),
991 };
992 let existing = root.children.iter_mut().find_map(|child| match child {
993 XmlNode::Element(element)
994 if element.local_name() == "Relationship"
995 && element.attribute("Type") == Some(relationship_type) =>
996 {
997 Some(element)
998 }
999 _ => None,
1000 });
1001 match existing {
1002 Some(element) => element.set_attribute("Target", target),
1003 None => {
1004 let id = format!("rId{}", max_relationship_id(&root) + 1);
1005 root.children.push(XmlNode::Element(
1006 XmlElement::new("Relationship")
1007 .with_attribute("Id", id)
1008 .with_attribute("Type", relationship_type)
1009 .with_attribute("Target", target),
1010 ));
1011 }
1012 }
1013 sink.store(relationships_path, serialize_xml(&root));
1014 Ok(())
1015}
1016
1017fn remove_relationship(
1018 sink: &mut PartSink<'_>,
1019 relationships_path: &str,
1020 relationship_type: &str,
1021 budget: &mut ParseBudget<'_>,
1022) -> Result<(), PptxError> {
1023 let Some(bytes) = sink.current(relationships_path) else {
1024 return Ok(());
1025 };
1026 let mut root = parse_xml(&bytes, relationships_path, budget)?;
1027 root.children.retain(|child| match child {
1028 XmlNode::Element(element) if element.local_name() == "Relationship" => {
1029 element.attribute("Type") != Some(relationship_type)
1030 }
1031 _ => true,
1032 });
1033 sink.store(relationships_path, serialize_xml(&root));
1034 Ok(())
1035}
1036
1037fn max_relationship_id(root: &XmlElement) -> usize {
1038 root.children_named("Relationship")
1039 .filter_map(|element| element.attribute("Id"))
1040 .filter_map(|id| id.strip_prefix("rId"))
1041 .filter_map(|number| number.parse::<usize>().ok())
1042 .max()
1043 .unwrap_or(0)
1044}
1045
1046fn set_content_type_override(
1047 sink: &mut PartSink<'_>,
1048 part_path: &str,
1049 content_type: &str,
1050 budget: &mut ParseBudget<'_>,
1051) -> Result<(), PptxError> {
1052 let path = "[Content_Types].xml";
1053 let bytes = sink
1054 .current(path)
1055 .ok_or_else(|| PptxError::MissingPart(path.to_owned()))?;
1056 let mut root = parse_xml(&bytes, path, budget)?;
1057 let prefix = root.name.rsplit_once(':').map_or("", |(prefix, _)| prefix);
1058 let override_name = qualified(prefix, "Override");
1059 let name = format!("/{part_path}");
1060 let existing = root.children.iter_mut().find_map(|child| match child {
1061 XmlNode::Element(element)
1062 if element.local_name() == "Override"
1063 && element.attribute("PartName") == Some(name.as_str()) =>
1064 {
1065 Some(element)
1066 }
1067 _ => None,
1068 });
1069 match existing {
1070 Some(element) => element.set_attribute("ContentType", content_type),
1071 None => root.children.push(XmlNode::Element(
1072 XmlElement::new(override_name)
1073 .with_attribute("PartName", name)
1074 .with_attribute("ContentType", content_type),
1075 )),
1076 }
1077 sink.store(path, serialize_xml(&root));
1078 Ok(())
1079}
1080
1081fn remove_content_type_override(
1082 sink: &mut PartSink<'_>,
1083 part_path: &str,
1084 budget: &mut ParseBudget<'_>,
1085) -> Result<(), PptxError> {
1086 let path = "[Content_Types].xml";
1087 let Some(bytes) = sink.current(path) else {
1088 return Ok(());
1089 };
1090 let mut root = parse_xml(&bytes, path, budget)?;
1091 let name = format!("/{part_path}");
1092 root.children.retain(|child| match child {
1093 XmlNode::Element(element) if element.local_name() == "Override" => {
1094 element.attribute("PartName") != Some(name.as_str())
1095 }
1096 _ => true,
1097 });
1098 sink.store(path, serialize_xml(&root));
1099 Ok(())
1100}
1101
1102fn prune_slide_references(root: &mut XmlElement, removed: &[&SlideReference]) {
1105 if removed.is_empty() {
1106 return;
1107 }
1108 let removed_relationship_ids: HashSet<&str> = removed
1109 .iter()
1110 .map(|reference| reference.relationship_id.as_str())
1111 .collect();
1112 let removed_slide_ids: HashSet<String> = removed
1113 .iter()
1114 .map(|reference| reference.id.to_string())
1115 .collect();
1116 for child in root.children.iter_mut() {
1117 let XmlNode::Element(element) = child else {
1118 continue;
1119 };
1120 match element.local_name() {
1121 "custShowLst" => remove_matching_descendants(element, &|candidate| {
1122 candidate.local_name() == "sld"
1123 && candidate.attributes.iter().any(|(key, value)| {
1124 key.ends_with(":id") && removed_relationship_ids.contains(value.as_str())
1125 })
1126 }),
1127 "extLst" => remove_matching_descendants(element, &|candidate| {
1128 candidate.local_name() == "sldId"
1129 && candidate
1130 .attribute("id")
1131 .is_some_and(|id| removed_slide_ids.contains(id))
1132 }),
1133 _ => {}
1134 }
1135 }
1136}
1137
1138fn remove_matching_descendants(element: &mut XmlElement, matches: &dyn Fn(&XmlElement) -> bool) {
1139 element.children.retain(|child| match child {
1140 XmlNode::Element(candidate) => !matches(candidate),
1141 _ => true,
1142 });
1143 for child in element.children.iter_mut() {
1144 if let XmlNode::Element(child) = child {
1145 remove_matching_descendants(child, matches);
1146 }
1147 }
1148}
1149
1150fn patch_slide_id_list(
1151 root: &mut XmlElement,
1152 final_order: &[FinalSlide<'_>],
1153 minted: &[MintedSlide],
1154 part: &str,
1155) -> Result<(), PptxError> {
1156 let presentation_prefix = resolve_prefix(root, PRESENTATIONML_NS, "p");
1157 let relationship_prefix = resolve_prefix(root, OFFICE_RELATIONSHIPS_NS, "r");
1158 let list_name = qualified(&presentation_prefix, "sldIdLst");
1159 if root.child_mut("sldIdLst").is_none() {
1160 let position = root
1161 .children
1162 .iter()
1163 .position(|child| {
1164 matches!(child, XmlNode::Element(element) if element.local_name() == "sldMasterIdLst")
1165 })
1166 .map(|index| index + 1)
1167 .unwrap_or(0);
1168 root.children.insert(
1169 position,
1170 XmlNode::Element(XmlElement::new(list_name.clone())),
1171 );
1172 }
1173 let relationship_attribute = qualified(&relationship_prefix, "id");
1174 let list = root
1175 .child_mut("sldIdLst")
1176 .ok_or_else(|| write_error(part, "missing slide id list"))?;
1177 let mut existing: HashMap<String, XmlElement> = HashMap::new();
1178 for child in std::mem::take(&mut list.children) {
1179 if let XmlNode::Element(element) = child
1180 && element.local_name() == "sldId"
1181 {
1182 let relationship_id = element.attribute(&relationship_attribute).or_else(|| {
1183 element
1184 .attributes
1185 .iter()
1186 .find(|(key, _)| key.ends_with(":id"))
1187 .map(|(_, value)| value.as_str())
1188 });
1189 if let Some(id) = relationship_id {
1190 existing.insert(id.to_owned(), element);
1191 }
1192 }
1193 }
1194 let entry_name = qualified(&presentation_prefix, "sldId");
1195 for slide in final_order {
1196 let element = match slide {
1197 FinalSlide::Existing(reference) => existing
1198 .remove(&reference.relationship_id)
1199 .unwrap_or_else(|| {
1200 XmlElement::new(entry_name.clone())
1201 .with_attribute("id", reference.id.to_string())
1202 .with_attribute(
1203 relationship_attribute.clone(),
1204 reference.relationship_id.clone(),
1205 )
1206 }),
1207 FinalSlide::Added(index) => {
1208 let slide = &minted[*index];
1209 XmlElement::new(entry_name.clone())
1210 .with_attribute("id", slide.slide_id.to_string())
1211 .with_attribute(
1212 relationship_attribute.clone(),
1213 slide.relationship_id.clone(),
1214 )
1215 }
1216 };
1217 list.children.push(XmlNode::Element(element));
1218 }
1219 Ok(())
1220}
1221
1222fn find_prefix(root: &XmlElement, namespace: &str) -> Option<String> {
1225 root.attributes.iter().find_map(|(key, value)| {
1226 if value != namespace {
1227 return None;
1228 }
1229 if key == "xmlns" {
1230 Some(String::new())
1231 } else {
1232 key.strip_prefix("xmlns:").map(str::to_owned)
1233 }
1234 })
1235}
1236
1237fn resolve_prefix(root: &mut XmlElement, namespace: &str, fallback: &str) -> String {
1238 if let Some(prefix) = find_prefix(root, namespace) {
1239 return prefix;
1240 }
1241 root.set_attribute(format!("xmlns:{fallback}"), namespace);
1242 fallback.to_owned()
1243}
1244
1245fn qualified(prefix: &str, local: &str) -> String {
1246 if prefix.is_empty() {
1247 local.to_owned()
1248 } else {
1249 format!("{prefix}:{local}")
1250 }
1251}
1252
1253struct Prefixes {
1254 drawing: String,
1255 presentation: String,
1256 relationship: String,
1257}
1258
1259impl Prefixes {
1260 fn from_root(root: &mut XmlElement) -> Self {
1261 Self {
1262 drawing: resolve_prefix(root, DRAWINGML_NS, "a"),
1263 presentation: resolve_prefix(root, PRESENTATIONML_NS, "p"),
1264 relationship: resolve_prefix(root, OFFICE_RELATIONSHIPS_NS, "r"),
1265 }
1266 }
1267
1268 fn drawing(&self, local: &str) -> String {
1269 qualified(&self.drawing, local)
1270 }
1271
1272 fn presentation(&self, local: &str) -> String {
1273 qualified(&self.presentation, local)
1274 }
1275
1276 fn relationship(&self, local: &str) -> String {
1277 qualified(&self.relationship, local)
1278 }
1279}
1280
1281fn patch_slide(
1285 root: &mut XmlElement,
1286 shapes: &[ShapeWrite],
1287 theme: Option<&Theme>,
1288 part: &str,
1289 elements: ShapeElements,
1290 sink: &mut PartSink<'_>,
1291 budget: &mut ParseBudget<'_>,
1292) -> Result<(), PptxError> {
1293 let prefixes = Prefixes::from_root(root);
1294 let mut next_shape_id = max_shape_id(root).checked_add(1);
1295 let tree = root
1296 .child_mut("cSld")
1297 .and_then(|common| common.child_mut("spTree"))
1298 .ok_or_else(|| write_error(part, "slide has no shape tree"))?;
1299 patch_shape_children(
1300 tree,
1301 shapes,
1302 &mut next_shape_id,
1303 theme,
1304 &prefixes,
1305 part,
1306 elements,
1307 sink,
1308 budget,
1309 )
1310}
1311
1312fn max_shape_id(root: &XmlElement) -> u32 {
1313 root.descendants_named("cNvPr")
1314 .iter()
1315 .filter_map(|element| element.attribute("id")?.parse::<u32>().ok())
1316 .max()
1317 .unwrap_or(1)
1318}
1319
1320fn alloc_shape_id(next_shape_id: &mut Option<u32>, part: &str) -> Result<u32, PptxError> {
1321 let shape_id =
1322 next_shape_id.ok_or_else(|| write_error(part, "the shape id space is exhausted"))?;
1323 if shape_id == 0 {
1324 return Err(write_error(part, "the shape id space is exhausted"));
1325 }
1326 *next_shape_id = shape_id.checked_add(1);
1327 Ok(shape_id)
1328}
1329
1330struct ShapeSlot {
1333 position: usize,
1334 path: Vec<usize>,
1335}
1336
1337#[allow(clippy::too_many_arguments)]
1341fn patch_shape_children(
1342 parent: &mut XmlElement,
1343 writes: &[ShapeWrite],
1344 next_shape_id: &mut Option<u32>,
1345 theme: Option<&Theme>,
1346 prefixes: &Prefixes,
1347 part: &str,
1348 elements: ShapeElements,
1349 sink: &mut PartSink<'_>,
1350 budget: &mut ParseBudget<'_>,
1351) -> Result<(), PptxError> {
1352 let mut slots: Vec<Option<XmlNode>> = std::mem::take(&mut parent.children)
1353 .into_iter()
1354 .map(Some)
1355 .collect();
1356 let mut shape_slots = shape_slots(&slots, elements);
1357 let mut kept = vec![false; shape_slots.len()];
1358 for write in writes {
1359 if let ShapeWrite::Keep { source_index } | ShapeWrite::Patch { source_index, .. } = write
1360 && let Some(flag) = kept.get_mut(*source_index)
1361 {
1362 *flag = true;
1363 }
1364 }
1365 let mut nested: BTreeMap<usize, (usize, Vec<Vec<usize>>)> = BTreeMap::new();
1368 for (slot, kept) in shape_slots.iter().zip(&kept) {
1369 if slot.path.is_empty() {
1370 continue;
1371 }
1372 let entry = nested.entry(slot.position).or_default();
1373 entry.0 += 1;
1374 if !kept {
1375 entry.1.push(slot.path.clone());
1376 }
1377 }
1378 for (position, (total, mut paths)) in nested {
1379 if paths.len() == total && branch_holds_only(&slots[position], &paths) {
1380 slots[position] = None;
1381 continue;
1382 }
1383 paths.sort();
1384 for path in paths.iter().rev() {
1385 let Some(node) = slots[position].as_mut() else {
1386 continue;
1387 };
1388 remove_at_path(node, path);
1389 for slot in shape_slots
1390 .iter_mut()
1391 .filter(|slot| slot.position == position)
1392 {
1393 shift_path(&mut slot.path, path);
1394 }
1395 }
1396 }
1397 let mut children = Vec::with_capacity(slots.len());
1398 let mut hosts: BTreeMap<usize, usize> = BTreeMap::new();
1399 let first_ext_list = |slots: &[Option<XmlNode>]| {
1400 slots
1401 .iter()
1402 .position(|slot| {
1403 matches!(slot, Some(XmlNode::Element(element)) if element.local_name() == "extLst")
1404 })
1405 .unwrap_or(slots.len())
1406 };
1407 let prologue_end = shape_slots
1408 .first()
1409 .map(|slot| slot.position)
1410 .unwrap_or_else(|| first_ext_list(&slots));
1411 emit_sibling_slots(&mut slots, &mut children, prologue_end, elements);
1412 let last_kept = writes
1413 .iter()
1414 .rposition(|write| !matches!(write, ShapeWrite::Add(_)));
1415 for (index, write) in writes.iter().enumerate() {
1416 match write {
1417 ShapeWrite::Keep { source_index } | ShapeWrite::Patch { source_index, .. } => {
1418 let slot = shape_slots.get(*source_index).ok_or_else(|| {
1419 write_error(part, format!("shape index {source_index} is not available"))
1420 })?;
1421 emit_sibling_slots(&mut slots, &mut children, slot.position, elements);
1422 let target = emit_source_element(&mut slots, &mut children, &mut hosts, slot)
1423 .ok_or_else(|| {
1424 write_error(
1425 part,
1426 format!("shape index {source_index} was already written"),
1427 )
1428 })?;
1429 if let ShapeWrite::Patch { patch, .. } = write {
1430 let element =
1431 element_at_path(&mut children[target], &slot.path).ok_or_else(|| {
1432 write_error(
1433 part,
1434 format!("shape index {source_index} is not available"),
1435 )
1436 })?;
1437 patch_shape(
1438 element,
1439 patch,
1440 next_shape_id,
1441 theme,
1442 prefixes,
1443 part,
1444 elements,
1445 sink,
1446 budget,
1447 )?;
1448 }
1449 }
1450 ShapeWrite::Add(add) => {
1451 if last_kept.is_none_or(|kept| index > kept) {
1454 let flush_end = first_ext_list(&slots);
1455 emit_sibling_slots(&mut slots, &mut children, flush_end, elements);
1456 }
1457 children.push(XmlNode::Element(add_shape_element(
1458 add,
1459 next_shape_id,
1460 prefixes,
1461 part,
1462 sink,
1463 budget,
1464 )?));
1465 }
1466 }
1467 }
1468 for slot in slots.into_iter().flatten() {
1469 if !matches!(&slot, XmlNode::Element(element) if elements.contains(element.local_name())) {
1470 children.push(slot);
1471 }
1472 }
1473 parent.children = children;
1474 Ok(())
1475}
1476
1477fn shape_slots(slots: &[Option<XmlNode>], elements: ShapeElements) -> Vec<ShapeSlot> {
1479 let mut found = Vec::new();
1480 for (position, slot) in slots.iter().enumerate() {
1481 let Some(XmlNode::Element(element)) = slot else {
1482 continue;
1483 };
1484 if elements.contains(element.local_name()) {
1485 found.push(ShapeSlot {
1486 position,
1487 path: Vec::new(),
1488 });
1489 } else if element.local_name() == "AlternateContent" {
1490 collect_branch_slots(element, position, &mut Vec::new(), elements, &mut found);
1491 }
1492 }
1493 found
1494}
1495
1496fn collect_branch_slots(
1497 alternate: &XmlElement,
1498 position: usize,
1499 path: &mut Vec<usize>,
1500 elements: ShapeElements,
1501 found: &mut Vec<ShapeSlot>,
1502) {
1503 let Some(index) = alternate_content_branch_index(alternate) else {
1504 return;
1505 };
1506 let XmlNode::Element(branch) = &alternate.children[index] else {
1507 return;
1508 };
1509 path.push(index);
1510 for (index, child) in branch.children.iter().enumerate() {
1511 let XmlNode::Element(child) = child else {
1512 continue;
1513 };
1514 path.push(index);
1515 if elements.contains(child.local_name()) {
1516 found.push(ShapeSlot {
1517 position,
1518 path: path.clone(),
1519 });
1520 } else if child.local_name() == "AlternateContent" {
1521 collect_branch_slots(child, position, path, elements, found);
1522 }
1523 path.pop();
1524 }
1525 path.pop();
1526}
1527
1528fn emit_source_element(
1531 slots: &mut [Option<XmlNode>],
1532 children: &mut Vec<XmlNode>,
1533 hosts: &mut BTreeMap<usize, usize>,
1534 slot: &ShapeSlot,
1535) -> Option<usize> {
1536 if let Some(host) = hosts.get(&slot.position) {
1537 return Some(*host);
1538 }
1539 let XmlNode::Element(element) = slots[slot.position].take()? else {
1540 return None;
1541 };
1542 children.push(XmlNode::Element(element));
1543 if !slot.path.is_empty() {
1544 hosts.insert(slot.position, children.len() - 1);
1545 }
1546 Some(children.len() - 1)
1547}
1548
1549fn branch_holds_only(slot: &Option<XmlNode>, paths: &[Vec<usize>]) -> bool {
1551 let Some(XmlNode::Element(alternate)) = slot else {
1552 return true;
1553 };
1554 let Some(index) = alternate_content_branch_index(alternate) else {
1555 return true;
1556 };
1557 let XmlNode::Element(branch) = &alternate.children[index] else {
1558 return true;
1559 };
1560 branch.children.iter().enumerate().all(|(child, node)| {
1561 !matches!(node, XmlNode::Element(_)) || paths.iter().any(|path| path == &[index, child])
1562 })
1563}
1564
1565fn remove_at_path(node: &mut XmlNode, path: &[usize]) {
1566 let Some((last, parents)) = path.split_last() else {
1567 return;
1568 };
1569 if let Some(parent) = element_at_path(node, parents)
1570 && *last < parent.children.len()
1571 {
1572 parent.children.remove(*last);
1573 }
1574}
1575
1576fn shift_path(path: &mut [usize], removed: &[usize]) {
1578 let Some((last, parents)) = removed.split_last() else {
1579 return;
1580 };
1581 if path.len() > parents.len() && path.starts_with(parents) && path[parents.len()] > *last {
1582 path[parents.len()] -= 1;
1583 }
1584}
1585
1586fn element_at_path<'a>(node: &'a mut XmlNode, path: &[usize]) -> Option<&'a mut XmlElement> {
1587 let XmlNode::Element(element) = node else {
1588 return None;
1589 };
1590 let mut current = element;
1591 for index in path {
1592 match current.children.get_mut(*index)? {
1593 XmlNode::Element(child) => current = child,
1594 XmlNode::Text(_) => return None,
1595 }
1596 }
1597 Some(current)
1598}
1599
1600fn emit_sibling_slots(
1602 slots: &mut [Option<XmlNode>],
1603 children: &mut Vec<XmlNode>,
1604 position: usize,
1605 elements: ShapeElements,
1606) {
1607 for slot in slots.iter_mut().take(position) {
1608 let is_other = !matches!(
1609 slot,
1610 Some(XmlNode::Element(element)) if elements.contains(element.local_name())
1611 );
1612 if is_other && let Some(node) = slot.take() {
1613 children.push(node);
1614 }
1615 }
1616}
1617
1618#[allow(clippy::too_many_arguments)]
1619fn patch_shape(
1620 element: &mut XmlElement,
1621 patch: &ShapePatch,
1622 next_shape_id: &mut Option<u32>,
1623 theme: Option<&Theme>,
1624 prefixes: &Prefixes,
1625 part: &str,
1626 elements: ShapeElements,
1627 sink: &mut PartSink<'_>,
1628 budget: &mut ParseBudget<'_>,
1629) -> Result<(), PptxError> {
1630 if patch.offset.is_some() || patch.extent.is_some() {
1631 patch_transform(element, patch, prefixes, part)?;
1632 }
1633 if let Some(fill) = &patch.fill {
1634 let properties = shape_properties_mut(element, part)?;
1635 set_fill(properties, fill, prefixes, part)?;
1636 }
1637 if let Some(outline) = &patch.outline {
1638 let properties = shape_properties_mut(element, part)?;
1639 set_outline(properties, outline, prefixes);
1640 }
1641 if let Some(adjust_values) = &patch.adjust_values {
1642 let properties = shape_properties_mut(element, part)?;
1643 set_adjust_values(properties, adjust_values, prefixes, part)?;
1644 }
1645 for text in &patch.texts {
1646 patch_text(element, text, theme, prefixes, part)?;
1647 }
1648 if !patch.children.is_empty() {
1649 patch_shape_children(
1650 element,
1651 &patch.children,
1652 next_shape_id,
1653 theme,
1654 prefixes,
1655 part,
1656 elements,
1657 sink,
1658 budget,
1659 )?;
1660 }
1661 Ok(())
1662}
1663
1664fn shape_properties_mut<'a>(
1665 element: &'a mut XmlElement,
1666 part: &str,
1667) -> Result<&'a mut XmlElement, PptxError> {
1668 let container = match element.local_name() {
1669 "grpSp" => "grpSpPr",
1670 _ => "spPr",
1671 };
1672 element
1673 .child_mut(container)
1674 .ok_or_else(|| write_error(part, "shape has no properties element"))
1675}
1676
1677fn patch_transform(
1682 element: &mut XmlElement,
1683 patch: &ShapePatch,
1684 prefixes: &Prefixes,
1685 part: &str,
1686) -> Result<(), PptxError> {
1687 let offset = patch
1688 .offset
1689 .or(patch.inherited.map(|inherited| (inherited.x, inherited.y)));
1690 let extent = patch.extent.or(patch
1691 .inherited
1692 .map(|inherited| (inherited.width, inherited.height)));
1693 let (container, transform_name) = match element.local_name() {
1694 "graphicFrame" => (None, prefixes.presentation("xfrm")),
1695 "grpSp" => (Some("grpSpPr"), prefixes.drawing("xfrm")),
1696 _ => (Some("spPr"), prefixes.drawing("xfrm")),
1697 };
1698 let parent = match container {
1699 Some(name) => element
1700 .child_mut(name)
1701 .ok_or_else(|| write_error(part, "shape has no properties element"))?,
1702 None => element,
1703 };
1704 let created = parent.child_mut("xfrm").is_none();
1705 if created {
1706 if offset.is_none() || extent.is_none() {
1707 return Err(write_error(
1708 part,
1709 "cannot write half a transform onto a shape without one",
1710 ));
1711 }
1712 let position = parent
1713 .children
1714 .iter()
1715 .position(|child| {
1716 matches!(child, XmlNode::Element(element) if element.local_name() != "nvGraphicFramePr")
1717 })
1718 .unwrap_or(parent.children.len());
1719 parent
1720 .children
1721 .insert(position, XmlNode::Element(XmlElement::new(transform_name)));
1722 }
1723 let transform = parent.child_mut("xfrm").expect("transform ensured above");
1724 if (created || patch.materializes)
1725 && let Some(inherited) = patch.inherited
1726 {
1727 if inherited.rotation_deg != 0.0 {
1728 transform.set_attribute("rot", format_fixed(inherited.rotation_deg * 60_000.0));
1729 }
1730 if inherited.flip_horizontal {
1731 transform.set_attribute("flipH", "1");
1732 }
1733 if inherited.flip_vertical {
1734 transform.set_attribute("flipV", "1");
1735 }
1736 }
1737 if let Some((x, y)) = offset
1738 && (patch.offset.is_some() || transform.child_mut("off").is_none())
1739 {
1740 if transform.child_mut("off").is_none() {
1741 transform.children.insert(
1742 0,
1743 XmlNode::Element(XmlElement::new(prefixes.drawing("off"))),
1744 );
1745 }
1746 let element = transform.child_mut("off").expect("offset ensured above");
1747 element.set_attribute("x", x.to_string());
1748 element.set_attribute("y", y.to_string());
1749 }
1750 if let Some((width, height)) = extent
1751 && (patch.extent.is_some() || transform.child_mut("ext").is_none())
1752 {
1753 if transform.child_mut("ext").is_none() {
1754 let position = transform
1755 .children
1756 .iter()
1757 .position(
1758 |child| matches!(child, XmlNode::Element(element) if element.local_name() == "off"),
1759 )
1760 .map(|index| index + 1)
1761 .unwrap_or(transform.children.len());
1762 transform.children.insert(
1763 position,
1764 XmlNode::Element(XmlElement::new(prefixes.drawing("ext"))),
1765 );
1766 }
1767 let element = transform.child_mut("ext").expect("extent ensured above");
1768 element.set_attribute("cx", width.to_string());
1769 element.set_attribute("cy", height.to_string());
1770 }
1771 Ok(())
1772}
1773
1774const FILL_ELEMENTS: [&str; 6] = [
1775 "noFill",
1776 "solidFill",
1777 "gradFill",
1778 "blipFill",
1779 "pattFill",
1780 "grpFill",
1781];
1782const POST_FILL_ELEMENTS: [&str; 6] = ["ln", "effectLst", "effectDag", "scene3d", "sp3d", "extLst"];
1783
1784fn set_fill(
1785 properties: &mut XmlElement,
1786 fill: &ShapeFill,
1787 prefixes: &Prefixes,
1788 part: &str,
1789) -> Result<(), PptxError> {
1790 let element = fill_element(fill, prefixes, part)?;
1791 let existing = properties.children.iter().position(|child| {
1792 matches!(child, XmlNode::Element(element) if FILL_ELEMENTS.contains(&element.local_name()))
1793 });
1794 match existing {
1795 Some(index) => properties.children[index] = XmlNode::Element(element),
1796 None => {
1797 let position = properties
1798 .children
1799 .iter()
1800 .position(|child| {
1801 matches!(
1802 child,
1803 XmlNode::Element(element)
1804 if POST_FILL_ELEMENTS.contains(&element.local_name())
1805 )
1806 })
1807 .unwrap_or(properties.children.len());
1808 properties
1809 .children
1810 .insert(position, XmlNode::Element(element));
1811 }
1812 }
1813 Ok(())
1814}
1815
1816fn fill_element(
1817 fill: &ShapeFill,
1818 prefixes: &Prefixes,
1819 part: &str,
1820) -> Result<XmlElement, PptxError> {
1821 match fill.fill_type.as_str() {
1822 "none" => Ok(XmlElement::new(prefixes.drawing("noFill"))),
1823 "solid" => {
1824 let color = fill
1825 .color
1826 .as_ref()
1827 .and_then(|color| color_element(color, prefixes))
1828 .ok_or_else(|| write_error(part, "solid fill without a serializable color"))?;
1829 Ok(XmlElement::new(prefixes.drawing("solidFill")).with_child(color))
1830 }
1831 "gradient" => {
1832 let gradient = fill
1833 .gradient
1834 .as_ref()
1835 .ok_or_else(|| write_error(part, "gradient fill without stops"))?;
1836 gradient_element(gradient, prefixes)
1837 .ok_or_else(|| write_error(part, "gradient stop without a color"))
1838 }
1839 other => Err(write_error(
1840 part,
1841 format!("unsupported fill type {other:?}"),
1842 )),
1843 }
1844}
1845
1846fn gradient_element(gradient: &GradientFill, prefixes: &Prefixes) -> Option<XmlElement> {
1847 let mut stops = XmlElement::new(prefixes.drawing("gsLst"));
1848 for stop in &gradient.stops {
1849 stops = stops.with_child(
1850 XmlElement::new(prefixes.drawing("gs"))
1851 .with_attribute("pos", format_fixed(stop.position))
1852 .with_child(color_element(&stop.color, prefixes)?),
1853 );
1854 }
1855 let mut element = XmlElement::new(prefixes.drawing("gradFill")).with_child(stops);
1856 match gradient.gradient_type.as_str() {
1857 "linear" => {
1858 element = element.with_child(
1859 XmlElement::new(prefixes.drawing("lin"))
1860 .with_attribute(
1861 "ang",
1862 format_fixed(gradient.angle.unwrap_or_default() * 60_000.0),
1863 )
1864 .with_attribute("scaled", "1"),
1865 );
1866 }
1867 kind => {
1868 let path = match kind {
1869 "radial" => "circle",
1870 "rectangular" => "rect",
1871 _ => "shape",
1872 };
1873 element = element
1874 .with_child(XmlElement::new(prefixes.drawing("path")).with_attribute("path", path));
1875 }
1876 }
1877 Some(element)
1878}
1879
1880fn color_element(color: &ColorValue, prefixes: &Prefixes) -> Option<XmlElement> {
1881 let mut element = if let Some(rgb) = &color.rgb {
1882 XmlElement::new(prefixes.drawing("srgbClr")).with_attribute("val", rgb.clone())
1883 } else if let Some(theme) = &color.theme_color {
1884 XmlElement::new(prefixes.drawing("schemeClr"))
1885 .with_attribute("val", denormalize_scheme_color(theme))
1886 } else {
1887 return None;
1888 };
1889 let fractions = [
1890 ("alpha", color.alpha),
1891 ("lumMod", color.luminance_modulation),
1892 ("lumOff", color.luminance_offset),
1893 ("satMod", color.saturation_modulation),
1894 ];
1895 for (name, value) in fractions {
1896 if let Some(value) = value {
1897 element = element.with_child(
1898 XmlElement::new(prefixes.drawing(name))
1899 .with_attribute("val", format_fixed(value * 100_000.0)),
1900 );
1901 }
1902 }
1903 let modifiers = [("tint", &color.theme_tint), ("shade", &color.theme_shade)];
1904 for (name, value) in modifiers {
1905 if let Some(byte) = value
1906 .as_deref()
1907 .and_then(|value| u8::from_str_radix(value, 16).ok())
1908 {
1909 element = element.with_child(
1910 XmlElement::new(prefixes.drawing(name))
1911 .with_attribute("val", format_fixed(f64::from(byte) / 255.0 * 100_000.0)),
1912 );
1913 }
1914 }
1915 Some(element)
1916}
1917
1918fn denormalize_scheme_color(value: &str) -> String {
1919 match value {
1920 "text1" => "tx1",
1921 "text2" => "tx2",
1922 "background1" => "bg1",
1923 "background2" => "bg2",
1924 value => value,
1925 }
1926 .to_owned()
1927}
1928
1929fn format_fixed(value: f64) -> String {
1930 (value.round() as i64).to_string()
1931}
1932
1933fn set_outline(properties: &mut XmlElement, outline: &ShapeOutline, prefixes: &Prefixes) {
1934 if let Some(line) = properties.child_mut("ln")
1935 && let Some(mut original) = crate::drawing::parse_outline_element(line)
1936 {
1937 original.width = outline.width;
1938 if original == *outline {
1939 if let Some(width) = outline.width {
1940 line.set_attribute("w", format_fixed(width));
1941 } else {
1942 line.attributes.remove("w");
1943 }
1944 return;
1945 }
1946 }
1947 let element = outline_element(outline, prefixes);
1948 let existing = properties.children.iter().position(
1949 |child| matches!(child, XmlNode::Element(element) if element.local_name() == "ln"),
1950 );
1951 match existing {
1952 Some(index) => properties.children[index] = XmlNode::Element(element),
1953 None => {
1954 let position = properties
1955 .children
1956 .iter()
1957 .position(|child| {
1958 matches!(
1959 child,
1960 XmlNode::Element(element)
1961 if POST_FILL_ELEMENTS[1..].contains(&element.local_name())
1962 )
1963 })
1964 .unwrap_or(properties.children.len());
1965 properties
1966 .children
1967 .insert(position, XmlNode::Element(element));
1968 }
1969 }
1970}
1971
1972fn outline_element(outline: &ShapeOutline, prefixes: &Prefixes) -> XmlElement {
1973 let mut element = XmlElement::new(prefixes.drawing("ln"));
1974 if *outline == ShapeOutline::default() {
1975 return element.with_child(XmlElement::new(prefixes.drawing("noFill")));
1976 }
1977 if let Some(width) = outline.width {
1978 element.set_attribute("w", format_fixed(width));
1979 }
1980 if let Some(cap) = &outline.cap {
1981 element.set_attribute("cap", cap.clone());
1982 }
1983 if let Some(color) = outline
1984 .color
1985 .as_ref()
1986 .and_then(|color| color_element(color, prefixes))
1987 {
1988 element =
1989 element.with_child(XmlElement::new(prefixes.drawing("solidFill")).with_child(color));
1990 } else if let Some(gradient) = outline
1991 .gradient
1992 .as_ref()
1993 .and_then(|gradient| gradient_element(gradient, prefixes))
1994 {
1995 element = element.with_child(gradient);
1996 }
1997 if let Some(style) = &outline.style {
1998 element = element.with_child(
1999 XmlElement::new(prefixes.drawing("prstDash")).with_attribute("val", style.clone()),
2000 );
2001 }
2002 if let Some(join) = &outline.join {
2003 element = element.with_child(XmlElement::new(prefixes.drawing(join)));
2004 }
2005 let ends = [
2006 ("headEnd", &outline.head_end),
2007 ("tailEnd", &outline.tail_end),
2008 ];
2009 for (name, end) in ends {
2010 if let Some(end) = end {
2011 let mut end_element = XmlElement::new(prefixes.drawing(name))
2012 .with_attribute("type", end.end_type.clone());
2013 if let Some(width) = &end.width {
2014 end_element.set_attribute("w", width.clone());
2015 }
2016 if let Some(length) = &end.length {
2017 end_element.set_attribute("len", length.clone());
2018 }
2019 element = element.with_child(end_element);
2020 }
2021 }
2022 element
2023}
2024
2025fn set_adjust_values(
2026 properties: &mut XmlElement,
2027 adjust_values: &BTreeMap<String, f64>,
2028 prefixes: &Prefixes,
2029 part: &str,
2030) -> Result<(), PptxError> {
2031 let Some(geometry) = properties.child_mut("prstGeom") else {
2032 return Err(write_error(
2033 part,
2034 "cannot write adjustments onto a shape without preset geometry",
2035 ));
2036 };
2037 let list_name = prefixes.drawing("avLst");
2038 if geometry.child_mut("avLst").is_none() {
2039 geometry
2040 .children
2041 .insert(0, XmlNode::Element(XmlElement::new(list_name)));
2042 }
2043 let list = geometry.child_mut("avLst").expect("list ensured above");
2044 for (name, value) in adjust_values {
2046 let formula = format!("val {}", format_fixed(value * 100_000.0));
2047 let existing = list.children.iter_mut().find_map(|child| match child {
2048 XmlNode::Element(element)
2049 if element.local_name() == "gd" && element.attribute("name") == Some(name) =>
2050 {
2051 Some(element)
2052 }
2053 _ => None,
2054 });
2055 match existing {
2056 Some(guide) => guide.set_attribute("fmla", formula),
2057 None => list.children.push(XmlNode::Element(
2058 XmlElement::new(prefixes.drawing("gd"))
2059 .with_attribute("name", name.clone())
2060 .with_attribute("fmla", formula),
2061 )),
2062 }
2063 }
2064 Ok(())
2065}
2066
2067fn patch_text(
2070 element: &mut XmlElement,
2071 text: &TextWrite,
2072 theme: Option<&Theme>,
2073 prefixes: &Prefixes,
2074 part: &str,
2075) -> Result<(), PptxError> {
2076 let body = match &text.target {
2077 TextTarget::Body => element.child_mut("txBody"),
2078 TextTarget::TableCell { row, cell } => element
2079 .child_mut("graphic")
2080 .and_then(|graphic| graphic.child_mut("graphicData"))
2081 .and_then(|data| data.child_mut("tbl"))
2082 .and_then(|table| nth_child_mut(table, "tr", *row))
2083 .and_then(|table_row| nth_child_mut(table_row, "tc", *cell))
2084 .and_then(|table_cell| table_cell.child_mut("txBody")),
2085 };
2086 let body = body.ok_or_else(|| write_error(part, "text target has no body"))?;
2087 rebuild_paragraphs(body, &text.paragraphs, theme, prefixes);
2088 Ok(())
2089}
2090
2091fn nth_child_mut<'a>(
2092 parent: &'a mut XmlElement,
2093 local: &str,
2094 index: usize,
2095) -> Option<&'a mut XmlElement> {
2096 parent
2097 .children
2098 .iter_mut()
2099 .filter_map(|child| match child {
2100 XmlNode::Element(element) if element.local_name() == local => Some(element),
2101 _ => None,
2102 })
2103 .nth(index)
2104}
2105
2106fn rebuild_paragraphs(
2107 body: &mut XmlElement,
2108 paragraphs: &[ParagraphWrite],
2109 theme: Option<&Theme>,
2110 prefixes: &Prefixes,
2111) {
2112 let mut preamble = Vec::new();
2113 let mut originals: Vec<Option<XmlElement>> = Vec::new();
2114 for child in std::mem::take(&mut body.children) {
2115 match child {
2116 XmlNode::Element(element) if element.local_name() == "p" => {
2117 originals.push(Some(element));
2118 }
2119 XmlNode::Text(text) if text.trim().is_empty() => {}
2120 other => preamble.push(other),
2121 }
2122 }
2123 let mut children = preamble;
2124 for paragraph in paragraphs {
2125 let source = paragraph
2126 .source_index
2127 .and_then(|index| originals.get_mut(index).and_then(Option::take));
2128 let element = match source {
2129 Some(element) if !paragraph.rebuild => element,
2130 source => build_paragraph(paragraph, source, theme, prefixes),
2131 };
2132 children.push(XmlNode::Element(element));
2133 }
2134 if children
2135 .iter()
2136 .all(|child| !matches!(child, XmlNode::Element(element) if element.local_name() == "p"))
2137 {
2138 children.push(XmlNode::Element(XmlElement::new(prefixes.drawing("p"))));
2139 }
2140 body.children = children;
2141}
2142
2143fn is_run_element(local: &str) -> bool {
2144 matches!(local, "r" | "br" | "fld")
2145}
2146
2147struct RunSegment<'a> {
2149 line_break: bool,
2150 text: &'a str,
2151 properties: &'a RunProperties,
2152}
2153
2154fn run_segments(runs: &[RunWrite]) -> Vec<RunSegment<'_>> {
2155 let mut segments = Vec::new();
2156 for run in runs {
2157 for (index, piece) in run.text.split('\n').enumerate() {
2158 if index > 0 {
2159 segments.push(RunSegment {
2160 line_break: true,
2161 text: "",
2162 properties: &run.properties,
2163 });
2164 }
2165 if !piece.is_empty() {
2166 segments.push(RunSegment {
2167 line_break: false,
2168 text: piece,
2169 properties: &run.properties,
2170 });
2171 }
2172 }
2173 }
2174 segments
2175}
2176
2177fn segment_matches(element: &XmlElement, segment: &RunSegment<'_>, theme: Option<&Theme>) -> bool {
2179 if element.local_name() == "br" {
2180 return segment.line_break;
2181 }
2182 if segment.line_break {
2183 return false;
2184 }
2185 let text = element
2186 .child("t")
2187 .map(XmlElement::text_content)
2188 .unwrap_or_default();
2189 if text != segment.text {
2190 return false;
2191 }
2192 let source = parse_run_properties(element.child("rPr"));
2193 let target = segment.properties;
2194 source.baseline_pct == target.baseline_pct
2195 && source.bold == target.bold
2196 && source.italic == target.italic
2197 && source.font_size_pt == target.font_size_pt
2198 && source.spacing_pt == target.spacing_pt
2199 && source.underline == target.underline
2200 && source.caps == target.caps
2201 && source.font_family == target.font_family
2202 && resolve_color_value_to_hex_with_theme(source.color.as_ref(), theme)
2203 == resolve_color_value_to_hex_with_theme(target.color.as_ref(), theme)
2204}
2205
2206fn build_paragraph(
2207 write: &ParagraphWrite,
2208 source: Option<XmlElement>,
2209 theme: Option<&Theme>,
2210 prefixes: &Prefixes,
2211) -> XmlElement {
2212 let (mut paragraph, source_runs) = match source {
2213 Some(mut element) => {
2214 let mut runs = Vec::new();
2215 let mut kept = Vec::new();
2216 for child in std::mem::take(&mut element.children) {
2217 match child {
2218 XmlNode::Element(child) if is_run_element(child.local_name()) => {
2219 runs.push(child);
2220 }
2221 XmlNode::Text(text) if text.trim().is_empty() => {}
2222 other => kept.push(other),
2223 }
2224 }
2225 element.children = kept;
2226 (element, runs)
2227 }
2228 None => (XmlElement::new(prefixes.drawing("p")), Vec::new()),
2229 };
2230 let fresh = write.source_index.is_none();
2231 if write.properties_changed || fresh {
2232 apply_paragraph_properties(&mut paragraph, write, prefixes);
2233 }
2234 let segments = run_segments(&write.runs);
2235 let mut front = 0;
2236 let mut back = 0;
2237 while front < source_runs.len()
2238 && front < segments.len()
2239 && segment_matches(&source_runs[front], &segments[front], theme)
2240 {
2241 let steals = front + 1 < source_runs.len() - back
2242 && segment_matches(&source_runs[front + 1], &segments[front], theme)
2243 && source_span_len(&source_runs, front + 1..source_runs.len() - back)
2244 > target_span_len(&segments, front + 1..segments.len() - back);
2245 if steals {
2246 break;
2247 }
2248 front += 1;
2249 }
2250 while back < source_runs.len() - front
2251 && back < segments.len() - front
2252 && segment_matches(
2253 &source_runs[source_runs.len() - 1 - back],
2254 &segments[segments.len() - 1 - back],
2255 theme,
2256 )
2257 {
2258 let src = source_runs.len() - 1 - back;
2259 let seg = segments.len() - 1 - back;
2260 let steals = src > front
2261 && segment_matches(&source_runs[src - 1], &segments[seg], theme)
2262 && source_span_len(&source_runs, front..src) > target_span_len(&segments, front..seg);
2263 if steals {
2264 break;
2265 }
2266 back += 1;
2267 }
2268 let tail_start = source_runs.len() - back;
2269 let rebuilt = span_elements(
2270 &segments[front..segments.len() - back],
2271 &source_runs,
2272 front..tail_start,
2273 theme,
2274 prefixes,
2275 );
2276 let mut runs: Vec<XmlNode> = Vec::with_capacity(segments.len());
2277 let mut source_runs = source_runs.into_iter();
2278 for element in source_runs.by_ref().take(front) {
2279 runs.push(XmlNode::Element(element));
2280 }
2281 runs.extend(rebuilt);
2282 for element in source_runs.skip(tail_start - front) {
2283 runs.push(XmlNode::Element(element));
2284 }
2285 let position = paragraph
2286 .children
2287 .iter()
2288 .position(|child| {
2289 matches!(child, XmlNode::Element(element) if element.local_name() == "endParaRPr")
2290 })
2291 .unwrap_or(paragraph.children.len());
2292 paragraph.children.splice(position..position, runs);
2293 paragraph
2294}
2295
2296struct TargetRange {
2298 end: usize,
2299 source: usize,
2300 verbatim: bool,
2302 field: bool,
2304}
2305
2306fn span_elements(
2308 segments: &[RunSegment<'_>],
2309 source_runs: &[XmlElement],
2310 span: Range<usize>,
2311 theme: Option<&Theme>,
2312 prefixes: &Prefixes,
2313) -> Vec<XmlNode> {
2314 if source_runs.is_empty() {
2315 return segments
2316 .iter()
2317 .map(|segment| XmlNode::Element(segment_element(segment, None, theme, prefixes)))
2318 .collect();
2319 }
2320 let ranges = align_span(segments, source_runs, span);
2321 let mut output = Vec::new();
2322 let mut offset = 0;
2323 let mut range_index = 0;
2324 let mut range_start = 0;
2325 for segment in segments {
2326 let mut remaining = segment.text;
2327 loop {
2328 while ranges[range_index].end <= offset {
2329 range_start = ranges[range_index].end;
2330 range_index += 1;
2331 }
2332 let range = &ranges[range_index];
2333 let source = &source_runs[range.source];
2334 if segment.line_break {
2335 output.push(XmlNode::Element(segment_element(
2336 segment,
2337 source.child("rPr"),
2338 theme,
2339 prefixes,
2340 )));
2341 offset += 1;
2342 break;
2343 }
2344 let length = remaining.len().min(range.end - offset);
2345 let piece = RunSegment {
2346 text: &remaining[..length],
2347 ..*segment
2348 };
2349 let element = if range.field && offset == range_start && offset + length == range.end {
2350 field_element(source, &piece, theme, prefixes)
2351 } else {
2352 segment_element(&piece, source.child("rPr"), theme, prefixes)
2353 };
2354 output.push(XmlNode::Element(element));
2355 offset += length;
2356 remaining = &remaining[length..];
2357 if remaining.is_empty() {
2358 break;
2359 }
2360 }
2361 }
2362 output
2363}
2364
2365fn run_text(run: &XmlElement) -> String {
2366 if run.local_name() == "br" {
2367 return "\n".to_owned();
2368 }
2369 run.child("t")
2370 .map(XmlElement::text_content)
2371 .unwrap_or_default()
2372}
2373
2374fn segment_text<'a>(segment: &RunSegment<'a>) -> &'a str {
2375 if segment.line_break {
2376 "\n"
2377 } else {
2378 segment.text
2379 }
2380}
2381
2382fn source_span_len(runs: &[XmlElement], span: Range<usize>) -> usize {
2383 runs[span].iter().map(|run| run_text(run).len()).sum()
2384}
2385
2386fn target_span_len(segments: &[RunSegment<'_>], span: Range<usize>) -> usize {
2387 segments[span]
2388 .iter()
2389 .map(|segment| segment_text(segment).len())
2390 .sum()
2391}
2392
2393fn push_range(ranges: &mut Vec<TargetRange>, end: usize, source: usize, verbatim: bool) {
2394 if end <= ranges.last().map_or(0, |last| last.end) {
2395 return;
2396 }
2397 match ranges.last_mut() {
2398 Some(last) if last.source == source && last.verbatim == verbatim => last.end = end,
2399 _ => ranges.push(TargetRange {
2400 end,
2401 source,
2402 verbatim,
2403 field: false,
2404 }),
2405 }
2406}
2407
2408fn align_span(
2409 segments: &[RunSegment<'_>],
2410 source_runs: &[XmlElement],
2411 span: Range<usize>,
2412) -> Vec<TargetRange> {
2413 let runs = &source_runs[span.clone()];
2414 let mut source_text = String::new();
2415 let mut source_ends = Vec::with_capacity(runs.len());
2416 for run in runs {
2417 source_text.push_str(&run_text(run));
2418 source_ends.push(source_text.len());
2419 }
2420 let target_text: String = segments.iter().map(segment_text).collect();
2421 let run_at = |offset: usize| {
2422 span.start
2423 + source_ends
2424 .iter()
2425 .position(|&end| end > offset)
2426 .unwrap_or(source_ends.len().saturating_sub(1))
2427 };
2428 let prefix: usize = source_text
2429 .chars()
2430 .zip(target_text.chars())
2431 .take_while(|(left, right)| left == right)
2432 .map(|(value, _)| value.len_utf8())
2433 .sum();
2434 let suffix: usize = source_text[prefix..]
2435 .chars()
2436 .rev()
2437 .zip(target_text[prefix..].chars().rev())
2438 .take_while(|(left, right)| left == right)
2439 .map(|(value, _)| value.len_utf8())
2440 .sum();
2441 let source_end = source_text.len() - suffix;
2442 let target_end = target_text.len() - suffix;
2443
2444 let mut ranges: Vec<TargetRange> = Vec::new();
2445 for (index, &end) in source_ends.iter().enumerate() {
2446 push_range(&mut ranges, end.min(prefix), span.start + index, true);
2447 }
2448 let seed = span.start.saturating_sub(1);
2449 let (mut source_offset, mut target_offset) = (prefix, prefix);
2450 let mut replaced: Option<usize> = None;
2451 for op in char_diff(
2452 &source_text[prefix..source_end],
2453 &target_text[prefix..target_end],
2454 ) {
2455 match op {
2456 DiffOp::Match(length) => {
2457 let mut done = 0;
2458 while done < length {
2459 let run = run_at(source_offset + done);
2460 let piece =
2461 (source_ends[run - span.start] - (source_offset + done)).min(length - done);
2462 push_range(&mut ranges, target_offset + done + piece, run, true);
2463 done += piece;
2464 }
2465 source_offset += length;
2466 target_offset += length;
2467 replaced = None;
2468 }
2469 DiffOp::Delete(length) => {
2470 if replaced.is_none() {
2471 replaced = Some(run_at(source_offset));
2472 }
2473 source_offset += length;
2474 }
2475 DiffOp::Insert(length) => {
2476 let source = replaced
2477 .take()
2478 .or_else(|| ranges.last().map(|range| range.source))
2479 .unwrap_or(seed);
2480 push_range(&mut ranges, target_offset + length, source, false);
2481 target_offset += length;
2482 }
2483 }
2484 }
2485 for (index, &end) in source_ends.iter().enumerate() {
2486 if end > source_end {
2487 push_range(
2488 &mut ranges,
2489 target_end + end - source_end,
2490 span.start + index,
2491 true,
2492 );
2493 }
2494 }
2495 let mut kept = vec![false; runs.len()];
2496 for range in &ranges {
2497 if range.verbatim {
2498 kept[range.source - span.start] = true;
2499 }
2500 }
2501 for field_idx in 0..runs.len() {
2502 if runs[field_idx].local_name() != "fld" || kept[field_idx] {
2503 continue;
2504 }
2505 let field_text = run_text(&runs[field_idx]);
2506 if field_text.is_empty() {
2507 continue;
2508 }
2509 let mut donor: Option<usize> = None;
2510 for candidate in [
2511 field_idx.checked_sub(1),
2512 field_idx.checked_add(1).filter(|next| *next < runs.len()),
2513 ]
2514 .into_iter()
2515 .flatten()
2516 {
2517 if runs[candidate].local_name() == "r"
2518 && kept[candidate]
2519 && run_text(&runs[candidate]) == field_text
2520 {
2521 donor = Some(candidate);
2522 break;
2523 }
2524 }
2525 if let Some(donor) = donor {
2526 for range in &mut ranges {
2527 if range.verbatim && range.source == span.start + donor {
2528 range.source = span.start + field_idx;
2529 }
2530 }
2531 kept[donor] = false;
2532 kept[field_idx] = true;
2533 }
2534 }
2535
2536 let mut verbatim_ranges = vec![0usize; runs.len()];
2537 for range in ranges.iter().filter(|range| range.verbatim) {
2538 verbatim_ranges[range.source - span.start] += 1;
2539 }
2540 let mut merged: Vec<TargetRange> = Vec::with_capacity(ranges.len());
2541 let mut start = 0;
2542 for mut range in ranges {
2543 range.field = range.verbatim && {
2544 let relative = range.source - span.start;
2545 let run_start = relative
2546 .checked_sub(1)
2547 .map_or(0, |previous| source_ends[previous]);
2548 runs[relative].local_name() == "fld"
2549 && verbatim_ranges[relative] == 1
2550 && range.end - start == source_ends[relative] - run_start
2551 };
2552 start = range.end;
2553 match merged.last_mut() {
2554 Some(last) if last.source == range.source && !last.field && !range.field => {
2555 last.end = range.end
2556 }
2557 _ => merged.push(range),
2558 }
2559 }
2560 merged
2561}
2562
2563enum DiffOp {
2565 Match(usize),
2566 Delete(usize),
2567 Insert(usize),
2568}
2569
2570const DIFF_CELL_LIMIT: usize = 1 << 20;
2571
2572fn push_op(ops: &mut Vec<DiffOp>, op: DiffOp) {
2573 match (ops.last_mut(), op) {
2574 (Some(DiffOp::Match(last)), DiffOp::Match(length))
2575 | (Some(DiffOp::Delete(last)), DiffOp::Delete(length))
2576 | (Some(DiffOp::Insert(last)), DiffOp::Insert(length)) => *last += length,
2577 (_, op) => ops.push(op),
2578 }
2579}
2580
2581fn char_diff(source: &str, target: &str) -> Vec<DiffOp> {
2583 let source_chars: Vec<char> = source.chars().collect();
2584 let target_chars: Vec<char> = target.chars().collect();
2585 let (rows, columns) = (source_chars.len(), target_chars.len());
2586 let mut ops = Vec::new();
2587 if rows == 0 || columns == 0 || rows.saturating_mul(columns) > DIFF_CELL_LIMIT {
2588 if rows > 0 {
2589 push_op(&mut ops, DiffOp::Delete(source.len()));
2590 }
2591 if columns > 0 {
2592 push_op(&mut ops, DiffOp::Insert(target.len()));
2593 }
2594 return ops;
2595 }
2596 let width = columns + 1;
2597 let mut table = vec![0u16; (rows + 1) * width];
2598 for row in (0..rows).rev() {
2599 for column in (0..columns).rev() {
2600 table[row * width + column] = if source_chars[row] == target_chars[column] {
2601 table[(row + 1) * width + column + 1] + 1
2602 } else {
2603 table[(row + 1) * width + column].max(table[row * width + column + 1])
2604 };
2605 }
2606 }
2607 let (mut row, mut column) = (0, 0);
2608 while row < rows && column < columns {
2609 if source_chars[row] == target_chars[column] {
2610 push_op(&mut ops, DiffOp::Match(source_chars[row].len_utf8()));
2611 row += 1;
2612 column += 1;
2613 } else if table[(row + 1) * width + column] >= table[row * width + column + 1] {
2614 push_op(&mut ops, DiffOp::Delete(source_chars[row].len_utf8()));
2615 row += 1;
2616 } else {
2617 push_op(&mut ops, DiffOp::Insert(target_chars[column].len_utf8()));
2618 column += 1;
2619 }
2620 }
2621 for value in &source_chars[row..] {
2622 push_op(&mut ops, DiffOp::Delete(value.len_utf8()));
2623 }
2624 for value in &target_chars[column..] {
2625 push_op(&mut ops, DiffOp::Insert(value.len_utf8()));
2626 }
2627 ops
2628}
2629
2630fn segment_properties(
2631 segment: &RunSegment<'_>,
2632 template: Option<&XmlElement>,
2633 theme: Option<&Theme>,
2634 prefixes: &Prefixes,
2635) -> Option<XmlElement> {
2636 match template.filter(|template| !segment.line_break || template.child("gradFill").is_some()) {
2637 Some(template) => {
2638 let mut base = template.clone();
2639 apply_run_properties(&mut base, segment.properties, theme, prefixes);
2640 Some(base)
2641 }
2642 None => run_properties_element(segment.properties, prefixes),
2643 }
2644}
2645
2646fn segment_element(
2647 segment: &RunSegment<'_>,
2648 template: Option<&XmlElement>,
2649 theme: Option<&Theme>,
2650 prefixes: &Prefixes,
2651) -> XmlElement {
2652 let mut element =
2653 XmlElement::new(prefixes.drawing(if segment.line_break { "br" } else { "r" }));
2654 if let Some(properties) = segment_properties(segment, template, theme, prefixes) {
2655 element = element.with_child(properties);
2656 }
2657 if segment.line_break {
2658 element
2659 } else {
2660 element.with_child(XmlElement::new(prefixes.drawing("t")).with_text(segment.text))
2661 }
2662}
2663
2664fn field_element(
2666 source: &XmlElement,
2667 segment: &RunSegment<'_>,
2668 theme: Option<&Theme>,
2669 prefixes: &Prefixes,
2670) -> XmlElement {
2671 let mut element = XmlElement::new(source.name.clone());
2672 element.attributes = source.attributes.clone();
2673 if let Some(properties) = segment_properties(segment, source.child("rPr"), theme, prefixes) {
2674 element.children.push(XmlNode::Element(properties));
2675 }
2676 element.children.extend(
2677 source
2678 .child_elements()
2679 .filter(|child| !matches!(child.local_name(), "rPr" | "t"))
2680 .cloned()
2681 .map(XmlNode::Element),
2682 );
2683 element.children.push(XmlNode::Element(
2684 XmlElement::new(prefixes.drawing("t")).with_text(segment.text),
2685 ));
2686 element
2687}
2688
2689const POST_LATIN_ELEMENTS: [&str; 7] = [
2690 "ea",
2691 "cs",
2692 "sym",
2693 "hlinkClick",
2694 "hlinkMouseOver",
2695 "rtl",
2696 "extLst",
2697];
2698
2699fn apply_run_properties(
2701 base: &mut XmlElement,
2702 properties: &RunProperties,
2703 theme: Option<&Theme>,
2704 prefixes: &Prefixes,
2705) {
2706 match properties.font_size_pt {
2707 Some(size) => base.set_attribute("sz", format_fixed(size * 100.0)),
2708 None => {
2709 base.attributes.remove("sz");
2710 }
2711 }
2712 match properties.spacing_pt {
2713 Some(spacing) => base.set_attribute("spc", format_fixed(spacing * 100.0)),
2714 None => {
2715 base.attributes.remove("spc");
2716 }
2717 }
2718 match properties.baseline_pct {
2719 Some(baseline) => base.set_attribute("baseline", format_fixed(baseline * 1000.0)),
2720 None => {
2721 base.attributes.remove("baseline");
2722 }
2723 }
2724 match properties.caps {
2725 Some(caps) => base.set_attribute("cap", caps.as_attribute()),
2726 None => {
2727 base.attributes.remove("cap");
2728 }
2729 }
2730 let toggles = [("b", properties.bold), ("i", properties.italic)];
2731 for (name, value) in toggles {
2732 match value {
2733 Some(value) => base.set_attribute(name, if value { "1" } else { "0" }),
2734 None => {
2735 base.attributes.remove(name);
2736 }
2737 }
2738 }
2739 match &properties.underline {
2740 Some(underline) => base.set_attribute("u", underline.clone()),
2741 None => {
2742 base.attributes.remove("u");
2743 }
2744 }
2745 let keeps_gradient = properties.color.as_ref().is_some_and(|color| {
2746 base.child("gradFill")
2747 .and_then(crate::drawing::run_gradient_color)
2748 .is_some_and(|authored| {
2749 &authored == color
2750 || resolve_color_value_to_hex_with_theme(Some(&authored), theme).is_some_and(
2751 |authored| {
2752 Some(authored)
2753 == resolve_color_value_to_hex_with_theme(Some(color), theme)
2754 },
2755 )
2756 })
2757 });
2758 let removed_fills: &[&str] = if keeps_gradient {
2761 &[]
2762 } else if properties.color.is_some() {
2763 &FILL_ELEMENTS
2764 } else {
2765 &["solidFill"]
2766 };
2767 base.children.retain(|child| {
2768 !matches!(
2769 child,
2770 XmlNode::Element(element) if removed_fills.contains(&element.local_name())
2771 )
2772 });
2773 if let Some(color) = properties
2774 .color
2775 .as_ref()
2776 .filter(|_| !keeps_gradient)
2777 .and_then(|color| color_element(color, prefixes))
2778 {
2779 let position = base
2780 .children
2781 .iter()
2782 .position(
2783 |child| !matches!(child, XmlNode::Element(element) if element.local_name() == "ln"),
2784 )
2785 .unwrap_or(base.children.len());
2786 base.children.insert(
2787 position,
2788 XmlNode::Element(XmlElement::new(prefixes.drawing("solidFill")).with_child(color)),
2789 );
2790 }
2791 base.children.retain(|child| {
2792 !matches!(
2793 child,
2794 XmlNode::Element(element) if element.local_name() == "latin"
2795 )
2796 });
2797 if let Some(family) = &properties.font_family {
2798 let position = base
2799 .children
2800 .iter()
2801 .position(|child| {
2802 matches!(
2803 child,
2804 XmlNode::Element(element)
2805 if POST_LATIN_ELEMENTS.contains(&element.local_name())
2806 )
2807 })
2808 .unwrap_or(base.children.len());
2809 base.children.insert(
2810 position,
2811 XmlNode::Element(
2812 XmlElement::new(prefixes.drawing("latin"))
2813 .with_attribute("typeface", family.clone()),
2814 ),
2815 );
2816 }
2817}
2818
2819fn apply_paragraph_properties(
2820 paragraph: &mut XmlElement,
2821 write: &ParagraphWrite,
2822 prefixes: &Prefixes,
2823) {
2824 let needs_properties = write.alignment.is_some() || write.level > 0 || write.bullet.is_some();
2825 if paragraph.child_mut("pPr").is_none() {
2826 if !needs_properties {
2827 return;
2828 }
2829 paragraph.children.insert(
2830 0,
2831 XmlNode::Element(XmlElement::new(prefixes.drawing("pPr"))),
2832 );
2833 }
2834 let properties = paragraph.child_mut("pPr").expect("ensured above");
2835 match &write.alignment {
2836 Some(alignment) => properties.set_attribute("algn", alignment.clone()),
2837 None => {
2838 properties.attributes.remove("algn");
2839 }
2840 }
2841 if write.level > 0 {
2842 properties.set_attribute("lvl", write.level.to_string());
2843 } else {
2844 properties.attributes.remove("lvl");
2845 }
2846 properties.children.retain(|child| {
2847 !matches!(
2848 child,
2849 XmlNode::Element(element)
2850 if matches!(element.local_name(), "buNone" | "buChar" | "buAutoNum")
2851 )
2852 });
2853 let bullet = match &write.bullet {
2854 Some(Bullet::Character { value }) => {
2855 Some(XmlElement::new(prefixes.drawing("buChar")).with_attribute("char", value.clone()))
2856 }
2857 Some(Bullet::AutoNumber {
2858 scheme,
2859 start_at,
2860 restart,
2861 }) => {
2862 let mut element = XmlElement::new(prefixes.drawing("buAutoNum"))
2863 .with_attribute("type", scheme.clone());
2864 if *restart || *start_at != 1 {
2865 element.set_attribute("startAt", start_at.to_string());
2866 }
2867 Some(element)
2868 }
2869 Some(Bullet::None) => Some(XmlElement::new(prefixes.drawing("buNone"))),
2870 None => None,
2871 };
2872 if let Some(bullet) = bullet {
2873 let position = properties
2874 .children
2875 .iter()
2876 .position(|child| {
2877 matches!(
2878 child,
2879 XmlNode::Element(element)
2880 if matches!(element.local_name(), "tabLst" | "defRPr" | "extLst")
2881 )
2882 })
2883 .unwrap_or(properties.children.len());
2884 properties
2885 .children
2886 .insert(position, XmlNode::Element(bullet));
2887 }
2888}
2889
2890fn run_properties_element(properties: &RunProperties, prefixes: &Prefixes) -> Option<XmlElement> {
2891 let mut element = XmlElement::new(prefixes.drawing("rPr"));
2892 let mut present = false;
2893 if let Some(language) = &properties.language {
2894 element.set_attribute("lang", language.clone());
2895 present = true;
2896 }
2897 if let Some(size) = properties.font_size_pt {
2898 element.set_attribute("sz", format_fixed(size * 100.0));
2899 present = true;
2900 }
2901 if let Some(spacing) = properties.spacing_pt {
2902 element.set_attribute("spc", format_fixed(spacing * 100.0));
2903 present = true;
2904 }
2905 if let Some(baseline) = properties.baseline_pct {
2906 element.set_attribute("baseline", format_fixed(baseline * 1000.0));
2907 present = true;
2908 }
2909 if let Some(bold) = properties.bold {
2910 element.set_attribute("b", if bold { "1" } else { "0" });
2911 present = true;
2912 }
2913 if let Some(italic) = properties.italic {
2914 element.set_attribute("i", if italic { "1" } else { "0" });
2915 present = true;
2916 }
2917 if let Some(underline) = &properties.underline {
2918 element.set_attribute("u", underline.clone());
2919 present = true;
2920 }
2921 if let Some(caps) = properties.caps {
2922 element.set_attribute("cap", caps.as_attribute());
2923 present = true;
2924 }
2925 if let Some(color) = properties
2926 .color
2927 .as_ref()
2928 .and_then(|color| color_element(color, prefixes))
2929 {
2930 element =
2931 element.with_child(XmlElement::new(prefixes.drawing("solidFill")).with_child(color));
2932 present = true;
2933 }
2934 if let Some(family) = &properties.font_family {
2935 element = element.with_child(
2936 XmlElement::new(prefixes.drawing("latin")).with_attribute("typeface", family.clone()),
2937 );
2938 present = true;
2939 }
2940 present.then_some(element)
2941}
2942
2943fn shape_element(
2946 add: &ShapeAdd,
2947 next_shape_id: &mut Option<u32>,
2948 prefixes: &Prefixes,
2949 part: &str,
2950) -> Result<XmlElement, PptxError> {
2951 if matches!(add.geometry.as_str(), "custom" | "group") {
2952 return Err(write_error(
2953 part,
2954 format!("unsupported geometry {:?} for a new shape", add.geometry),
2955 ));
2956 }
2957 let shape_id = alloc_shape_id(next_shape_id, part)?;
2958 let non_visual = XmlElement::new(prefixes.presentation("nvSpPr"))
2959 .with_child(
2960 XmlElement::new(prefixes.presentation("cNvPr"))
2961 .with_attribute("id", shape_id.to_string())
2962 .with_attribute("name", add.name.clone()),
2963 )
2964 .with_child(XmlElement::new(prefixes.presentation("cNvSpPr")))
2965 .with_child(XmlElement::new(prefixes.presentation("nvPr")));
2966 let transform = XmlElement::new(prefixes.drawing("xfrm"))
2967 .with_child(
2968 XmlElement::new(prefixes.drawing("off"))
2969 .with_attribute("x", add.x.to_string())
2970 .with_attribute("y", add.y.to_string()),
2971 )
2972 .with_child(
2973 XmlElement::new(prefixes.drawing("ext"))
2974 .with_attribute("cx", add.width.to_string())
2975 .with_attribute("cy", add.height.to_string()),
2976 );
2977 let mut adjust_list = XmlElement::new(prefixes.drawing("avLst"));
2978 for (name, value) in &add.adjust_values {
2979 adjust_list = adjust_list.with_child(
2980 XmlElement::new(prefixes.drawing("gd"))
2981 .with_attribute("name", name.clone())
2982 .with_attribute("fmla", format!("val {}", format_fixed(value * 100_000.0))),
2983 );
2984 }
2985 let geometry = XmlElement::new(prefixes.drawing("prstGeom"))
2986 .with_attribute("prst", add.geometry.clone())
2987 .with_child(adjust_list);
2988 let mut properties = XmlElement::new(prefixes.presentation("spPr"))
2989 .with_child(transform)
2990 .with_child(geometry);
2991 if let Some(fill) = &add.fill {
2992 properties = properties.with_child(fill_element(fill, prefixes, part)?);
2993 }
2994 if let Some(outline) = &add.outline {
2995 properties = properties.with_child(outline_element(outline, prefixes));
2996 }
2997 let mut shape = XmlElement::new(prefixes.presentation("sp"))
2998 .with_child(non_visual)
2999 .with_child(properties);
3000 if let Some(paragraphs) = &add.paragraphs {
3001 let mut body = XmlElement::new(prefixes.presentation("txBody"))
3002 .with_child(XmlElement::new(prefixes.drawing("bodyPr")))
3003 .with_child(XmlElement::new(prefixes.drawing("lstStyle")));
3004 for paragraph in paragraphs {
3005 body = body.with_child(build_paragraph(paragraph, None, None, prefixes));
3006 }
3007 if paragraphs.is_empty() {
3008 body = body.with_child(XmlElement::new(prefixes.drawing("p")));
3009 }
3010 shape = shape.with_child(body);
3011 }
3012 Ok(shape)
3013}
3014
3015fn add_shape_element(
3017 add: &ShapeAdd,
3018 next_shape_id: &mut Option<u32>,
3019 prefixes: &Prefixes,
3020 part: &str,
3021 sink: &mut PartSink<'_>,
3022 budget: &mut ParseBudget<'_>,
3023) -> Result<XmlElement, PptxError> {
3024 match &add.picture {
3025 Some(picture) => {
3026 picture_shape_element(add, picture, next_shape_id, prefixes, part, sink, budget)
3027 }
3028 None => shape_element(add, next_shape_id, prefixes, part),
3029 }
3030}
3031
3032fn image_content_type_extension(content_type: &str) -> Option<&'static str> {
3033 match content_type {
3034 "image/png" => Some("png"),
3035 "image/jpeg" | "image/jpg" => Some("jpeg"),
3036 "image/gif" => Some("gif"),
3037 "image/bmp" => Some("bmp"),
3038 "image/tiff" => Some("tiff"),
3039 "image/webp" => Some("webp"),
3040 "image/svg+xml" => Some("svg"),
3041 _ => None,
3042 }
3043}
3044
3045fn image_extension(content_type: &str) -> Result<&'static str, PptxError> {
3046 image_content_type_extension(content_type)
3047 .ok_or_else(|| write_error("media", format!("unsupported image type {content_type:?}")))
3048}
3049
3050pub fn is_supported_image_content_type(content_type: &str) -> bool {
3052 image_content_type_extension(content_type).is_some()
3053}
3054
3055fn next_media_part_path(
3056 package: &PptxPackage,
3057 new_parts: &[(String, Vec<u8>)],
3058 extension: &str,
3059) -> String {
3060 let existing: HashSet<&str> = package
3061 .parts
3062 .iter()
3063 .map(|part| part.path.as_str())
3064 .chain(new_parts.iter().map(|(path, _)| path.as_str()))
3065 .collect();
3066 let mut number = 1_u64;
3067 while existing.contains(format!("ppt/media/image{number}.{extension}").as_str()) {
3068 number += 1;
3069 }
3070 format!("ppt/media/image{number}.{extension}")
3071}
3072
3073fn ensure_media_content_type(
3075 sink: &mut PartSink<'_>,
3076 extension: &str,
3077 part_path: &str,
3078 content_type: &str,
3079 budget: &mut ParseBudget<'_>,
3080) -> Result<(), PptxError> {
3081 let path = "[Content_Types].xml";
3082 let bytes = sink
3083 .current(path)
3084 .ok_or_else(|| PptxError::MissingPart(path.to_owned()))?;
3085 let mut root = parse_xml(&bytes, path, budget)?;
3086 let default = root.children.iter().find_map(|child| match child {
3087 XmlNode::Element(element)
3088 if element.local_name() == "Default"
3089 && element
3090 .attribute("Extension")
3091 .is_some_and(|value| value.eq_ignore_ascii_case(extension)) =>
3092 {
3093 Some(element)
3094 }
3095 _ => None,
3096 });
3097 if let Some(default) = default {
3098 if default.attribute("ContentType") != Some(content_type) {
3099 set_content_type_override(sink, part_path, content_type, budget)?;
3100 }
3101 } else {
3102 let prefix = root.name.rsplit_once(':').map_or("", |(prefix, _)| prefix);
3103 root.children.push(XmlNode::Element(
3104 XmlElement::new(qualified(prefix, "Default"))
3105 .with_attribute("Extension", extension)
3106 .with_attribute("ContentType", content_type),
3107 ));
3108 sink.store(path, serialize_xml(&root));
3109 }
3110 Ok(())
3111}
3112
3113fn add_relationship(
3116 sink: &mut PartSink<'_>,
3117 relationships_path: &str,
3118 relationship_type: &str,
3119 target: &str,
3120 budget: &mut ParseBudget<'_>,
3121) -> Result<String, PptxError> {
3122 let mut root = match sink.current(relationships_path) {
3123 Some(bytes) => parse_xml(&bytes, relationships_path, budget)?,
3124 None => XmlElement::new("Relationships").with_attribute("xmlns", PACKAGE_RELATIONSHIPS_NS),
3125 };
3126 let used: HashSet<&str> = root
3127 .children_named("Relationship")
3128 .filter_map(|element| element.attribute("Id"))
3129 .collect();
3130 let mut number = 1_u64;
3131 while used.contains(format!("rId{number}").as_str()) {
3132 number += 1;
3133 }
3134 let id = format!("rId{number}");
3135 let prefix = root.name.rsplit_once(':').map_or("", |(prefix, _)| prefix);
3136 root.children.push(XmlNode::Element(
3137 XmlElement::new(qualified(prefix, "Relationship"))
3138 .with_attribute("Id", id.clone())
3139 .with_attribute("Type", relationship_type)
3140 .with_attribute("Target", target),
3141 ));
3142 sink.store(relationships_path, serialize_xml(&root));
3143 Ok(id)
3144}
3145
3146fn picture_shape_element(
3147 add: &ShapeAdd,
3148 picture: &PictureAdd,
3149 next_shape_id: &mut Option<u32>,
3150 prefixes: &Prefixes,
3151 slide_part_path: &str,
3152 sink: &mut PartSink<'_>,
3153 budget: &mut ParseBudget<'_>,
3154) -> Result<XmlElement, PptxError> {
3155 let extension = image_extension(&picture.content_type)?;
3156 let media_part_path = next_media_part_path(sink.package, sink.new_parts.as_slice(), extension);
3157 sink.store(&media_part_path, picture.media_bytes.clone());
3158 ensure_media_content_type(
3159 sink,
3160 extension,
3161 &media_part_path,
3162 &picture.content_type,
3163 budget,
3164 )?;
3165 let relationships_path = slide_relationships_path(slide_part_path);
3166 let target = relative_target(slide_part_path, &media_part_path);
3167 let relationship_id = add_relationship(
3168 sink,
3169 &relationships_path,
3170 IMAGE_RELATIONSHIP_TYPE,
3171 &target,
3172 budget,
3173 )?;
3174
3175 let shape_id = alloc_shape_id(next_shape_id, slide_part_path)?;
3176 let non_visual = XmlElement::new(prefixes.presentation("nvPicPr"))
3177 .with_child(
3178 XmlElement::new(prefixes.presentation("cNvPr"))
3179 .with_attribute("id", shape_id.to_string())
3180 .with_attribute("name", add.name.clone()),
3181 )
3182 .with_child(
3183 XmlElement::new(prefixes.presentation("cNvPicPr")).with_child(
3184 XmlElement::new(prefixes.drawing("picLocks")).with_attribute("noChangeAspect", "1"),
3185 ),
3186 )
3187 .with_child(XmlElement::new(prefixes.presentation("nvPr")));
3188 let blip_fill = XmlElement::new(prefixes.presentation("blipFill"))
3189 .with_child(
3190 XmlElement::new(prefixes.drawing("blip"))
3191 .with_attribute(prefixes.relationship("embed"), relationship_id),
3192 )
3193 .with_child(
3194 XmlElement::new(prefixes.drawing("stretch"))
3195 .with_child(XmlElement::new(prefixes.drawing("fillRect"))),
3196 );
3197 let transform = XmlElement::new(prefixes.drawing("xfrm"))
3198 .with_child(
3199 XmlElement::new(prefixes.drawing("off"))
3200 .with_attribute("x", add.x.to_string())
3201 .with_attribute("y", add.y.to_string()),
3202 )
3203 .with_child(
3204 XmlElement::new(prefixes.drawing("ext"))
3205 .with_attribute("cx", add.width.to_string())
3206 .with_attribute("cy", add.height.to_string()),
3207 );
3208 let geometry = XmlElement::new(prefixes.drawing("prstGeom"))
3209 .with_attribute("prst", "rect")
3210 .with_child(XmlElement::new(prefixes.drawing("avLst")));
3211 let properties = XmlElement::new(prefixes.presentation("spPr"))
3212 .with_child(transform)
3213 .with_child(geometry);
3214 Ok(XmlElement::new(prefixes.presentation("pic"))
3215 .with_child(non_visual)
3216 .with_child(blip_fill)
3217 .with_child(properties))
3218}
3219
3220fn slide_xml(
3221 name: Option<&str>,
3222 shapes: &[ShapeAdd],
3223 part: &str,
3224 sink: &mut PartSink<'_>,
3225 budget: &mut ParseBudget<'_>,
3226) -> Result<Vec<u8>, PptxError> {
3227 let mut root = XmlElement::new("p:sld")
3228 .with_attribute("xmlns:a", DRAWINGML_NS)
3229 .with_attribute("xmlns:r", OFFICE_RELATIONSHIPS_NS)
3230 .with_attribute("xmlns:p", PRESENTATIONML_NS);
3231 let prefixes = Prefixes {
3232 drawing: "a".to_owned(),
3233 presentation: "p".to_owned(),
3234 relationship: "r".to_owned(),
3235 };
3236 let group_transform = XmlElement::new(prefixes.drawing("xfrm"))
3237 .with_child(
3238 XmlElement::new(prefixes.drawing("off"))
3239 .with_attribute("x", "0")
3240 .with_attribute("y", "0"),
3241 )
3242 .with_child(
3243 XmlElement::new(prefixes.drawing("ext"))
3244 .with_attribute("cx", "0")
3245 .with_attribute("cy", "0"),
3246 )
3247 .with_child(
3248 XmlElement::new(prefixes.drawing("chOff"))
3249 .with_attribute("x", "0")
3250 .with_attribute("y", "0"),
3251 )
3252 .with_child(
3253 XmlElement::new(prefixes.drawing("chExt"))
3254 .with_attribute("cx", "0")
3255 .with_attribute("cy", "0"),
3256 );
3257 let mut tree = XmlElement::new(prefixes.presentation("spTree"))
3258 .with_child(
3259 XmlElement::new(prefixes.presentation("nvGrpSpPr"))
3260 .with_child(
3261 XmlElement::new(prefixes.presentation("cNvPr"))
3262 .with_attribute("id", "1")
3263 .with_attribute("name", ""),
3264 )
3265 .with_child(XmlElement::new(prefixes.presentation("cNvGrpSpPr")))
3266 .with_child(XmlElement::new(prefixes.presentation("nvPr"))),
3267 )
3268 .with_child(XmlElement::new(prefixes.presentation("grpSpPr")).with_child(group_transform));
3269 let mut next_shape_id = Some(2);
3270 for shape in shapes {
3271 tree = tree.with_child(add_shape_element(
3272 shape,
3273 &mut next_shape_id,
3274 &prefixes,
3275 part,
3276 sink,
3277 budget,
3278 )?);
3279 }
3280 let mut common = XmlElement::new(prefixes.presentation("cSld"));
3281 if let Some(name) = name {
3282 common.set_attribute("name", name);
3283 }
3284 root = root.with_child(common.with_child(tree)).with_child(
3285 XmlElement::new(prefixes.presentation("clrMapOvr"))
3286 .with_child(XmlElement::new(prefixes.drawing("masterClrMapping"))),
3287 );
3288 Ok(serialize_xml(&root))
3289}
3290
3291fn slide_relationships_xml(part_path: &str, layout_part_path: &str) -> Vec<u8> {
3292 let root = XmlElement::new("Relationships")
3293 .with_attribute(
3294 "xmlns",
3295 "http://schemas.openxmlformats.org/package/2006/relationships",
3296 )
3297 .with_child(
3298 XmlElement::new("Relationship")
3299 .with_attribute("Id", "rId1")
3300 .with_attribute("Type", SLIDE_LAYOUT_RELATIONSHIP_TYPE)
3301 .with_attribute("Target", relative_target(part_path, layout_part_path)),
3302 );
3303 serialize_xml(&root)
3304}
3305
3306#[cfg(test)]
3307mod tests {
3308 use super::*;
3309 use crate::ParseLimits;
3310 use crate::drawing::parse_run_properties;
3311 use crate::model::ShapeElements;
3312 use crate::xml::{ParseBudget, parse_xml};
3313
3314 #[test]
3315 fn a_new_gradient_outline_writes_its_stops_and_angle() {
3316 let package =
3317 crate::parse_pptx(include_bytes!("../tests/fixtures/gradient-outline.pptx")).unwrap();
3318 let crate::ShapeNode::Shape(shape) = &package.slides[0].shapes[0] else {
3319 panic!("shape")
3320 };
3321 let outline = shape.outline.as_ref().unwrap();
3322 let mut properties = XmlElement::new("p:spPr");
3323 let prefixes = Prefixes::from_root(&mut properties);
3324 set_outline(&mut properties, outline, &prefixes);
3325 let line = properties.child("ln").unwrap();
3326 assert_eq!(line.attribute("w"), Some("76200"));
3327 let gradient = line.child("gradFill").unwrap();
3328 assert_eq!(gradient.child("gsLst").unwrap().child_elements().count(), 3);
3329 assert_eq!(gradient.child("lin").unwrap().attribute("ang"), Some("0"));
3330 assert!(line.child("solidFill").is_none());
3331 assert_eq!(
3332 crate::drawing::parse_outline_element(line).as_ref(),
3333 Some(outline)
3334 );
3335 }
3336
3337 #[test]
3338 fn source_ordinals_cover_the_shapes_an_alternate_content_contributes() {
3339 let limits = ParseLimits::default();
3340 let mut budget = ParseBudget::new(&limits);
3341 let part = "ppt/slides/slide1.xml";
3342 let mut root = parse_xml(
3343 br#"<p:sld><p:cSld><p:spTree><p:sp><p:nvSpPr><p:cNvPr id="2" name="one"/></p:nvSpPr></p:sp><mc:AlternateContent><mc:Choice xmlns:p14="http://schemas.microsoft.com/office/powerpoint/2010/main" Requires="p14"><p:sp><p:nvSpPr><p:cNvPr id="3" name="choice"/></p:nvSpPr></p:sp></mc:Choice><mc:Fallback><p:sp><p:nvSpPr><p:cNvPr id="4" name="two"/></p:nvSpPr></p:sp><p:pic><p:nvPicPr><p:cNvPr id="5" name="three"/></p:nvPicPr></p:pic></mc:Fallback></mc:AlternateContent><p:sp><p:nvSpPr><p:cNvPr id="6" name="four"/></p:nvSpPr></p:sp></p:spTree></p:cSld></p:sld>"#,
3344 part,
3345 &mut budget,
3346 )
3347 .unwrap();
3348 let parsed = crate::drawing::common_slide_data(
3349 &root,
3350 &[],
3351 part,
3352 &mut budget,
3353 ShapeElements::WithConnectors,
3354 )
3355 .unwrap();
3356 let tree = root
3357 .child_mut("cSld")
3358 .and_then(|common| common.child_mut("spTree"))
3359 .unwrap();
3360 let slots: Vec<Option<XmlNode>> = std::mem::take(&mut tree.children)
3361 .into_iter()
3362 .map(Some)
3363 .collect();
3364
3365 let found = shape_slots(&slots, ShapeElements::WithConnectors);
3366
3367 assert_eq!(found.len(), parsed.shapes.len());
3368 let names: Vec<String> = found
3369 .iter()
3370 .map(|slot| {
3371 let mut node = slots[slot.position].clone().unwrap();
3372 let element = element_at_path(&mut node, &slot.path).unwrap();
3373 element.descendants_named("cNvPr")[0]
3374 .attribute("name")
3375 .unwrap()
3376 .to_owned()
3377 })
3378 .collect();
3379 assert_eq!(names, ["one", "two", "three", "four"]);
3380 }
3381
3382 #[test]
3383 fn deleting_one_of_the_shapes_a_branch_holds_leaves_the_others_in_place() {
3384 let limits = ParseLimits::default();
3385 let mut budget = ParseBudget::new(&limits);
3386 let part = "ppt/slides/slide1.xml";
3387 let mut root = parse_xml(
3388 br#"<p:sld><p:cSld><p:spTree><p:sp><p:nvSpPr><p:cNvPr id="2" name="one"/></p:nvSpPr><p:spPr/></p:sp><mc:AlternateContent><mc:Fallback><p:sp><p:nvSpPr><p:cNvPr id="3" name="two"/></p:nvSpPr><p:spPr/></p:sp><p:sp><p:nvSpPr><p:cNvPr id="4" name="three"/></p:nvSpPr><p:spPr/></p:sp></mc:Fallback></mc:AlternateContent></p:spTree></p:cSld></p:sld>"#,
3389 part,
3390 &mut budget,
3391 )
3392 .unwrap();
3393
3394 let package = PptxPackage::default();
3395 let mut replacements = HashMap::new();
3396 let mut new_parts = Vec::new();
3397 let mut sink = PartSink {
3398 package: &package,
3399 replacements: &mut replacements,
3400 new_parts: &mut new_parts,
3401 };
3402 patch_slide(
3403 &mut root,
3404 &[
3405 ShapeWrite::Keep { source_index: 0 },
3406 ShapeWrite::Patch {
3407 source_index: 2,
3408 patch: Box::new(ShapePatch {
3409 fill: Some(ShapeFill {
3410 color: Some(ColorValue::from_attribute("DC2626")),
3411 ..ShapeFill::named("solid")
3412 }),
3413 ..ShapePatch::default()
3414 }),
3415 },
3416 ],
3417 None,
3418 part,
3419 ShapeElements::WithConnectors,
3420 &mut sink,
3421 &mut budget,
3422 )
3423 .unwrap();
3424
3425 let xml = String::from_utf8(serialize_xml(&root)).unwrap();
3426 assert!(xml.contains(r#"name="one""#));
3427 assert!(!xml.contains(r#"name="two""#), "{xml}");
3428 let survivor = xml.split(r#"name="three""#).nth(1).unwrap();
3429 assert!(survivor.contains(r#"<a:srgbClr val="DC2626"/>"#), "{xml}");
3430 }
3431
3432 #[test]
3433 fn a_branch_that_still_holds_unmodeled_markup_outlives_its_only_shape() {
3434 let limits = ParseLimits::default();
3435 let mut budget = ParseBudget::new(&limits);
3436 let part = "ppt/slides/slide1.xml";
3437 let mut root = parse_xml(
3438 br#"<p:sld><p:cSld><p:spTree><mc:AlternateContent><mc:Fallback><p:sp><p:nvSpPr><p:cNvPr id="2" name="one"/></p:nvSpPr></p:sp><p:cxnSp><p:nvCxnSpPr><p:cNvPr id="3" name="two"/></p:nvCxnSpPr></p:cxnSp></mc:Fallback></mc:AlternateContent></p:spTree></p:cSld></p:sld>"#,
3439 part,
3440 &mut budget,
3441 )
3442 .unwrap();
3443
3444 let package = PptxPackage::default();
3445 let mut replacements = HashMap::new();
3446 let mut new_parts = Vec::new();
3447 let mut sink = PartSink {
3448 package: &package,
3449 replacements: &mut replacements,
3450 new_parts: &mut new_parts,
3451 };
3452 patch_slide(
3453 &mut root,
3454 &[],
3455 None,
3456 part,
3457 ShapeElements::WithoutConnectors,
3458 &mut sink,
3459 &mut budget,
3460 )
3461 .unwrap();
3462
3463 let xml = String::from_utf8(serialize_xml(&root)).unwrap();
3464 assert!(!xml.contains(r#"name="one""#), "{xml}");
3465 assert!(xml.contains(r#"name="two""#), "{xml}");
3466 }
3467
3468 fn run_properties(xml: &[u8]) -> (XmlElement, Prefixes, RunProperties) {
3469 let limits = ParseLimits::default();
3470 let mut budget = ParseBudget::new(&limits);
3471 let mut root = parse_xml(xml, "ppt/slides/slide1.xml", &mut budget).unwrap();
3472 let prefixes = Prefixes::from_root(&mut root);
3473 let properties = parse_run_properties(Some(&root));
3474 (root, prefixes, properties)
3475 }
3476
3477 #[test]
3478 fn an_unedited_gradient_fill_survives_a_rewrite() {
3479 let (mut element, prefixes, properties) = run_properties(
3480 br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" lang="en"><a:gradFill><a:gsLst><a:gs pos="0"><a:srgbClr val="FFFFFF"/></a:gs><a:gs pos="100000"><a:srgbClr val="FFFFFF"/></a:gs></a:gsLst></a:gradFill></a:rPr>"#,
3481 );
3482 assert!(
3483 properties.color.is_some(),
3484 "the gradient should flatten to a colour"
3485 );
3486 let gradient = element.child("gradFill").unwrap().clone();
3487 apply_run_properties(&mut element, &properties, None, &prefixes);
3488 assert_eq!(element.child("gradFill"), Some(&gradient));
3489 let serialized = String::from_utf8(serialize_xml(&element)).unwrap();
3490 assert!(
3491 serialized.contains("gradFill"),
3492 "the authored gradient must be left alone"
3493 );
3494 assert!(
3495 !serialized.contains("solidFill"),
3496 "it must not be rewritten as a solid fill"
3497 );
3498 }
3499
3500 #[test]
3501 fn an_edited_colour_replaces_the_gradient() {
3502 let (mut element, prefixes, mut properties) = run_properties(
3503 br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" lang="en"><a:gradFill><a:gsLst><a:gs pos="0"><a:srgbClr val="FFFFFF"/></a:gs></a:gsLst></a:gradFill></a:rPr>"#,
3504 );
3505 properties.color = Some(ColorValue {
3506 rgb: Some("FF0000".to_owned()),
3507 ..ColorValue::default()
3508 });
3509 apply_run_properties(&mut element, &properties, None, &prefixes);
3510 let serialized = String::from_utf8(serialize_xml(&element)).unwrap();
3511 assert!(!serialized.contains("gradFill"));
3512 assert!(serialized.contains("solidFill"));
3513 assert_eq!(parse_run_properties(Some(&element)).color, properties.color);
3514 }
3515
3516 #[test]
3517 fn the_writer_recognises_exactly_what_the_parser_reads() {
3518 for local in ["sp", "cxnSp", "pic", "graphicFrame", "grpSp"] {
3519 assert!(
3520 ShapeElements::WithConnectors.contains(local),
3521 "{local} is parsed as a shape but not recognised here"
3522 );
3523 }
3524 for local in ["txBody", "nvGrpSpPr", "extLst"] {
3525 assert!(
3526 !ShapeElements::WithConnectors.contains(local),
3527 "{local} is not a shape element"
3528 );
3529 }
3530 }
3531
3532 #[test]
3533 fn the_pre_connector_set_leaves_out_only_connectors() {
3534 assert!(!ShapeElements::WithoutConnectors.contains("cxnSp"));
3535 for local in ["sp", "pic", "graphicFrame", "grpSp"] {
3536 assert!(ShapeElements::WithoutConnectors.contains(local));
3537 }
3538 }
3539
3540 const LINKED_PARAGRAPH: &[u8] = br#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><a:r><a:rPr lang="en-US" dirty="0"/><a:t>See </a:t></a:r><a:r><a:rPr lang="en-US" strike="sngStrike"><a:hlinkClick r:id="rId2"/></a:rPr><a:t>the docs</a:t></a:r><a:r><a:rPr lang="en-US"/><a:t> today</a:t></a:r></a:p>"#;
3541 const LINK_PROPERTIES: &str =
3542 r#"<a:rPr lang="en-US" strike="sngStrike"><a:hlinkClick r:id="rId2"/></a:rPr>"#;
3543 const FIELD_PARAGRAPH: &[u8] = br#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:fld id="{A}" type="slidenum"><a:rPr lang="en-US"/><a:t>1</a:t></a:fld><a:r><a:rPr lang="en-US"/><a:t> of </a:t></a:r><a:fld id="{B}" type="datetime1"><a:rPr lang="en-US"/><a:t>2024</a:t></a:fld></a:p>"#;
3544
3545 fn rebuilt_paragraph(xml: &[u8], runs: &[(&str, RunProperties)]) -> String {
3546 let limits = ParseLimits::default();
3547 let mut budget = ParseBudget::new(&limits);
3548 let mut root = parse_xml(xml, "ppt/slides/slide1.xml", &mut budget).unwrap();
3549 let prefixes = Prefixes::from_root(&mut root);
3550 let write = ParagraphWrite {
3551 source_index: Some(0),
3552 rebuild: true,
3553 properties_changed: false,
3554 alignment: None,
3555 level: 0,
3556 bullet: None,
3557 runs: runs
3558 .iter()
3559 .map(|(text, properties)| RunWrite {
3560 text: (*text).to_owned(),
3561 properties: properties.clone(),
3562 })
3563 .collect(),
3564 };
3565 let paragraph = build_paragraph(&write, Some(root), None, &prefixes);
3566 String::from_utf8(serialize_xml(¶graph)).unwrap()
3567 }
3568
3569 #[test]
3570 fn an_edit_spanning_several_runs_keeps_each_survivor_on_its_source_run() {
3571 let xml = rebuilt_paragraph(
3572 LINKED_PARAGRAPH,
3573 &[("Seethe doc now", RunProperties::default())],
3574 );
3575 assert!(
3576 xml.contains(r#"<a:r><a:rPr dirty="0" lang="en-US"/><a:t>See</a:t></a:r>"#),
3577 "{xml}"
3578 );
3579 assert!(
3580 xml.contains(&format!("<a:r>{LINK_PROPERTIES}<a:t>the doc</a:t></a:r>")),
3581 "{xml}"
3582 );
3583 assert!(
3584 xml.contains(r#"<a:r><a:rPr lang="en-US"/><a:t> now</a:t></a:r>"#),
3585 "{xml}"
3586 );
3587 assert_eq!(xml.matches("<a:r>").count(), 3, "{xml}");
3588 }
3589
3590 #[test]
3591 fn a_line_break_inside_the_span_keeps_the_runs_around_it_aligned() {
3592 let xml = rebuilt_paragraph(
3593 br#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><a:r><a:rPr lang="en-US"/><a:t>First</a:t></a:r><a:br/><a:r><a:rPr><a:hlinkClick r:id="rId2"/></a:rPr><a:t>Second</a:t></a:r></a:p>"#,
3594 &[("Firs\nSecond!", RunProperties::default())],
3595 );
3596 assert!(
3597 xml.contains(
3598 r#"<a:r><a:rPr lang="en-US"/><a:t>Firs</a:t></a:r><a:br/><a:r><a:rPr><a:hlinkClick r:id="rId2"/></a:rPr><a:t>Second!</a:t></a:r>"#
3599 ),
3600 "{xml}"
3601 );
3602 }
3603
3604 #[test]
3605 fn a_field_survives_an_edit_around_it_and_degrades_once_its_text_changes() {
3606 let xml = rebuilt_paragraph(
3607 FIELD_PARAGRAPH,
3608 &[("1 out of 2025", RunProperties::default())],
3609 );
3610 assert!(
3611 xml.contains(
3612 r#"<a:fld id="{A}" type="slidenum"><a:rPr lang="en-US"/><a:t>1</a:t></a:fld><a:r><a:rPr lang="en-US"/><a:t> out of </a:t></a:r><a:r><a:rPr lang="en-US"/><a:t>2025</a:t></a:r>"#
3613 ),
3614 "{xml}"
3615 );
3616 assert!(!xml.contains("datetime1"), "{xml}");
3617
3618 let xml = rebuilt_paragraph(FIELD_PARAGRAPH, &[("1X of 2024", RunProperties::default())]);
3619 assert!(
3620 xml.contains(
3621 r#"<a:fld id="{A}" type="slidenum"><a:rPr lang="en-US"/><a:t>1</a:t></a:fld><a:r><a:rPr lang="en-US"/><a:t>X</a:t></a:r><a:r><a:rPr lang="en-US"/><a:t> of </a:t></a:r><a:fld id="{B}" type="datetime1"><a:rPr lang="en-US"/><a:t>2024</a:t></a:fld>"#
3622 ),
3623 "{xml}"
3624 );
3625 }
3626
3627 #[test]
3628 fn text_typed_at_the_end_of_a_link_stays_inside_it() {
3629 let xml = rebuilt_paragraph(
3630 LINKED_PARAGRAPH,
3631 &[("See the docsX today", RunProperties::default())],
3632 );
3633 assert!(
3634 xml.contains(&format!("<a:r>{LINK_PROPERTIES}<a:t>the docsX</a:t></a:r>")),
3635 "{xml}"
3636 );
3637
3638 let bold = RunProperties {
3639 bold: Some(true),
3640 ..RunProperties::default()
3641 };
3642 let xml = rebuilt_paragraph(
3643 LINKED_PARAGRAPH,
3644 &[
3645 ("See ", RunProperties::default()),
3646 ("the docs", RunProperties::default()),
3647 ("X", bold),
3648 (" today", RunProperties::default()),
3649 ],
3650 );
3651 assert!(
3652 xml.contains(&format!(
3653 r#"<a:r>{LINK_PROPERTIES}<a:t>the docs</a:t></a:r><a:r><a:rPr b="1" lang="en-US" strike="sngStrike"><a:hlinkClick r:id="rId2"/></a:rPr><a:t>X</a:t></a:r><a:r><a:rPr lang="en-US"/><a:t> today</a:t></a:r>"#
3654 )),
3655 "{xml}"
3656 );
3657 }
3658
3659 #[test]
3660 fn deleting_a_linked_run_drops_its_link() {
3661 let xml = rebuilt_paragraph(
3662 LINKED_PARAGRAPH,
3663 &[("See today", RunProperties::default())],
3664 );
3665 assert!(!xml.contains("hlinkClick"), "{xml}");
3666 assert!(
3667 xml.contains(
3668 r#"<a:r><a:rPr dirty="0" lang="en-US"/><a:t>See </a:t></a:r><a:r><a:rPr lang="en-US"/><a:t> today</a:t></a:r>"#
3669 ),
3670 "{xml}"
3671 );
3672 }
3673
3674 #[test]
3675 fn a_plain_duplicate_at_the_front_does_not_steal_the_field() {
3676 let xml = rebuilt_paragraph(
3677 br#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:r><a:rPr lang="en-US"/><a:t>1</a:t></a:r><a:fld id="{A}" type="slidenum"><a:rPr lang="en-US"/><a:t>1</a:t></a:fld></a:p>"#,
3678 &[("1", RunProperties::default())],
3679 );
3680 assert!(
3681 xml.contains(
3682 r#"<a:fld id="{A}" type="slidenum"><a:rPr lang="en-US"/><a:t>1</a:t></a:fld>"#
3683 ),
3684 "{xml}"
3685 );
3686 assert_eq!(xml.matches("<a:r>").count(), 0, "{xml}");
3687 }
3688
3689 #[test]
3690 fn a_plain_duplicate_at_the_back_does_not_steal_the_field() {
3691 let xml = rebuilt_paragraph(
3692 br#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:r><a:rPr lang="en-US"/><a:t>X</a:t></a:r><a:fld id="{A}" type="slidenum"><a:rPr lang="en-US"/><a:t>1</a:t></a:fld><a:r><a:rPr lang="en-US"/><a:t>1</a:t></a:r></a:p>"#,
3693 &[
3694 ("X", RunProperties::default()),
3695 ("1", RunProperties::default()),
3696 ],
3697 );
3698 assert!(
3699 xml.contains(
3700 r#"<a:fld id="{A}" type="slidenum"><a:rPr lang="en-US"/><a:t>1</a:t></a:fld>"#
3701 ),
3702 "{xml}"
3703 );
3704 assert_eq!(xml.matches("<a:r>").count(), 1, "{xml}");
3705 assert!(
3706 !xml.contains(r#"<a:r><a:rPr lang="en-US"/><a:t>1</a:t></a:r>"#),
3707 "{xml}"
3708 );
3709 }
3710
3711 #[test]
3712 fn adjustments_without_preset_geometry_error_instead_of_no_opting() {
3713 let limits = ParseLimits::default();
3714 let mut budget = ParseBudget::new(&limits);
3715 let part = "ppt/slides/slide1.xml";
3716 let mut root = parse_xml(
3717 br#"<p:sld><p:cSld><p:spTree><p:sp><p:nvSpPr><p:cNvPr id="2" name="custom"/></p:nvSpPr><p:spPr><a:custGeom><a:pathLst><a:path w="10" h="10"><a:moveTo><a:pt x="0" y="0"/></a:moveTo><a:close/></a:path></a:pathLst></a:custGeom></p:spPr></p:sp></p:spTree></p:cSld></p:sld>"#,
3718 part,
3719 &mut budget,
3720 )
3721 .unwrap();
3722
3723 let package = PptxPackage::default();
3724 let mut replacements = HashMap::new();
3725 let mut new_parts = Vec::new();
3726 let mut sink = PartSink {
3727 package: &package,
3728 replacements: &mut replacements,
3729 new_parts: &mut new_parts,
3730 };
3731 let error = patch_slide(
3732 &mut root,
3733 &[ShapeWrite::Patch {
3734 source_index: 0,
3735 patch: Box::new(ShapePatch {
3736 adjust_values: Some(BTreeMap::from([("adj".to_owned(), 0.25)])),
3737 ..ShapePatch::default()
3738 }),
3739 }],
3740 None,
3741 part,
3742 ShapeElements::WithConnectors,
3743 &mut sink,
3744 &mut budget,
3745 )
3746 .unwrap_err();
3747 assert!(
3748 matches!(error, crate::PptxError::Write { ref message, .. } if message.contains("preset geometry")),
3749 "{error:?}"
3750 );
3751 }
3752}