1use std::collections::{BTreeMap, HashMap, HashSet};
6
7use ooxml_drawingml::{
8 ColorValue, ShapeFill, ShapeOutline, Theme, resolve_color_value_to_hex_with_theme,
9};
10
11use crate::PptxError;
12use crate::drawing::parse_run_properties;
13use crate::model::{Bullet, PptxPackage, RunProperties, SlideReference};
14use crate::xml::{ParseBudget, ParseLimits, XmlElement, XmlNode, parse_xml, serialize_xml};
15
16const DRAWINGML_NS: &str = "http://schemas.openxmlformats.org/drawingml/2006/main";
17const PRESENTATIONML_NS: &str = "http://schemas.openxmlformats.org/presentationml/2006/main";
18const OFFICE_RELATIONSHIPS_NS: &str =
19 "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
20const SLIDE_RELATIONSHIP_TYPE: &str =
21 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide";
22const SLIDE_LAYOUT_RELATIONSHIP_TYPE: &str =
23 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout";
24const SLIDE_CONTENT_TYPE: &str =
25 "application/vnd.openxmlformats-officedocument.presentationml.slide+xml";
26const MIN_SLIDE_ID: u32 = 256;
27const MAX_SLIDE_ID: u32 = 2_147_483_647;
28
29pub struct DeckWrite {
31 pub slides: Vec<SlideWrite>,
32}
33
34pub enum SlideWrite {
35 Keep { part_path: String },
37 Patch {
39 part_path: String,
40 shapes: Vec<ShapeWrite>,
41 },
42 Add {
44 name: Option<String>,
45 layout_part_path: Option<String>,
46 shapes: Vec<ShapeAdd>,
47 },
48}
49
50pub enum ShapeWrite {
51 Keep {
54 source_index: usize,
55 },
56 Patch {
57 source_index: usize,
58 patch: Box<ShapePatch>,
59 },
60 Add(Box<ShapeAdd>),
61}
62
63#[derive(Default)]
64pub struct ShapePatch {
65 pub offset: Option<(i64, i64)>,
68 pub extent: Option<(i64, i64)>,
69 pub inherited: Option<InheritedTransform>,
71 pub fill: Option<ShapeFill>,
72 pub outline: Option<ShapeOutline>,
74 pub adjust_values: Option<BTreeMap<String, f64>>,
75 pub texts: Vec<TextWrite>,
76 pub children: Vec<ShapeWrite>,
78}
79
80#[derive(Clone, Copy)]
82pub struct InheritedTransform {
83 pub x: i64,
84 pub y: i64,
85 pub width: i64,
86 pub height: i64,
87 pub rotation_deg: f64,
88 pub flip_horizontal: bool,
89 pub flip_vertical: bool,
90}
91
92pub struct TextWrite {
93 pub target: TextTarget,
94 pub paragraphs: Vec<ParagraphWrite>,
95}
96
97pub enum TextTarget {
98 Body,
99 TableCell { row: usize, cell: usize },
100}
101
102pub struct ParagraphWrite {
103 pub source_index: Option<usize>,
105 pub rebuild: bool,
107 pub properties_changed: bool,
108 pub alignment: Option<String>,
109 pub level: u32,
110 pub bullet: Option<Bullet>,
111 pub runs: Vec<RunWrite>,
112}
113
114pub struct RunWrite {
115 pub text: String,
116 pub properties: RunProperties,
117}
118
119pub struct ShapeAdd {
120 pub name: String,
121 pub geometry: String,
122 pub x: i64,
123 pub y: i64,
124 pub width: i64,
125 pub height: i64,
126 pub adjust_values: BTreeMap<String, f64>,
127 pub fill: Option<ShapeFill>,
128 pub outline: Option<ShapeOutline>,
129 pub paragraphs: Option<Vec<ParagraphWrite>>,
130}
131
132pub fn write_pptx_with_edits(
135 package: &PptxPackage,
136 deck: &DeckWrite,
137) -> Result<Vec<u8>, PptxError> {
138 let limits = ParseLimits::default();
139 let mut budget = ParseBudget::new(&limits);
140
141 let final_paths: HashSet<&str> = deck
142 .slides
143 .iter()
144 .filter_map(|slide| match slide {
145 SlideWrite::Keep { part_path } | SlideWrite::Patch { part_path, .. } => {
146 Some(part_path.as_str())
147 }
148 SlideWrite::Add { .. } => None,
149 })
150 .collect();
151 let removed: Vec<&SlideReference> = package
152 .presentation
153 .slides
154 .iter()
155 .filter(|reference| !final_paths.contains(reference.part_path.as_str()))
156 .collect();
157
158 let mut replacements: HashMap<String, Vec<u8>> = HashMap::new();
159 let mut new_parts: Vec<(String, Vec<u8>)> = Vec::new();
160
161 for slide in &deck.slides {
162 if let SlideWrite::Patch { part_path, shapes } = slide {
163 let bytes = package
164 .part_bytes(part_path)
165 .ok_or_else(|| PptxError::MissingPart(part_path.clone()))?;
166 let mut root = parse_xml(bytes, part_path, &mut budget)?;
167 let theme = slide_theme(package, part_path);
168 patch_slide(&mut root, shapes, theme, part_path)?;
169 replacements.insert(part_path.clone(), serialize_xml(&root));
170 }
171 }
172
173 let mut minted = Vec::new();
174 let mut next_slide_number = next_slide_number(package);
175 let mut minted_slide_id: Option<u32> = None;
176 let mut next_relationship = next_relationship_number(package);
177 let mut structural = false;
178 let mut final_order: Vec<FinalSlide<'_>> = Vec::new();
179 for slide in &deck.slides {
180 match slide {
181 SlideWrite::Keep { part_path } | SlideWrite::Patch { part_path, .. } => {
182 let reference = package
183 .presentation
184 .slides
185 .iter()
186 .find(|reference| &reference.part_path == part_path)
187 .ok_or_else(|| write_error(part_path, "not a slide of this deck"))?;
188 final_order.push(FinalSlide::Existing(reference));
189 }
190 SlideWrite::Add {
191 name,
192 layout_part_path,
193 shapes,
194 } => {
195 structural = true;
196 let part_path = format!("ppt/slides/slide{next_slide_number}.xml");
197 next_slide_number += 1;
198 let relationship_id = format!("rId{next_relationship}");
199 next_relationship += 1;
200 let slide_id = match minted_slide_id {
201 None => next_slide_id(package)?,
202 Some(previous) => previous
203 .checked_add(1)
204 .filter(|id| *id <= MAX_SLIDE_ID)
205 .ok_or_else(|| {
206 write_error(
207 &package.presentation.part_path,
208 "the slide id space is exhausted",
209 )
210 })?,
211 };
212 minted_slide_id = Some(slide_id);
213 let layout = layout_part_path
214 .clone()
215 .filter(|path| {
216 package
217 .layouts
218 .iter()
219 .any(|layout| &layout.part_path == path)
220 })
221 .or_else(|| {
222 package
223 .layouts
224 .first()
225 .map(|layout| layout.part_path.clone())
226 });
227 new_parts.push((
228 part_path.clone(),
229 slide_xml(name.as_deref(), shapes, &part_path)?,
230 ));
231 if let Some(layout) = &layout {
232 new_parts.push((
233 slide_relationships_path(&part_path),
234 slide_relationships_xml(&part_path, layout),
235 ));
236 }
237 minted.push(MintedSlide {
238 part_path,
239 relationship_id,
240 slide_id,
241 });
242 final_order.push(FinalSlide::Added(minted.len() - 1));
243 }
244 }
245 }
246 let original_paths: Vec<&str> = package
247 .presentation
248 .slides
249 .iter()
250 .map(|reference| reference.part_path.as_str())
251 .collect();
252 let kept_paths: Vec<&str> = final_order
253 .iter()
254 .filter_map(|slide| match slide {
255 FinalSlide::Existing(reference) => Some(reference.part_path.as_str()),
256 FinalSlide::Added(_) => None,
257 })
258 .collect();
259 structural = structural || kept_paths != original_paths;
260
261 let mut removed_paths = HashSet::new();
262 for reference in &removed {
263 removed_paths.insert(reference.part_path.clone());
264 removed_paths.insert(slide_relationships_path(&reference.part_path));
265 }
266 let orphans = orphaned_parts(package, &removed);
267 for orphan in &orphans {
268 removed_paths.insert(orphan.clone());
269 removed_paths.insert(slide_relationships_path(orphan));
270 }
271
272 if structural {
273 patch_structure(
274 package,
275 &final_order,
276 &minted,
277 &removed,
278 &orphans,
279 &mut replacements,
280 &mut budget,
281 )?;
282 }
283 let mut parts = Vec::with_capacity(package.parts.len() + new_parts.len());
284 for part in &package.parts {
285 if removed_paths.contains(&part.path) {
286 continue;
287 }
288 match replacements.remove(&part.path) {
289 Some(bytes) => parts.push((part.path.clone(), bytes)),
290 None => parts.push((part.path.clone(), part.bytes.clone())),
291 }
292 }
293 parts.extend(new_parts);
294 ooxml_opc::rezip_parts(&parts).map_err(PptxError::Container)
295}
296
297struct MintedSlide {
298 part_path: String,
299 relationship_id: String,
300 slide_id: u32,
301}
302
303enum FinalSlide<'a> {
304 Existing(&'a SlideReference),
305 Added(usize),
306}
307
308fn write_error(part: &str, message: impl Into<String>) -> PptxError {
309 PptxError::Write {
310 part: part.to_owned(),
311 message: message.into(),
312 }
313}
314
315fn next_slide_number(package: &PptxPackage) -> usize {
316 let paths: HashSet<&str> = package
317 .parts
318 .iter()
319 .map(|part| part.path.as_str())
320 .collect();
321 let mut number = package
322 .parts
323 .iter()
324 .filter_map(|part| {
325 part.path
326 .strip_prefix("ppt/slides/slide")?
327 .strip_suffix(".xml")?
328 .parse::<usize>()
329 .ok()
330 })
331 .max()
332 .unwrap_or(0)
333 + 1;
334 while paths.contains(format!("ppt/slides/slide{number}.xml").as_str())
335 || paths.contains(format!("ppt/slides/_rels/slide{number}.xml.rels").as_str())
336 {
337 number += 1;
338 }
339 number
340}
341
342fn next_slide_id(package: &PptxPackage) -> Result<u32, PptxError> {
343 let next = package
344 .presentation
345 .slides
346 .iter()
347 .map(|reference| reference.id)
348 .max()
349 .unwrap_or(0)
350 .max(MIN_SLIDE_ID - 1)
351 .checked_add(1)
352 .filter(|id| *id <= MAX_SLIDE_ID);
353 next.ok_or_else(|| {
354 write_error(
355 &package.presentation.part_path,
356 "the slide id space is exhausted",
357 )
358 })
359}
360
361fn next_relationship_number(package: &PptxPackage) -> usize {
362 package
363 .relationships
364 .get(&package.presentation.part_path)
365 .into_iter()
366 .flatten()
367 .filter_map(|relationship| relationship.id.strip_prefix("rId")?.parse::<usize>().ok())
368 .max()
369 .unwrap_or(0)
370 + 1
371}
372
373fn slide_relationships_path(part_path: &str) -> String {
374 match part_path.rsplit_once('/') {
375 Some((directory, name)) => format!("{directory}/_rels/{name}.rels"),
376 None => format!("_rels/{part_path}.rels"),
377 }
378}
379
380fn orphaned_parts(package: &PptxPackage, removed: &[&SlideReference]) -> Vec<String> {
383 if removed.is_empty() {
384 return Vec::new();
385 }
386 let excluded: HashSet<&str> = removed
387 .iter()
388 .map(|reference| reference.part_path.as_str())
389 .collect();
390 let before = reachable_parts(package, &HashSet::new());
391 let after = reachable_parts(package, &excluded);
392 before
393 .into_iter()
394 .filter(|part| !after.contains(part) && !excluded.contains(part.as_str()))
395 .collect()
396}
397
398fn reachable_parts(package: &PptxPackage, excluded: &HashSet<&str>) -> HashSet<String> {
399 let mut reachable = HashSet::new();
400 let mut queue = vec![String::new()];
401 while let Some(source) = queue.pop() {
402 for relationship in package.relationships.get(&source).into_iter().flatten() {
403 let Some(target) = &relationship.resolved_target else {
404 continue;
405 };
406 if excluded.contains(target.as_str()) {
407 continue;
408 }
409 if reachable.insert(target.clone()) {
410 queue.push(target.clone());
411 }
412 }
413 }
414 reachable
415}
416
417fn relative_target(from_part: &str, to_part: &str) -> String {
418 let from: Vec<&str> = from_part.split('/').collect();
419 let to: Vec<&str> = to_part.split('/').collect();
420 let from_directory = &from[..from.len().saturating_sub(1)];
421 let to_directory = &to[..to.len().saturating_sub(1)];
422 let shared = from_directory
423 .iter()
424 .zip(to_directory)
425 .take_while(|(a, b)| a == b)
426 .count();
427 let mut segments: Vec<String> = vec!["..".to_owned(); from_directory.len() - shared];
428 segments.extend(to[shared..].iter().map(|segment| (*segment).to_owned()));
429 segments.join("/")
430}
431
432fn patch_structure(
435 package: &PptxPackage,
436 final_order: &[FinalSlide<'_>],
437 minted: &[MintedSlide],
438 removed: &[&SlideReference],
439 orphans: &[String],
440 replacements: &mut HashMap<String, Vec<u8>>,
441 budget: &mut ParseBudget<'_>,
442) -> Result<(), PptxError> {
443 let presentation_path = package.presentation.part_path.as_str();
444 let bytes = package
445 .part_bytes(presentation_path)
446 .ok_or_else(|| PptxError::MissingPart(presentation_path.to_owned()))?;
447 let mut root = parse_xml(bytes, presentation_path, budget)?;
448 patch_slide_id_list(&mut root, final_order, minted, presentation_path)?;
449 prune_slide_references(&mut root, removed);
450 replacements.insert(presentation_path.to_owned(), serialize_xml(&root));
451
452 let relationships_path = slide_relationships_path(presentation_path);
453 let bytes = package
454 .part_bytes(&relationships_path)
455 .ok_or_else(|| PptxError::MissingPart(relationships_path.clone()))?;
456 let mut root = parse_xml(bytes, &relationships_path, budget)?;
457 let removed_ids: HashSet<&str> = removed
458 .iter()
459 .map(|reference| reference.relationship_id.as_str())
460 .collect();
461 root.children.retain(|child| match child {
462 XmlNode::Element(element) if element.local_name() == "Relationship" => element
463 .attribute("Id")
464 .is_none_or(|id| !removed_ids.contains(id)),
465 _ => true,
466 });
467 for slide in minted {
468 root.children.push(XmlNode::Element(
469 XmlElement::new("Relationship")
470 .with_attribute("Id", slide.relationship_id.clone())
471 .with_attribute("Type", SLIDE_RELATIONSHIP_TYPE)
472 .with_attribute(
473 "Target",
474 relative_target(presentation_path, &slide.part_path),
475 ),
476 ));
477 }
478 replacements.insert(relationships_path, serialize_xml(&root));
479
480 let content_types_path = "[Content_Types].xml";
481 let bytes = package
482 .part_bytes(content_types_path)
483 .ok_or_else(|| PptxError::MissingPart(content_types_path.to_owned()))?;
484 let mut root = parse_xml(bytes, content_types_path, budget)?;
485 let removed_names: HashSet<String> = removed
486 .iter()
487 .map(|reference| format!("/{}", reference.part_path))
488 .chain(orphans.iter().map(|orphan| format!("/{orphan}")))
489 .collect();
490 root.children.retain(|child| match child {
491 XmlNode::Element(element) if element.local_name() == "Override" => element
492 .attribute("PartName")
493 .is_none_or(|name| !removed_names.contains(name)),
494 _ => true,
495 });
496 for slide in minted {
497 root.children.push(XmlNode::Element(
498 XmlElement::new("Override")
499 .with_attribute("PartName", format!("/{}", slide.part_path))
500 .with_attribute("ContentType", SLIDE_CONTENT_TYPE),
501 ));
502 }
503 replacements.insert(content_types_path.to_owned(), serialize_xml(&root));
504 Ok(())
505}
506
507fn prune_slide_references(root: &mut XmlElement, removed: &[&SlideReference]) {
510 if removed.is_empty() {
511 return;
512 }
513 let removed_relationship_ids: HashSet<&str> = removed
514 .iter()
515 .map(|reference| reference.relationship_id.as_str())
516 .collect();
517 let removed_slide_ids: HashSet<String> = removed
518 .iter()
519 .map(|reference| reference.id.to_string())
520 .collect();
521 for child in root.children.iter_mut() {
522 let XmlNode::Element(element) = child else {
523 continue;
524 };
525 match element.local_name() {
526 "custShowLst" => remove_matching_descendants(element, &|candidate| {
527 candidate.local_name() == "sld"
528 && candidate.attributes.iter().any(|(key, value)| {
529 key.ends_with(":id") && removed_relationship_ids.contains(value.as_str())
530 })
531 }),
532 "extLst" => remove_matching_descendants(element, &|candidate| {
533 candidate.local_name() == "sldId"
534 && candidate
535 .attribute("id")
536 .is_some_and(|id| removed_slide_ids.contains(id))
537 }),
538 _ => {}
539 }
540 }
541}
542
543fn remove_matching_descendants(element: &mut XmlElement, matches: &dyn Fn(&XmlElement) -> bool) {
544 element.children.retain(|child| match child {
545 XmlNode::Element(candidate) => !matches(candidate),
546 _ => true,
547 });
548 for child in element.children.iter_mut() {
549 if let XmlNode::Element(child) = child {
550 remove_matching_descendants(child, matches);
551 }
552 }
553}
554
555fn patch_slide_id_list(
556 root: &mut XmlElement,
557 final_order: &[FinalSlide<'_>],
558 minted: &[MintedSlide],
559 part: &str,
560) -> Result<(), PptxError> {
561 let presentation_prefix = resolve_prefix(root, PRESENTATIONML_NS, "p");
562 let relationship_prefix = resolve_prefix(root, OFFICE_RELATIONSHIPS_NS, "r");
563 let list_name = qualified(&presentation_prefix, "sldIdLst");
564 if root.child_mut("sldIdLst").is_none() {
565 let position = root
566 .children
567 .iter()
568 .position(|child| {
569 matches!(child, XmlNode::Element(element) if element.local_name() == "sldMasterIdLst")
570 })
571 .map(|index| index + 1)
572 .unwrap_or(0);
573 root.children.insert(
574 position,
575 XmlNode::Element(XmlElement::new(list_name.clone())),
576 );
577 }
578 let relationship_attribute = qualified(&relationship_prefix, "id");
579 let list = root
580 .child_mut("sldIdLst")
581 .ok_or_else(|| write_error(part, "missing slide id list"))?;
582 let mut existing: HashMap<String, XmlElement> = HashMap::new();
583 for child in std::mem::take(&mut list.children) {
584 if let XmlNode::Element(element) = child
585 && element.local_name() == "sldId"
586 {
587 let relationship_id = element.attribute(&relationship_attribute).or_else(|| {
588 element
589 .attributes
590 .iter()
591 .find(|(key, _)| key.ends_with(":id"))
592 .map(|(_, value)| value.as_str())
593 });
594 if let Some(id) = relationship_id {
595 existing.insert(id.to_owned(), element);
596 }
597 }
598 }
599 let entry_name = qualified(&presentation_prefix, "sldId");
600 for slide in final_order {
601 let element = match slide {
602 FinalSlide::Existing(reference) => existing
603 .remove(&reference.relationship_id)
604 .unwrap_or_else(|| {
605 XmlElement::new(entry_name.clone())
606 .with_attribute("id", reference.id.to_string())
607 .with_attribute(
608 relationship_attribute.clone(),
609 reference.relationship_id.clone(),
610 )
611 }),
612 FinalSlide::Added(index) => {
613 let slide = &minted[*index];
614 XmlElement::new(entry_name.clone())
615 .with_attribute("id", slide.slide_id.to_string())
616 .with_attribute(
617 relationship_attribute.clone(),
618 slide.relationship_id.clone(),
619 )
620 }
621 };
622 list.children.push(XmlNode::Element(element));
623 }
624 Ok(())
625}
626
627fn find_prefix(root: &XmlElement, namespace: &str) -> Option<String> {
630 root.attributes.iter().find_map(|(key, value)| {
631 if value != namespace {
632 return None;
633 }
634 if key == "xmlns" {
635 Some(String::new())
636 } else {
637 key.strip_prefix("xmlns:").map(str::to_owned)
638 }
639 })
640}
641
642fn resolve_prefix(root: &mut XmlElement, namespace: &str, fallback: &str) -> String {
643 if let Some(prefix) = find_prefix(root, namespace) {
644 return prefix;
645 }
646 root.set_attribute(format!("xmlns:{fallback}"), namespace);
647 fallback.to_owned()
648}
649
650fn qualified(prefix: &str, local: &str) -> String {
651 if prefix.is_empty() {
652 local.to_owned()
653 } else {
654 format!("{prefix}:{local}")
655 }
656}
657
658struct Prefixes {
659 drawing: String,
660 presentation: String,
661}
662
663impl Prefixes {
664 fn from_root(root: &mut XmlElement) -> Self {
665 Self {
666 drawing: resolve_prefix(root, DRAWINGML_NS, "a"),
667 presentation: resolve_prefix(root, PRESENTATIONML_NS, "p"),
668 }
669 }
670
671 fn drawing(&self, local: &str) -> String {
672 qualified(&self.drawing, local)
673 }
674
675 fn presentation(&self, local: &str) -> String {
676 qualified(&self.presentation, local)
677 }
678}
679
680fn is_shape_element(local: &str) -> bool {
683 matches!(local, "sp" | "pic" | "graphicFrame" | "grpSp")
684}
685
686fn patch_slide(
687 root: &mut XmlElement,
688 shapes: &[ShapeWrite],
689 theme: Option<&Theme>,
690 part: &str,
691) -> Result<(), PptxError> {
692 let prefixes = Prefixes::from_root(root);
693 let mut next_shape_id = max_shape_id(root) + 1;
694 let tree = root
695 .child_mut("cSld")
696 .and_then(|common| common.child_mut("spTree"))
697 .ok_or_else(|| write_error(part, "slide has no shape tree"))?;
698 patch_shape_children(tree, shapes, &mut next_shape_id, theme, &prefixes, part)
699}
700
701fn slide_theme<'a>(package: &'a PptxPackage, part_path: &str) -> Option<&'a Theme> {
703 let slide = package
704 .slides
705 .iter()
706 .find(|slide| slide.part_path == part_path);
707 let layout = slide
708 .and_then(|slide| slide.layout_part_path.as_deref())
709 .and_then(|path| {
710 package
711 .layouts
712 .iter()
713 .find(|layout| layout.part_path == path)
714 })
715 .or_else(|| package.layouts.first());
716 let master = layout
717 .and_then(|layout| layout.master_part_path.as_deref())
718 .and_then(|path| {
719 package
720 .masters
721 .iter()
722 .find(|master| master.part_path == path)
723 })
724 .or_else(|| {
725 layout.and_then(|layout| {
726 package.masters.iter().find(|master| {
727 master
728 .layout_part_paths
729 .iter()
730 .any(|path| path == &layout.part_path)
731 })
732 })
733 })
734 .or_else(|| package.masters.first());
735 master
736 .and_then(|master| master.theme_part_path.as_deref())
737 .and_then(|path| package.themes.iter().find(|theme| theme.part_path == path))
738 .map(|part| &part.theme)
739 .or_else(|| package.themes.first().map(|part| &part.theme))
740}
741
742fn max_shape_id(root: &XmlElement) -> u32 {
743 root.descendants_named("cNvPr")
744 .iter()
745 .filter_map(|element| element.attribute("id")?.parse::<u32>().ok())
746 .max()
747 .unwrap_or(1)
748}
749
750fn patch_shape_children(
754 parent: &mut XmlElement,
755 writes: &[ShapeWrite],
756 next_shape_id: &mut u32,
757 theme: Option<&Theme>,
758 prefixes: &Prefixes,
759 part: &str,
760) -> Result<(), PptxError> {
761 let mut slots: Vec<Option<XmlNode>> = std::mem::take(&mut parent.children)
762 .into_iter()
763 .map(Some)
764 .collect();
765 let shape_slots: Vec<usize> = slots
766 .iter()
767 .enumerate()
768 .filter_map(|(position, slot)| match slot {
769 Some(XmlNode::Element(element)) if is_shape_element(element.local_name()) => {
770 Some(position)
771 }
772 _ => None,
773 })
774 .collect();
775 let mut children = Vec::with_capacity(slots.len());
776 let first_ext_list = |slots: &[Option<XmlNode>]| {
777 slots
778 .iter()
779 .position(|slot| {
780 matches!(slot, Some(XmlNode::Element(element)) if element.local_name() == "extLst")
781 })
782 .unwrap_or(slots.len())
783 };
784 let prologue_end = shape_slots
785 .first()
786 .copied()
787 .unwrap_or_else(|| first_ext_list(&slots));
788 emit_sibling_slots(&mut slots, &mut children, prologue_end);
789 let last_kept = writes
790 .iter()
791 .rposition(|write| !matches!(write, ShapeWrite::Add(_)));
792 for (index, write) in writes.iter().enumerate() {
793 match write {
794 ShapeWrite::Keep { source_index } | ShapeWrite::Patch { source_index, .. } => {
795 let position = *shape_slots.get(*source_index).ok_or_else(|| {
796 write_error(part, format!("shape index {source_index} is not available"))
797 })?;
798 emit_sibling_slots(&mut slots, &mut children, position);
799 let mut element = match slots[position].take() {
800 Some(XmlNode::Element(element)) => element,
801 _ => {
802 return Err(write_error(
803 part,
804 format!("shape index {source_index} was already written"),
805 ));
806 }
807 };
808 if let ShapeWrite::Patch { patch, .. } = write {
809 patch_shape(&mut element, patch, next_shape_id, theme, prefixes, part)?;
810 }
811 children.push(XmlNode::Element(element));
812 }
813 ShapeWrite::Add(add) => {
814 if last_kept.is_none_or(|kept| index > kept) {
817 let flush_end = first_ext_list(&slots);
818 emit_sibling_slots(&mut slots, &mut children, flush_end);
819 }
820 children.push(XmlNode::Element(shape_element(
821 add,
822 next_shape_id,
823 prefixes,
824 part,
825 )?));
826 }
827 }
828 }
829 for slot in slots.into_iter().flatten() {
830 if !matches!(&slot, XmlNode::Element(element) if is_shape_element(element.local_name())) {
831 children.push(slot);
832 }
833 }
834 parent.children = children;
835 Ok(())
836}
837
838fn emit_sibling_slots(slots: &mut [Option<XmlNode>], children: &mut Vec<XmlNode>, position: usize) {
840 for slot in slots.iter_mut().take(position) {
841 let is_other = !matches!(
842 slot,
843 Some(XmlNode::Element(element)) if is_shape_element(element.local_name())
844 );
845 if is_other && let Some(node) = slot.take() {
846 children.push(node);
847 }
848 }
849}
850
851fn patch_shape(
852 element: &mut XmlElement,
853 patch: &ShapePatch,
854 next_shape_id: &mut u32,
855 theme: Option<&Theme>,
856 prefixes: &Prefixes,
857 part: &str,
858) -> Result<(), PptxError> {
859 if patch.offset.is_some() || patch.extent.is_some() {
860 patch_transform(element, patch, prefixes, part)?;
861 }
862 if let Some(fill) = &patch.fill {
863 let properties = shape_properties_mut(element, part)?;
864 set_fill(properties, fill, prefixes, part)?;
865 }
866 if let Some(outline) = &patch.outline {
867 let properties = shape_properties_mut(element, part)?;
868 set_outline(properties, outline, prefixes);
869 }
870 if let Some(adjust_values) = &patch.adjust_values {
871 let properties = shape_properties_mut(element, part)?;
872 set_adjust_values(properties, adjust_values, prefixes);
873 }
874 for text in &patch.texts {
875 patch_text(element, text, theme, prefixes, part)?;
876 }
877 if !patch.children.is_empty() {
878 patch_shape_children(
879 element,
880 &patch.children,
881 next_shape_id,
882 theme,
883 prefixes,
884 part,
885 )?;
886 }
887 Ok(())
888}
889
890fn shape_properties_mut<'a>(
891 element: &'a mut XmlElement,
892 part: &str,
893) -> Result<&'a mut XmlElement, PptxError> {
894 let container = match element.local_name() {
895 "grpSp" => "grpSpPr",
896 _ => "spPr",
897 };
898 element
899 .child_mut(container)
900 .ok_or_else(|| write_error(part, "shape has no properties element"))
901}
902
903fn patch_transform(
907 element: &mut XmlElement,
908 patch: &ShapePatch,
909 prefixes: &Prefixes,
910 part: &str,
911) -> Result<(), PptxError> {
912 let offset = patch
913 .offset
914 .or(patch.inherited.map(|inherited| (inherited.x, inherited.y)));
915 let extent = patch.extent.or(patch
916 .inherited
917 .map(|inherited| (inherited.width, inherited.height)));
918 let (container, transform_name) = match element.local_name() {
919 "graphicFrame" => (None, prefixes.presentation("xfrm")),
920 "grpSp" => (Some("grpSpPr"), prefixes.drawing("xfrm")),
921 _ => (Some("spPr"), prefixes.drawing("xfrm")),
922 };
923 let parent = match container {
924 Some(name) => element
925 .child_mut(name)
926 .ok_or_else(|| write_error(part, "shape has no properties element"))?,
927 None => element,
928 };
929 if parent.child_mut("xfrm").is_none() {
930 if offset.is_none() || extent.is_none() {
931 return Err(write_error(
932 part,
933 "cannot write half a transform onto a shape without one",
934 ));
935 }
936 let position = parent
937 .children
938 .iter()
939 .position(|child| {
940 matches!(child, XmlNode::Element(element) if element.local_name() != "nvGraphicFramePr")
941 })
942 .unwrap_or(parent.children.len());
943 let mut created = XmlElement::new(transform_name);
944 if let Some(inherited) = patch.inherited {
945 if inherited.rotation_deg != 0.0 {
946 created.set_attribute("rot", format_fixed(inherited.rotation_deg * 60_000.0));
947 }
948 if inherited.flip_horizontal {
949 created.set_attribute("flipH", "1");
950 }
951 if inherited.flip_vertical {
952 created.set_attribute("flipV", "1");
953 }
954 }
955 parent.children.insert(position, XmlNode::Element(created));
956 }
957 let transform = parent.child_mut("xfrm").expect("transform ensured above");
958 if let Some((x, y)) = offset
959 && (patch.offset.is_some() || transform.child_mut("off").is_none())
960 {
961 if transform.child_mut("off").is_none() {
962 transform.children.insert(
963 0,
964 XmlNode::Element(XmlElement::new(prefixes.drawing("off"))),
965 );
966 }
967 let element = transform.child_mut("off").expect("offset ensured above");
968 element.set_attribute("x", x.to_string());
969 element.set_attribute("y", y.to_string());
970 }
971 if let Some((width, height)) = extent
972 && (patch.extent.is_some() || transform.child_mut("ext").is_none())
973 {
974 if transform.child_mut("ext").is_none() {
975 let position = transform
976 .children
977 .iter()
978 .position(
979 |child| matches!(child, XmlNode::Element(element) if element.local_name() == "off"),
980 )
981 .map(|index| index + 1)
982 .unwrap_or(transform.children.len());
983 transform.children.insert(
984 position,
985 XmlNode::Element(XmlElement::new(prefixes.drawing("ext"))),
986 );
987 }
988 let element = transform.child_mut("ext").expect("extent ensured above");
989 element.set_attribute("cx", width.to_string());
990 element.set_attribute("cy", height.to_string());
991 }
992 Ok(())
993}
994
995const FILL_ELEMENTS: [&str; 6] = [
996 "noFill",
997 "solidFill",
998 "gradFill",
999 "blipFill",
1000 "pattFill",
1001 "grpFill",
1002];
1003const POST_FILL_ELEMENTS: [&str; 6] = ["ln", "effectLst", "effectDag", "scene3d", "sp3d", "extLst"];
1004
1005fn set_fill(
1006 properties: &mut XmlElement,
1007 fill: &ShapeFill,
1008 prefixes: &Prefixes,
1009 part: &str,
1010) -> Result<(), PptxError> {
1011 let element = fill_element(fill, prefixes, part)?;
1012 let existing = properties.children.iter().position(|child| {
1013 matches!(child, XmlNode::Element(element) if FILL_ELEMENTS.contains(&element.local_name()))
1014 });
1015 match existing {
1016 Some(index) => properties.children[index] = XmlNode::Element(element),
1017 None => {
1018 let position = properties
1019 .children
1020 .iter()
1021 .position(|child| {
1022 matches!(
1023 child,
1024 XmlNode::Element(element)
1025 if POST_FILL_ELEMENTS.contains(&element.local_name())
1026 )
1027 })
1028 .unwrap_or(properties.children.len());
1029 properties
1030 .children
1031 .insert(position, XmlNode::Element(element));
1032 }
1033 }
1034 Ok(())
1035}
1036
1037fn fill_element(
1038 fill: &ShapeFill,
1039 prefixes: &Prefixes,
1040 part: &str,
1041) -> Result<XmlElement, PptxError> {
1042 match fill.fill_type.as_str() {
1043 "none" => Ok(XmlElement::new(prefixes.drawing("noFill"))),
1044 "solid" => {
1045 let color = fill
1046 .color
1047 .as_ref()
1048 .and_then(|color| color_element(color, prefixes))
1049 .ok_or_else(|| write_error(part, "solid fill without a serializable color"))?;
1050 Ok(XmlElement::new(prefixes.drawing("solidFill")).with_child(color))
1051 }
1052 "gradient" => {
1053 let gradient = fill
1054 .gradient
1055 .as_ref()
1056 .ok_or_else(|| write_error(part, "gradient fill without stops"))?;
1057 let mut stops = XmlElement::new(prefixes.drawing("gsLst"));
1058 for stop in &gradient.stops {
1059 let color = color_element(&stop.color, prefixes)
1060 .ok_or_else(|| write_error(part, "gradient stop without a color"))?;
1061 stops = stops.with_child(
1062 XmlElement::new(prefixes.drawing("gs"))
1063 .with_attribute("pos", format_fixed(stop.position))
1064 .with_child(color),
1065 );
1066 }
1067 let mut element = XmlElement::new(prefixes.drawing("gradFill")).with_child(stops);
1068 match gradient.gradient_type.as_str() {
1069 "linear" => {
1070 element = element.with_child(
1071 XmlElement::new(prefixes.drawing("lin"))
1072 .with_attribute(
1073 "ang",
1074 format_fixed(gradient.angle.unwrap_or_default() * 60_000.0),
1075 )
1076 .with_attribute("scaled", "1"),
1077 );
1078 }
1079 kind => {
1080 let path = match kind {
1081 "radial" => "circle",
1082 "rectangular" => "rect",
1083 _ => "shape",
1084 };
1085 element = element.with_child(
1086 XmlElement::new(prefixes.drawing("path")).with_attribute("path", path),
1087 );
1088 }
1089 }
1090 Ok(element)
1091 }
1092 other => Err(write_error(
1093 part,
1094 format!("unsupported fill type {other:?}"),
1095 )),
1096 }
1097}
1098
1099fn color_element(color: &ColorValue, prefixes: &Prefixes) -> Option<XmlElement> {
1100 let mut element = if let Some(rgb) = &color.rgb {
1101 XmlElement::new(prefixes.drawing("srgbClr")).with_attribute("val", rgb.clone())
1102 } else if let Some(theme) = &color.theme_color {
1103 XmlElement::new(prefixes.drawing("schemeClr"))
1104 .with_attribute("val", denormalize_scheme_color(theme))
1105 } else {
1106 return None;
1107 };
1108 let fractions = [
1109 ("alpha", color.alpha),
1110 ("lumMod", color.luminance_modulation),
1111 ("lumOff", color.luminance_offset),
1112 ("satMod", color.saturation_modulation),
1113 ];
1114 for (name, value) in fractions {
1115 if let Some(value) = value {
1116 element = element.with_child(
1117 XmlElement::new(prefixes.drawing(name))
1118 .with_attribute("val", format_fixed(value * 100_000.0)),
1119 );
1120 }
1121 }
1122 let modifiers = [("tint", &color.theme_tint), ("shade", &color.theme_shade)];
1123 for (name, value) in modifiers {
1124 if let Some(byte) = value
1125 .as_deref()
1126 .and_then(|value| u8::from_str_radix(value, 16).ok())
1127 {
1128 element = element.with_child(
1129 XmlElement::new(prefixes.drawing(name))
1130 .with_attribute("val", format_fixed(f64::from(byte) / 255.0 * 100_000.0)),
1131 );
1132 }
1133 }
1134 Some(element)
1135}
1136
1137fn denormalize_scheme_color(value: &str) -> String {
1138 match value {
1139 "text1" => "tx1",
1140 "text2" => "tx2",
1141 "background1" => "bg1",
1142 "background2" => "bg2",
1143 value => value,
1144 }
1145 .to_owned()
1146}
1147
1148fn format_fixed(value: f64) -> String {
1149 (value.round() as i64).to_string()
1150}
1151
1152fn set_outline(properties: &mut XmlElement, outline: &ShapeOutline, prefixes: &Prefixes) {
1153 let element = outline_element(outline, prefixes);
1154 let existing = properties.children.iter().position(
1155 |child| matches!(child, XmlNode::Element(element) if element.local_name() == "ln"),
1156 );
1157 match existing {
1158 Some(index) => properties.children[index] = XmlNode::Element(element),
1159 None => {
1160 let position = properties
1161 .children
1162 .iter()
1163 .position(|child| {
1164 matches!(
1165 child,
1166 XmlNode::Element(element)
1167 if POST_FILL_ELEMENTS[1..].contains(&element.local_name())
1168 )
1169 })
1170 .unwrap_or(properties.children.len());
1171 properties
1172 .children
1173 .insert(position, XmlNode::Element(element));
1174 }
1175 }
1176}
1177
1178fn outline_element(outline: &ShapeOutline, prefixes: &Prefixes) -> XmlElement {
1179 let mut element = XmlElement::new(prefixes.drawing("ln"));
1180 if *outline == ShapeOutline::default() {
1181 return element.with_child(XmlElement::new(prefixes.drawing("noFill")));
1182 }
1183 if let Some(width) = outline.width {
1184 element.set_attribute("w", format_fixed(width));
1185 }
1186 if let Some(cap) = &outline.cap {
1187 element.set_attribute("cap", cap.clone());
1188 }
1189 if let Some(color) = outline
1190 .color
1191 .as_ref()
1192 .and_then(|color| color_element(color, prefixes))
1193 {
1194 element =
1195 element.with_child(XmlElement::new(prefixes.drawing("solidFill")).with_child(color));
1196 }
1197 if let Some(style) = &outline.style {
1198 element = element.with_child(
1199 XmlElement::new(prefixes.drawing("prstDash")).with_attribute("val", style.clone()),
1200 );
1201 }
1202 if let Some(join) = &outline.join {
1203 element = element.with_child(XmlElement::new(prefixes.drawing(join)));
1204 }
1205 let ends = [
1206 ("headEnd", &outline.head_end),
1207 ("tailEnd", &outline.tail_end),
1208 ];
1209 for (name, end) in ends {
1210 if let Some(end) = end {
1211 let mut end_element = XmlElement::new(prefixes.drawing(name))
1212 .with_attribute("type", end.end_type.clone());
1213 if let Some(width) = &end.width {
1214 end_element.set_attribute("w", width.clone());
1215 }
1216 if let Some(length) = &end.length {
1217 end_element.set_attribute("len", length.clone());
1218 }
1219 element = element.with_child(end_element);
1220 }
1221 }
1222 element
1223}
1224
1225fn set_adjust_values(
1226 properties: &mut XmlElement,
1227 adjust_values: &BTreeMap<String, f64>,
1228 prefixes: &Prefixes,
1229) {
1230 let Some(geometry) = properties.child_mut("prstGeom") else {
1232 return;
1233 };
1234 let list_name = prefixes.drawing("avLst");
1235 if geometry.child_mut("avLst").is_none() {
1236 geometry
1237 .children
1238 .insert(0, XmlNode::Element(XmlElement::new(list_name)));
1239 }
1240 let list = geometry.child_mut("avLst").expect("list ensured above");
1241 for (name, value) in adjust_values {
1243 let formula = format!("val {}", format_fixed(value * 100_000.0));
1244 let existing = list.children.iter_mut().find_map(|child| match child {
1245 XmlNode::Element(element)
1246 if element.local_name() == "gd" && element.attribute("name") == Some(name) =>
1247 {
1248 Some(element)
1249 }
1250 _ => None,
1251 });
1252 match existing {
1253 Some(guide) => guide.set_attribute("fmla", formula),
1254 None => list.children.push(XmlNode::Element(
1255 XmlElement::new(prefixes.drawing("gd"))
1256 .with_attribute("name", name.clone())
1257 .with_attribute("fmla", formula),
1258 )),
1259 }
1260 }
1261}
1262
1263fn patch_text(
1266 element: &mut XmlElement,
1267 text: &TextWrite,
1268 theme: Option<&Theme>,
1269 prefixes: &Prefixes,
1270 part: &str,
1271) -> Result<(), PptxError> {
1272 let body = match &text.target {
1273 TextTarget::Body => element.child_mut("txBody"),
1274 TextTarget::TableCell { row, cell } => element
1275 .child_mut("graphic")
1276 .and_then(|graphic| graphic.child_mut("graphicData"))
1277 .and_then(|data| data.child_mut("tbl"))
1278 .and_then(|table| nth_child_mut(table, "tr", *row))
1279 .and_then(|table_row| nth_child_mut(table_row, "tc", *cell))
1280 .and_then(|table_cell| table_cell.child_mut("txBody")),
1281 };
1282 let body = body.ok_or_else(|| write_error(part, "text target has no body"))?;
1283 rebuild_paragraphs(body, &text.paragraphs, theme, prefixes);
1284 Ok(())
1285}
1286
1287fn nth_child_mut<'a>(
1288 parent: &'a mut XmlElement,
1289 local: &str,
1290 index: usize,
1291) -> Option<&'a mut XmlElement> {
1292 parent
1293 .children
1294 .iter_mut()
1295 .filter_map(|child| match child {
1296 XmlNode::Element(element) if element.local_name() == local => Some(element),
1297 _ => None,
1298 })
1299 .nth(index)
1300}
1301
1302fn rebuild_paragraphs(
1303 body: &mut XmlElement,
1304 paragraphs: &[ParagraphWrite],
1305 theme: Option<&Theme>,
1306 prefixes: &Prefixes,
1307) {
1308 let mut preamble = Vec::new();
1309 let mut originals: Vec<Option<XmlElement>> = Vec::new();
1310 for child in std::mem::take(&mut body.children) {
1311 match child {
1312 XmlNode::Element(element) if element.local_name() == "p" => {
1313 originals.push(Some(element));
1314 }
1315 XmlNode::Text(text) if text.trim().is_empty() => {}
1316 other => preamble.push(other),
1317 }
1318 }
1319 let mut children = preamble;
1320 for paragraph in paragraphs {
1321 let source = paragraph
1322 .source_index
1323 .and_then(|index| originals.get_mut(index).and_then(Option::take));
1324 let element = match source {
1325 Some(element) if !paragraph.rebuild => element,
1326 source => build_paragraph(paragraph, source, theme, prefixes),
1327 };
1328 children.push(XmlNode::Element(element));
1329 }
1330 if children
1331 .iter()
1332 .all(|child| !matches!(child, XmlNode::Element(element) if element.local_name() == "p"))
1333 {
1334 children.push(XmlNode::Element(XmlElement::new(prefixes.drawing("p"))));
1335 }
1336 body.children = children;
1337}
1338
1339fn is_run_element(local: &str) -> bool {
1340 matches!(local, "r" | "br" | "fld")
1341}
1342
1343struct RunSegment<'a> {
1345 line_break: bool,
1346 text: &'a str,
1347 properties: &'a RunProperties,
1348}
1349
1350fn run_segments(runs: &[RunWrite]) -> Vec<RunSegment<'_>> {
1351 let mut segments = Vec::new();
1352 for run in runs {
1353 for (index, piece) in run.text.split('\n').enumerate() {
1354 if index > 0 {
1355 segments.push(RunSegment {
1356 line_break: true,
1357 text: "",
1358 properties: &run.properties,
1359 });
1360 }
1361 if !piece.is_empty() {
1362 segments.push(RunSegment {
1363 line_break: false,
1364 text: piece,
1365 properties: &run.properties,
1366 });
1367 }
1368 }
1369 }
1370 segments
1371}
1372
1373fn segment_matches(element: &XmlElement, segment: &RunSegment<'_>, theme: Option<&Theme>) -> bool {
1378 if element.local_name() == "br" {
1379 return segment.line_break;
1380 }
1381 if segment.line_break {
1382 return false;
1383 }
1384 let text = element
1385 .child("t")
1386 .map(XmlElement::text_content)
1387 .unwrap_or_default();
1388 if text != segment.text {
1389 return false;
1390 }
1391 let source = parse_run_properties(element.child("rPr"));
1392 let target = segment.properties;
1393 source.bold == target.bold
1394 && source.italic == target.italic
1395 && source.font_size_pt == target.font_size_pt
1396 && source.underline == target.underline
1397 && source.font_family == target.font_family
1398 && resolve_color_value_to_hex_with_theme(source.color.as_ref(), theme)
1399 == resolve_color_value_to_hex_with_theme(target.color.as_ref(), theme)
1400}
1401
1402fn build_paragraph(
1403 write: &ParagraphWrite,
1404 source: Option<XmlElement>,
1405 theme: Option<&Theme>,
1406 prefixes: &Prefixes,
1407) -> XmlElement {
1408 let (mut paragraph, source_runs) = match source {
1409 Some(mut element) => {
1410 let mut runs = Vec::new();
1411 let mut kept = Vec::new();
1412 for child in std::mem::take(&mut element.children) {
1413 match child {
1414 XmlNode::Element(child) if is_run_element(child.local_name()) => {
1415 runs.push(child);
1416 }
1417 XmlNode::Text(text) if text.trim().is_empty() => {}
1418 other => kept.push(other),
1419 }
1420 }
1421 element.children = kept;
1422 (element, runs)
1423 }
1424 None => (XmlElement::new(prefixes.drawing("p")), Vec::new()),
1425 };
1426 let fresh = write.source_index.is_none();
1427 if write.properties_changed || fresh {
1428 apply_paragraph_properties(&mut paragraph, write, prefixes);
1429 }
1430 let segments = run_segments(&write.runs);
1431 let mut front = 0;
1432 while front < source_runs.len()
1433 && front < segments.len()
1434 && segment_matches(&source_runs[front], &segments[front], theme)
1435 {
1436 front += 1;
1437 }
1438 let mut back = 0;
1439 while back < source_runs.len() - front
1440 && back < segments.len() - front
1441 && segment_matches(
1442 &source_runs[source_runs.len() - 1 - back],
1443 &segments[segments.len() - 1 - back],
1444 theme,
1445 )
1446 {
1447 back += 1;
1448 }
1449 let tail_start = source_runs.len() - back;
1450 let template = (tail_start - front == 1 && source_runs[front].local_name() == "r")
1453 .then(|| source_runs[front].child("rPr").cloned())
1454 .flatten();
1455 let mut runs: Vec<XmlNode> = Vec::with_capacity(segments.len());
1456 let mut source_runs = source_runs.into_iter();
1457 for element in source_runs.by_ref().take(front) {
1458 runs.push(XmlNode::Element(element));
1459 }
1460 for segment in &segments[front..segments.len() - back] {
1461 runs.push(XmlNode::Element(segment_element(
1462 segment,
1463 template.as_ref(),
1464 prefixes,
1465 )));
1466 }
1467 for element in source_runs.skip(tail_start - front) {
1468 runs.push(XmlNode::Element(element));
1469 }
1470 let position = paragraph
1471 .children
1472 .iter()
1473 .position(|child| {
1474 matches!(child, XmlNode::Element(element) if element.local_name() == "endParaRPr")
1475 })
1476 .unwrap_or(paragraph.children.len());
1477 paragraph.children.splice(position..position, runs);
1478 paragraph
1479}
1480
1481fn segment_element(
1482 segment: &RunSegment<'_>,
1483 template: Option<&XmlElement>,
1484 prefixes: &Prefixes,
1485) -> XmlElement {
1486 if segment.line_break {
1487 let mut line_break = XmlElement::new(prefixes.drawing("br"));
1488 if let Some(properties) = run_properties_element(segment.properties, prefixes) {
1489 line_break = line_break.with_child(properties);
1490 }
1491 return line_break;
1492 }
1493 let properties = match template {
1494 Some(template) => {
1495 let mut base = template.clone();
1496 apply_run_properties(&mut base, segment.properties, prefixes);
1497 Some(base)
1498 }
1499 None => run_properties_element(segment.properties, prefixes),
1500 };
1501 let mut element = XmlElement::new(prefixes.drawing("r"));
1502 if let Some(properties) = properties {
1503 element = element.with_child(properties);
1504 }
1505 element.with_child(XmlElement::new(prefixes.drawing("t")).with_text(segment.text))
1506}
1507
1508const POST_LATIN_ELEMENTS: [&str; 7] = [
1509 "ea",
1510 "cs",
1511 "sym",
1512 "hlinkClick",
1513 "hlinkMouseOver",
1514 "rtl",
1515 "extLst",
1516];
1517
1518fn apply_run_properties(base: &mut XmlElement, properties: &RunProperties, prefixes: &Prefixes) {
1521 match properties.font_size_pt {
1522 Some(size) => base.set_attribute("sz", format_fixed(size * 100.0)),
1523 None => {
1524 base.attributes.remove("sz");
1525 }
1526 }
1527 let toggles = [("b", properties.bold), ("i", properties.italic)];
1528 for (name, value) in toggles {
1529 match value {
1530 Some(value) => base.set_attribute(name, if value { "1" } else { "0" }),
1531 None => {
1532 base.attributes.remove(name);
1533 }
1534 }
1535 }
1536 match &properties.underline {
1537 Some(underline) => base.set_attribute("u", underline.clone()),
1538 None => {
1539 base.attributes.remove("u");
1540 }
1541 }
1542 let removed_fills: &[&str] = if properties.color.is_some() {
1545 &FILL_ELEMENTS
1546 } else {
1547 &["solidFill"]
1548 };
1549 base.children.retain(|child| {
1550 !matches!(
1551 child,
1552 XmlNode::Element(element) if removed_fills.contains(&element.local_name())
1553 )
1554 });
1555 if let Some(color) = properties
1556 .color
1557 .as_ref()
1558 .and_then(|color| color_element(color, prefixes))
1559 {
1560 let position = base
1561 .children
1562 .iter()
1563 .position(
1564 |child| !matches!(child, XmlNode::Element(element) if element.local_name() == "ln"),
1565 )
1566 .unwrap_or(base.children.len());
1567 base.children.insert(
1568 position,
1569 XmlNode::Element(XmlElement::new(prefixes.drawing("solidFill")).with_child(color)),
1570 );
1571 }
1572 base.children.retain(|child| {
1573 !matches!(
1574 child,
1575 XmlNode::Element(element) if element.local_name() == "latin"
1576 )
1577 });
1578 if let Some(family) = &properties.font_family {
1579 let position = base
1580 .children
1581 .iter()
1582 .position(|child| {
1583 matches!(
1584 child,
1585 XmlNode::Element(element)
1586 if POST_LATIN_ELEMENTS.contains(&element.local_name())
1587 )
1588 })
1589 .unwrap_or(base.children.len());
1590 base.children.insert(
1591 position,
1592 XmlNode::Element(
1593 XmlElement::new(prefixes.drawing("latin"))
1594 .with_attribute("typeface", family.clone()),
1595 ),
1596 );
1597 }
1598}
1599
1600fn apply_paragraph_properties(
1601 paragraph: &mut XmlElement,
1602 write: &ParagraphWrite,
1603 prefixes: &Prefixes,
1604) {
1605 let needs_properties = write.alignment.is_some() || write.level > 0 || write.bullet.is_some();
1606 if paragraph.child_mut("pPr").is_none() {
1607 if !needs_properties {
1608 return;
1609 }
1610 paragraph.children.insert(
1611 0,
1612 XmlNode::Element(XmlElement::new(prefixes.drawing("pPr"))),
1613 );
1614 }
1615 let properties = paragraph.child_mut("pPr").expect("ensured above");
1616 match &write.alignment {
1617 Some(alignment) => properties.set_attribute("algn", alignment.clone()),
1618 None => {
1619 properties.attributes.remove("algn");
1620 }
1621 }
1622 if write.level > 0 {
1623 properties.set_attribute("lvl", write.level.to_string());
1624 } else {
1625 properties.attributes.remove("lvl");
1626 }
1627 properties.children.retain(|child| {
1628 !matches!(
1629 child,
1630 XmlNode::Element(element)
1631 if matches!(element.local_name(), "buNone" | "buChar" | "buAutoNum")
1632 )
1633 });
1634 let bullet = match &write.bullet {
1635 Some(Bullet::Character { value }) => {
1636 Some(XmlElement::new(prefixes.drawing("buChar")).with_attribute("char", value.clone()))
1637 }
1638 Some(Bullet::AutoNumber { scheme, start_at }) => {
1639 let mut element = XmlElement::new(prefixes.drawing("buAutoNum"))
1640 .with_attribute("type", scheme.clone());
1641 if *start_at != 1 {
1642 element.set_attribute("startAt", start_at.to_string());
1643 }
1644 Some(element)
1645 }
1646 Some(Bullet::None) => Some(XmlElement::new(prefixes.drawing("buNone"))),
1647 None => None,
1648 };
1649 if let Some(bullet) = bullet {
1650 let position = properties
1651 .children
1652 .iter()
1653 .position(|child| {
1654 matches!(
1655 child,
1656 XmlNode::Element(element)
1657 if matches!(element.local_name(), "tabLst" | "defRPr" | "extLst")
1658 )
1659 })
1660 .unwrap_or(properties.children.len());
1661 properties
1662 .children
1663 .insert(position, XmlNode::Element(bullet));
1664 }
1665}
1666
1667fn run_properties_element(properties: &RunProperties, prefixes: &Prefixes) -> Option<XmlElement> {
1668 let mut element = XmlElement::new(prefixes.drawing("rPr"));
1669 let mut present = false;
1670 if let Some(language) = &properties.language {
1671 element.set_attribute("lang", language.clone());
1672 present = true;
1673 }
1674 if let Some(size) = properties.font_size_pt {
1675 element.set_attribute("sz", format_fixed(size * 100.0));
1676 present = true;
1677 }
1678 if let Some(bold) = properties.bold {
1679 element.set_attribute("b", if bold { "1" } else { "0" });
1680 present = true;
1681 }
1682 if let Some(italic) = properties.italic {
1683 element.set_attribute("i", if italic { "1" } else { "0" });
1684 present = true;
1685 }
1686 if let Some(underline) = &properties.underline {
1687 element.set_attribute("u", underline.clone());
1688 present = true;
1689 }
1690 if let Some(color) = properties
1691 .color
1692 .as_ref()
1693 .and_then(|color| color_element(color, prefixes))
1694 {
1695 element =
1696 element.with_child(XmlElement::new(prefixes.drawing("solidFill")).with_child(color));
1697 present = true;
1698 }
1699 if let Some(family) = &properties.font_family {
1700 element = element.with_child(
1701 XmlElement::new(prefixes.drawing("latin")).with_attribute("typeface", family.clone()),
1702 );
1703 present = true;
1704 }
1705 present.then_some(element)
1706}
1707
1708fn shape_element(
1711 add: &ShapeAdd,
1712 next_shape_id: &mut u32,
1713 prefixes: &Prefixes,
1714 part: &str,
1715) -> Result<XmlElement, PptxError> {
1716 if matches!(add.geometry.as_str(), "custom" | "group") {
1717 return Err(write_error(
1718 part,
1719 format!("unsupported geometry {:?} for a new shape", add.geometry),
1720 ));
1721 }
1722 let shape_id = *next_shape_id;
1723 *next_shape_id += 1;
1724 let non_visual = XmlElement::new(prefixes.presentation("nvSpPr"))
1725 .with_child(
1726 XmlElement::new(prefixes.presentation("cNvPr"))
1727 .with_attribute("id", shape_id.to_string())
1728 .with_attribute("name", add.name.clone()),
1729 )
1730 .with_child(XmlElement::new(prefixes.presentation("cNvSpPr")))
1731 .with_child(XmlElement::new(prefixes.presentation("nvPr")));
1732 let transform = XmlElement::new(prefixes.drawing("xfrm"))
1733 .with_child(
1734 XmlElement::new(prefixes.drawing("off"))
1735 .with_attribute("x", add.x.to_string())
1736 .with_attribute("y", add.y.to_string()),
1737 )
1738 .with_child(
1739 XmlElement::new(prefixes.drawing("ext"))
1740 .with_attribute("cx", add.width.to_string())
1741 .with_attribute("cy", add.height.to_string()),
1742 );
1743 let mut adjust_list = XmlElement::new(prefixes.drawing("avLst"));
1744 for (name, value) in &add.adjust_values {
1745 adjust_list = adjust_list.with_child(
1746 XmlElement::new(prefixes.drawing("gd"))
1747 .with_attribute("name", name.clone())
1748 .with_attribute("fmla", format!("val {}", format_fixed(value * 100_000.0))),
1749 );
1750 }
1751 let geometry = XmlElement::new(prefixes.drawing("prstGeom"))
1752 .with_attribute("prst", add.geometry.clone())
1753 .with_child(adjust_list);
1754 let mut properties = XmlElement::new(prefixes.presentation("spPr"))
1755 .with_child(transform)
1756 .with_child(geometry);
1757 if let Some(fill) = &add.fill {
1758 properties = properties.with_child(fill_element(fill, prefixes, part)?);
1759 }
1760 if let Some(outline) = &add.outline {
1761 properties = properties.with_child(outline_element(outline, prefixes));
1762 }
1763 let mut shape = XmlElement::new(prefixes.presentation("sp"))
1764 .with_child(non_visual)
1765 .with_child(properties);
1766 if let Some(paragraphs) = &add.paragraphs {
1767 let mut body = XmlElement::new(prefixes.presentation("txBody"))
1768 .with_child(XmlElement::new(prefixes.drawing("bodyPr")))
1769 .with_child(XmlElement::new(prefixes.drawing("lstStyle")));
1770 for paragraph in paragraphs {
1771 body = body.with_child(build_paragraph(paragraph, None, None, prefixes));
1772 }
1773 if paragraphs.is_empty() {
1774 body = body.with_child(XmlElement::new(prefixes.drawing("p")));
1775 }
1776 shape = shape.with_child(body);
1777 }
1778 Ok(shape)
1779}
1780
1781fn slide_xml(name: Option<&str>, shapes: &[ShapeAdd], part: &str) -> Result<Vec<u8>, PptxError> {
1782 let mut root = XmlElement::new("p:sld")
1783 .with_attribute("xmlns:a", DRAWINGML_NS)
1784 .with_attribute("xmlns:r", OFFICE_RELATIONSHIPS_NS)
1785 .with_attribute("xmlns:p", PRESENTATIONML_NS);
1786 let prefixes = Prefixes {
1787 drawing: "a".to_owned(),
1788 presentation: "p".to_owned(),
1789 };
1790 let group_transform = XmlElement::new(prefixes.drawing("xfrm"))
1791 .with_child(
1792 XmlElement::new(prefixes.drawing("off"))
1793 .with_attribute("x", "0")
1794 .with_attribute("y", "0"),
1795 )
1796 .with_child(
1797 XmlElement::new(prefixes.drawing("ext"))
1798 .with_attribute("cx", "0")
1799 .with_attribute("cy", "0"),
1800 )
1801 .with_child(
1802 XmlElement::new(prefixes.drawing("chOff"))
1803 .with_attribute("x", "0")
1804 .with_attribute("y", "0"),
1805 )
1806 .with_child(
1807 XmlElement::new(prefixes.drawing("chExt"))
1808 .with_attribute("cx", "0")
1809 .with_attribute("cy", "0"),
1810 );
1811 let mut tree = XmlElement::new(prefixes.presentation("spTree"))
1812 .with_child(
1813 XmlElement::new(prefixes.presentation("nvGrpSpPr"))
1814 .with_child(
1815 XmlElement::new(prefixes.presentation("cNvPr"))
1816 .with_attribute("id", "1")
1817 .with_attribute("name", ""),
1818 )
1819 .with_child(XmlElement::new(prefixes.presentation("cNvGrpSpPr")))
1820 .with_child(XmlElement::new(prefixes.presentation("nvPr"))),
1821 )
1822 .with_child(XmlElement::new(prefixes.presentation("grpSpPr")).with_child(group_transform));
1823 let mut next_shape_id = 2;
1824 for shape in shapes {
1825 tree = tree.with_child(shape_element(shape, &mut next_shape_id, &prefixes, part)?);
1826 }
1827 let mut common = XmlElement::new(prefixes.presentation("cSld"));
1828 if let Some(name) = name {
1829 common.set_attribute("name", name);
1830 }
1831 root = root.with_child(common.with_child(tree)).with_child(
1832 XmlElement::new(prefixes.presentation("clrMapOvr"))
1833 .with_child(XmlElement::new(prefixes.drawing("masterClrMapping"))),
1834 );
1835 Ok(serialize_xml(&root))
1836}
1837
1838fn slide_relationships_xml(part_path: &str, layout_part_path: &str) -> Vec<u8> {
1839 let root = XmlElement::new("Relationships")
1840 .with_attribute(
1841 "xmlns",
1842 "http://schemas.openxmlformats.org/package/2006/relationships",
1843 )
1844 .with_child(
1845 XmlElement::new("Relationship")
1846 .with_attribute("Id", "rId1")
1847 .with_attribute("Type", SLIDE_LAYOUT_RELATIONSHIP_TYPE)
1848 .with_attribute("Target", relative_target(part_path, layout_part_path)),
1849 );
1850 serialize_xml(&root)
1851}