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::{
16 Bullet, PptxPackage, RunProperties, ShapeElements, SlideReference, TextAutofit,
17};
18use crate::xml::{
19 DRAWINGML_NS, PRESENTATIONML_NS, ParseBudget, ParseLimits, XmlElement, XmlNode,
20 alternate_content_branch_index, parse_xml, serialize_xml, serialize_xml_fragment,
21};
22
23const OFFICE_RELATIONSHIPS_NS: &str =
24 "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
25const SLIDE_RELATIONSHIP_TYPE: &str =
26 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide";
27const SLIDE_LAYOUT_RELATIONSHIP_TYPE: &str =
28 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout";
29const STRICT_SLIDE_LAYOUT_RELATIONSHIP_TYPE: &str =
30 "http://purl.oclc.org/ooxml/officeDocument/relationships/slideLayout";
31const SLIDE_CONTENT_TYPE: &str =
32 "application/vnd.openxmlformats-officedocument.presentationml.slide+xml";
33const IMAGE_RELATIONSHIP_TYPE: &str =
34 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
35const MIN_SLIDE_ID: u32 = 256;
36const MAX_SLIDE_ID: u32 = 2_147_483_647;
37
38pub struct DeckWrite {
40 pub slides: Vec<SlideWrite>,
41 pub comments: Option<CommentsWrite>,
42 pub notes: Option<NotesWrite>,
43}
44
45pub struct NotesWrite {
47 pub per_slide: Vec<(CommentSlide, String)>,
48}
49
50const NOTES_SLIDE_RELATIONSHIP_TYPE: &str =
51 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide";
52
53pub enum SlideWrite {
54 Keep { part_path: String },
56 Patch {
58 part_path: String,
59 shapes: Vec<ShapeWrite>,
60 background: Option<BackgroundWrite>,
61 layout_part_path: Option<String>,
62 },
63 Clone {
64 part_path: String,
65 shapes: Vec<ShapeWrite>,
66 background: Option<BackgroundWrite>,
67 layout_part_path: Option<String>,
68 },
69 Add {
71 name: Option<String>,
72 layout_part_path: Option<String>,
73 shapes: Vec<ShapeAdd>,
74 background: Option<Box<ShapeFill>>,
75 },
76}
77
78#[derive(Clone, Debug, PartialEq)]
81pub enum BackgroundWrite {
82 Set(Box<ShapeFill>),
83 Clear,
84}
85
86pub enum ShapeWrite {
87 Keep {
90 source_index: usize,
91 },
92 Patch {
93 source_index: usize,
94 patch: Box<ShapePatch>,
95 },
96 Add(Box<ShapeAdd>),
97}
98
99#[derive(Default)]
100pub struct ShapePatch {
101 pub placeholder: Option<crate::Placeholder>,
102 pub offset: Option<(i64, i64)>,
105 pub extent: Option<(i64, i64)>,
106 pub inherited: Option<InheritedTransform>,
108 pub materializes: bool,
111 pub fill: Option<ShapeFill>,
112 pub outline: Option<ShapeOutline>,
114 pub adjust_values: Option<BTreeMap<String, f64>>,
115 pub table_structure: Option<TableStructure>,
116 pub texts: Vec<TextWrite>,
117 pub children: Vec<ShapeWrite>,
119}
120
121#[derive(Clone, Copy)]
123pub struct InheritedTransform {
124 pub x: i64,
125 pub y: i64,
126 pub width: i64,
127 pub height: i64,
128 pub rotation_deg: f64,
129 pub flip_horizontal: bool,
130 pub flip_vertical: bool,
131}
132
133#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
134#[serde(rename_all = "camelCase", deny_unknown_fields)]
135pub struct TableStructure {
136 pub generation: String,
137 pub story_keys: Vec<String>,
138 pub grid: Vec<i64>,
139 pub columns: Vec<usize>,
140 pub copied_columns: Vec<bool>,
141 pub rows: Vec<TableRowSource>,
142}
143
144#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
145#[serde(rename_all = "camelCase", deny_unknown_fields)]
146pub struct TableRowSource {
147 pub source_row: usize,
148 pub copied: bool,
149 pub copied_cells: Vec<bool>,
150 pub height: i64,
151 pub cells: Vec<[usize; 2]>,
152}
153
154pub struct TextWrite {
155 pub target: TextTarget,
156 pub autofit: Option<TextAutofit>,
157 pub paragraphs: Vec<ParagraphWrite>,
158}
159
160pub enum TextTarget {
161 Body,
162 TableCell { row: usize, cell: usize },
163}
164
165pub struct ParagraphWrite {
166 pub source_index: Option<usize>,
168 pub source_paragraphs: Vec<Option<String>>,
169 pub preserved_xml: Option<String>,
170 pub rebuild: bool,
172 pub properties_changed: bool,
173 pub alignment: Option<String>,
174 pub level: u32,
175 pub bullet: Option<Bullet>,
176 pub runs: Vec<RunWrite>,
177}
178
179pub struct RunWrite {
180 pub text: String,
181 pub properties: RunProperties,
182}
183
184pub struct ShapeAdd {
185 pub name: String,
186 pub geometry: String,
187 pub text_box: bool,
188 pub x: i64,
189 pub y: i64,
190 pub width: i64,
191 pub height: i64,
192 pub adjust_values: BTreeMap<String, f64>,
193 pub fill: Option<ShapeFill>,
194 pub outline: Option<ShapeOutline>,
195 pub paragraphs: Option<Vec<ParagraphWrite>>,
196 pub picture: Option<PictureAdd>,
198}
199
200pub struct PictureAdd {
201 pub media_bytes: Vec<u8>,
202 pub content_type: String,
203}
204
205pub fn write_pptx_with_edits(
208 package: &PptxPackage,
209 deck: &DeckWrite,
210) -> Result<Vec<u8>, PptxError> {
211 let limits = ParseLimits::default();
212 let mut budget = ParseBudget::new(&limits);
213
214 let final_paths: HashSet<&str> = deck
215 .slides
216 .iter()
217 .filter_map(|slide| match slide {
218 SlideWrite::Keep { part_path } | SlideWrite::Patch { part_path, .. } => {
219 Some(part_path.as_str())
220 }
221 SlideWrite::Add { .. } | SlideWrite::Clone { .. } => None,
222 })
223 .collect();
224 let removed: Vec<&SlideReference> = package
225 .presentation
226 .slides
227 .iter()
228 .filter(|reference| !final_paths.contains(reference.part_path.as_str()))
229 .collect();
230
231 let mut replacements: HashMap<String, Vec<u8>> = HashMap::new();
232 let mut new_parts: Vec<(String, Vec<u8>)> = Vec::new();
233
234 let mut minted = Vec::new();
235 let mut next_slide_number = next_slide_number(package);
236 let mut minted_slide_id: Option<u32> = None;
237 let mut next_relationship = next_relationship_number(package);
238 let mut structural = false;
239 let mut final_order: Vec<FinalSlide<'_>> = Vec::new();
240 let mut minted_by_slide: HashMap<usize, usize> = HashMap::new();
241 let mut cloned_dependencies = Vec::new();
242 {
243 let mut sink = PartSink {
244 package,
245 replacements: &mut replacements,
246 new_parts: &mut new_parts,
247 };
248
249 for slide in &deck.slides {
250 if let SlideWrite::Patch {
251 part_path,
252 shapes,
253 background,
254 layout_part_path,
255 } = slide
256 {
257 let bytes = package
258 .part_bytes(part_path)
259 .ok_or_else(|| PptxError::MissingPart(part_path.clone()))?;
260 let mut root = parse_xml(bytes, part_path, &mut budget)?;
261 let original_root = root.clone();
262 let theme =
263 crate::slide_theme(package, Some(part_path), layout_part_path.as_deref());
264 patch_slide(
265 &mut root,
266 shapes,
267 background.as_ref(),
268 Some(&theme),
269 part_path,
270 package.shape_elements,
271 &mut sink,
272 &mut budget,
273 )?;
274 if (layout_part_path.is_some() || has_placeholder_patch(shapes))
275 && background.is_none()
276 && only_placeholder_edits(shapes)
277 {
278 root.attributes.clone_from(&original_root.attributes);
279 }
280 if root != original_root {
281 let xml = if layout_part_path.is_some() || has_placeholder_patch(shapes) {
282 crate::attribute_patch::patch_attributes(bytes, &original_root, &root)
283 .unwrap_or_else(|| serialize_xml(&root))
284 } else {
285 serialize_xml(&root)
286 };
287 sink.store(part_path, xml);
288 }
289 if let Some(layout) = layout_part_path {
290 patch_layout_relationship(&mut sink, part_path, layout, &mut budget)?;
291 }
292 }
293 }
294
295 for (slide_index, slide) in deck.slides.iter().enumerate() {
296 match slide {
297 SlideWrite::Keep { part_path } | SlideWrite::Patch { part_path, .. } => {
298 let reference = package
299 .presentation
300 .slides
301 .iter()
302 .find(|reference| &reference.part_path == part_path)
303 .ok_or_else(|| write_error(part_path, "not a slide of this deck"))?;
304 final_order.push(FinalSlide::Existing(reference));
305 }
306 SlideWrite::Add { .. } | SlideWrite::Clone { .. } => {
307 structural = true;
308 let part_path = format!("ppt/slides/slide{next_slide_number}.xml");
309 next_slide_number += 1;
310 let relationship_id = format!("rId{next_relationship}");
311 next_relationship += 1;
312 let slide_id = match minted_slide_id {
313 None => next_slide_id(package)?,
314 Some(previous) => previous
315 .checked_add(1)
316 .filter(|id| *id <= MAX_SLIDE_ID)
317 .ok_or_else(|| {
318 write_error(
319 &package.presentation.part_path,
320 "the slide id space is exhausted",
321 )
322 })?,
323 };
324 minted_slide_id = Some(slide_id);
325 match slide {
326 SlideWrite::Clone {
327 part_path: source,
328 shapes,
329 background,
330 layout_part_path,
331 } => {
332 let bytes = package
333 .part_bytes(source)
334 .ok_or_else(|| PptxError::MissingPart(source.clone()))?;
335 let mut root = parse_xml(bytes, source, &mut budget)?;
336 let source_rels = slide_relationships_path(source);
337 if let Some(bytes) = package.part_bytes(&source_rels) {
338 let mut rels = parse_xml(bytes, &source_rels, &mut budget)?;
339 rels.children.retain(|child| match child {
340 XmlNode::Element(element) => {
341 element.attribute("Type").is_none_or(|kind| {
342 !kind.ends_with("/comments")
343 && !kind.ends_with("/comment")
344 })
345 }
346 _ => true,
347 });
348 sink.store(
349 &slide_relationships_path(&part_path),
350 serialize_xml(&rels),
351 );
352 }
353 cloned_dependencies.extend(
354 package
355 .relationships
356 .get(source)
357 .into_iter()
358 .flatten()
359 .filter(|relationship| {
360 !relationship.has_type("/notesSlide")
361 && !relationship.has_type("/comments")
362 && !relationship.has_type("/comment")
363 })
364 .filter_map(|relationship| {
365 relationship.resolved_target.clone()
366 }),
367 );
368 cloned_dependencies.extend(clone_notes_part(
369 &mut sink,
370 source,
371 &part_path,
372 &mut budget,
373 )?);
374 if let Some(layout) = layout_part_path {
375 patch_layout_relationship(
376 &mut sink,
377 &part_path,
378 layout,
379 &mut budget,
380 )?;
381 }
382 let theme = crate::slide_theme(
383 package,
384 Some(source),
385 layout_part_path.as_deref(),
386 );
387 patch_slide(
388 &mut root,
389 shapes,
390 background.as_ref(),
391 Some(&theme),
392 &part_path,
393 package.shape_elements,
394 &mut sink,
395 &mut budget,
396 )?;
397 sink.store(&part_path, serialize_xml(&root));
398 }
399 SlideWrite::Add {
400 name,
401 layout_part_path,
402 shapes,
403 background,
404 } => {
405 let layout = layout_part_path
406 .clone()
407 .filter(|path| {
408 package
409 .layouts
410 .iter()
411 .any(|layout| &layout.part_path == path)
412 })
413 .or_else(|| {
414 package
415 .layouts
416 .first()
417 .map(|layout| layout.part_path.clone())
418 });
419 if let Some(layout) = &layout {
420 sink.store(
421 &slide_relationships_path(&part_path),
422 slide_relationships_xml(&part_path, layout),
423 );
424 }
425 let xml = slide_xml(
426 name.as_deref(),
427 shapes,
428 background.as_deref(),
429 &part_path,
430 &mut sink,
431 &mut budget,
432 )?;
433 sink.store(&part_path, xml);
434 }
435 _ => unreachable!(),
436 }
437 minted.push(MintedSlide {
438 part_path,
439 relationship_id,
440 slide_id,
441 });
442 minted_by_slide.insert(slide_index, minted.len() - 1);
443 final_order.push(FinalSlide::Added(minted.len() - 1));
444 }
445 }
446 }
447 }
448 let original_paths: Vec<&str> = package
449 .presentation
450 .slides
451 .iter()
452 .map(|reference| reference.part_path.as_str())
453 .collect();
454 let kept_paths: Vec<&str> = final_order
455 .iter()
456 .filter_map(|slide| match slide {
457 FinalSlide::Existing(reference) => Some(reference.part_path.as_str()),
458 FinalSlide::Added(_) => None,
459 })
460 .collect();
461 structural = structural || kept_paths != original_paths;
462
463 let mut removed_paths = HashSet::new();
464 for reference in &removed {
465 removed_paths.insert(reference.part_path.clone());
466 removed_paths.insert(slide_relationships_path(&reference.part_path));
467 }
468 let orphans = orphaned_parts(package, &removed, &cloned_dependencies);
469 for orphan in &orphans {
470 removed_paths.insert(orphan.clone());
471 removed_paths.insert(slide_relationships_path(orphan));
472 }
473
474 if structural {
475 patch_structure(
476 package,
477 &final_order,
478 &minted,
479 &removed,
480 &orphans,
481 &mut replacements,
482 &mut budget,
483 )?;
484 }
485 if let Some(comments) = &deck.comments {
486 patch_comment_parts(
487 package,
488 comments,
489 &MintedSlides {
490 slides: &minted,
491 by_slide_index: &minted_by_slide,
492 },
493 &mut replacements,
494 &mut new_parts,
495 &mut removed_paths,
496 &mut budget,
497 )?;
498 }
499 if let Some(notes) = &deck.notes {
500 patch_notes_parts(
501 package,
502 notes,
503 &MintedSlides {
504 slides: &minted,
505 by_slide_index: &minted_by_slide,
506 },
507 &mut replacements,
508 &mut new_parts,
509 &mut removed_paths,
510 &mut budget,
511 )?;
512 }
513 {
514 let mut sink = PartSink {
515 package,
516 replacements: &mut replacements,
517 new_parts: &mut new_parts,
518 };
519 for slide in &deck.slides {
520 if let SlideWrite::Patch {
521 part_path,
522 layout_part_path: Some(_),
523 ..
524 } = slide
525 {
526 let path = slide_relationships_path(part_path);
527 if let (Some(source), Some(current)) =
528 (package.part_bytes(&path), sink.current(&path))
529 {
530 let before = parse_xml(source, &path, &mut budget)?;
531 let after = parse_xml(¤t, &path, &mut budget)?;
532 if after.children.len() >= before.children.len() {
533 let mut retained = after.clone();
534 retained.children.truncate(before.children.len());
535 if let Some(bytes) =
536 crate::attribute_patch::patch_attributes(source, &before, &retained)
537 .and_then(|bytes| {
538 append_relationships(
539 bytes,
540 &after.children[before.children.len()..],
541 )
542 })
543 {
544 sink.store(&path, bytes);
545 }
546 }
547 }
548 }
549 }
550 }
551 let mut parts = Vec::with_capacity(package.parts.len() + new_parts.len());
552 for part in &package.parts {
553 if removed_paths.contains(&part.path) {
554 continue;
555 }
556 match replacements.remove(&part.path) {
557 Some(bytes) => parts.push((part.path.clone(), bytes)),
558 None => parts.push((part.path.clone(), part.bytes.clone())),
559 }
560 }
561 parts.extend(new_parts);
562 ooxml_opc::rezip_parts_preserving(&parts, package.source_container.as_bytes())
563 .map_err(PptxError::Container)
564}
565
566struct MintedSlide {
567 part_path: String,
568 relationship_id: String,
569 slide_id: u32,
570}
571
572enum FinalSlide<'a> {
573 Existing(&'a SlideReference),
574 Added(usize),
575}
576
577fn write_error(part: &str, message: impl Into<String>) -> PptxError {
578 PptxError::Write {
579 part: part.to_owned(),
580 message: message.into(),
581 }
582}
583
584fn next_slide_number(package: &PptxPackage) -> usize {
585 let paths: HashSet<&str> = package
586 .parts
587 .iter()
588 .map(|part| part.path.as_str())
589 .collect();
590 let mut number = package
591 .parts
592 .iter()
593 .filter_map(|part| {
594 part.path
595 .strip_prefix("ppt/slides/slide")?
596 .strip_suffix(".xml")?
597 .parse::<usize>()
598 .ok()
599 })
600 .max()
601 .unwrap_or(0)
602 + 1;
603 while paths.contains(format!("ppt/slides/slide{number}.xml").as_str())
604 || paths.contains(format!("ppt/slides/_rels/slide{number}.xml.rels").as_str())
605 {
606 number += 1;
607 }
608 number
609}
610
611fn next_slide_id(package: &PptxPackage) -> Result<u32, PptxError> {
612 let next = package
613 .presentation
614 .slides
615 .iter()
616 .map(|reference| reference.id)
617 .max()
618 .unwrap_or(0)
619 .max(MIN_SLIDE_ID - 1)
620 .checked_add(1)
621 .filter(|id| *id <= MAX_SLIDE_ID);
622 next.ok_or_else(|| {
623 write_error(
624 &package.presentation.part_path,
625 "the slide id space is exhausted",
626 )
627 })
628}
629
630fn next_relationship_number(package: &PptxPackage) -> usize {
631 package
632 .relationships
633 .get(&package.presentation.part_path)
634 .into_iter()
635 .flatten()
636 .filter_map(|relationship| relationship.id.strip_prefix("rId")?.parse::<usize>().ok())
637 .max()
638 .unwrap_or(0)
639 + 1
640}
641
642fn slide_relationships_path(part_path: &str) -> String {
643 match part_path.rsplit_once('/') {
644 Some((directory, name)) => format!("{directory}/_rels/{name}.rels"),
645 None => format!("_rels/{part_path}.rels"),
646 }
647}
648
649fn orphaned_parts(
652 package: &PptxPackage,
653 removed: &[&SlideReference],
654 cloned_dependencies: &[String],
655) -> Vec<String> {
656 if removed.is_empty() {
657 return Vec::new();
658 }
659 let excluded: HashSet<&str> = removed
660 .iter()
661 .map(|reference| reference.part_path.as_str())
662 .collect();
663 let before = reachable_parts(package, &HashSet::new(), &[]);
664 let after = reachable_parts(package, &excluded, cloned_dependencies);
665 before
666 .into_iter()
667 .filter(|part| !after.contains(part) && !excluded.contains(part.as_str()))
668 .collect()
669}
670
671fn reachable_parts(
672 package: &PptxPackage,
673 excluded: &HashSet<&str>,
674 roots: &[String],
675) -> HashSet<String> {
676 let mut reachable: HashSet<String> = roots
677 .iter()
678 .filter(|part| !excluded.contains(part.as_str()))
679 .cloned()
680 .collect();
681 let mut queue: Vec<String> = reachable.iter().cloned().collect();
682 queue.push(String::new());
683 while let Some(source) = queue.pop() {
684 for relationship in package.relationships.get(&source).into_iter().flatten() {
685 let Some(target) = &relationship.resolved_target else {
686 continue;
687 };
688 if excluded.contains(target.as_str()) {
689 continue;
690 }
691 if reachable.insert(target.clone()) {
692 queue.push(target.clone());
693 }
694 }
695 }
696 reachable
697}
698
699fn relative_target(from_part: &str, to_part: &str) -> String {
700 let from: Vec<&str> = from_part.split('/').collect();
701 let to: Vec<&str> = to_part.split('/').collect();
702 let from_directory = &from[..from.len().saturating_sub(1)];
703 let to_directory = &to[..to.len().saturating_sub(1)];
704 let shared = from_directory
705 .iter()
706 .zip(to_directory)
707 .take_while(|(a, b)| a == b)
708 .count();
709 let mut segments: Vec<String> = vec!["..".to_owned(); from_directory.len() - shared];
710 segments.extend(to[shared..].iter().map(|segment| (*segment).to_owned()));
711 segments.join("/")
712}
713
714fn patch_structure(
717 package: &PptxPackage,
718 final_order: &[FinalSlide<'_>],
719 minted: &[MintedSlide],
720 removed: &[&SlideReference],
721 orphans: &[String],
722 replacements: &mut HashMap<String, Vec<u8>>,
723 budget: &mut ParseBudget<'_>,
724) -> Result<(), PptxError> {
725 let presentation_path = package.presentation.part_path.as_str();
726 let bytes = package
727 .part_bytes(presentation_path)
728 .ok_or_else(|| PptxError::MissingPart(presentation_path.to_owned()))?;
729 let mut root = parse_xml(bytes, presentation_path, budget)?;
730 patch_slide_id_list(&mut root, final_order, minted, presentation_path)?;
731 prune_slide_references(&mut root, removed);
732 replacements.insert(presentation_path.to_owned(), serialize_xml(&root));
733
734 let relationships_path = slide_relationships_path(presentation_path);
735 let bytes = package
736 .part_bytes(&relationships_path)
737 .ok_or_else(|| PptxError::MissingPart(relationships_path.clone()))?;
738 let mut root = parse_xml(bytes, &relationships_path, budget)?;
739 let removed_ids: HashSet<&str> = removed
740 .iter()
741 .map(|reference| reference.relationship_id.as_str())
742 .collect();
743 root.children.retain(|child| match child {
744 XmlNode::Element(element) if element.local_name() == "Relationship" => element
745 .attribute("Id")
746 .is_none_or(|id| !removed_ids.contains(id)),
747 _ => true,
748 });
749 for slide in minted {
750 root.children.push(XmlNode::Element(
751 XmlElement::new("Relationship")
752 .with_attribute("Id", slide.relationship_id.clone())
753 .with_attribute("Type", SLIDE_RELATIONSHIP_TYPE)
754 .with_attribute(
755 "Target",
756 relative_target(presentation_path, &slide.part_path),
757 ),
758 ));
759 }
760 replacements.insert(relationships_path, serialize_xml(&root));
761
762 let content_types_path = "[Content_Types].xml";
763 let bytes = match replacements.get(content_types_path) {
764 Some(bytes) => bytes.clone(),
765 None => package
766 .part_bytes(content_types_path)
767 .ok_or_else(|| PptxError::MissingPart(content_types_path.to_owned()))?
768 .to_vec(),
769 };
770 let mut root = parse_xml(&bytes, content_types_path, budget)?;
771 let removed_names: HashSet<String> = removed
772 .iter()
773 .map(|reference| format!("/{}", reference.part_path))
774 .chain(orphans.iter().map(|orphan| format!("/{orphan}")))
775 .collect();
776 root.children.retain(|child| match child {
777 XmlNode::Element(element) if element.local_name() == "Override" => element
778 .attribute("PartName")
779 .is_none_or(|name| !removed_names.contains(name)),
780 _ => true,
781 });
782 for slide in minted {
783 root.children.push(XmlNode::Element(
784 XmlElement::new("Override")
785 .with_attribute("PartName", format!("/{}", slide.part_path))
786 .with_attribute("ContentType", SLIDE_CONTENT_TYPE),
787 ));
788 }
789 replacements.insert(content_types_path.to_owned(), serialize_xml(&root));
790 Ok(())
791}
792
793const PACKAGE_RELATIONSHIPS_NS: &str =
794 "http://schemas.openxmlformats.org/package/2006/relationships";
795
796struct MintedSlides<'a> {
797 slides: &'a [MintedSlide],
798 by_slide_index: &'a HashMap<usize, usize>,
799}
800
801impl MintedSlides<'_> {
802 fn get(&self, slide_index: usize) -> Option<&MintedSlide> {
803 self.by_slide_index
804 .get(&slide_index)
805 .and_then(|at| self.slides.get(*at))
806 }
807}
808
809fn patch_comment_parts(
810 package: &PptxPackage,
811 write: &CommentsWrite,
812 minted: &MintedSlides<'_>,
813 replacements: &mut HashMap<String, Vec<u8>>,
814 new_parts: &mut Vec<(String, Vec<u8>)>,
815 removed_paths: &mut HashSet<String>,
816 budget: &mut ParseBudget<'_>,
817) -> Result<(), PptxError> {
818 let mut taken: HashSet<String> = package
819 .parts
820 .iter()
821 .map(|part| part.path.clone())
822 .chain(
823 package
824 .relationships
825 .values()
826 .flatten()
827 .filter(|relationship| relationship.is_type(write.comments_relationship_type()))
828 .filter_map(|relationship| relationship.resolved_target.clone()),
829 )
830 .collect();
831
832 let mut sink = PartSink {
833 package,
834 replacements,
835 new_parts,
836 };
837 for (slide, comments) in &write.per_slide {
838 let (slide_part_path, slide_id) = match slide {
839 CommentSlide::Existing(part_path) => {
840 let reference = package
841 .presentation
842 .slides
843 .iter()
844 .find(|reference| &reference.part_path == part_path)
845 .ok_or_else(|| write_error(part_path, "not a slide of this deck"))?;
846 (part_path.clone(), reference.id)
847 }
848 CommentSlide::Added(index) => {
849 let Some(slide) = minted.get(*index) else {
850 continue;
851 };
852 (slide.part_path.clone(), slide.slide_id)
853 }
854 };
855 let relationships_path = slide_relationships_path(&slide_part_path);
856 let existing = existing_comment_part(package, &slide_part_path, write);
857
858 if comments.is_empty() {
859 if let Some(part_path) = existing {
860 removed_paths.insert(part_path.clone());
861 sink.forget(&part_path);
862 remove_content_type_override(&mut sink, &part_path, budget)?;
863 remove_relationship(
864 &mut sink,
865 &relationships_path,
866 write.comments_relationship_type(),
867 budget,
868 )?;
869 }
870 continue;
871 }
872
873 let is_existing = existing.is_some();
874 let part_path = match existing {
875 Some(part_path) => part_path,
876 None => mint_comment_part_path(write, &slide_part_path, &mut taken),
877 };
878 removed_paths.remove(&part_path);
879 let bytes = match sink.current(&part_path) {
880 Some(bytes) => crate::comment_patch::patch_comments_xml(
881 &bytes, &part_path, write, comments, slide_id, budget,
882 )?,
883 None => comments_xml(write, comments, slide_id),
884 };
885 sink.store(&part_path, bytes);
886 if !is_existing {
887 set_content_type_override(
888 &mut sink,
889 &part_path,
890 write.comments_content_type(),
891 budget,
892 )?;
893 set_relationship(
894 &mut sink,
895 &relationships_path,
896 write.comments_relationship_type(),
897 &relative_target(&slide_part_path, &part_path),
898 budget,
899 )?;
900 }
901 }
902
903 let presentation_relationships = slide_relationships_path(&package.presentation.part_path);
904 let authors_path = existing_authors_part(package, write)
905 .unwrap_or_else(|| write.authors_part_path().to_owned());
906 if write.authors.is_empty() {
907 removed_paths.insert(authors_path.clone());
908 sink.forget(&authors_path);
909 remove_content_type_override(&mut sink, &authors_path, budget)?;
910 remove_relationship(
911 &mut sink,
912 &presentation_relationships,
913 write.authors_relationship_type(),
914 budget,
915 )?;
916 } else {
917 removed_paths.remove(&authors_path);
918 let existing = sink.current(&authors_path);
919 let bytes = match &existing {
920 Some(bytes) => {
921 crate::comment_patch::patch_authors_xml(bytes, &authors_path, write, budget)?
922 }
923 None => authors_xml(write),
924 };
925 sink.store(&authors_path, bytes);
926 if existing.is_none() {
927 set_content_type_override(
928 &mut sink,
929 &authors_path,
930 write.authors_content_type(),
931 budget,
932 )?;
933 set_relationship(
934 &mut sink,
935 &presentation_relationships,
936 write.authors_relationship_type(),
937 &relative_target(&package.presentation.part_path, &authors_path),
938 budget,
939 )?;
940 }
941 }
942
943 if package.comment_flavor != Some(write.flavor) {
944 drop_other_flavor(package, write, &mut sink, removed_paths, budget)?;
945 }
946 Ok(())
947}
948
949fn clone_notes_part(
950 sink: &mut PartSink<'_>,
951 source_slide: &str,
952 copy_slide: &str,
953 budget: &mut ParseBudget<'_>,
954) -> Result<Vec<String>, PptxError> {
955 let Some(source_notes) = existing_notes_part(sink.package, source_slide) else {
956 return Ok(Vec::new());
957 };
958 let mut taken = sink
959 .package
960 .parts
961 .iter()
962 .map(|part| part.path.clone())
963 .chain(sink.new_parts.iter().map(|(path, _)| path.clone()))
964 .chain(
965 sink.package
966 .relationships
967 .values()
968 .flatten()
969 .filter_map(|relationship| relationship.resolved_target.clone()),
970 )
971 .collect();
972 let copy_notes = mint_notes_part_path(copy_slide, &mut taken);
973 let bytes = sink
974 .package
975 .part_bytes(&source_notes)
976 .ok_or_else(|| PptxError::MissingPart(source_notes.clone()))?
977 .to_vec();
978 sink.store(©_notes, bytes);
979 set_content_type_override(sink, ©_notes, crate::notes::CT_NOTES_SLIDE, budget)?;
980 set_relationship(
981 sink,
982 &slide_relationships_path(copy_slide),
983 NOTES_SLIDE_RELATIONSHIP_TYPE,
984 &relative_target(copy_slide, ©_notes),
985 budget,
986 )?;
987 let source_rels = slide_relationships_path(&source_notes);
988 let copy_rels = slide_relationships_path(©_notes);
989 let mut dependencies = Vec::new();
990 if let Some(bytes) = sink.package.part_bytes(&source_rels) {
991 let mut root = parse_xml(bytes, &source_rels, budget)?;
992 root.children.retain(|child| match child {
993 XmlNode::Element(element) => element
994 .attribute("Type")
995 .is_none_or(|kind| !kind.ends_with("/comments") && !kind.ends_with("/comment")),
996 _ => true,
997 });
998 for child in &mut root.children {
999 let XmlNode::Element(element) = child else {
1000 continue;
1001 };
1002 if element.local_name() != "Relationship"
1003 || element.attribute("TargetMode") == Some("External")
1004 {
1005 continue;
1006 }
1007 let target = if element
1008 .attribute("Type")
1009 .is_some_and(|kind| kind.ends_with("/slide"))
1010 {
1011 copy_slide.to_owned()
1012 } else if let Some(target) = element.attribute("Target") {
1013 let target = crate::relationships::resolve_target(&source_notes, target)?;
1014 dependencies.push(target.clone());
1015 target
1016 } else {
1017 continue;
1018 };
1019 element.set_attribute("Target", relative_target(©_notes, &target));
1020 }
1021 sink.store(©_rels, serialize_xml(&root));
1022 }
1023 set_relationship(
1024 sink,
1025 ©_rels,
1026 SLIDE_RELATIONSHIP_TYPE,
1027 &relative_target(©_notes, copy_slide),
1028 budget,
1029 )?;
1030 Ok(dependencies)
1031}
1032
1033fn patch_notes_parts(
1034 package: &PptxPackage,
1035 write: &NotesWrite,
1036 minted: &MintedSlides<'_>,
1037 replacements: &mut HashMap<String, Vec<u8>>,
1038 new_parts: &mut Vec<(String, Vec<u8>)>,
1039 removed_paths: &mut HashSet<String>,
1040 budget: &mut ParseBudget<'_>,
1041) -> Result<(), PptxError> {
1042 let mut taken: HashSet<String> = package
1043 .parts
1044 .iter()
1045 .map(|part| part.path.clone())
1046 .chain(new_parts.iter().map(|(path, _)| path.clone()))
1047 .chain(
1048 package
1049 .relationships
1050 .values()
1051 .flatten()
1052 .filter(|relationship| relationship.is_type(NOTES_SLIDE_RELATIONSHIP_TYPE))
1053 .filter_map(|relationship| relationship.resolved_target.clone()),
1054 )
1055 .collect();
1056
1057 let mut sink = PartSink {
1058 package,
1059 replacements,
1060 new_parts,
1061 };
1062 for (slide, text) in &write.per_slide {
1063 let slide_part_path = match slide {
1064 CommentSlide::Existing(part_path) => part_path.clone(),
1065 CommentSlide::Added(index) => {
1066 let Some(slide) = minted.get(*index) else {
1067 continue;
1068 };
1069 slide.part_path.clone()
1070 }
1071 };
1072 let relationships_path = slide_relationships_path(&slide_part_path);
1073 let existing = sink
1074 .current(&relationships_path)
1075 .map(|bytes| {
1076 crate::relationships::parse_relationships(
1077 &bytes,
1078 &relationships_path,
1079 &slide_part_path,
1080 budget,
1081 )
1082 })
1083 .transpose()?
1084 .and_then(|relationships| crate::notes::slide_notes_part(&relationships));
1085
1086 if text.is_empty() && existing.is_none() {
1087 continue;
1088 }
1089
1090 let is_existing = existing.is_some();
1091 let part_path = match existing {
1092 Some(part_path) => part_path,
1093 None => mint_notes_part_path(&slide_part_path, &mut taken),
1094 };
1095 removed_paths.remove(&part_path);
1096 let bytes = match sink.current(&part_path) {
1097 Some(bytes) => crate::notes::patch_notes_xml(&bytes, &part_path, text, budget)?,
1098 None => crate::notes::notes_slide_xml(text),
1099 };
1100 sink.store(&part_path, bytes);
1101 if !is_existing {
1102 set_content_type_override(&mut sink, &part_path, crate::notes::CT_NOTES_SLIDE, budget)?;
1103 set_relationship(
1104 &mut sink,
1105 &relationships_path,
1106 NOTES_SLIDE_RELATIONSHIP_TYPE,
1107 &relative_target(&slide_part_path, &part_path),
1108 budget,
1109 )?;
1110 let notes_relationships = slide_relationships_path(&part_path);
1111 set_relationship(
1112 &mut sink,
1113 ¬es_relationships,
1114 SLIDE_RELATIONSHIP_TYPE,
1115 &relative_target(&part_path, &slide_part_path),
1116 budget,
1117 )?;
1118 if let Some(master) = package
1119 .relationships
1120 .get(&package.presentation.part_path)
1121 .into_iter()
1122 .flatten()
1123 .find(|relationship| relationship.has_type("/notesMaster"))
1124 .and_then(|relationship| relationship.resolved_target.as_deref())
1125 {
1126 set_relationship(
1127 &mut sink,
1128 ¬es_relationships,
1129 &format!("{OFFICE_RELATIONSHIPS_NS}/notesMaster"),
1130 &relative_target(&part_path, master),
1131 budget,
1132 )?;
1133 }
1134 }
1135 }
1136 Ok(())
1137}
1138
1139fn existing_notes_part(package: &PptxPackage, slide_part_path: &str) -> Option<String> {
1140 package
1141 .relationships
1142 .get(slide_part_path)?
1143 .iter()
1144 .find(|relationship| relationship.is_type(NOTES_SLIDE_RELATIONSHIP_TYPE))
1145 .and_then(|relationship| relationship.resolved_target.clone())
1146}
1147
1148fn mint_notes_part_path(slide_part_path: &str, taken: &mut HashSet<String>) -> String {
1149 let preferred = slide_number(slide_part_path);
1150 let mut number = preferred;
1151 loop {
1152 let candidate = format!("ppt/notesSlides/notesSlide{number}.xml");
1153 if taken.insert(candidate.clone()) {
1154 return candidate;
1155 }
1156 number += 1;
1157 }
1158}
1159
1160fn mint_comment_part_path(
1161 write: &CommentsWrite,
1162 slide_part_path: &str,
1163 taken: &mut HashSet<String>,
1164) -> String {
1165 let preferred = slide_number(slide_part_path);
1166 let mut number = preferred;
1167 loop {
1168 let candidate = write.comments_part_path(number);
1169 if taken.insert(candidate.clone()) {
1170 return candidate;
1171 }
1172 number += 1;
1173 }
1174}
1175
1176fn drop_other_flavor(
1177 package: &PptxPackage,
1178 write: &CommentsWrite,
1179 sink: &mut PartSink<'_>,
1180 removed_paths: &mut HashSet<String>,
1181 budget: &mut ParseBudget<'_>,
1182) -> Result<(), PptxError> {
1183 let other = CommentsWrite {
1184 flavor: match write.flavor {
1185 CommentFlavor::Legacy => CommentFlavor::Modern,
1186 CommentFlavor::Modern => CommentFlavor::Legacy,
1187 },
1188 authors: Vec::new(),
1189 per_slide: Vec::new(),
1190 };
1191 for reference in &package.presentation.slides {
1192 let Some(part_path) = existing_comment_part(package, &reference.part_path, &other) else {
1193 continue;
1194 };
1195 removed_paths.insert(part_path.clone());
1196 sink.forget(&part_path);
1197 remove_content_type_override(sink, &part_path, budget)?;
1198 remove_relationship(
1199 sink,
1200 &slide_relationships_path(&reference.part_path),
1201 other.comments_relationship_type(),
1202 budget,
1203 )?;
1204 }
1205 let authors_path = existing_authors_part(package, &other)
1206 .unwrap_or_else(|| other.authors_part_path().to_owned());
1207 if package.part_bytes(&authors_path).is_some() {
1208 removed_paths.insert(authors_path.clone());
1209 sink.forget(&authors_path);
1210 remove_content_type_override(sink, &authors_path, budget)?;
1211 remove_relationship(
1212 sink,
1213 &slide_relationships_path(&package.presentation.part_path),
1214 other.authors_relationship_type(),
1215 budget,
1216 )?;
1217 }
1218 Ok(())
1219}
1220
1221fn existing_authors_part(package: &PptxPackage, write: &CommentsWrite) -> Option<String> {
1222 package
1223 .relationships
1224 .get(&package.presentation.part_path)?
1225 .iter()
1226 .find(|relationship| relationship.is_type(write.authors_relationship_type()))
1227 .and_then(|relationship| relationship.resolved_target.clone())
1228}
1229
1230fn existing_comment_part(
1231 package: &PptxPackage,
1232 slide_part_path: &str,
1233 write: &CommentsWrite,
1234) -> Option<String> {
1235 package
1236 .relationships
1237 .get(slide_part_path)?
1238 .iter()
1239 .find(|relationship| relationship.is_type(write.comments_relationship_type()))
1240 .and_then(|relationship| relationship.resolved_target.clone())
1241}
1242
1243fn slide_number(slide_part_path: &str) -> usize {
1244 slide_part_path
1245 .rsplit_once("/slide")
1246 .and_then(|(_, tail)| tail.strip_suffix(".xml"))
1247 .and_then(|digits| digits.parse().ok())
1248 .unwrap_or(1)
1249}
1250
1251struct PartSink<'a> {
1252 package: &'a PptxPackage,
1253 replacements: &'a mut HashMap<String, Vec<u8>>,
1254 new_parts: &'a mut Vec<(String, Vec<u8>)>,
1255}
1256
1257impl PartSink<'_> {
1258 fn current(&self, path: &str) -> Option<Vec<u8>> {
1259 if let Some(bytes) = self.replacements.get(path) {
1260 return Some(bytes.clone());
1261 }
1262 if let Some((_, bytes)) = self.new_parts.iter().find(|(name, _)| name == path) {
1263 return Some(bytes.clone());
1264 }
1265 self.package.part_bytes(path).map(<[u8]>::to_vec)
1266 }
1267
1268 fn store(&mut self, path: &str, bytes: Vec<u8>) {
1269 if self.package.part_bytes(path).is_some() {
1270 self.replacements.insert(path.to_owned(), bytes);
1271 return;
1272 }
1273 match self.new_parts.iter_mut().find(|(name, _)| name == path) {
1274 Some((_, existing)) => *existing = bytes,
1275 None => self.new_parts.push((path.to_owned(), bytes)),
1276 }
1277 }
1278
1279 fn forget(&mut self, path: &str) {
1280 self.replacements.remove(path);
1281 self.new_parts.retain(|(name, _)| name != path);
1282 }
1283}
1284
1285fn append_relationships(bytes: Vec<u8>, children: &[XmlNode]) -> Option<Vec<u8>> {
1286 if children.is_empty() {
1287 return Some(bytes);
1288 }
1289 let mut additions = Vec::new();
1290 for child in children {
1291 let XmlNode::Element(child) = child else {
1292 return None;
1293 };
1294 additions.extend(serialize_xml_fragment(child));
1295 }
1296 let mut reader = quick_xml::Reader::from_reader(bytes.as_slice());
1297 let mut depth = 0;
1298 loop {
1299 let start = reader.buffer_position() as usize;
1300 match reader.read_event().ok()? {
1301 quick_xml::events::Event::Start(_) => depth += 1,
1302 quick_xml::events::Event::End(_) => {
1303 depth -= 1;
1304 if depth == 0 {
1305 let mut output = bytes[..start].to_vec();
1306 output.extend(additions);
1307 output.extend_from_slice(&bytes[start..]);
1308 return Some(output);
1309 }
1310 }
1311 quick_xml::events::Event::Eof => return None,
1312 _ => {}
1313 }
1314 }
1315}
1316
1317fn only_placeholder_edits(shapes: &[ShapeWrite]) -> bool {
1318 shapes.iter().all(|shape| match shape {
1319 ShapeWrite::Keep { .. } => true,
1320 ShapeWrite::Patch { patch, .. } => {
1321 patch.offset.is_none()
1322 && patch.extent.is_none()
1323 && patch.inherited.is_none()
1324 && !patch.materializes
1325 && patch.fill.is_none()
1326 && patch.outline.is_none()
1327 && patch.adjust_values.is_none()
1328 && patch.table_structure.is_none()
1329 && patch.texts.is_empty()
1330 && only_placeholder_edits(&patch.children)
1331 }
1332 ShapeWrite::Add(_) => false,
1333 })
1334}
1335
1336fn has_placeholder_patch(shapes: &[ShapeWrite]) -> bool {
1337 shapes.iter().any(|shape| {
1338 matches!(shape, ShapeWrite::Patch { patch, .. }
1339 if patch.placeholder.is_some() || has_placeholder_patch(&patch.children))
1340 })
1341}
1342
1343fn is_slide_layout_relationship(element: &XmlElement) -> bool {
1344 element.local_name() == "Relationship"
1345 && matches!(
1346 element.attribute("Type"),
1347 Some(SLIDE_LAYOUT_RELATIONSHIP_TYPE | STRICT_SLIDE_LAYOUT_RELATIONSHIP_TYPE)
1348 )
1349}
1350
1351fn patch_layout_relationship(
1352 sink: &mut PartSink<'_>,
1353 slide: &str,
1354 layout: &str,
1355 budget: &mut ParseBudget<'_>,
1356) -> Result<(), PptxError> {
1357 let path = slide_relationships_path(slide);
1358 let bytes = sink
1359 .current(&path)
1360 .ok_or_else(|| PptxError::InvalidSlideLayoutRelationships {
1361 part: slide.to_owned(),
1362 count: 0,
1363 })?;
1364 let mut root = parse_xml(&bytes, &path, budget)?;
1365 let before = root.clone();
1366 let count = root
1367 .child_elements()
1368 .filter(|element| is_slide_layout_relationship(element))
1369 .count();
1370 if count != 1 {
1371 return Err(PptxError::InvalidSlideLayoutRelationships {
1372 part: slide.to_owned(),
1373 count,
1374 });
1375 }
1376 for child in &mut root.children {
1377 if let XmlNode::Element(element) = child
1378 && is_slide_layout_relationship(element)
1379 {
1380 element.set_attribute("Target", relative_target(slide, layout));
1381 }
1382 }
1383 let output = crate::attribute_patch::patch_attributes(&bytes, &before, &root)
1384 .ok_or_else(|| write_error(&path, "could not patch slide layout relationship"))?;
1385 sink.store(&path, output);
1386 Ok(())
1387}
1388
1389fn set_relationship(
1390 sink: &mut PartSink<'_>,
1391 relationships_path: &str,
1392 relationship_type: &str,
1393 target: &str,
1394 budget: &mut ParseBudget<'_>,
1395) -> Result<(), PptxError> {
1396 let mut root = match sink.current(relationships_path) {
1397 Some(bytes) => parse_xml(&bytes, relationships_path, budget)?,
1398 None => XmlElement::new("Relationships").with_attribute("xmlns", PACKAGE_RELATIONSHIPS_NS),
1399 };
1400 let existing = root.children.iter_mut().find_map(|child| match child {
1401 XmlNode::Element(element)
1402 if element.local_name() == "Relationship"
1403 && element.attribute("Type") == Some(relationship_type) =>
1404 {
1405 Some(element)
1406 }
1407 _ => None,
1408 });
1409 match existing {
1410 Some(element) => element.set_attribute("Target", target),
1411 None => {
1412 let id = format!("rId{}", max_relationship_id(&root) + 1);
1413 root.children.push(XmlNode::Element(
1414 XmlElement::new("Relationship")
1415 .with_attribute("Id", id)
1416 .with_attribute("Type", relationship_type)
1417 .with_attribute("Target", target),
1418 ));
1419 }
1420 }
1421 sink.store(relationships_path, serialize_xml(&root));
1422 Ok(())
1423}
1424
1425fn remove_relationship(
1426 sink: &mut PartSink<'_>,
1427 relationships_path: &str,
1428 relationship_type: &str,
1429 budget: &mut ParseBudget<'_>,
1430) -> Result<(), PptxError> {
1431 let Some(bytes) = sink.current(relationships_path) else {
1432 return Ok(());
1433 };
1434 let mut root = parse_xml(&bytes, relationships_path, budget)?;
1435 root.children.retain(|child| match child {
1436 XmlNode::Element(element) if element.local_name() == "Relationship" => {
1437 element.attribute("Type") != Some(relationship_type)
1438 }
1439 _ => true,
1440 });
1441 sink.store(relationships_path, serialize_xml(&root));
1442 Ok(())
1443}
1444
1445fn max_relationship_id(root: &XmlElement) -> usize {
1446 root.children_named("Relationship")
1447 .filter_map(|element| element.attribute("Id"))
1448 .filter_map(|id| id.strip_prefix("rId"))
1449 .filter_map(|number| number.parse::<usize>().ok())
1450 .max()
1451 .unwrap_or(0)
1452}
1453
1454fn set_content_type_override(
1455 sink: &mut PartSink<'_>,
1456 part_path: &str,
1457 content_type: &str,
1458 budget: &mut ParseBudget<'_>,
1459) -> Result<(), PptxError> {
1460 let path = "[Content_Types].xml";
1461 let bytes = sink
1462 .current(path)
1463 .ok_or_else(|| PptxError::MissingPart(path.to_owned()))?;
1464 let mut root = parse_xml(&bytes, path, budget)?;
1465 let prefix = root.name.rsplit_once(':').map_or("", |(prefix, _)| prefix);
1466 let override_name = qualified(prefix, "Override");
1467 let name = format!("/{part_path}");
1468 let existing = root.children.iter_mut().find_map(|child| match child {
1469 XmlNode::Element(element)
1470 if element.local_name() == "Override"
1471 && element.attribute("PartName") == Some(name.as_str()) =>
1472 {
1473 Some(element)
1474 }
1475 _ => None,
1476 });
1477 match existing {
1478 Some(element) => element.set_attribute("ContentType", content_type),
1479 None => root.children.push(XmlNode::Element(
1480 XmlElement::new(override_name)
1481 .with_attribute("PartName", name)
1482 .with_attribute("ContentType", content_type),
1483 )),
1484 }
1485 sink.store(path, serialize_xml(&root));
1486 Ok(())
1487}
1488
1489fn remove_content_type_override(
1490 sink: &mut PartSink<'_>,
1491 part_path: &str,
1492 budget: &mut ParseBudget<'_>,
1493) -> Result<(), PptxError> {
1494 let path = "[Content_Types].xml";
1495 let Some(bytes) = sink.current(path) else {
1496 return Ok(());
1497 };
1498 let mut root = parse_xml(&bytes, path, budget)?;
1499 let name = format!("/{part_path}");
1500 root.children.retain(|child| match child {
1501 XmlNode::Element(element) if element.local_name() == "Override" => {
1502 element.attribute("PartName") != Some(name.as_str())
1503 }
1504 _ => true,
1505 });
1506 sink.store(path, serialize_xml(&root));
1507 Ok(())
1508}
1509
1510fn prune_slide_references(root: &mut XmlElement, removed: &[&SlideReference]) {
1513 if removed.is_empty() {
1514 return;
1515 }
1516 let removed_relationship_ids: HashSet<&str> = removed
1517 .iter()
1518 .map(|reference| reference.relationship_id.as_str())
1519 .collect();
1520 let removed_slide_ids: HashSet<String> = removed
1521 .iter()
1522 .map(|reference| reference.id.to_string())
1523 .collect();
1524 for child in root.children.iter_mut() {
1525 let XmlNode::Element(element) = child else {
1526 continue;
1527 };
1528 match element.local_name() {
1529 "custShowLst" => remove_matching_descendants(element, &|candidate| {
1530 candidate.local_name() == "sld"
1531 && candidate.attributes.iter().any(|(key, value)| {
1532 key.ends_with(":id") && removed_relationship_ids.contains(value.as_str())
1533 })
1534 }),
1535 "extLst" => remove_matching_descendants(element, &|candidate| {
1536 candidate.local_name() == "sldId"
1537 && candidate
1538 .attribute("id")
1539 .is_some_and(|id| removed_slide_ids.contains(id))
1540 }),
1541 _ => {}
1542 }
1543 }
1544}
1545
1546fn remove_matching_descendants(element: &mut XmlElement, matches: &dyn Fn(&XmlElement) -> bool) {
1547 element.children.retain(|child| match child {
1548 XmlNode::Element(candidate) => !matches(candidate),
1549 _ => true,
1550 });
1551 for child in element.children.iter_mut() {
1552 if let XmlNode::Element(child) = child {
1553 remove_matching_descendants(child, matches);
1554 }
1555 }
1556}
1557
1558fn patch_slide_id_list(
1559 root: &mut XmlElement,
1560 final_order: &[FinalSlide<'_>],
1561 minted: &[MintedSlide],
1562 part: &str,
1563) -> Result<(), PptxError> {
1564 let presentation_prefix = resolve_prefix(root, PRESENTATIONML_NS, "p");
1565 let relationship_prefix = resolve_prefix(root, OFFICE_RELATIONSHIPS_NS, "r");
1566 let list_name = qualified(&presentation_prefix, "sldIdLst");
1567 if root.child_mut("sldIdLst").is_none() {
1568 let position = root
1569 .children
1570 .iter()
1571 .position(|child| {
1572 matches!(child, XmlNode::Element(element) if element.local_name() == "sldMasterIdLst")
1573 })
1574 .map(|index| index + 1)
1575 .unwrap_or(0);
1576 root.children.insert(
1577 position,
1578 XmlNode::Element(XmlElement::new(list_name.clone())),
1579 );
1580 }
1581 let relationship_attribute = qualified(&relationship_prefix, "id");
1582 let list = root
1583 .child_mut("sldIdLst")
1584 .ok_or_else(|| write_error(part, "missing slide id list"))?;
1585 let mut existing: HashMap<String, XmlElement> = HashMap::new();
1586 for child in std::mem::take(&mut list.children) {
1587 if let XmlNode::Element(element) = child
1588 && element.local_name() == "sldId"
1589 {
1590 let relationship_id = element.attribute(&relationship_attribute).or_else(|| {
1591 element
1592 .attributes
1593 .iter()
1594 .find(|(key, _)| key.ends_with(":id"))
1595 .map(|(_, value)| value.as_str())
1596 });
1597 if let Some(id) = relationship_id {
1598 existing.insert(id.to_owned(), element);
1599 }
1600 }
1601 }
1602 let entry_name = qualified(&presentation_prefix, "sldId");
1603 for slide in final_order {
1604 let element = match slide {
1605 FinalSlide::Existing(reference) => existing
1606 .remove(&reference.relationship_id)
1607 .unwrap_or_else(|| {
1608 XmlElement::new(entry_name.clone())
1609 .with_attribute("id", reference.id.to_string())
1610 .with_attribute(
1611 relationship_attribute.clone(),
1612 reference.relationship_id.clone(),
1613 )
1614 }),
1615 FinalSlide::Added(index) => {
1616 let slide = &minted[*index];
1617 XmlElement::new(entry_name.clone())
1618 .with_attribute("id", slide.slide_id.to_string())
1619 .with_attribute(
1620 relationship_attribute.clone(),
1621 slide.relationship_id.clone(),
1622 )
1623 }
1624 };
1625 list.children.push(XmlNode::Element(element));
1626 }
1627 Ok(())
1628}
1629
1630fn find_prefix(root: &XmlElement, namespace: &str) -> Option<String> {
1633 root.attributes.iter().find_map(|(key, value)| {
1634 if value != namespace {
1635 return None;
1636 }
1637 if key == "xmlns" {
1638 Some(String::new())
1639 } else {
1640 key.strip_prefix("xmlns:").map(str::to_owned)
1641 }
1642 })
1643}
1644
1645fn resolve_prefix(root: &mut XmlElement, namespace: &str, fallback: &str) -> String {
1646 if let Some(prefix) = find_prefix(root, namespace) {
1647 return prefix;
1648 }
1649 root.set_attribute(format!("xmlns:{fallback}"), namespace);
1650 fallback.to_owned()
1651}
1652
1653fn qualified(prefix: &str, local: &str) -> String {
1654 if prefix.is_empty() {
1655 local.to_owned()
1656 } else {
1657 format!("{prefix}:{local}")
1658 }
1659}
1660
1661struct Prefixes {
1662 drawing: String,
1663 presentation: String,
1664 relationship: String,
1665}
1666
1667impl Prefixes {
1668 fn from_root(root: &mut XmlElement) -> Self {
1669 Self {
1670 drawing: resolve_prefix(root, DRAWINGML_NS, "a"),
1671 presentation: resolve_prefix(root, PRESENTATIONML_NS, "p"),
1672 relationship: resolve_prefix(root, OFFICE_RELATIONSHIPS_NS, "r"),
1673 }
1674 }
1675
1676 fn drawing(&self, local: &str) -> String {
1677 qualified(&self.drawing, local)
1678 }
1679
1680 fn presentation(&self, local: &str) -> String {
1681 qualified(&self.presentation, local)
1682 }
1683
1684 fn relationship(&self, local: &str) -> String {
1685 qualified(&self.relationship, local)
1686 }
1687}
1688
1689#[allow(clippy::too_many_arguments)]
1693fn patch_slide(
1694 root: &mut XmlElement,
1695 shapes: &[ShapeWrite],
1696 background: Option<&BackgroundWrite>,
1697 theme: Option<&Theme>,
1698 part: &str,
1699 elements: ShapeElements,
1700 sink: &mut PartSink<'_>,
1701 budget: &mut ParseBudget<'_>,
1702) -> Result<(), PptxError> {
1703 let prefixes = Prefixes::from_root(root);
1704 if let Some(background) = background {
1705 let common = root
1706 .child_mut("cSld")
1707 .ok_or_else(|| write_error(part, "slide has no common slide data"))?;
1708 patch_background(common, background, &prefixes, part)?;
1709 }
1710 let mut next_shape_id = max_shape_id(root).checked_add(1);
1711 let tree = root
1712 .child_mut("cSld")
1713 .and_then(|common| common.child_mut("spTree"))
1714 .ok_or_else(|| write_error(part, "slide has no shape tree"))?;
1715 patch_shape_children(
1716 tree,
1717 shapes,
1718 &mut next_shape_id,
1719 theme,
1720 &prefixes,
1721 part,
1722 elements,
1723 sink,
1724 budget,
1725 )
1726}
1727
1728fn patch_background(
1730 common: &mut XmlElement,
1731 background: &BackgroundWrite,
1732 prefixes: &Prefixes,
1733 part: &str,
1734) -> Result<(), PptxError> {
1735 common.children.retain(|child| match child {
1736 XmlNode::Element(element) => element.local_name() != "bg",
1737 _ => true,
1738 });
1739 if let BackgroundWrite::Set(fill) = background {
1740 common.children.insert(
1741 0,
1742 XmlNode::Element(background_element(fill, prefixes, part)?),
1743 );
1744 }
1745 Ok(())
1746}
1747
1748fn background_element(
1750 fill: &ShapeFill,
1751 prefixes: &Prefixes,
1752 part: &str,
1753) -> Result<XmlElement, PptxError> {
1754 Ok(XmlElement::new(prefixes.presentation("bg")).with_child(
1755 XmlElement::new(prefixes.presentation("bgPr"))
1756 .with_child(fill_element(fill, prefixes, part)?)
1757 .with_child(XmlElement::new(prefixes.drawing("effectLst"))),
1758 ))
1759}
1760
1761fn max_shape_id(root: &XmlElement) -> u32 {
1762 root.descendants_named("cNvPr")
1763 .iter()
1764 .filter_map(|element| element.attribute("id")?.parse::<u32>().ok())
1765 .max()
1766 .unwrap_or(1)
1767}
1768
1769fn alloc_shape_id(next_shape_id: &mut Option<u32>, part: &str) -> Result<u32, PptxError> {
1770 let shape_id =
1771 next_shape_id.ok_or_else(|| write_error(part, "the shape id space is exhausted"))?;
1772 if shape_id == 0 {
1773 return Err(write_error(part, "the shape id space is exhausted"));
1774 }
1775 *next_shape_id = shape_id.checked_add(1);
1776 Ok(shape_id)
1777}
1778
1779struct ShapeSlot {
1782 position: usize,
1783 path: Vec<usize>,
1784}
1785
1786#[allow(clippy::too_many_arguments)]
1790fn patch_shape_children(
1791 parent: &mut XmlElement,
1792 writes: &[ShapeWrite],
1793 next_shape_id: &mut Option<u32>,
1794 theme: Option<&Theme>,
1795 prefixes: &Prefixes,
1796 part: &str,
1797 elements: ShapeElements,
1798 sink: &mut PartSink<'_>,
1799 budget: &mut ParseBudget<'_>,
1800) -> Result<(), PptxError> {
1801 let mut slots: Vec<Option<XmlNode>> = std::mem::take(&mut parent.children)
1802 .into_iter()
1803 .map(Some)
1804 .collect();
1805 let mut shape_slots = shape_slots(&slots, elements);
1806 let mut kept = vec![false; shape_slots.len()];
1807 for write in writes {
1808 if let ShapeWrite::Keep { source_index } | ShapeWrite::Patch { source_index, .. } = write
1809 && let Some(flag) = kept.get_mut(*source_index)
1810 {
1811 *flag = true;
1812 }
1813 }
1814 let mut nested: BTreeMap<usize, (usize, Vec<Vec<usize>>)> = BTreeMap::new();
1817 for (slot, kept) in shape_slots.iter().zip(&kept) {
1818 if slot.path.is_empty() {
1819 continue;
1820 }
1821 let entry = nested.entry(slot.position).or_default();
1822 entry.0 += 1;
1823 if !kept {
1824 entry.1.push(slot.path.clone());
1825 }
1826 }
1827 for (position, (total, mut paths)) in nested {
1828 if paths.len() == total && branch_holds_only(&slots[position], &paths) {
1829 slots[position] = None;
1830 continue;
1831 }
1832 paths.sort();
1833 for path in paths.iter().rev() {
1834 let Some(node) = slots[position].as_mut() else {
1835 continue;
1836 };
1837 remove_at_path(node, path);
1838 for slot in shape_slots
1839 .iter_mut()
1840 .filter(|slot| slot.position == position)
1841 {
1842 shift_path(&mut slot.path, path);
1843 }
1844 }
1845 }
1846 let mut children = Vec::with_capacity(slots.len());
1847 let mut hosts: BTreeMap<usize, usize> = BTreeMap::new();
1848 let first_ext_list = |slots: &[Option<XmlNode>]| {
1849 slots
1850 .iter()
1851 .position(|slot| {
1852 matches!(slot, Some(XmlNode::Element(element)) if element.local_name() == "extLst")
1853 })
1854 .unwrap_or(slots.len())
1855 };
1856 let prologue_end = shape_slots
1857 .first()
1858 .map(|slot| slot.position)
1859 .unwrap_or_else(|| first_ext_list(&slots));
1860 emit_sibling_slots(&mut slots, &mut children, prologue_end, elements);
1861 let last_kept = writes
1862 .iter()
1863 .rposition(|write| !matches!(write, ShapeWrite::Add(_)));
1864 for (index, write) in writes.iter().enumerate() {
1865 match write {
1866 ShapeWrite::Keep { source_index } | ShapeWrite::Patch { source_index, .. } => {
1867 let slot = shape_slots.get(*source_index).ok_or_else(|| {
1868 write_error(part, format!("shape index {source_index} is not available"))
1869 })?;
1870 emit_sibling_slots(&mut slots, &mut children, slot.position, elements);
1871 let target = emit_source_element(&mut slots, &mut children, &mut hosts, slot)
1872 .ok_or_else(|| {
1873 write_error(
1874 part,
1875 format!("shape index {source_index} was already written"),
1876 )
1877 })?;
1878 if let ShapeWrite::Patch { patch, .. } = write {
1879 let element =
1880 element_at_path(&mut children[target], &slot.path).ok_or_else(|| {
1881 write_error(
1882 part,
1883 format!("shape index {source_index} is not available"),
1884 )
1885 })?;
1886 patch_shape(
1887 element,
1888 patch,
1889 next_shape_id,
1890 theme,
1891 prefixes,
1892 part,
1893 elements,
1894 sink,
1895 budget,
1896 )?;
1897 }
1898 }
1899 ShapeWrite::Add(add) => {
1900 if last_kept.is_none_or(|kept| index > kept) {
1903 let flush_end = first_ext_list(&slots);
1904 emit_sibling_slots(&mut slots, &mut children, flush_end, elements);
1905 }
1906 children.push(XmlNode::Element(add_shape_element(
1907 add,
1908 next_shape_id,
1909 prefixes,
1910 part,
1911 sink,
1912 budget,
1913 )?));
1914 }
1915 }
1916 }
1917 for slot in slots.into_iter().flatten() {
1918 if !matches!(&slot, XmlNode::Element(element) if elements.contains(element.local_name())) {
1919 children.push(slot);
1920 }
1921 }
1922 parent.children = children;
1923 Ok(())
1924}
1925
1926fn shape_slots(slots: &[Option<XmlNode>], elements: ShapeElements) -> Vec<ShapeSlot> {
1928 let mut found = Vec::new();
1929 for (position, slot) in slots.iter().enumerate() {
1930 let Some(XmlNode::Element(element)) = slot else {
1931 continue;
1932 };
1933 if elements.contains(element.local_name()) {
1934 found.push(ShapeSlot {
1935 position,
1936 path: Vec::new(),
1937 });
1938 } else if element.local_name() == "AlternateContent" {
1939 collect_branch_slots(element, position, &mut Vec::new(), elements, &mut found);
1940 }
1941 }
1942 found
1943}
1944
1945fn collect_branch_slots(
1946 alternate: &XmlElement,
1947 position: usize,
1948 path: &mut Vec<usize>,
1949 elements: ShapeElements,
1950 found: &mut Vec<ShapeSlot>,
1951) {
1952 let Some(index) = alternate_content_branch_index(alternate) else {
1953 return;
1954 };
1955 let XmlNode::Element(branch) = &alternate.children[index] else {
1956 return;
1957 };
1958 path.push(index);
1959 for (index, child) in branch.children.iter().enumerate() {
1960 let XmlNode::Element(child) = child else {
1961 continue;
1962 };
1963 path.push(index);
1964 if elements.contains(child.local_name()) {
1965 found.push(ShapeSlot {
1966 position,
1967 path: path.clone(),
1968 });
1969 } else if child.local_name() == "AlternateContent" {
1970 collect_branch_slots(child, position, path, elements, found);
1971 }
1972 path.pop();
1973 }
1974 path.pop();
1975}
1976
1977fn emit_source_element(
1980 slots: &mut [Option<XmlNode>],
1981 children: &mut Vec<XmlNode>,
1982 hosts: &mut BTreeMap<usize, usize>,
1983 slot: &ShapeSlot,
1984) -> Option<usize> {
1985 if let Some(host) = hosts.get(&slot.position) {
1986 return Some(*host);
1987 }
1988 let XmlNode::Element(element) = slots[slot.position].take()? else {
1989 return None;
1990 };
1991 children.push(XmlNode::Element(element));
1992 if !slot.path.is_empty() {
1993 hosts.insert(slot.position, children.len() - 1);
1994 }
1995 Some(children.len() - 1)
1996}
1997
1998fn branch_holds_only(slot: &Option<XmlNode>, paths: &[Vec<usize>]) -> bool {
2000 let Some(XmlNode::Element(alternate)) = slot else {
2001 return true;
2002 };
2003 let Some(index) = alternate_content_branch_index(alternate) else {
2004 return true;
2005 };
2006 let XmlNode::Element(branch) = &alternate.children[index] else {
2007 return true;
2008 };
2009 branch.children.iter().enumerate().all(|(child, node)| {
2010 !matches!(node, XmlNode::Element(_)) || paths.iter().any(|path| path == &[index, child])
2011 })
2012}
2013
2014fn remove_at_path(node: &mut XmlNode, path: &[usize]) {
2015 let Some((last, parents)) = path.split_last() else {
2016 return;
2017 };
2018 if let Some(parent) = element_at_path(node, parents)
2019 && *last < parent.children.len()
2020 {
2021 parent.children.remove(*last);
2022 }
2023}
2024
2025fn shift_path(path: &mut [usize], removed: &[usize]) {
2027 let Some((last, parents)) = removed.split_last() else {
2028 return;
2029 };
2030 if path.len() > parents.len() && path.starts_with(parents) && path[parents.len()] > *last {
2031 path[parents.len()] -= 1;
2032 }
2033}
2034
2035fn element_at_path<'a>(node: &'a mut XmlNode, path: &[usize]) -> Option<&'a mut XmlElement> {
2036 let XmlNode::Element(element) = node else {
2037 return None;
2038 };
2039 let mut current = element;
2040 for index in path {
2041 match current.children.get_mut(*index)? {
2042 XmlNode::Element(child) => current = child,
2043 XmlNode::Text(_) => return None,
2044 }
2045 }
2046 Some(current)
2047}
2048
2049fn emit_sibling_slots(
2051 slots: &mut [Option<XmlNode>],
2052 children: &mut Vec<XmlNode>,
2053 position: usize,
2054 elements: ShapeElements,
2055) {
2056 for slot in slots.iter_mut().take(position) {
2057 let is_other = !matches!(
2058 slot,
2059 Some(XmlNode::Element(element)) if elements.contains(element.local_name())
2060 );
2061 if is_other && let Some(node) = slot.take() {
2062 children.push(node);
2063 }
2064 }
2065}
2066
2067#[allow(clippy::too_many_arguments)]
2068fn patch_shape(
2069 element: &mut XmlElement,
2070 patch: &ShapePatch,
2071 next_shape_id: &mut Option<u32>,
2072 theme: Option<&Theme>,
2073 prefixes: &Prefixes,
2074 part: &str,
2075 elements: ShapeElements,
2076 sink: &mut PartSink<'_>,
2077 budget: &mut ParseBudget<'_>,
2078) -> Result<(), PptxError> {
2079 if let Some(placeholder) = &patch.placeholder {
2080 let non_visual = element
2081 .children
2082 .iter_mut()
2083 .find_map(|child| match child {
2084 XmlNode::Element(child)
2085 if matches!(
2086 child.local_name(),
2087 "nvSpPr" | "nvPicPr" | "nvGraphicFramePr" | "nvGrpSpPr" | "nvCxnSpPr"
2088 ) =>
2089 {
2090 Some(child)
2091 }
2092 _ => None,
2093 })
2094 .and_then(|child| child.child_mut("nvPr"))
2095 .and_then(|child| child.child_mut("ph"))
2096 .ok_or_else(|| write_error(part, "mapped shape has no placeholder element"))?;
2097 non_visual
2098 .attributes
2099 .retain(|name, _| name == "xmlns" || name.starts_with("xmlns:"));
2100 for (name, value) in [
2101 ("type", placeholder.placeholder_type.clone()),
2102 ("idx", placeholder.index.map(|index| index.to_string())),
2103 ("orient", placeholder.orientation.clone()),
2104 ("sz", placeholder.size.clone()),
2105 ] {
2106 if let Some(value) = value {
2107 non_visual.set_attribute(name, value);
2108 }
2109 }
2110 }
2111 if patch.offset.is_some() || patch.extent.is_some() {
2112 patch_transform(element, patch, prefixes, part)?;
2113 }
2114 if let Some(fill) = &patch.fill {
2115 let properties = shape_properties_mut(element, part)?;
2116 set_fill(properties, fill, prefixes, part)?;
2117 }
2118 if let Some(outline) = &patch.outline {
2119 let properties = shape_properties_mut(element, part)?;
2120 set_outline(properties, outline, prefixes);
2121 }
2122 if let Some(adjust_values) = &patch.adjust_values {
2123 let properties = shape_properties_mut(element, part)?;
2124 set_adjust_values(properties, adjust_values, prefixes, part)?;
2125 }
2126 if let Some(structure) = &patch.table_structure {
2127 patch_table(element, structure, part)?;
2128 }
2129 for text in &patch.texts {
2130 let empty_runs = if patch.table_structure.is_some() {
2131 empty_table_runs(element, text, theme, prefixes)
2132 } else {
2133 Vec::new()
2134 };
2135 patch_text(element, text, theme, prefixes, part)?;
2136 if let TextTarget::TableCell { row, cell } = text.target {
2137 for (paragraph, run) in empty_runs {
2138 let body = table_cell_mut(element, row, cell)
2139 .and_then(|cell| cell.child_mut("txBody"))
2140 .ok_or_else(|| write_error(part, "table cell has no text body"))?;
2141 let paragraph = nth_child_mut(body, "p", paragraph)
2142 .ok_or_else(|| write_error(part, "table cell paragraph is missing"))?;
2143 let at = paragraph.children.iter().position(|node| matches!(node, XmlNode::Element(element) if element.local_name() == "endParaRPr")).unwrap_or(paragraph.children.len());
2144 paragraph.children.insert(at, XmlNode::Element(run));
2145 }
2146 }
2147 }
2148 if !patch.children.is_empty() {
2149 patch_shape_children(
2150 element,
2151 &patch.children,
2152 next_shape_id,
2153 theme,
2154 prefixes,
2155 part,
2156 elements,
2157 sink,
2158 budget,
2159 )?;
2160 }
2161 Ok(())
2162}
2163
2164fn shape_properties_mut<'a>(
2165 element: &'a mut XmlElement,
2166 part: &str,
2167) -> Result<&'a mut XmlElement, PptxError> {
2168 let container = match element.local_name() {
2169 "grpSp" => "grpSpPr",
2170 _ => "spPr",
2171 };
2172 element
2173 .child_mut(container)
2174 .ok_or_else(|| write_error(part, "shape has no properties element"))
2175}
2176
2177fn patch_transform(
2182 element: &mut XmlElement,
2183 patch: &ShapePatch,
2184 prefixes: &Prefixes,
2185 part: &str,
2186) -> Result<(), PptxError> {
2187 let offset = patch
2188 .offset
2189 .or(patch.inherited.map(|inherited| (inherited.x, inherited.y)));
2190 let extent = patch.extent.or(patch
2191 .inherited
2192 .map(|inherited| (inherited.width, inherited.height)));
2193 let (container, transform_name) = match element.local_name() {
2194 "graphicFrame" => (None, prefixes.presentation("xfrm")),
2195 "grpSp" => (Some("grpSpPr"), prefixes.drawing("xfrm")),
2196 _ => (Some("spPr"), prefixes.drawing("xfrm")),
2197 };
2198 let parent = match container {
2199 Some(name) => element
2200 .child_mut(name)
2201 .ok_or_else(|| write_error(part, "shape has no properties element"))?,
2202 None => element,
2203 };
2204 let created = parent.child_mut("xfrm").is_none();
2205 if created {
2206 if offset.is_none() || extent.is_none() {
2207 return Err(write_error(
2208 part,
2209 "cannot write half a transform onto a shape without one",
2210 ));
2211 }
2212 let position = parent
2213 .children
2214 .iter()
2215 .position(|child| {
2216 matches!(child, XmlNode::Element(element) if element.local_name() != "nvGraphicFramePr")
2217 })
2218 .unwrap_or(parent.children.len());
2219 parent
2220 .children
2221 .insert(position, XmlNode::Element(XmlElement::new(transform_name)));
2222 }
2223 let transform = parent.child_mut("xfrm").expect("transform ensured above");
2224 if (created || patch.materializes)
2225 && let Some(inherited) = patch.inherited
2226 {
2227 if inherited.rotation_deg != 0.0 {
2228 transform.set_attribute("rot", format_fixed(inherited.rotation_deg * 60_000.0));
2229 }
2230 if inherited.flip_horizontal {
2231 transform.set_attribute("flipH", "1");
2232 }
2233 if inherited.flip_vertical {
2234 transform.set_attribute("flipV", "1");
2235 }
2236 }
2237 if let Some((x, y)) = offset
2238 && (patch.offset.is_some() || transform.child_mut("off").is_none())
2239 {
2240 if transform.child_mut("off").is_none() {
2241 transform.children.insert(
2242 0,
2243 XmlNode::Element(XmlElement::new(prefixes.drawing("off"))),
2244 );
2245 }
2246 let element = transform.child_mut("off").expect("offset ensured above");
2247 element.set_attribute("x", x.to_string());
2248 element.set_attribute("y", y.to_string());
2249 }
2250 if let Some((width, height)) = extent
2251 && (patch.extent.is_some() || transform.child_mut("ext").is_none())
2252 {
2253 if transform.child_mut("ext").is_none() {
2254 let position = transform
2255 .children
2256 .iter()
2257 .position(
2258 |child| matches!(child, XmlNode::Element(element) if element.local_name() == "off"),
2259 )
2260 .map(|index| index + 1)
2261 .unwrap_or(transform.children.len());
2262 transform.children.insert(
2263 position,
2264 XmlNode::Element(XmlElement::new(prefixes.drawing("ext"))),
2265 );
2266 }
2267 let element = transform.child_mut("ext").expect("extent ensured above");
2268 element.set_attribute("cx", width.to_string());
2269 element.set_attribute("cy", height.to_string());
2270 }
2271 Ok(())
2272}
2273
2274const FILL_ELEMENTS: [&str; 6] = [
2275 "noFill",
2276 "solidFill",
2277 "gradFill",
2278 "blipFill",
2279 "pattFill",
2280 "grpFill",
2281];
2282const POST_FILL_ELEMENTS: [&str; 6] = ["ln", "effectLst", "effectDag", "scene3d", "sp3d", "extLst"];
2283
2284fn set_fill(
2285 properties: &mut XmlElement,
2286 fill: &ShapeFill,
2287 prefixes: &Prefixes,
2288 part: &str,
2289) -> Result<(), PptxError> {
2290 let element = fill_element(fill, prefixes, part)?;
2291 let existing = properties.children.iter().position(|child| {
2292 matches!(child, XmlNode::Element(element) if FILL_ELEMENTS.contains(&element.local_name()))
2293 });
2294 match existing {
2295 Some(index) => properties.children[index] = XmlNode::Element(element),
2296 None => {
2297 let position = properties
2298 .children
2299 .iter()
2300 .position(|child| {
2301 matches!(
2302 child,
2303 XmlNode::Element(element)
2304 if POST_FILL_ELEMENTS.contains(&element.local_name())
2305 )
2306 })
2307 .unwrap_or(properties.children.len());
2308 properties
2309 .children
2310 .insert(position, XmlNode::Element(element));
2311 }
2312 }
2313 Ok(())
2314}
2315
2316fn fill_element(
2317 fill: &ShapeFill,
2318 prefixes: &Prefixes,
2319 part: &str,
2320) -> Result<XmlElement, PptxError> {
2321 match fill.fill_type.as_str() {
2322 "none" => Ok(XmlElement::new(prefixes.drawing("noFill"))),
2323 "solid" => {
2324 let color = fill
2325 .color
2326 .as_ref()
2327 .and_then(|color| color_element(color, prefixes))
2328 .ok_or_else(|| write_error(part, "solid fill without a serializable color"))?;
2329 Ok(XmlElement::new(prefixes.drawing("solidFill")).with_child(color))
2330 }
2331 "gradient" => {
2332 let gradient = fill
2333 .gradient
2334 .as_ref()
2335 .ok_or_else(|| write_error(part, "gradient fill without stops"))?;
2336 gradient_element(gradient, prefixes)
2337 .ok_or_else(|| write_error(part, "gradient stop without a color"))
2338 }
2339 other => Err(write_error(
2340 part,
2341 format!("unsupported fill type {other:?}"),
2342 )),
2343 }
2344}
2345
2346fn gradient_element(gradient: &GradientFill, prefixes: &Prefixes) -> Option<XmlElement> {
2347 let mut stops = XmlElement::new(prefixes.drawing("gsLst"));
2348 for stop in &gradient.stops {
2349 stops = stops.with_child(
2350 XmlElement::new(prefixes.drawing("gs"))
2351 .with_attribute("pos", format_fixed(stop.position))
2352 .with_child(color_element(&stop.color, prefixes)?),
2353 );
2354 }
2355 let mut element = XmlElement::new(prefixes.drawing("gradFill")).with_child(stops);
2356 match gradient.gradient_type.as_str() {
2357 "linear" => {
2358 element = element.with_child(
2359 XmlElement::new(prefixes.drawing("lin"))
2360 .with_attribute(
2361 "ang",
2362 format_fixed(gradient.angle.unwrap_or_default() * 60_000.0),
2363 )
2364 .with_attribute("scaled", "1"),
2365 );
2366 }
2367 kind => {
2368 let path = match kind {
2369 "radial" => "circle",
2370 "rectangular" => "rect",
2371 _ => "shape",
2372 };
2373 element = element
2374 .with_child(XmlElement::new(prefixes.drawing("path")).with_attribute("path", path));
2375 }
2376 }
2377 Some(element)
2378}
2379
2380fn color_element(color: &ColorValue, prefixes: &Prefixes) -> Option<XmlElement> {
2381 let mut element = if let Some(rgb) = &color.rgb {
2382 XmlElement::new(prefixes.drawing("srgbClr")).with_attribute("val", rgb.clone())
2383 } else if let Some(theme) = &color.theme_color {
2384 XmlElement::new(prefixes.drawing("schemeClr"))
2385 .with_attribute("val", denormalize_scheme_color(theme))
2386 } else {
2387 return None;
2388 };
2389 if !color.transforms.is_empty() {
2390 for transform in color.drawingml_transforms() {
2391 let mut child = XmlElement::new(prefixes.drawing(transform.xml_name()));
2392 if let Some(value) = transform.xml_value() {
2393 child = child.with_attribute("val", value);
2394 }
2395 element = element.with_child(child);
2396 }
2397 return Some(element);
2398 }
2399 let fractions = [
2400 ("alpha", color.alpha),
2401 ("lumMod", color.luminance_modulation),
2402 ("lumOff", color.luminance_offset),
2403 ("satMod", color.saturation_modulation),
2404 ];
2405 for (name, value) in fractions {
2406 if let Some(value) = value {
2407 element = element.with_child(
2408 XmlElement::new(prefixes.drawing(name))
2409 .with_attribute("val", format_fixed(value * 100_000.0)),
2410 );
2411 }
2412 }
2413 let modifiers = [("tint", &color.theme_tint), ("shade", &color.theme_shade)];
2414 for (name, value) in modifiers {
2415 if let Some(byte) = value
2416 .as_deref()
2417 .and_then(|value| u8::from_str_radix(value, 16).ok())
2418 {
2419 element = element.with_child(
2420 XmlElement::new(prefixes.drawing(name))
2421 .with_attribute("val", format_fixed(f64::from(byte) / 255.0 * 100_000.0)),
2422 );
2423 }
2424 }
2425 Some(element)
2426}
2427
2428fn denormalize_scheme_color(value: &str) -> String {
2429 match value {
2430 "text1" => "tx1",
2431 "text2" => "tx2",
2432 "background1" => "bg1",
2433 "background2" => "bg2",
2434 value => value,
2435 }
2436 .to_owned()
2437}
2438
2439fn format_fixed(value: f64) -> String {
2440 (value.round() as i64).to_string()
2441}
2442
2443fn set_outline(properties: &mut XmlElement, outline: &ShapeOutline, prefixes: &Prefixes) {
2444 if let Some(line) = properties.child_mut("ln")
2445 && let Some(mut original) = crate::drawing::parse_outline_element(line)
2446 {
2447 original.width = outline.width;
2448 if original == *outline {
2449 if let Some(width) = outline.width {
2450 line.set_attribute("w", format_fixed(width));
2451 } else {
2452 line.attributes.remove("w");
2453 }
2454 return;
2455 }
2456 }
2457 let element = outline_element(outline, prefixes);
2458 let existing = properties.children.iter().position(
2459 |child| matches!(child, XmlNode::Element(element) if element.local_name() == "ln"),
2460 );
2461 match existing {
2462 Some(index) => properties.children[index] = XmlNode::Element(element),
2463 None => {
2464 let position = properties
2465 .children
2466 .iter()
2467 .position(|child| {
2468 matches!(
2469 child,
2470 XmlNode::Element(element)
2471 if POST_FILL_ELEMENTS[1..].contains(&element.local_name())
2472 )
2473 })
2474 .unwrap_or(properties.children.len());
2475 properties
2476 .children
2477 .insert(position, XmlNode::Element(element));
2478 }
2479 }
2480}
2481
2482fn outline_element(outline: &ShapeOutline, prefixes: &Prefixes) -> XmlElement {
2483 let mut element = XmlElement::new(prefixes.drawing("ln"));
2484 if *outline == ShapeOutline::default() {
2485 return element.with_child(XmlElement::new(prefixes.drawing("noFill")));
2486 }
2487 if let Some(width) = outline.width {
2488 element.set_attribute("w", format_fixed(width));
2489 }
2490 if let Some(cap) = &outline.cap {
2491 element.set_attribute("cap", cap.clone());
2492 }
2493 if let Some(color) = outline
2494 .color
2495 .as_ref()
2496 .and_then(|color| color_element(color, prefixes))
2497 {
2498 element =
2499 element.with_child(XmlElement::new(prefixes.drawing("solidFill")).with_child(color));
2500 } else if let Some(gradient) = outline
2501 .gradient
2502 .as_ref()
2503 .and_then(|gradient| gradient_element(gradient, prefixes))
2504 {
2505 element = element.with_child(gradient);
2506 }
2507 if let Some(style) = &outline.style {
2508 element = element.with_child(
2509 XmlElement::new(prefixes.drawing("prstDash")).with_attribute("val", style.clone()),
2510 );
2511 }
2512 if let Some(join) = &outline.join {
2513 element = element.with_child(XmlElement::new(prefixes.drawing(join)));
2514 }
2515 let ends = [
2516 ("headEnd", &outline.head_end),
2517 ("tailEnd", &outline.tail_end),
2518 ];
2519 for (name, end) in ends {
2520 if let Some(end) = end {
2521 let mut end_element = XmlElement::new(prefixes.drawing(name))
2522 .with_attribute("type", end.end_type.clone());
2523 if let Some(width) = &end.width {
2524 end_element.set_attribute("w", width.clone());
2525 }
2526 if let Some(length) = &end.length {
2527 end_element.set_attribute("len", length.clone());
2528 }
2529 element = element.with_child(end_element);
2530 }
2531 }
2532 element
2533}
2534
2535fn set_adjust_values(
2536 properties: &mut XmlElement,
2537 adjust_values: &BTreeMap<String, f64>,
2538 prefixes: &Prefixes,
2539 part: &str,
2540) -> Result<(), PptxError> {
2541 let Some(geometry) = properties.child_mut("prstGeom") else {
2542 return Err(write_error(
2543 part,
2544 "cannot write adjustments onto a shape without preset geometry",
2545 ));
2546 };
2547 let list_name = prefixes.drawing("avLst");
2548 if geometry.child_mut("avLst").is_none() {
2549 geometry
2550 .children
2551 .insert(0, XmlNode::Element(XmlElement::new(list_name)));
2552 }
2553 let list = geometry.child_mut("avLst").expect("list ensured above");
2554 for (name, value) in adjust_values {
2556 let formula = format!("val {}", format_fixed(value * 100_000.0));
2557 let existing = list.children.iter_mut().find_map(|child| match child {
2558 XmlNode::Element(element)
2559 if element.local_name() == "gd" && element.attribute("name") == Some(name) =>
2560 {
2561 Some(element)
2562 }
2563 _ => None,
2564 });
2565 match existing {
2566 Some(guide) => guide.set_attribute("fmla", formula),
2567 None => list.children.push(XmlNode::Element(
2568 XmlElement::new(prefixes.drawing("gd"))
2569 .with_attribute("name", name.clone())
2570 .with_attribute("fmla", formula),
2571 )),
2572 }
2573 }
2574 Ok(())
2575}
2576
2577pub fn text_paragraph_xml(
2581 package: &PptxPackage,
2582 part: &str,
2583 shape_path: &[usize],
2584 target: &TextTarget,
2585) -> Result<Vec<String>, PptxError> {
2586 let Some(bytes) = package.part_bytes(part) else {
2587 return Ok(Vec::new());
2588 };
2589 let limits = ParseLimits::default();
2590 let mut budget = ParseBudget::new(&limits);
2591 let mut root = parse_xml(bytes, part, &mut budget)?;
2592 let mut element = root
2593 .child_mut("cSld")
2594 .and_then(|data| data.child_mut("spTree"))
2595 .ok_or_else(|| write_error(part, "missing shape tree"))?
2596 .clone();
2597 for &index in shape_path {
2598 let mut slots: Vec<_> = element.children.into_iter().map(Some).collect();
2599 let paths = shape_slots(&slots, package.shape_elements);
2600 let slot = paths
2601 .get(index)
2602 .ok_or_else(|| write_error(part, "missing source shape"))?;
2603 element = element_at_path(slots[slot.position].as_mut().unwrap(), &slot.path)
2604 .ok_or_else(|| write_error(part, "missing source shape"))?
2605 .clone();
2606 }
2607 let body = match target {
2608 TextTarget::Body => element.child_mut("txBody"),
2609 TextTarget::TableCell { row, cell } => element
2610 .child_mut("graphic")
2611 .and_then(|graphic| graphic.child_mut("graphicData"))
2612 .and_then(|data| data.child_mut("tbl"))
2613 .and_then(|table| nth_child_mut(table, "tr", *row))
2614 .and_then(|row| nth_child_mut(row, "tc", *cell))
2615 .and_then(|cell| cell.child_mut("txBody")),
2616 };
2617 Ok(body
2618 .into_iter()
2619 .flat_map(|body| body.children_named("p"))
2620 .map(|paragraph| String::from_utf8(serialize_xml_fragment(paragraph)).unwrap())
2621 .collect())
2622}
2623
2624fn table_cell_mut(element: &mut XmlElement, row: usize, cell: usize) -> Option<&mut XmlElement> {
2625 element
2626 .child_mut("graphic")
2627 .and_then(|graphic| graphic.child_mut("graphicData"))
2628 .and_then(|data| data.child_mut("tbl"))
2629 .and_then(|table| nth_child_mut(table, "tr", row))
2630 .and_then(|row| nth_child_mut(row, "tc", cell))
2631}
2632
2633fn empty_table_runs(
2634 element: &mut XmlElement,
2635 text: &TextWrite,
2636 theme: Option<&Theme>,
2637 prefixes: &Prefixes,
2638) -> Vec<(usize, XmlElement)> {
2639 let TextTarget::TableCell { row, cell } = text.target else {
2640 return Vec::new();
2641 };
2642 let Some(body) = table_cell_mut(element, row, cell).and_then(|cell| cell.child_mut("txBody"))
2643 else {
2644 return Vec::new();
2645 };
2646 text.paragraphs
2647 .iter()
2648 .enumerate()
2649 .filter_map(|(index, paragraph)| {
2650 if !paragraph.rebuild || paragraph.runs.iter().any(|run| !run.text.is_empty()) {
2651 return None;
2652 }
2653 let source = body.children_named("p").nth(paragraph.source_index?)?;
2654 let mut run = source.children_named("r").next()?.clone();
2655 run.children.retain(
2656 |child| matches!(child, XmlNode::Element(element) if element.local_name() == "rPr"),
2657 );
2658 if let Some(style) = paragraph.runs.first() {
2659 if run.child("rPr").is_none() {
2660 run.children.insert(
2661 0,
2662 XmlNode::Element(XmlElement::new(prefixes.drawing("rPr"))),
2663 );
2664 }
2665 apply_run_properties(
2666 run.child_mut("rPr").unwrap(),
2667 &style.properties,
2668 theme,
2669 prefixes,
2670 );
2671 }
2672 let name = source.children_named("r").next()?.child("t")?.name.clone();
2673 run.children.push(XmlNode::Element(XmlElement::new(name)));
2674 Some((index, run))
2675 })
2676 .collect()
2677}
2678
2679fn patch_table(
2680 element: &mut XmlElement,
2681 structure: &TableStructure,
2682 part: &str,
2683) -> Result<(), PptxError> {
2684 if structure.grid.is_empty()
2685 || structure.rows.is_empty()
2686 || structure.grid.len() != structure.columns.len()
2687 || structure.grid.len() != structure.copied_columns.len()
2688 || structure.rows.iter().any(|row| {
2689 row.cells.len() != structure.grid.len()
2690 || row.copied_cells.len() != structure.grid.len()
2691 })
2692 {
2693 return Err(write_error(
2694 part,
2695 "table structure grid and cell counts must agree",
2696 ));
2697 }
2698 let table = element
2699 .child_mut("graphic")
2700 .and_then(|graphic| graphic.child_mut("graphicData"))
2701 .and_then(|data| data.child_mut("tbl"))
2702 .ok_or_else(|| write_error(part, "table structure target is not a table"))?;
2703 let mut used: HashSet<String> = table
2704 .descendants_named("rowId")
2705 .into_iter()
2706 .chain(table.descendants_named("colId"))
2707 .chain(table.descendants_named("creationId"))
2708 .flat_map(|element| element.attributes.values())
2709 .cloned()
2710 .collect();
2711 let mut next_id = used
2712 .iter()
2713 .filter_map(|value| value.parse::<u64>().ok())
2714 .max()
2715 .unwrap_or(0);
2716 let original_rows: Vec<_> = table.children_named("tr").cloned().collect();
2717 let grid = table
2718 .child_mut("tblGrid")
2719 .ok_or_else(|| write_error(part, "table has no grid"))?;
2720 let originals: Vec<_> = grid.children_named("gridCol").cloned().collect();
2721 let columns = structure
2722 .grid
2723 .iter()
2724 .zip(&structure.columns)
2725 .zip(&structure.copied_columns)
2726 .map(|((width, source), copied)| {
2727 let mut column = originals
2728 .get(*source)
2729 .cloned()
2730 .ok_or_else(|| write_error(part, "table source grid column is out of range"))?;
2731 column.set_attribute("w", width.to_string());
2732 if *copied {
2733 fresh_table_ids(&mut column, &mut next_id, &mut used, part)?;
2734 }
2735 Ok(XmlNode::Element(column))
2736 })
2737 .collect::<Result<Vec<_>, PptxError>>()?;
2738 replace_children(grid, "gridCol", columns);
2739 let rows = structure
2740 .rows
2741 .iter()
2742 .map(|source| {
2743 let mut row = original_rows
2744 .get(source.source_row)
2745 .cloned()
2746 .ok_or_else(|| write_error(part, "table source row is out of range"))?;
2747 row.set_attribute("h", source.height.to_string());
2748 let cells = source
2749 .cells
2750 .iter()
2751 .zip(&source.copied_cells)
2752 .map(|(&[r, c], copied)| {
2753 let mut cell = original_rows
2754 .get(r)
2755 .and_then(|row| row.children_named("tc").nth(c))
2756 .cloned()
2757 .ok_or_else(|| write_error(part, "table source cell is out of range"))?;
2758 if *copied && !source.copied {
2759 fresh_table_ids(&mut cell, &mut next_id, &mut used, part)?;
2760 }
2761 Ok(XmlNode::Element(cell))
2762 })
2763 .collect::<Result<Vec<_>, PptxError>>()?;
2764 replace_children(&mut row, "tc", cells);
2765 if source.copied {
2766 fresh_table_ids(&mut row, &mut next_id, &mut used, part)?;
2767 }
2768 Ok(XmlNode::Element(row))
2769 })
2770 .collect::<Result<Vec<_>, PptxError>>()?;
2771 replace_children(table, "tr", rows);
2772 Ok(())
2773}
2774
2775fn fresh_table_ids(
2776 element: &mut XmlElement,
2777 next: &mut u64,
2778 used: &mut HashSet<String>,
2779 part: &str,
2780) -> Result<(), PptxError> {
2781 if matches!(element.local_name(), "rowId" | "colId" | "creationId") {
2782 for (key, value) in &mut element.attributes {
2783 if matches!(key.rsplit(':').next(), Some("val" | "id")) {
2784 loop {
2785 *next = next
2786 .checked_add(1)
2787 .filter(|id| *id <= u64::from(u32::MAX))
2788 .ok_or_else(|| write_error(part, "table identifier range exhausted"))?;
2789 let candidate = if value.parse::<u64>().is_ok() {
2790 next.to_string()
2791 } else {
2792 format!("{{FFFFFFFF-FFFF-4FFF-8FFF-{:012X}}}", *next)
2793 };
2794 if used.insert(candidate.clone()) {
2795 *value = candidate;
2796 break;
2797 }
2798 }
2799 }
2800 }
2801 }
2802 for child in &mut element.children {
2803 if let XmlNode::Element(child) = child {
2804 fresh_table_ids(child, next, used, part)?;
2805 }
2806 }
2807 Ok(())
2808}
2809
2810fn replace_children(parent: &mut XmlElement, local: &str, replacements: Vec<XmlNode>) {
2811 let at = parent
2812 .children
2813 .iter()
2814 .position(
2815 |child| matches!(child, XmlNode::Element(element) if element.local_name() == local),
2816 )
2817 .unwrap_or(parent.children.len());
2818 parent.children.retain(
2819 |child| !matches!(child, XmlNode::Element(element) if element.local_name() == local),
2820 );
2821 parent.children.splice(at..at, replacements);
2822}
2823
2824fn patch_text(
2825 element: &mut XmlElement,
2826 text: &TextWrite,
2827 theme: Option<&Theme>,
2828 prefixes: &Prefixes,
2829 part: &str,
2830) -> Result<(), PptxError> {
2831 let body = match &text.target {
2832 TextTarget::Body => element.child_mut("txBody"),
2833 TextTarget::TableCell { row, cell } => element
2834 .child_mut("graphic")
2835 .and_then(|graphic| graphic.child_mut("graphicData"))
2836 .and_then(|data| data.child_mut("tbl"))
2837 .and_then(|table| nth_child_mut(table, "tr", *row))
2838 .and_then(|table_row| nth_child_mut(table_row, "tc", *cell))
2839 .and_then(|table_cell| table_cell.child_mut("txBody")),
2840 };
2841 let body = body.ok_or_else(|| write_error(part, "text target has no body"))?;
2842 if let Some(TextAutofit::Normal {
2843 font_scale,
2844 line_space_reduction,
2845 }) = &text.autofit
2846 {
2847 if body.child("bodyPr").is_none() {
2848 body.children.insert(
2849 0,
2850 XmlNode::Element(XmlElement::new(prefixes.drawing("bodyPr"))),
2851 );
2852 }
2853 let properties = body.child_mut("bodyPr").unwrap();
2854 if properties.child("normAutofit").is_none() {
2855 let position = properties.children.iter().position(|child| {
2856 matches!(child, XmlNode::Element(element) if matches!(element.local_name(), "scene3d" | "sp3d" | "flatTx" | "extLst"))
2857 }).unwrap_or(properties.children.len());
2858 properties.children.insert(
2859 position,
2860 XmlNode::Element(XmlElement::new(prefixes.drawing("normAutofit"))),
2861 );
2862 }
2863 let autofit = properties.child_mut("normAutofit").unwrap();
2864 if let Some(scale) = font_scale {
2865 autofit.set_attribute("fontScale", format_fixed(scale * 100_000.0));
2866 }
2867 if let Some(reduction) = line_space_reduction {
2868 autofit.set_attribute("lnSpcReduction", format_fixed(reduction * 100_000.0));
2869 }
2870 }
2871 rebuild_paragraphs(body, &text.paragraphs, theme, prefixes)?;
2872 Ok(())
2873}
2874
2875fn nth_child_mut<'a>(
2876 parent: &'a mut XmlElement,
2877 local: &str,
2878 index: usize,
2879) -> Option<&'a mut XmlElement> {
2880 parent
2881 .children
2882 .iter_mut()
2883 .filter_map(|child| match child {
2884 XmlNode::Element(element) if element.local_name() == local => Some(element),
2885 _ => None,
2886 })
2887 .nth(index)
2888}
2889
2890fn rebuild_paragraphs(
2891 body: &mut XmlElement,
2892 paragraphs: &[ParagraphWrite],
2893 theme: Option<&Theme>,
2894 prefixes: &Prefixes,
2895) -> Result<(), PptxError> {
2896 let mut preamble = Vec::new();
2897 let mut originals: Vec<XmlElement> = Vec::new();
2898 for child in std::mem::take(&mut body.children) {
2899 match child {
2900 XmlNode::Element(element) if element.local_name() == "p" => {
2901 originals.push(element);
2902 }
2903 XmlNode::Text(text) if text.trim().is_empty() => {}
2904 other => preamble.push(other),
2905 }
2906 }
2907 let mut children = preamble;
2908 let limits = ParseLimits::default();
2909 let mut budget = ParseBudget::new(&limits);
2910 for paragraph in paragraphs {
2911 let source = if let Some(first) = paragraph.source_paragraphs.first() {
2912 first
2913 .as_ref()
2914 .map(|xml| parse_xml(xml.as_bytes(), "paragraph", &mut budget))
2915 .transpose()?
2916 } else if let Some(xml) = ¶graph.preserved_xml {
2917 Some(parse_xml(xml.as_bytes(), "paragraph", &mut budget)?)
2918 } else {
2919 paragraph
2920 .source_index
2921 .and_then(|index| originals.get(index).cloned())
2922 };
2923 let mut source = source;
2924 for xml in paragraph.source_paragraphs.iter().skip(1) {
2925 let source = source.get_or_insert_with(|| XmlElement::new(prefixes.drawing("p")));
2926 if let Some(xml) = xml {
2927 let next = parse_xml(xml.as_bytes(), "paragraph", &mut budget)?;
2928 if next.child("endParaRPr").is_some() {
2929 source.children.retain(|child| {
2930 !matches!(child, XmlNode::Element(element) if element.local_name() == "endParaRPr")
2931 });
2932 }
2933 source.children.extend(
2934 next.child_elements()
2935 .filter(|child| {
2936 is_run_element(child.local_name()) || child.local_name() == "endParaRPr"
2937 })
2938 .cloned()
2939 .map(XmlNode::Element),
2940 );
2941 }
2942 }
2943 let element = match source {
2944 Some(element) if !paragraph.rebuild => element,
2945 source => build_paragraph(paragraph, source, theme, prefixes),
2946 };
2947 children.push(XmlNode::Element(element));
2948 }
2949 if children
2950 .iter()
2951 .all(|child| !matches!(child, XmlNode::Element(element) if element.local_name() == "p"))
2952 {
2953 children.push(XmlNode::Element(XmlElement::new(prefixes.drawing("p"))));
2954 }
2955 body.children = children;
2956 Ok(())
2957}
2958
2959fn is_run_element(local: &str) -> bool {
2960 matches!(local, "r" | "br" | "fld")
2961}
2962
2963struct RunSegment<'a> {
2965 line_break: bool,
2966 text: &'a str,
2967 properties: &'a RunProperties,
2968}
2969
2970fn run_segments(runs: &[RunWrite]) -> Vec<RunSegment<'_>> {
2971 let mut segments = Vec::new();
2972 for run in runs {
2973 for (index, piece) in run.text.split('\n').enumerate() {
2974 if index > 0 {
2975 segments.push(RunSegment {
2976 line_break: true,
2977 text: "",
2978 properties: &run.properties,
2979 });
2980 }
2981 if !piece.is_empty() {
2982 segments.push(RunSegment {
2983 line_break: false,
2984 text: piece,
2985 properties: &run.properties,
2986 });
2987 }
2988 }
2989 }
2990 segments
2991}
2992
2993fn segment_matches(element: &XmlElement, segment: &RunSegment<'_>, theme: Option<&Theme>) -> bool {
2995 if element.local_name() == "br" {
2996 return segment.line_break;
2997 }
2998 if segment.line_break {
2999 return false;
3000 }
3001 let text = element
3002 .child("t")
3003 .map(XmlElement::text_content)
3004 .unwrap_or_default();
3005 if text != segment.text {
3006 return false;
3007 }
3008 let source = parse_run_properties(element.child("rPr"));
3009 let target = segment.properties;
3010 source.baseline_pct == target.baseline_pct
3011 && source.bold == target.bold
3012 && source.italic == target.italic
3013 && source.font_size_pt == target.font_size_pt
3014 && source.spacing_pt == target.spacing_pt
3015 && source.kern_pt == target.kern_pt
3016 && source.underline == target.underline
3017 && source.caps == target.caps
3018 && source.font_family == target.font_family
3019 && resolve_color_value_to_hex_with_theme(source.color.as_ref(), theme)
3020 == resolve_color_value_to_hex_with_theme(target.color.as_ref(), theme)
3021}
3022
3023fn build_paragraph(
3024 write: &ParagraphWrite,
3025 source: Option<XmlElement>,
3026 theme: Option<&Theme>,
3027 prefixes: &Prefixes,
3028) -> XmlElement {
3029 let (mut paragraph, source_runs) = match source {
3030 Some(mut element) => {
3031 let mut runs = Vec::new();
3032 let mut kept = Vec::new();
3033 for child in std::mem::take(&mut element.children) {
3034 match child {
3035 XmlNode::Element(child) if is_run_element(child.local_name()) => {
3036 runs.push(child);
3037 }
3038 XmlNode::Text(text) if text.trim().is_empty() => {}
3039 other => kept.push(other),
3040 }
3041 }
3042 element.children = kept;
3043 (element, runs)
3044 }
3045 None => (XmlElement::new(prefixes.drawing("p")), Vec::new()),
3046 };
3047 let fresh = write.source_index.is_none();
3048 if write.properties_changed || fresh {
3049 apply_paragraph_properties(&mut paragraph, write, prefixes);
3050 }
3051 let segments = run_segments(&write.runs);
3052 let mut front = 0;
3053 let mut back = 0;
3054 while front < source_runs.len()
3055 && front < segments.len()
3056 && segment_matches(&source_runs[front], &segments[front], theme)
3057 {
3058 let steals = front + 1 < source_runs.len() - back
3059 && segment_matches(&source_runs[front + 1], &segments[front], theme)
3060 && source_span_len(&source_runs, front + 1..source_runs.len() - back)
3061 > target_span_len(&segments, front + 1..segments.len() - back);
3062 if steals {
3063 break;
3064 }
3065 front += 1;
3066 }
3067 while back < source_runs.len() - front
3068 && back < segments.len() - front
3069 && segment_matches(
3070 &source_runs[source_runs.len() - 1 - back],
3071 &segments[segments.len() - 1 - back],
3072 theme,
3073 )
3074 {
3075 let src = source_runs.len() - 1 - back;
3076 let seg = segments.len() - 1 - back;
3077 let steals = src > front
3078 && segment_matches(&source_runs[src - 1], &segments[seg], theme)
3079 && source_span_len(&source_runs, front..src) > target_span_len(&segments, front..seg);
3080 if steals {
3081 break;
3082 }
3083 back += 1;
3084 }
3085 let tail_start = source_runs.len() - back;
3086 let rebuilt = span_elements(
3087 &segments[front..segments.len() - back],
3088 &source_runs,
3089 front..tail_start,
3090 theme,
3091 prefixes,
3092 );
3093 let mut runs: Vec<XmlNode> = Vec::with_capacity(segments.len());
3094 let mut source_runs = source_runs.into_iter();
3095 for element in source_runs.by_ref().take(front) {
3096 runs.push(XmlNode::Element(element));
3097 }
3098 runs.extend(rebuilt);
3099 for element in source_runs.skip(tail_start - front) {
3100 runs.push(XmlNode::Element(element));
3101 }
3102 let position = paragraph
3103 .children
3104 .iter()
3105 .position(|child| {
3106 matches!(child, XmlNode::Element(element) if element.local_name() == "endParaRPr")
3107 })
3108 .unwrap_or(paragraph.children.len());
3109 paragraph.children.splice(position..position, runs);
3110 paragraph
3111}
3112
3113struct TargetRange {
3115 end: usize,
3116 source: usize,
3117 verbatim: bool,
3119 field: bool,
3121}
3122
3123fn span_elements(
3125 segments: &[RunSegment<'_>],
3126 source_runs: &[XmlElement],
3127 span: Range<usize>,
3128 theme: Option<&Theme>,
3129 prefixes: &Prefixes,
3130) -> Vec<XmlNode> {
3131 if source_runs.is_empty() {
3132 return segments
3133 .iter()
3134 .map(|segment| XmlNode::Element(segment_element(segment, None, theme, prefixes)))
3135 .collect();
3136 }
3137 let ranges = align_span(segments, source_runs, span);
3138 let mut output = Vec::new();
3139 let mut offset = 0;
3140 let mut range_index = 0;
3141 let mut range_start = 0;
3142 for segment in segments {
3143 let mut remaining = segment.text;
3144 loop {
3145 while ranges[range_index].end <= offset {
3146 range_start = ranges[range_index].end;
3147 range_index += 1;
3148 }
3149 let range = &ranges[range_index];
3150 let source = &source_runs[range.source];
3151 if segment.line_break {
3152 let template = if source.local_name() == "br"
3153 || source
3154 .child("rPr")
3155 .is_some_and(|properties| properties.child("gradFill").is_some())
3156 {
3157 source.child("rPr")
3158 } else {
3159 None
3160 };
3161 output.push(XmlNode::Element(segment_element(
3162 segment, template, theme, prefixes,
3163 )));
3164 offset += 1;
3165 break;
3166 }
3167 let length = remaining.len().min(range.end - offset);
3168 let piece = RunSegment {
3169 text: &remaining[..length],
3170 ..*segment
3171 };
3172 let element = if range.field && offset == range_start && offset + length == range.end {
3173 field_element(source, &piece, theme, prefixes)
3174 } else {
3175 segment_element(&piece, source.child("rPr"), theme, prefixes)
3176 };
3177 output.push(XmlNode::Element(element));
3178 offset += length;
3179 remaining = &remaining[length..];
3180 if remaining.is_empty() {
3181 break;
3182 }
3183 }
3184 }
3185 output
3186}
3187
3188fn run_text(run: &XmlElement) -> String {
3189 if run.local_name() == "br" {
3190 return "\n".to_owned();
3191 }
3192 run.child("t")
3193 .map(XmlElement::text_content)
3194 .unwrap_or_default()
3195}
3196
3197fn segment_text<'a>(segment: &RunSegment<'a>) -> &'a str {
3198 if segment.line_break {
3199 "\n"
3200 } else {
3201 segment.text
3202 }
3203}
3204
3205fn source_span_len(runs: &[XmlElement], span: Range<usize>) -> usize {
3206 runs[span].iter().map(|run| run_text(run).len()).sum()
3207}
3208
3209fn target_span_len(segments: &[RunSegment<'_>], span: Range<usize>) -> usize {
3210 segments[span]
3211 .iter()
3212 .map(|segment| segment_text(segment).len())
3213 .sum()
3214}
3215
3216fn push_range(ranges: &mut Vec<TargetRange>, end: usize, source: usize, verbatim: bool) {
3217 if end <= ranges.last().map_or(0, |last| last.end) {
3218 return;
3219 }
3220 match ranges.last_mut() {
3221 Some(last) if last.source == source && last.verbatim == verbatim => last.end = end,
3222 _ => ranges.push(TargetRange {
3223 end,
3224 source,
3225 verbatim,
3226 field: false,
3227 }),
3228 }
3229}
3230
3231fn align_span(
3232 segments: &[RunSegment<'_>],
3233 source_runs: &[XmlElement],
3234 span: Range<usize>,
3235) -> Vec<TargetRange> {
3236 let runs = &source_runs[span.clone()];
3237 let mut source_text = String::new();
3238 let mut source_ends = Vec::with_capacity(runs.len());
3239 for run in runs {
3240 source_text.push_str(&run_text(run));
3241 source_ends.push(source_text.len());
3242 }
3243 let target_text: String = segments.iter().map(segment_text).collect();
3244 let run_at = |offset: usize| {
3245 span.start
3246 + source_ends
3247 .iter()
3248 .position(|&end| end > offset)
3249 .unwrap_or(source_ends.len().saturating_sub(1))
3250 };
3251 let prefix: usize = source_text
3252 .chars()
3253 .zip(target_text.chars())
3254 .take_while(|(left, right)| left == right)
3255 .map(|(value, _)| value.len_utf8())
3256 .sum();
3257 let suffix: usize = source_text[prefix..]
3258 .chars()
3259 .rev()
3260 .zip(target_text[prefix..].chars().rev())
3261 .take_while(|(left, right)| left == right)
3262 .map(|(value, _)| value.len_utf8())
3263 .sum();
3264 let source_end = source_text.len() - suffix;
3265 let target_end = target_text.len() - suffix;
3266
3267 let mut ranges: Vec<TargetRange> = Vec::new();
3268 for (index, &end) in source_ends.iter().enumerate() {
3269 push_range(&mut ranges, end.min(prefix), span.start + index, true);
3270 }
3271 let seed = span.start.saturating_sub(1);
3272 let (mut source_offset, mut target_offset) = (prefix, prefix);
3273 let mut replaced: Option<usize> = None;
3274 for op in char_diff(
3275 &source_text[prefix..source_end],
3276 &target_text[prefix..target_end],
3277 ) {
3278 match op {
3279 DiffOp::Match(length) => {
3280 let mut done = 0;
3281 while done < length {
3282 let run = run_at(source_offset + done);
3283 let piece =
3284 (source_ends[run - span.start] - (source_offset + done)).min(length - done);
3285 push_range(&mut ranges, target_offset + done + piece, run, true);
3286 done += piece;
3287 }
3288 source_offset += length;
3289 target_offset += length;
3290 replaced = None;
3291 }
3292 DiffOp::Delete(length) => {
3293 if replaced.is_none() {
3294 replaced = Some(run_at(source_offset));
3295 }
3296 source_offset += length;
3297 }
3298 DiffOp::Insert(length) => {
3299 let source = replaced
3300 .take()
3301 .or_else(|| ranges.last().map(|range| range.source))
3302 .unwrap_or(seed);
3303 push_range(&mut ranges, target_offset + length, source, false);
3304 target_offset += length;
3305 }
3306 }
3307 }
3308 for (index, &end) in source_ends.iter().enumerate() {
3309 if end > source_end {
3310 push_range(
3311 &mut ranges,
3312 target_end + end - source_end,
3313 span.start + index,
3314 true,
3315 );
3316 }
3317 }
3318 let mut kept = vec![false; runs.len()];
3319 for range in &ranges {
3320 if range.verbatim {
3321 kept[range.source - span.start] = true;
3322 }
3323 }
3324 for field_idx in 0..runs.len() {
3325 if runs[field_idx].local_name() != "fld" || kept[field_idx] {
3326 continue;
3327 }
3328 let field_text = run_text(&runs[field_idx]);
3329 if field_text.is_empty() {
3330 continue;
3331 }
3332 let mut donor: Option<usize> = None;
3333 for candidate in [
3334 field_idx.checked_sub(1),
3335 field_idx.checked_add(1).filter(|next| *next < runs.len()),
3336 ]
3337 .into_iter()
3338 .flatten()
3339 {
3340 if runs[candidate].local_name() == "r"
3341 && kept[candidate]
3342 && run_text(&runs[candidate]) == field_text
3343 {
3344 donor = Some(candidate);
3345 break;
3346 }
3347 }
3348 if let Some(donor) = donor {
3349 for range in &mut ranges {
3350 if range.verbatim && range.source == span.start + donor {
3351 range.source = span.start + field_idx;
3352 }
3353 }
3354 kept[donor] = false;
3355 kept[field_idx] = true;
3356 }
3357 }
3358
3359 let mut verbatim_ranges = vec![0usize; runs.len()];
3360 for range in ranges.iter().filter(|range| range.verbatim) {
3361 verbatim_ranges[range.source - span.start] += 1;
3362 }
3363 let mut merged: Vec<TargetRange> = Vec::with_capacity(ranges.len());
3364 let mut start = 0;
3365 for mut range in ranges {
3366 range.field = range.verbatim && {
3367 let relative = range.source - span.start;
3368 let run_start = relative
3369 .checked_sub(1)
3370 .map_or(0, |previous| source_ends[previous]);
3371 runs[relative].local_name() == "fld"
3372 && verbatim_ranges[relative] == 1
3373 && range.end - start == source_ends[relative] - run_start
3374 };
3375 start = range.end;
3376 match merged.last_mut() {
3377 Some(last) if last.source == range.source && !last.field && !range.field => {
3378 last.end = range.end
3379 }
3380 _ => merged.push(range),
3381 }
3382 }
3383 merged
3384}
3385
3386enum DiffOp {
3388 Match(usize),
3389 Delete(usize),
3390 Insert(usize),
3391}
3392
3393const DIFF_CELL_LIMIT: usize = 1 << 20;
3394
3395fn push_op(ops: &mut Vec<DiffOp>, op: DiffOp) {
3396 match (ops.last_mut(), op) {
3397 (Some(DiffOp::Match(last)), DiffOp::Match(length))
3398 | (Some(DiffOp::Delete(last)), DiffOp::Delete(length))
3399 | (Some(DiffOp::Insert(last)), DiffOp::Insert(length)) => *last += length,
3400 (_, op) => ops.push(op),
3401 }
3402}
3403
3404fn char_diff(source: &str, target: &str) -> Vec<DiffOp> {
3406 let source_chars: Vec<char> = source.chars().collect();
3407 let target_chars: Vec<char> = target.chars().collect();
3408 let (rows, columns) = (source_chars.len(), target_chars.len());
3409 let mut ops = Vec::new();
3410 if rows == 0 || columns == 0 || rows.saturating_mul(columns) > DIFF_CELL_LIMIT {
3411 if rows > 0 {
3412 push_op(&mut ops, DiffOp::Delete(source.len()));
3413 }
3414 if columns > 0 {
3415 push_op(&mut ops, DiffOp::Insert(target.len()));
3416 }
3417 return ops;
3418 }
3419 let width = columns + 1;
3420 let mut table = vec![0u16; (rows + 1) * width];
3421 for row in (0..rows).rev() {
3422 for column in (0..columns).rev() {
3423 table[row * width + column] = if source_chars[row] == target_chars[column] {
3424 table[(row + 1) * width + column + 1] + 1
3425 } else {
3426 table[(row + 1) * width + column].max(table[row * width + column + 1])
3427 };
3428 }
3429 }
3430 let (mut row, mut column) = (0, 0);
3431 while row < rows && column < columns {
3432 if source_chars[row] == target_chars[column] {
3433 push_op(&mut ops, DiffOp::Match(source_chars[row].len_utf8()));
3434 row += 1;
3435 column += 1;
3436 } else if table[(row + 1) * width + column] >= table[row * width + column + 1] {
3437 push_op(&mut ops, DiffOp::Delete(source_chars[row].len_utf8()));
3438 row += 1;
3439 } else {
3440 push_op(&mut ops, DiffOp::Insert(target_chars[column].len_utf8()));
3441 column += 1;
3442 }
3443 }
3444 for value in &source_chars[row..] {
3445 push_op(&mut ops, DiffOp::Delete(value.len_utf8()));
3446 }
3447 for value in &target_chars[column..] {
3448 push_op(&mut ops, DiffOp::Insert(value.len_utf8()));
3449 }
3450 ops
3451}
3452
3453fn segment_properties(
3454 segment: &RunSegment<'_>,
3455 template: Option<&XmlElement>,
3456 theme: Option<&Theme>,
3457 prefixes: &Prefixes,
3458) -> Option<XmlElement> {
3459 match template {
3460 Some(template) => {
3461 let mut base = template.clone();
3462 apply_run_properties(&mut base, segment.properties, theme, prefixes);
3463 Some(base)
3464 }
3465 None => run_properties_element(segment.properties, prefixes),
3466 }
3467}
3468
3469fn segment_element(
3470 segment: &RunSegment<'_>,
3471 template: Option<&XmlElement>,
3472 theme: Option<&Theme>,
3473 prefixes: &Prefixes,
3474) -> XmlElement {
3475 let mut element =
3476 XmlElement::new(prefixes.drawing(if segment.line_break { "br" } else { "r" }));
3477 if let Some(properties) = segment_properties(segment, template, theme, prefixes) {
3478 element = element.with_child(properties);
3479 }
3480 if segment.line_break {
3481 element
3482 } else {
3483 element.with_child(XmlElement::new(prefixes.drawing("t")).with_text(segment.text))
3484 }
3485}
3486
3487fn field_element(
3489 source: &XmlElement,
3490 segment: &RunSegment<'_>,
3491 theme: Option<&Theme>,
3492 prefixes: &Prefixes,
3493) -> XmlElement {
3494 let mut element = XmlElement::new(source.name.clone());
3495 element.attributes = source.attributes.clone();
3496 if let Some(properties) = segment_properties(segment, source.child("rPr"), theme, prefixes) {
3497 element.children.push(XmlNode::Element(properties));
3498 }
3499 element.children.extend(
3500 source
3501 .child_elements()
3502 .filter(|child| !matches!(child.local_name(), "rPr" | "t"))
3503 .cloned()
3504 .map(XmlNode::Element),
3505 );
3506 element.children.push(XmlNode::Element(
3507 XmlElement::new(prefixes.drawing("t")).with_text(segment.text),
3508 ));
3509 element
3510}
3511
3512const POST_LATIN_ELEMENTS: [&str; 7] = [
3513 "ea",
3514 "cs",
3515 "sym",
3516 "hlinkClick",
3517 "hlinkMouseOver",
3518 "rtl",
3519 "extLst",
3520];
3521
3522fn is_flattened_colour_snapshot(
3523 authored: Option<&ColorValue>,
3524 requested: Option<&ColorValue>,
3525 theme: Option<&Theme>,
3526) -> bool {
3527 let (Some(authored), Some(requested)) = (authored, requested) else {
3528 return false;
3529 };
3530 if !requested.transforms.is_empty()
3531 || requested.theme_color.is_some()
3532 || requested.theme_tint.is_some()
3533 || requested.theme_shade.is_some()
3534 || requested.auto.is_some()
3535 || requested.luminance_modulation.is_some()
3536 || requested.luminance_offset.is_some()
3537 || requested.saturation_modulation.is_some()
3538 {
3539 return false;
3540 }
3541 if requested.alpha.is_some()
3542 && ooxml_drawingml::resolve_color_value_alpha(requested)
3543 != ooxml_drawingml::resolve_color_value_alpha(authored)
3544 {
3545 return false;
3546 }
3547 resolve_color_value_to_hex_with_theme(Some(authored), theme)
3548 .zip(requested.rgb.as_deref())
3549 .is_some_and(|(hex, rgb)| rgb.eq_ignore_ascii_case(&hex[1..]))
3550}
3551
3552fn apply_run_properties(
3554 base: &mut XmlElement,
3555 properties: &RunProperties,
3556 theme: Option<&Theme>,
3557 prefixes: &Prefixes,
3558) {
3559 match properties.font_size_pt {
3560 Some(size) => base.set_attribute("sz", format_fixed(size * 100.0)),
3561 None => {
3562 base.attributes.remove("sz");
3563 }
3564 }
3565 match properties.spacing_pt {
3566 Some(spacing) => base.set_attribute("spc", format_fixed(spacing * 100.0)),
3567 None => {
3568 base.attributes.remove("spc");
3569 }
3570 }
3571 match properties.kern_pt {
3572 Some(kern) => base.set_attribute("kern", format_fixed(kern * 100.0)),
3573 None => {
3574 base.attributes.remove("kern");
3575 }
3576 }
3577 match properties.baseline_pct {
3578 Some(baseline) => base.set_attribute("baseline", format_fixed(baseline * 1000.0)),
3579 None => {
3580 base.attributes.remove("baseline");
3581 }
3582 }
3583 match properties.caps {
3584 Some(caps) => base.set_attribute("cap", caps.as_attribute()),
3585 None => {
3586 base.attributes.remove("cap");
3587 }
3588 }
3589 let toggles = [("b", properties.bold), ("i", properties.italic)];
3590 for (name, value) in toggles {
3591 match value {
3592 Some(value) => base.set_attribute(name, if value { "1" } else { "0" }),
3593 None => {
3594 base.attributes.remove(name);
3595 }
3596 }
3597 }
3598 match &properties.underline {
3599 Some(underline) => base.set_attribute("u", underline.clone()),
3600 None => {
3601 base.attributes.remove("u");
3602 }
3603 }
3604 let authored = parse_run_properties(Some(base)).color;
3605 let keeps_fill = authored == properties.color
3606 || is_flattened_colour_snapshot(authored.as_ref(), properties.color.as_ref(), theme);
3607 let removed_fills: &[&str] = if keeps_fill {
3610 &[]
3611 } else if properties.color.is_some() {
3612 &FILL_ELEMENTS
3613 } else {
3614 &["solidFill"]
3615 };
3616 base.children.retain(|child| {
3617 !matches!(
3618 child,
3619 XmlNode::Element(element) if removed_fills.contains(&element.local_name())
3620 )
3621 });
3622 if let Some(color) = properties
3623 .color
3624 .as_ref()
3625 .filter(|_| !keeps_fill)
3626 .and_then(|color| color_element(color, prefixes))
3627 {
3628 let position = base
3629 .children
3630 .iter()
3631 .position(
3632 |child| !matches!(child, XmlNode::Element(element) if element.local_name() == "ln"),
3633 )
3634 .unwrap_or(base.children.len());
3635 base.children.insert(
3636 position,
3637 XmlNode::Element(XmlElement::new(prefixes.drawing("solidFill")).with_child(color)),
3638 );
3639 }
3640 let Some(family) = &properties.font_family else {
3641 base.children.retain(|child| {
3642 !matches!(
3643 child,
3644 XmlNode::Element(element) if element.local_name() == "latin"
3645 )
3646 });
3647 return;
3648 };
3649 if let Some(latin) = base.child_mut("latin") {
3650 if latin.attribute("typeface") != Some(family.as_str()) {
3651 latin
3653 .attributes
3654 .retain(|name, _| name == "xmlns" || name.starts_with("xmlns:"));
3655 latin.set_attribute("typeface", family.clone());
3656 }
3657 } else {
3658 let position = base
3659 .children
3660 .iter()
3661 .position(|child| {
3662 matches!(
3663 child,
3664 XmlNode::Element(element)
3665 if POST_LATIN_ELEMENTS.contains(&element.local_name())
3666 )
3667 })
3668 .unwrap_or(base.children.len());
3669 base.children.insert(
3670 position,
3671 XmlNode::Element(
3672 XmlElement::new(prefixes.drawing("latin"))
3673 .with_attribute("typeface", family.clone()),
3674 ),
3675 );
3676 }
3677}
3678
3679fn apply_paragraph_properties(
3680 paragraph: &mut XmlElement,
3681 write: &ParagraphWrite,
3682 prefixes: &Prefixes,
3683) {
3684 let needs_properties = write.alignment.is_some() || write.level > 0 || write.bullet.is_some();
3685 if paragraph.child_mut("pPr").is_none() {
3686 if !needs_properties {
3687 return;
3688 }
3689 paragraph.children.insert(
3690 0,
3691 XmlNode::Element(XmlElement::new(prefixes.drawing("pPr"))),
3692 );
3693 }
3694 let properties = paragraph.child_mut("pPr").expect("ensured above");
3695 match &write.alignment {
3696 Some(alignment) => properties.set_attribute("algn", alignment.clone()),
3697 None => {
3698 properties.attributes.remove("algn");
3699 }
3700 }
3701 if write.level > 0 {
3702 properties.set_attribute("lvl", write.level.to_string());
3703 } else {
3704 properties.attributes.remove("lvl");
3705 }
3706 properties.children.retain(|child| {
3707 !matches!(
3708 child,
3709 XmlNode::Element(element)
3710 if matches!(element.local_name(), "buNone" | "buChar" | "buAutoNum")
3711 )
3712 });
3713 let bullet = match &write.bullet {
3714 Some(Bullet::Character { value }) => {
3715 Some(XmlElement::new(prefixes.drawing("buChar")).with_attribute("char", value.clone()))
3716 }
3717 Some(Bullet::AutoNumber {
3718 scheme,
3719 start_at,
3720 restart,
3721 }) => {
3722 let mut element = XmlElement::new(prefixes.drawing("buAutoNum"))
3723 .with_attribute("type", scheme.clone());
3724 if *restart || *start_at != 1 {
3725 element.set_attribute("startAt", start_at.to_string());
3726 }
3727 Some(element)
3728 }
3729 Some(Bullet::None) => Some(XmlElement::new(prefixes.drawing("buNone"))),
3730 None => None,
3731 };
3732 if let Some(bullet) = bullet {
3733 let position = properties
3734 .children
3735 .iter()
3736 .position(|child| {
3737 matches!(
3738 child,
3739 XmlNode::Element(element)
3740 if matches!(element.local_name(), "tabLst" | "defRPr" | "extLst")
3741 )
3742 })
3743 .unwrap_or(properties.children.len());
3744 properties
3745 .children
3746 .insert(position, XmlNode::Element(bullet));
3747 }
3748}
3749
3750fn run_properties_element(properties: &RunProperties, prefixes: &Prefixes) -> Option<XmlElement> {
3751 let mut element = XmlElement::new(prefixes.drawing("rPr"));
3752 let mut present = false;
3753 if let Some(language) = &properties.language {
3754 element.set_attribute("lang", language.clone());
3755 present = true;
3756 }
3757 if let Some(size) = properties.font_size_pt {
3758 element.set_attribute("sz", format_fixed(size * 100.0));
3759 present = true;
3760 }
3761 if let Some(spacing) = properties.spacing_pt {
3762 element.set_attribute("spc", format_fixed(spacing * 100.0));
3763 present = true;
3764 }
3765 if let Some(kern) = properties.kern_pt {
3766 element.set_attribute("kern", format_fixed(kern * 100.0));
3767 present = true;
3768 }
3769 if let Some(baseline) = properties.baseline_pct {
3770 element.set_attribute("baseline", format_fixed(baseline * 1000.0));
3771 present = true;
3772 }
3773 if let Some(bold) = properties.bold {
3774 element.set_attribute("b", if bold { "1" } else { "0" });
3775 present = true;
3776 }
3777 if let Some(italic) = properties.italic {
3778 element.set_attribute("i", if italic { "1" } else { "0" });
3779 present = true;
3780 }
3781 if let Some(underline) = &properties.underline {
3782 element.set_attribute("u", underline.clone());
3783 present = true;
3784 }
3785 if let Some(caps) = properties.caps {
3786 element.set_attribute("cap", caps.as_attribute());
3787 present = true;
3788 }
3789 if let Some(color) = properties
3790 .color
3791 .as_ref()
3792 .and_then(|color| color_element(color, prefixes))
3793 {
3794 element =
3795 element.with_child(XmlElement::new(prefixes.drawing("solidFill")).with_child(color));
3796 present = true;
3797 }
3798 if let Some(family) = &properties.font_family {
3799 element = element.with_child(
3800 XmlElement::new(prefixes.drawing("latin")).with_attribute("typeface", family.clone()),
3801 );
3802 present = true;
3803 }
3804 present.then_some(element)
3805}
3806
3807fn shape_element(
3810 add: &ShapeAdd,
3811 next_shape_id: &mut Option<u32>,
3812 prefixes: &Prefixes,
3813 part: &str,
3814) -> Result<XmlElement, PptxError> {
3815 if matches!(add.geometry.as_str(), "custom" | "group") {
3816 return Err(write_error(
3817 part,
3818 format!("unsupported geometry {:?} for a new shape", add.geometry),
3819 ));
3820 }
3821 let shape_id = alloc_shape_id(next_shape_id, part)?;
3822 let mut non_visual_properties = XmlElement::new(prefixes.presentation("cNvSpPr"));
3823 if add.text_box {
3824 non_visual_properties.set_attribute("txBox", "1");
3825 }
3826 let non_visual = XmlElement::new(prefixes.presentation("nvSpPr"))
3827 .with_child(
3828 XmlElement::new(prefixes.presentation("cNvPr"))
3829 .with_attribute("id", shape_id.to_string())
3830 .with_attribute("name", add.name.clone()),
3831 )
3832 .with_child(non_visual_properties)
3833 .with_child(XmlElement::new(prefixes.presentation("nvPr")));
3834 let transform = XmlElement::new(prefixes.drawing("xfrm"))
3835 .with_child(
3836 XmlElement::new(prefixes.drawing("off"))
3837 .with_attribute("x", add.x.to_string())
3838 .with_attribute("y", add.y.to_string()),
3839 )
3840 .with_child(
3841 XmlElement::new(prefixes.drawing("ext"))
3842 .with_attribute("cx", add.width.to_string())
3843 .with_attribute("cy", add.height.to_string()),
3844 );
3845 let mut adjust_list = XmlElement::new(prefixes.drawing("avLst"));
3846 for (name, value) in &add.adjust_values {
3847 adjust_list = adjust_list.with_child(
3848 XmlElement::new(prefixes.drawing("gd"))
3849 .with_attribute("name", name.clone())
3850 .with_attribute("fmla", format!("val {}", format_fixed(value * 100_000.0))),
3851 );
3852 }
3853 let geometry = XmlElement::new(prefixes.drawing("prstGeom"))
3854 .with_attribute("prst", add.geometry.clone())
3855 .with_child(adjust_list);
3856 let mut properties = XmlElement::new(prefixes.presentation("spPr"))
3857 .with_child(transform)
3858 .with_child(geometry);
3859 if let Some(fill) = &add.fill {
3860 properties = properties.with_child(fill_element(fill, prefixes, part)?);
3861 }
3862 if let Some(outline) = &add.outline {
3863 properties = properties.with_child(outline_element(outline, prefixes));
3864 }
3865 let mut shape = XmlElement::new(prefixes.presentation("sp"))
3866 .with_child(non_visual)
3867 .with_child(properties);
3868 if let Some(paragraphs) = &add.paragraphs {
3869 let mut body = XmlElement::new(prefixes.presentation("txBody"))
3870 .with_child(XmlElement::new(prefixes.drawing("bodyPr")))
3871 .with_child(XmlElement::new(prefixes.drawing("lstStyle")));
3872 for paragraph in paragraphs {
3873 body = body.with_child(build_paragraph(paragraph, None, None, prefixes));
3874 }
3875 if paragraphs.is_empty() {
3876 body = body.with_child(XmlElement::new(prefixes.drawing("p")));
3877 }
3878 shape = shape.with_child(body);
3879 }
3880 Ok(shape)
3881}
3882
3883fn add_shape_element(
3885 add: &ShapeAdd,
3886 next_shape_id: &mut Option<u32>,
3887 prefixes: &Prefixes,
3888 part: &str,
3889 sink: &mut PartSink<'_>,
3890 budget: &mut ParseBudget<'_>,
3891) -> Result<XmlElement, PptxError> {
3892 match &add.picture {
3893 Some(picture) => {
3894 picture_shape_element(add, picture, next_shape_id, prefixes, part, sink, budget)
3895 }
3896 None => shape_element(add, next_shape_id, prefixes, part),
3897 }
3898}
3899
3900fn image_content_type_extension(content_type: &str) -> Option<&'static str> {
3901 match content_type {
3902 "image/png" => Some("png"),
3903 "image/jpeg" | "image/jpg" => Some("jpeg"),
3904 "image/gif" => Some("gif"),
3905 "image/bmp" => Some("bmp"),
3906 "image/tiff" => Some("tiff"),
3907 "image/webp" => Some("webp"),
3908 "image/svg+xml" => Some("svg"),
3909 _ => None,
3910 }
3911}
3912
3913fn image_extension(content_type: &str) -> Result<&'static str, PptxError> {
3914 image_content_type_extension(content_type)
3915 .ok_or_else(|| write_error("media", format!("unsupported image type {content_type:?}")))
3916}
3917
3918pub fn is_supported_image_content_type(content_type: &str) -> bool {
3920 image_content_type_extension(content_type).is_some()
3921}
3922
3923fn next_media_part_path(
3924 package: &PptxPackage,
3925 new_parts: &[(String, Vec<u8>)],
3926 extension: &str,
3927) -> String {
3928 let existing: HashSet<&str> = package
3929 .parts
3930 .iter()
3931 .map(|part| part.path.as_str())
3932 .chain(new_parts.iter().map(|(path, _)| path.as_str()))
3933 .collect();
3934 let mut number = 1_u64;
3935 while existing.contains(format!("ppt/media/image{number}.{extension}").as_str()) {
3936 number += 1;
3937 }
3938 format!("ppt/media/image{number}.{extension}")
3939}
3940
3941fn ensure_media_content_type(
3943 sink: &mut PartSink<'_>,
3944 extension: &str,
3945 part_path: &str,
3946 content_type: &str,
3947 budget: &mut ParseBudget<'_>,
3948) -> Result<(), PptxError> {
3949 let path = "[Content_Types].xml";
3950 let bytes = sink
3951 .current(path)
3952 .ok_or_else(|| PptxError::MissingPart(path.to_owned()))?;
3953 let mut root = parse_xml(&bytes, path, budget)?;
3954 let default = root.children.iter().find_map(|child| match child {
3955 XmlNode::Element(element)
3956 if element.local_name() == "Default"
3957 && element
3958 .attribute("Extension")
3959 .is_some_and(|value| value.eq_ignore_ascii_case(extension)) =>
3960 {
3961 Some(element)
3962 }
3963 _ => None,
3964 });
3965 if let Some(default) = default {
3966 if default.attribute("ContentType") != Some(content_type) {
3967 set_content_type_override(sink, part_path, content_type, budget)?;
3968 }
3969 } else {
3970 let prefix = root.name.rsplit_once(':').map_or("", |(prefix, _)| prefix);
3971 root.children.push(XmlNode::Element(
3972 XmlElement::new(qualified(prefix, "Default"))
3973 .with_attribute("Extension", extension)
3974 .with_attribute("ContentType", content_type),
3975 ));
3976 sink.store(path, serialize_xml(&root));
3977 }
3978 Ok(())
3979}
3980
3981fn add_relationship(
3984 sink: &mut PartSink<'_>,
3985 relationships_path: &str,
3986 relationship_type: &str,
3987 target: &str,
3988 budget: &mut ParseBudget<'_>,
3989) -> Result<String, PptxError> {
3990 let mut root = match sink.current(relationships_path) {
3991 Some(bytes) => parse_xml(&bytes, relationships_path, budget)?,
3992 None => XmlElement::new("Relationships").with_attribute("xmlns", PACKAGE_RELATIONSHIPS_NS),
3993 };
3994 let used: HashSet<&str> = root
3995 .children_named("Relationship")
3996 .filter_map(|element| element.attribute("Id"))
3997 .collect();
3998 let mut number = 1_u64;
3999 while used.contains(format!("rId{number}").as_str()) {
4000 number += 1;
4001 }
4002 let id = format!("rId{number}");
4003 let prefix = root.name.rsplit_once(':').map_or("", |(prefix, _)| prefix);
4004 root.children.push(XmlNode::Element(
4005 XmlElement::new(qualified(prefix, "Relationship"))
4006 .with_attribute("Id", id.clone())
4007 .with_attribute("Type", relationship_type)
4008 .with_attribute("Target", target),
4009 ));
4010 sink.store(relationships_path, serialize_xml(&root));
4011 Ok(id)
4012}
4013
4014fn picture_shape_element(
4015 add: &ShapeAdd,
4016 picture: &PictureAdd,
4017 next_shape_id: &mut Option<u32>,
4018 prefixes: &Prefixes,
4019 slide_part_path: &str,
4020 sink: &mut PartSink<'_>,
4021 budget: &mut ParseBudget<'_>,
4022) -> Result<XmlElement, PptxError> {
4023 let extension = image_extension(&picture.content_type)?;
4024 let media_part_path = next_media_part_path(sink.package, sink.new_parts.as_slice(), extension);
4025 sink.store(&media_part_path, picture.media_bytes.clone());
4026 ensure_media_content_type(
4027 sink,
4028 extension,
4029 &media_part_path,
4030 &picture.content_type,
4031 budget,
4032 )?;
4033 let relationships_path = slide_relationships_path(slide_part_path);
4034 let target = relative_target(slide_part_path, &media_part_path);
4035 let relationship_id = add_relationship(
4036 sink,
4037 &relationships_path,
4038 IMAGE_RELATIONSHIP_TYPE,
4039 &target,
4040 budget,
4041 )?;
4042
4043 let shape_id = alloc_shape_id(next_shape_id, slide_part_path)?;
4044 let non_visual = XmlElement::new(prefixes.presentation("nvPicPr"))
4045 .with_child(
4046 XmlElement::new(prefixes.presentation("cNvPr"))
4047 .with_attribute("id", shape_id.to_string())
4048 .with_attribute("name", add.name.clone()),
4049 )
4050 .with_child(
4051 XmlElement::new(prefixes.presentation("cNvPicPr")).with_child(
4052 XmlElement::new(prefixes.drawing("picLocks")).with_attribute("noChangeAspect", "1"),
4053 ),
4054 )
4055 .with_child(XmlElement::new(prefixes.presentation("nvPr")));
4056 let blip_fill = XmlElement::new(prefixes.presentation("blipFill"))
4057 .with_child(
4058 XmlElement::new(prefixes.drawing("blip"))
4059 .with_attribute(prefixes.relationship("embed"), relationship_id),
4060 )
4061 .with_child(
4062 XmlElement::new(prefixes.drawing("stretch"))
4063 .with_child(XmlElement::new(prefixes.drawing("fillRect"))),
4064 );
4065 let transform = XmlElement::new(prefixes.drawing("xfrm"))
4066 .with_child(
4067 XmlElement::new(prefixes.drawing("off"))
4068 .with_attribute("x", add.x.to_string())
4069 .with_attribute("y", add.y.to_string()),
4070 )
4071 .with_child(
4072 XmlElement::new(prefixes.drawing("ext"))
4073 .with_attribute("cx", add.width.to_string())
4074 .with_attribute("cy", add.height.to_string()),
4075 );
4076 let geometry = XmlElement::new(prefixes.drawing("prstGeom"))
4077 .with_attribute("prst", "rect")
4078 .with_child(XmlElement::new(prefixes.drawing("avLst")));
4079 let properties = XmlElement::new(prefixes.presentation("spPr"))
4080 .with_child(transform)
4081 .with_child(geometry);
4082 Ok(XmlElement::new(prefixes.presentation("pic"))
4083 .with_child(non_visual)
4084 .with_child(blip_fill)
4085 .with_child(properties))
4086}
4087
4088fn slide_xml(
4089 name: Option<&str>,
4090 shapes: &[ShapeAdd],
4091 background: Option<&ShapeFill>,
4092 part: &str,
4093 sink: &mut PartSink<'_>,
4094 budget: &mut ParseBudget<'_>,
4095) -> Result<Vec<u8>, PptxError> {
4096 let mut root = XmlElement::new("p:sld")
4097 .with_attribute("xmlns:a", DRAWINGML_NS)
4098 .with_attribute("xmlns:r", OFFICE_RELATIONSHIPS_NS)
4099 .with_attribute("xmlns:p", PRESENTATIONML_NS);
4100 let prefixes = Prefixes {
4101 drawing: "a".to_owned(),
4102 presentation: "p".to_owned(),
4103 relationship: "r".to_owned(),
4104 };
4105 let group_transform = XmlElement::new(prefixes.drawing("xfrm"))
4106 .with_child(
4107 XmlElement::new(prefixes.drawing("off"))
4108 .with_attribute("x", "0")
4109 .with_attribute("y", "0"),
4110 )
4111 .with_child(
4112 XmlElement::new(prefixes.drawing("ext"))
4113 .with_attribute("cx", "0")
4114 .with_attribute("cy", "0"),
4115 )
4116 .with_child(
4117 XmlElement::new(prefixes.drawing("chOff"))
4118 .with_attribute("x", "0")
4119 .with_attribute("y", "0"),
4120 )
4121 .with_child(
4122 XmlElement::new(prefixes.drawing("chExt"))
4123 .with_attribute("cx", "0")
4124 .with_attribute("cy", "0"),
4125 );
4126 let mut tree = XmlElement::new(prefixes.presentation("spTree"))
4127 .with_child(
4128 XmlElement::new(prefixes.presentation("nvGrpSpPr"))
4129 .with_child(
4130 XmlElement::new(prefixes.presentation("cNvPr"))
4131 .with_attribute("id", "1")
4132 .with_attribute("name", ""),
4133 )
4134 .with_child(XmlElement::new(prefixes.presentation("cNvGrpSpPr")))
4135 .with_child(XmlElement::new(prefixes.presentation("nvPr"))),
4136 )
4137 .with_child(XmlElement::new(prefixes.presentation("grpSpPr")).with_child(group_transform));
4138 let mut next_shape_id = Some(2);
4139 for shape in shapes {
4140 tree = tree.with_child(add_shape_element(
4141 shape,
4142 &mut next_shape_id,
4143 &prefixes,
4144 part,
4145 sink,
4146 budget,
4147 )?);
4148 }
4149 let mut common = XmlElement::new(prefixes.presentation("cSld"));
4150 if let Some(name) = name {
4151 common.set_attribute("name", name);
4152 }
4153 if let Some(fill) = background {
4154 common = common.with_child(background_element(fill, &prefixes, part)?);
4155 }
4156 root = root.with_child(common.with_child(tree)).with_child(
4157 XmlElement::new(prefixes.presentation("clrMapOvr"))
4158 .with_child(XmlElement::new(prefixes.drawing("masterClrMapping"))),
4159 );
4160 Ok(serialize_xml(&root))
4161}
4162
4163fn slide_relationships_xml(part_path: &str, layout_part_path: &str) -> Vec<u8> {
4164 let root = XmlElement::new("Relationships")
4165 .with_attribute(
4166 "xmlns",
4167 "http://schemas.openxmlformats.org/package/2006/relationships",
4168 )
4169 .with_child(
4170 XmlElement::new("Relationship")
4171 .with_attribute("Id", "rId1")
4172 .with_attribute("Type", SLIDE_LAYOUT_RELATIONSHIP_TYPE)
4173 .with_attribute("Target", relative_target(part_path, layout_part_path)),
4174 );
4175 serialize_xml(&root)
4176}
4177
4178#[cfg(test)]
4179mod tests {
4180 use super::*;
4181 use crate::ParseLimits;
4182 use crate::drawing::parse_run_properties;
4183 use crate::model::ShapeElements;
4184 use crate::xml::{ParseBudget, parse_xml};
4185
4186 #[test]
4187 fn a_labelled_ellipse_is_not_written_as_a_text_box() {
4188 let add = ShapeAdd {
4189 name: "Labelled ellipse".to_owned(),
4190 geometry: "ellipse".to_owned(),
4191 text_box: false,
4192 x: 0,
4193 y: 0,
4194 width: 1_000_000,
4195 height: 500_000,
4196 adjust_values: BTreeMap::new(),
4197 fill: None,
4198 outline: None,
4199 paragraphs: Some(vec![ParagraphWrite {
4200 source_paragraphs: Vec::new(),
4201 source_index: None,
4202 preserved_xml: None,
4203 rebuild: true,
4204 properties_changed: true,
4205 alignment: None,
4206 level: 0,
4207 bullet: None,
4208 runs: vec![RunWrite {
4209 text: "Ellipse label".to_owned(),
4210 properties: RunProperties::default(),
4211 }],
4212 }]),
4213 picture: None,
4214 };
4215 let mut root = XmlElement::new("p:sld");
4216 let prefixes = Prefixes::from_root(&mut root);
4217 let shape = shape_element(&add, &mut Some(2), &prefixes, "ppt/slides/slide1.xml").unwrap();
4218 assert!(shape.child("txBody").unwrap().child("p").is_some());
4219 assert_eq!(
4220 shape
4221 .child("spPr")
4222 .unwrap()
4223 .child("prstGeom")
4224 .unwrap()
4225 .attribute("prst"),
4226 Some("ellipse")
4227 );
4228 assert_eq!(
4229 shape
4230 .child("nvSpPr")
4231 .unwrap()
4232 .child("cNvSpPr")
4233 .unwrap()
4234 .attribute("txBox"),
4235 None
4236 );
4237 }
4238
4239 #[test]
4240 fn changing_outline_width_preserves_unchanged_color_source() {
4241 let limits = ParseLimits::default();
4242 let mut properties = parse_xml(
4243 br#"<p:spPr><a:ln w="9525"><a:solidFill><a:schemeClr val="accent1"><a:tint val="50%"/><a:lumMod val="60000"/><a:extLst/></a:schemeClr></a:solidFill></a:ln></p:spPr>"#,
4244 "shape.xml",
4245 &mut ParseBudget::new(&limits),
4246 )
4247 .unwrap();
4248 let original_color = properties
4249 .child("ln")
4250 .unwrap()
4251 .child("solidFill")
4252 .unwrap()
4253 .clone();
4254 let mut outline =
4255 crate::drawing::parse_outline_element(properties.child("ln").unwrap()).unwrap();
4256 outline.width = Some(19050.0);
4257 let prefixes = Prefixes::from_root(&mut properties);
4258 set_outline(&mut properties, &outline, &prefixes);
4259 let line = properties.child("ln").unwrap();
4260 assert_eq!(line.attribute("w"), Some("19050"));
4261 assert_eq!(line.child("solidFill"), Some(&original_color));
4262 }
4263
4264 #[test]
4265 fn rebuilt_color_preserves_transform_order_and_exact_percentages() {
4266 let limits = ParseLimits::default();
4267 let source = parse_xml(
4268 br#"<a:solidFill><a:srgbClr val="4472C4"><a:tint val="50000"/><a:lumMod val="75000"/><a:shade val="12345"/><a:shade val="60000"/><a:hueOff val="-5400000"/><a:comp/><a:alpha val="50000"/></a:srgbClr></a:solidFill>"#,
4269 "color.xml",
4270 &mut ParseBudget::new(&limits),
4271 )
4272 .unwrap();
4273 let color = crate::drawing::parse_color_container(&source).unwrap();
4274 let mut properties = XmlElement::new("p:spPr");
4275 let prefixes = Prefixes::from_root(&mut properties);
4276 let written = color_element(&color, &prefixes).unwrap();
4277 assert_eq!(
4278 written
4279 .child_elements()
4280 .map(|child| (child.local_name(), child.attribute("val")))
4281 .collect::<Vec<_>>(),
4282 vec![
4283 ("tint", Some("50000")),
4284 ("lumMod", Some("75000")),
4285 ("shade", Some("12345")),
4286 ("shade", Some("60000")),
4287 ("hueOff", Some("-5400000")),
4288 ("comp", None),
4289 ("alpha", Some("50000")),
4290 ]
4291 );
4292 assert_eq!(
4293 crate::drawing::parse_color_container(
4294 &XmlElement::new("a:solidFill").with_child(written)
4295 ),
4296 Some(color)
4297 );
4298 }
4299
4300 #[test]
4301 fn mixed_color_alpha_survives_serialization() {
4302 use ooxml_drawingml::{ColorTransform, resolve_color_value_to_rgba_hex};
4303
4304 for step in [None, Some(ColorTransform::AlphaMod(0.5))] {
4305 let color = ColorValue {
4306 rgb: Some("112233".to_owned()),
4307 theme_tint: Some("00".to_owned()),
4308 alpha: Some(0.25),
4309 transforms: std::iter::once(ColorTransform::Tint(1.0))
4310 .chain(step)
4311 .collect(),
4312 ..Default::default()
4313 };
4314 let mut properties = XmlElement::new("p:spPr");
4315 let prefixes = Prefixes::from_root(&mut properties);
4316 let written = color_element(&color, &prefixes).unwrap();
4317 assert_eq!(written.child("alpha").is_some(), step.is_none());
4318 assert_eq!(written.child_elements().count(), 2);
4319 let reopened = crate::drawing::parse_color_container(
4320 &XmlElement::new("a:solidFill").with_child(written),
4321 )
4322 .unwrap();
4323 let expected = if step.is_none() {
4324 "#11223340"
4325 } else {
4326 "#11223380"
4327 };
4328 assert_eq!(
4329 resolve_color_value_to_rgba_hex(Some(&reopened), None).as_deref(),
4330 Some(expected)
4331 );
4332 }
4333 }
4334
4335 #[test]
4336 fn a_new_gradient_outline_writes_its_stops_and_angle() {
4337 let package =
4338 crate::parse_pptx(include_bytes!("../tests/fixtures/gradient-outline.pptx")).unwrap();
4339 let crate::ShapeNode::Shape(shape) = &package.slides[0].shapes[0] else {
4340 panic!("shape")
4341 };
4342 let outline = shape.outline.as_ref().unwrap();
4343 let mut properties = XmlElement::new("p:spPr");
4344 let prefixes = Prefixes::from_root(&mut properties);
4345 set_outline(&mut properties, outline, &prefixes);
4346 let line = properties.child("ln").unwrap();
4347 assert_eq!(line.attribute("w"), Some("76200"));
4348 let gradient = line.child("gradFill").unwrap();
4349 assert_eq!(gradient.child("gsLst").unwrap().child_elements().count(), 3);
4350 assert_eq!(gradient.child("lin").unwrap().attribute("ang"), Some("0"));
4351 assert!(line.child("solidFill").is_none());
4352 assert_eq!(
4353 crate::drawing::parse_outline_element(line).as_ref(),
4354 Some(outline)
4355 );
4356 }
4357
4358 #[test]
4359 fn source_ordinals_cover_the_shapes_an_alternate_content_contributes() {
4360 let limits = ParseLimits::default();
4361 let mut budget = ParseBudget::new(&limits);
4362 let part = "ppt/slides/slide1.xml";
4363 let mut root = parse_xml(
4364 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>"#,
4365 part,
4366 &mut budget,
4367 )
4368 .unwrap();
4369 let parsed = crate::drawing::common_slide_data(
4370 &root,
4371 &[],
4372 part,
4373 &mut budget,
4374 ShapeElements::WithConnectors,
4375 )
4376 .unwrap();
4377 let tree = root
4378 .child_mut("cSld")
4379 .and_then(|common| common.child_mut("spTree"))
4380 .unwrap();
4381 let slots: Vec<Option<XmlNode>> = std::mem::take(&mut tree.children)
4382 .into_iter()
4383 .map(Some)
4384 .collect();
4385
4386 let found = shape_slots(&slots, ShapeElements::WithConnectors);
4387
4388 assert_eq!(found.len(), parsed.shapes.len());
4389 let names: Vec<String> = found
4390 .iter()
4391 .map(|slot| {
4392 let mut node = slots[slot.position].clone().unwrap();
4393 let element = element_at_path(&mut node, &slot.path).unwrap();
4394 element.descendants_named("cNvPr")[0]
4395 .attribute("name")
4396 .unwrap()
4397 .to_owned()
4398 })
4399 .collect();
4400 assert_eq!(names, ["one", "two", "three", "four"]);
4401 }
4402
4403 #[test]
4404 fn deleting_one_of_the_shapes_a_branch_holds_leaves_the_others_in_place() {
4405 let limits = ParseLimits::default();
4406 let mut budget = ParseBudget::new(&limits);
4407 let part = "ppt/slides/slide1.xml";
4408 let mut root = parse_xml(
4409 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>"#,
4410 part,
4411 &mut budget,
4412 )
4413 .unwrap();
4414
4415 let package = PptxPackage::default();
4416 let mut replacements = HashMap::new();
4417 let mut new_parts = Vec::new();
4418 let mut sink = PartSink {
4419 package: &package,
4420 replacements: &mut replacements,
4421 new_parts: &mut new_parts,
4422 };
4423 patch_slide(
4424 &mut root,
4425 &[
4426 ShapeWrite::Keep { source_index: 0 },
4427 ShapeWrite::Patch {
4428 source_index: 2,
4429 patch: Box::new(ShapePatch {
4430 fill: Some(ShapeFill {
4431 color: Some(ColorValue::from_attribute("DC2626")),
4432 ..ShapeFill::named("solid")
4433 }),
4434 ..ShapePatch::default()
4435 }),
4436 },
4437 ],
4438 None,
4439 None,
4440 part,
4441 ShapeElements::WithConnectors,
4442 &mut sink,
4443 &mut budget,
4444 )
4445 .unwrap();
4446
4447 let xml = String::from_utf8(serialize_xml(&root)).unwrap();
4448 assert!(xml.contains(r#"name="one""#));
4449 assert!(!xml.contains(r#"name="two""#), "{xml}");
4450 let survivor = xml.split(r#"name="three""#).nth(1).unwrap();
4451 assert!(survivor.contains(r#"<a:srgbClr val="DC2626"/>"#), "{xml}");
4452 }
4453
4454 fn patch_background_fixture(xml: &[u8], background: Option<&BackgroundWrite>) -> String {
4455 let limits = ParseLimits::default();
4456 let mut budget = ParseBudget::new(&limits);
4457 let part = "ppt/slides/slide1.xml";
4458 let mut root = parse_xml(xml, part, &mut budget).unwrap();
4459 let package = PptxPackage::default();
4460 let mut replacements = HashMap::new();
4461 let mut new_parts = Vec::new();
4462 let mut sink = PartSink {
4463 package: &package,
4464 replacements: &mut replacements,
4465 new_parts: &mut new_parts,
4466 };
4467 patch_slide(
4468 &mut root,
4469 &[ShapeWrite::Keep { source_index: 0 }],
4470 background,
4471 None,
4472 part,
4473 ShapeElements::WithConnectors,
4474 &mut sink,
4475 &mut budget,
4476 )
4477 .unwrap();
4478 String::from_utf8(serialize_xml(&root)).unwrap()
4479 }
4480
4481 #[test]
4482 fn a_new_background_precedes_the_shape_tree() {
4483 let xml = patch_background_fixture(
4484 br#"<p:sld><p:cSld><p:spTree><p:sp><p:nvSpPr><p:cNvPr id="2" name="one"/></p:nvSpPr></p:sp></p:spTree></p:cSld></p:sld>"#,
4485 Some(&BackgroundWrite::Set(Box::new(ShapeFill {
4486 color: Some(ColorValue::from_attribute("0F1720")),
4487 ..ShapeFill::named("solid")
4488 }))),
4489 );
4490 let common = xml.find("<p:cSld>").unwrap();
4491 let bg = xml.find("<p:bg>").unwrap();
4492 let effects = xml.find("<a:effectLst/>").unwrap();
4493 let tree = xml.find("<p:spTree>").unwrap();
4494 assert!(common < bg && bg < effects && effects < tree, "{xml}");
4495 assert!(xml.contains(r#"<a:srgbClr val="0F1720"/>"#), "{xml}");
4496 }
4497
4498 #[test]
4499 fn clearing_a_background_removes_only_the_declaration() {
4500 let xml = patch_background_fixture(
4501 br#"<p:sld><p:cSld><p:bg><p:bgPr><a:solidFill><a:srgbClr val="0F1720"/></a:solidFill><a:effectLst/></p:bgPr></p:bg><p:spTree><p:sp><p:nvSpPr><p:cNvPr id="2" name="one"/></p:nvSpPr></p:sp></p:spTree></p:cSld></p:sld>"#,
4502 Some(&BackgroundWrite::Clear),
4503 );
4504 assert!(!xml.contains("<p:bg>"), "{xml}");
4505 assert!(xml.contains(r#"name="one""#), "{xml}");
4506 }
4507
4508 #[test]
4509 fn an_untouched_background_survives_a_shape_patch() {
4510 let limits = ParseLimits::default();
4511 let mut budget = ParseBudget::new(&limits);
4512 let part = "ppt/slides/slide1.xml";
4513 let mut root = parse_xml(
4514 br#"<p:sld><p:cSld><p:bg><p:bgPr><a:solidFill><a:srgbClr val="0F1720"/></a:solidFill><a:effectLst/></p:bgPr></p:bg><p:spTree><p:sp><p:nvSpPr><p:cNvPr id="2" name="one"/></p:nvSpPr><p:spPr/></p:sp></p:spTree></p:cSld></p:sld>"#,
4515 part,
4516 &mut budget,
4517 )
4518 .unwrap();
4519 let package = PptxPackage::default();
4520 let mut replacements = HashMap::new();
4521 let mut new_parts = Vec::new();
4522 let mut sink = PartSink {
4523 package: &package,
4524 replacements: &mut replacements,
4525 new_parts: &mut new_parts,
4526 };
4527 patch_slide(
4528 &mut root,
4529 &[ShapeWrite::Patch {
4530 source_index: 0,
4531 patch: Box::new(ShapePatch {
4532 fill: Some(ShapeFill {
4533 color: Some(ColorValue::from_attribute("DC2626")),
4534 ..ShapeFill::named("solid")
4535 }),
4536 ..ShapePatch::default()
4537 }),
4538 }],
4539 None,
4540 None,
4541 part,
4542 ShapeElements::WithConnectors,
4543 &mut sink,
4544 &mut budget,
4545 )
4546 .unwrap();
4547
4548 let xml = String::from_utf8(serialize_xml(&root)).unwrap();
4549 assert!(xml.contains(r#"<a:srgbClr val="0F1720"/>"#), "{xml}");
4550 assert!(xml.contains(r#"<a:srgbClr val="DC2626"/>"#), "{xml}");
4551 }
4552
4553 #[test]
4554 fn a_patched_use_bg_fill_shape_keeps_the_attribute() {
4555 let limits = ParseLimits::default();
4556 let mut budget = ParseBudget::new(&limits);
4557 let part = "ppt/slides/slide1.xml";
4558 let mut root = parse_xml(
4559 br#"<p:sld><p:cSld><p:spTree><p:sp useBgFill="1"><p:nvSpPr><p:cNvPr id="2" name="one"/></p:nvSpPr><p:spPr/></p:sp></p:spTree></p:cSld></p:sld>"#,
4560 part,
4561 &mut budget,
4562 )
4563 .unwrap();
4564 let package = PptxPackage::default();
4565 let mut replacements = HashMap::new();
4566 let mut new_parts = Vec::new();
4567 let mut sink = PartSink {
4568 package: &package,
4569 replacements: &mut replacements,
4570 new_parts: &mut new_parts,
4571 };
4572 patch_slide(
4573 &mut root,
4574 &[ShapeWrite::Patch {
4575 source_index: 0,
4576 patch: Box::new(ShapePatch {
4577 fill: Some(ShapeFill {
4578 color: Some(ColorValue::from_attribute("DC2626")),
4579 ..ShapeFill::named("solid")
4580 }),
4581 ..ShapePatch::default()
4582 }),
4583 }],
4584 None,
4585 None,
4586 part,
4587 ShapeElements::WithConnectors,
4588 &mut sink,
4589 &mut budget,
4590 )
4591 .unwrap();
4592
4593 let xml = String::from_utf8(serialize_xml(&root)).unwrap();
4594 assert!(xml.contains(r#"useBgFill="1""#), "{xml}");
4595 assert!(xml.contains(r#"<a:srgbClr val="DC2626"/>"#), "{xml}");
4596 }
4597
4598 #[test]
4599 fn an_added_slide_background_precedes_the_shape_tree() {
4600 let package = PptxPackage::default();
4601 let mut replacements = HashMap::new();
4602 let mut new_parts = Vec::new();
4603 let mut sink = PartSink {
4604 package: &package,
4605 replacements: &mut replacements,
4606 new_parts: &mut new_parts,
4607 };
4608 let limits = ParseLimits::default();
4609 let mut budget = ParseBudget::new(&limits);
4610 let fill = ShapeFill {
4611 color: Some(ColorValue::from_attribute("F8FAFC")),
4612 ..ShapeFill::named("solid")
4613 };
4614 let xml = slide_xml(
4615 None,
4616 &[],
4617 Some(&fill),
4618 "ppt/slides/slide9.xml",
4619 &mut sink,
4620 &mut budget,
4621 )
4622 .unwrap();
4623 let xml = String::from_utf8(xml).unwrap();
4624 let common = xml.find("<p:cSld>").unwrap();
4625 let bg = xml.find("<p:bg>").unwrap();
4626 let tree = xml.find("<p:spTree>").unwrap();
4627 assert!(common < bg && bg < tree, "{xml}");
4628 assert!(xml.contains(r#"<a:srgbClr val="F8FAFC"/>"#), "{xml}");
4629 }
4630
4631 #[test]
4632 fn a_branch_that_still_holds_unmodeled_markup_outlives_its_only_shape() {
4633 let limits = ParseLimits::default();
4634 let mut budget = ParseBudget::new(&limits);
4635 let part = "ppt/slides/slide1.xml";
4636 let mut root = parse_xml(
4637 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>"#,
4638 part,
4639 &mut budget,
4640 )
4641 .unwrap();
4642
4643 let package = PptxPackage::default();
4644 let mut replacements = HashMap::new();
4645 let mut new_parts = Vec::new();
4646 let mut sink = PartSink {
4647 package: &package,
4648 replacements: &mut replacements,
4649 new_parts: &mut new_parts,
4650 };
4651 patch_slide(
4652 &mut root,
4653 &[],
4654 None,
4655 None,
4656 part,
4657 ShapeElements::WithoutConnectors,
4658 &mut sink,
4659 &mut budget,
4660 )
4661 .unwrap();
4662
4663 let xml = String::from_utf8(serialize_xml(&root)).unwrap();
4664 assert!(!xml.contains(r#"name="one""#), "{xml}");
4665 assert!(xml.contains(r#"name="two""#), "{xml}");
4666 }
4667
4668 fn run_properties(xml: &[u8]) -> (XmlElement, Prefixes, RunProperties) {
4669 let limits = ParseLimits::default();
4670 let mut budget = ParseBudget::new(&limits);
4671 let mut root = parse_xml(xml, "ppt/slides/slide1.xml", &mut budget).unwrap();
4672 let prefixes = Prefixes::from_root(&mut root);
4673 let properties = parse_run_properties(Some(&root));
4674 (root, prefixes, properties)
4675 }
4676
4677 #[test]
4678 fn an_unedited_gradient_fill_survives_a_rewrite() {
4679 let (mut element, prefixes, properties) = run_properties(
4680 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>"#,
4681 );
4682 assert!(
4683 properties.color.is_some(),
4684 "the gradient should flatten to a colour"
4685 );
4686 let gradient = element.child("gradFill").unwrap().clone();
4687 apply_run_properties(&mut element, &properties, None, &prefixes);
4688 assert_eq!(element.child("gradFill"), Some(&gradient));
4689 let serialized = String::from_utf8(serialize_xml(&element)).unwrap();
4690 assert!(
4691 serialized.contains("gradFill"),
4692 "the authored gradient must be left alone"
4693 );
4694 assert!(
4695 !serialized.contains("solidFill"),
4696 "it must not be rewritten as a solid fill"
4697 );
4698 }
4699
4700 #[test]
4701 fn a_lowercase_flattened_colour_keeps_the_gradient() {
4702 let (mut element, prefixes, mut properties) = run_properties(
4703 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="FF0000"/></a:gs><a:gs pos="100000"><a:srgbClr val="0000FF"/></a:gs></a:gsLst></a:gradFill></a:rPr>"#,
4704 );
4705 properties.color = Some(ColorValue {
4706 rgb: Some("ff0000".to_owned()),
4707 ..ColorValue::default()
4708 });
4709 let gradient = element.child("gradFill").unwrap().clone();
4710 apply_run_properties(&mut element, &properties, None, &prefixes);
4711 assert_eq!(element.child("gradFill"), Some(&gradient));
4712 }
4713
4714 #[test]
4715 fn an_edited_colour_replaces_the_gradient() {
4716 let (mut element, prefixes, mut properties) = run_properties(
4717 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>"#,
4718 );
4719 properties.color = Some(ColorValue {
4720 rgb: Some("FF0000".to_owned()),
4721 ..ColorValue::default()
4722 });
4723 apply_run_properties(&mut element, &properties, None, &prefixes);
4724 let serialized = String::from_utf8(serialize_xml(&element)).unwrap();
4725 assert!(!serialized.contains("gradFill"));
4726 assert!(serialized.contains("solidFill"));
4727 assert_eq!(parse_run_properties(Some(&element)).color, properties.color);
4728 }
4729
4730 #[test]
4731 fn the_writer_recognises_exactly_what_the_parser_reads() {
4732 for local in ["sp", "cxnSp", "pic", "graphicFrame", "grpSp"] {
4733 assert!(
4734 ShapeElements::WithConnectors.contains(local),
4735 "{local} is parsed as a shape but not recognised here"
4736 );
4737 }
4738 for local in ["txBody", "nvGrpSpPr", "extLst"] {
4739 assert!(
4740 !ShapeElements::WithConnectors.contains(local),
4741 "{local} is not a shape element"
4742 );
4743 }
4744 }
4745
4746 #[test]
4747 fn the_pre_connector_set_leaves_out_only_connectors() {
4748 assert!(!ShapeElements::WithoutConnectors.contains("cxnSp"));
4749 for local in ["sp", "pic", "graphicFrame", "grpSp"] {
4750 assert!(ShapeElements::WithoutConnectors.contains(local));
4751 }
4752 }
4753
4754 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>"#;
4755 const LINK_PROPERTIES: &str =
4756 r#"<a:rPr lang="en-US" strike="sngStrike"><a:hlinkClick r:id="rId2"/></a:rPr>"#;
4757 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>"#;
4758
4759 fn rebuilt_paragraph(xml: &[u8], runs: &[(&str, RunProperties)]) -> String {
4760 let limits = ParseLimits::default();
4761 let mut budget = ParseBudget::new(&limits);
4762 let mut root = parse_xml(xml, "ppt/slides/slide1.xml", &mut budget).unwrap();
4763 let prefixes = Prefixes::from_root(&mut root);
4764 let write = ParagraphWrite {
4765 source_paragraphs: Vec::new(),
4766 source_index: Some(0),
4767 preserved_xml: None,
4768 rebuild: true,
4769 properties_changed: false,
4770 alignment: None,
4771 level: 0,
4772 bullet: None,
4773 runs: runs
4774 .iter()
4775 .map(|(text, properties)| RunWrite {
4776 text: (*text).to_owned(),
4777 properties: properties.clone(),
4778 })
4779 .collect(),
4780 };
4781 let paragraph = build_paragraph(&write, Some(root), None, &prefixes);
4782 String::from_utf8(serialize_xml(¶graph)).unwrap()
4783 }
4784
4785 #[test]
4786 fn an_edit_spanning_several_runs_keeps_each_survivor_on_its_source_run() {
4787 let xml = rebuilt_paragraph(
4788 LINKED_PARAGRAPH,
4789 &[("Seethe doc now", RunProperties::default())],
4790 );
4791 assert!(
4792 xml.contains(r#"<a:r><a:rPr dirty="0" lang="en-US"/><a:t>See</a:t></a:r>"#),
4793 "{xml}"
4794 );
4795 assert!(
4796 xml.contains(&format!("<a:r>{LINK_PROPERTIES}<a:t>the doc</a:t></a:r>")),
4797 "{xml}"
4798 );
4799 assert!(
4800 xml.contains(r#"<a:r><a:rPr lang="en-US"/><a:t> now</a:t></a:r>"#),
4801 "{xml}"
4802 );
4803 assert_eq!(xml.matches("<a:r>").count(), 3, "{xml}");
4804 }
4805
4806 #[test]
4807 fn a_line_break_inside_the_span_keeps_the_runs_around_it_aligned() {
4808 let xml = rebuilt_paragraph(
4809 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>"#,
4810 &[("Firs\nSecond!", RunProperties::default())],
4811 );
4812 assert!(
4813 xml.contains(
4814 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>"#
4815 ),
4816 "{xml}"
4817 );
4818 }
4819
4820 const FONT_PARAGRAPH: &str = r#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:pPr algn="ctr"/><a:r><a:rPr lang="en-US"><a:highlight><a:srgbClr val="FFFF00"/></a:highlight><a:latin charset="0" panose="020B0604020202020204" pitchFamily="34" typeface="Arial" vendorHint="1"/><a:cs charset="0" panose="020B0604020202020204" pitchFamily="34" typeface="Arial"/></a:rPr><a:t>38.8%</a:t></a:r><a:r><a:rPr b="1" lang="en-US"><a:latin charset="0" panose="020B0604020202020204" pitchFamily="34" typeface="Arial"/></a:rPr><a:t> of cases</a:t></a:r></a:p>"#;
4821 const ARIAL_LATIN: &str = r#"<a:latin charset="0" panose="020B0604020202020204" pitchFamily="34" typeface="Arial" vendorHint="1"/>"#;
4822
4823 fn font_runs(family: &str) -> [(&'static str, RunProperties); 2] {
4824 let arial = RunProperties {
4825 font_family: Some("Arial".to_owned()),
4826 ..RunProperties::default()
4827 };
4828 [
4829 (
4830 "39.8%",
4831 RunProperties {
4832 font_family: Some(family.to_owned()),
4833 ..RunProperties::default()
4834 },
4835 ),
4836 (
4837 " of cases",
4838 RunProperties {
4839 bold: Some(true),
4840 ..arial
4841 },
4842 ),
4843 ]
4844 }
4845
4846 fn edited_font_body(latin: &str) -> String {
4847 let (_, body) = FONT_PARAGRAPH.split_once('>').unwrap();
4848 let edited = body.replace("38.8%", "39.8%");
4849 edited.replacen(ARIAL_LATIN, latin, 1)
4850 }
4851
4852 #[test]
4853 fn a_text_edit_keeps_the_latin_font_element_whole() {
4854 let xml = rebuilt_paragraph(FONT_PARAGRAPH.as_bytes(), &font_runs("Arial"));
4855 assert!(xml.ends_with(&edited_font_body(ARIAL_LATIN)), "{xml}");
4856 }
4857
4858 #[test]
4859 fn a_new_font_family_drops_the_old_face_metrics_in_place() {
4860 let xml = rebuilt_paragraph(FONT_PARAGRAPH.as_bytes(), &font_runs("Calibri"));
4861 let calibri = edited_font_body(r#"<a:latin typeface="Calibri"/>"#);
4862 assert!(xml.ends_with(&calibri), "{xml}");
4863 }
4864
4865 const FILL_PARAGRAPH: &str = r#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:r><a:rPr lang="en-US"><a:solidFill><a:schemeClr val="accent6"><a:lumMod val="75000"/></a:schemeClr></a:solidFill></a:rPr><a:t>amongst</a:t></a:r><a:r><a:rPr lang="en-US"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill></a:rPr><a:t> them</a:t></a:r></a:p>"#;
4866
4867 fn hex_runs() -> Vec<(&'static str, RunProperties)> {
4869 let limits = ParseLimits::default();
4870 let mut budget = ParseBudget::new(&limits);
4871 let path = "ppt/slides/slide1.xml";
4872 let root = parse_xml(FILL_PARAGRAPH.as_bytes(), path, &mut budget).unwrap();
4873 root.child_elements()
4874 .zip(["among", " then"])
4875 .map(|(run, text)| {
4876 let color = parse_run_properties(run.child("rPr")).color;
4877 let hex = resolve_color_value_to_hex_with_theme(color.as_ref(), None);
4878 let color = ColorValue {
4879 rgb: hex.map(|hex| hex[1..].to_owned()),
4880 ..ColorValue::default()
4881 };
4882 let properties = RunProperties {
4883 color: Some(color),
4884 ..RunProperties::default()
4885 };
4886 (text, properties)
4887 })
4888 .collect()
4889 }
4890
4891 #[test]
4892 fn a_text_edit_keeps_theme_colours_and_their_transforms() {
4893 let xml = rebuilt_paragraph(FILL_PARAGRAPH.as_bytes(), &hex_runs());
4894 let (_, body) = FILL_PARAGRAPH.split_once('>').unwrap();
4895 let edited = body.replace("amongst", "among").replace(" them", " then");
4896 assert!(xml.ends_with(&edited), "{xml}");
4897 }
4898
4899 #[test]
4900 fn an_explicit_colour_edit_replaces_the_theme_fill() {
4901 let mut runs = hex_runs();
4902 runs[0].1.color = Some(ColorValue {
4903 rgb: Some("FF0000".to_owned()),
4904 ..ColorValue::default()
4905 });
4906 let xml = rebuilt_paragraph(FILL_PARAGRAPH.as_bytes(), &runs);
4907 let red = r#"<a:rPr lang="en-US"><a:solidFill><a:srgbClr val="FF0000"/></a:solidFill></a:rPr><a:t>among</a:t>"#;
4908 assert!(xml.contains(red), "{xml}");
4909 assert!(xml.contains(r#"<a:schemeClr val="tx1"/>"#), "{xml}");
4910 }
4911
4912 #[test]
4913 fn an_alpha_change_on_an_unchanged_theme_colour_is_written() {
4914 let xml = rebuilt_paragraph(
4915 br#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:r><a:rPr lang="en-US"><a:solidFill><a:schemeClr val="tx1"><a:alpha val="50000"/></a:schemeClr></a:solidFill></a:rPr><a:t>amongst</a:t></a:r></a:p>"#,
4916 &[(
4917 "among",
4918 RunProperties {
4919 color: Some(ColorValue {
4920 theme_color: Some("text1".to_owned()),
4921 alpha: Some(1.0),
4922 ..ColorValue::default()
4923 }),
4924 ..RunProperties::default()
4925 },
4926 )],
4927 );
4928 assert!(
4929 xml.contains(r#"<a:schemeClr val="tx1"><a:alpha val="100000"/></a:schemeClr>"#),
4930 "{xml}"
4931 );
4932 assert!(!xml.contains(r#"val="50000""#), "{xml}");
4933 }
4934
4935 #[test]
4936 fn switching_theme_slots_with_the_same_rgb_writes_the_new_slot() {
4937 let xml = rebuilt_paragraph(
4938 br#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:r><a:rPr lang="en-US"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill></a:rPr><a:t>amongst</a:t></a:r></a:p>"#,
4939 &[(
4940 "among",
4941 RunProperties {
4942 color: Some(ColorValue {
4943 theme_color: Some("dk1".to_owned()),
4944 ..ColorValue::default()
4945 }),
4946 ..RunProperties::default()
4947 },
4948 )],
4949 );
4950 assert!(xml.contains(r#"<a:schemeClr val="dk1"/>"#), "{xml}");
4951 assert!(!xml.contains(r#"<a:schemeClr val="tx1"/>"#), "{xml}");
4952 }
4953
4954 #[test]
4955 fn an_unchanged_transformed_theme_colour_survives_byte_identical() {
4956 let xml = rebuilt_paragraph(
4957 br#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:r><a:rPr lang="en-US"><a:solidFill><a:schemeClr val="accent1"><a:lumMod val="60000"/><a:lumOff val="40000"/></a:schemeClr></a:solidFill></a:rPr><a:t>amongst</a:t></a:r></a:p>"#,
4958 &[(
4959 "among",
4960 RunProperties {
4961 color: Some(ColorValue {
4962 rgb: Some("8FAADC".to_owned()),
4963 ..ColorValue::default()
4964 }),
4965 ..RunProperties::default()
4966 },
4967 )],
4968 );
4969 assert!(
4970 xml.contains(
4971 r#"<a:solidFill><a:schemeClr val="accent1"><a:lumMod val="60000"/><a:lumOff val="40000"/></a:schemeClr></a:solidFill>"#
4972 ),
4973 "{xml}"
4974 );
4975 assert!(!xml.contains("8FAADC"), "{xml}");
4976 }
4977
4978 #[test]
4979 fn an_edit_spanning_a_styled_line_break_keeps_the_break_properties() {
4980 let xml = rebuilt_paragraph(
4981 br#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:r><a:rPr lang="en-US"/><a:t>First line</a:t></a:r><a:br><a:rPr lang="en-US"><a:solidFill><a:schemeClr val="accent1"><a:lumMod val="60000"/><a:lumOff val="40000"/></a:schemeClr></a:solidFill><a:latin charset="0" panose="020B0604020202020204" pitchFamily="34" typeface="Arial"/><a:ea typeface="MS Gothic"/><a:cs typeface="Arial"/></a:rPr></a:br><a:r><a:rPr lang="en-US"/><a:t>Second line</a:t></a:r></a:p>"#,
4982 &[(
4983 "First line changed\nSecond line changed",
4984 RunProperties {
4985 color: Some(ColorValue {
4986 rgb: Some("8FAADC".to_owned()),
4987 ..ColorValue::default()
4988 }),
4989 font_family: Some("Arial".to_owned()),
4990 ..RunProperties::default()
4991 },
4992 )],
4993 );
4994 assert!(
4995 xml.contains(
4996 r#"<a:br><a:rPr lang="en-US"><a:solidFill><a:schemeClr val="accent1"><a:lumMod val="60000"/><a:lumOff val="40000"/></a:schemeClr></a:solidFill><a:latin charset="0" panose="020B0604020202020204" pitchFamily="34" typeface="Arial"/><a:ea typeface="MS Gothic"/><a:cs typeface="Arial"/></a:rPr></a:br>"#
4997 ),
4998 "{xml}"
4999 );
5000 }
5001
5002 #[test]
5003 fn an_inserted_line_break_does_not_inherit_a_neighbouring_run_properties() {
5004 let xml = rebuilt_paragraph(
5005 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" strike="sngStrike"><a:solidFill><a:srgbClr val="FF0000"/></a:solidFill><a:hlinkClick r:id="rId2"/></a:rPr><a:t>HelloWorld</a:t></a:r></a:p>"#,
5006 &[("Hello\nWorld", RunProperties::default())],
5007 );
5008 assert!(xml.contains("<a:br/>"), "{xml}");
5009 }
5010
5011 #[test]
5012 fn a_field_survives_an_edit_around_it_and_degrades_once_its_text_changes() {
5013 let xml = rebuilt_paragraph(
5014 FIELD_PARAGRAPH,
5015 &[("1 out of 2025", RunProperties::default())],
5016 );
5017 assert!(
5018 xml.contains(
5019 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>"#
5020 ),
5021 "{xml}"
5022 );
5023 assert!(!xml.contains("datetime1"), "{xml}");
5024
5025 let xml = rebuilt_paragraph(FIELD_PARAGRAPH, &[("1X of 2024", RunProperties::default())]);
5026 assert!(
5027 xml.contains(
5028 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>"#
5029 ),
5030 "{xml}"
5031 );
5032 }
5033
5034 #[test]
5035 fn text_typed_at_the_end_of_a_link_stays_inside_it() {
5036 let xml = rebuilt_paragraph(
5037 LINKED_PARAGRAPH,
5038 &[("See the docsX today", RunProperties::default())],
5039 );
5040 assert!(
5041 xml.contains(&format!("<a:r>{LINK_PROPERTIES}<a:t>the docsX</a:t></a:r>")),
5042 "{xml}"
5043 );
5044
5045 let bold = RunProperties {
5046 bold: Some(true),
5047 ..RunProperties::default()
5048 };
5049 let xml = rebuilt_paragraph(
5050 LINKED_PARAGRAPH,
5051 &[
5052 ("See ", RunProperties::default()),
5053 ("the docs", RunProperties::default()),
5054 ("X", bold),
5055 (" today", RunProperties::default()),
5056 ],
5057 );
5058 assert!(
5059 xml.contains(&format!(
5060 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>"#
5061 )),
5062 "{xml}"
5063 );
5064 }
5065
5066 #[test]
5067 fn deleting_a_linked_run_drops_its_link() {
5068 let xml = rebuilt_paragraph(
5069 LINKED_PARAGRAPH,
5070 &[("See today", RunProperties::default())],
5071 );
5072 assert!(!xml.contains("hlinkClick"), "{xml}");
5073 assert!(
5074 xml.contains(
5075 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>"#
5076 ),
5077 "{xml}"
5078 );
5079 }
5080
5081 #[test]
5082 fn a_plain_duplicate_at_the_front_does_not_steal_the_field() {
5083 let xml = rebuilt_paragraph(
5084 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>"#,
5085 &[("1", RunProperties::default())],
5086 );
5087 assert!(
5088 xml.contains(
5089 r#"<a:fld id="{A}" type="slidenum"><a:rPr lang="en-US"/><a:t>1</a:t></a:fld>"#
5090 ),
5091 "{xml}"
5092 );
5093 assert_eq!(xml.matches("<a:r>").count(), 0, "{xml}");
5094 }
5095
5096 #[test]
5097 fn a_plain_duplicate_at_the_back_does_not_steal_the_field() {
5098 let xml = rebuilt_paragraph(
5099 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>"#,
5100 &[
5101 ("X", RunProperties::default()),
5102 ("1", RunProperties::default()),
5103 ],
5104 );
5105 assert!(
5106 xml.contains(
5107 r#"<a:fld id="{A}" type="slidenum"><a:rPr lang="en-US"/><a:t>1</a:t></a:fld>"#
5108 ),
5109 "{xml}"
5110 );
5111 assert_eq!(xml.matches("<a:r>").count(), 1, "{xml}");
5112 assert!(
5113 !xml.contains(r#"<a:r><a:rPr lang="en-US"/><a:t>1</a:t></a:r>"#),
5114 "{xml}"
5115 );
5116 }
5117
5118 #[test]
5119 fn adjustments_without_preset_geometry_error_instead_of_no_opting() {
5120 let limits = ParseLimits::default();
5121 let mut budget = ParseBudget::new(&limits);
5122 let part = "ppt/slides/slide1.xml";
5123 let mut root = parse_xml(
5124 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>"#,
5125 part,
5126 &mut budget,
5127 )
5128 .unwrap();
5129
5130 let package = PptxPackage::default();
5131 let mut replacements = HashMap::new();
5132 let mut new_parts = Vec::new();
5133 let mut sink = PartSink {
5134 package: &package,
5135 replacements: &mut replacements,
5136 new_parts: &mut new_parts,
5137 };
5138 let error = patch_slide(
5139 &mut root,
5140 &[ShapeWrite::Patch {
5141 source_index: 0,
5142 patch: Box::new(ShapePatch {
5143 adjust_values: Some(BTreeMap::from([("adj".to_owned(), 0.25)])),
5144 ..ShapePatch::default()
5145 }),
5146 }],
5147 None,
5148 None,
5149 part,
5150 ShapeElements::WithConnectors,
5151 &mut sink,
5152 &mut budget,
5153 )
5154 .unwrap_err();
5155 assert!(
5156 matches!(error, crate::PptxError::Write { ref message, .. } if message.contains("preset geometry")),
5157 "{error:?}"
5158 );
5159 }
5160}