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