1use std::collections::{BTreeMap, HashMap, HashSet};
2
3use ooxml_drawingml::{ColorMap, TableStyleList, Theme};
4
5use crate::chart::parse_chart_part;
6use crate::comments::{
7 Comment, CommentAuthor, CommentFlavor, authors_part, parse_comment_authors, parse_comments,
8 slide_comment_parts,
9};
10use crate::drawing::{
11 common_slide_data, parse_diagram_drawing, parse_paragraph_properties, parse_style_levels,
12 parse_text_styles,
13};
14use crate::model::*;
15use crate::relationships::{Relationship, parse_relationships, relationship_types};
16use crate::table_style::parse_table_styles;
17use crate::theme::{parse_background_pictures, parse_format_scheme, parse_theme};
18use crate::xml::{ParseBudget, XmlElement, parse_xml};
19use crate::{ParseLimits, PptxError};
20
21pub fn parse_pptx(data: &[u8]) -> Result<PptxPackage, PptxError> {
22 parse_pptx_with_limits(data, &ParseLimits::default())
23}
24
25pub fn parse_pptx_with_limits(data: &[u8], limits: &ParseLimits) -> Result<PptxPackage, PptxError> {
26 parse_package(data, limits, ShapeElements::WithConnectors)
27}
28
29pub fn parse_pptx_without_connectors(data: &[u8]) -> Result<PptxPackage, PptxError> {
31 parse_package(
32 data,
33 &ParseLimits::default(),
34 ShapeElements::WithoutConnectors,
35 )
36}
37
38fn parse_package(
39 data: &[u8],
40 limits: &ParseLimits,
41 shape_elements: ShapeElements,
42) -> Result<PptxPackage, PptxError> {
43 let source_parts = ooxml_opc::unzip_parts(data).map_err(PptxError::Container)?;
44 let parts: HashMap<&str, &[u8]> = source_parts
45 .iter()
46 .map(|(path, bytes)| (path.as_str(), bytes.as_slice()))
47 .collect();
48 let mut budget = ParseBudget::new(limits);
49 let relationships = parse_package_relationships(&source_parts, &mut budget)?;
50 let presentation_path = relationships
51 .get("")
52 .and_then(|entries| {
53 entries
54 .iter()
55 .find(|relationship| relationship.has_type("/officeDocument"))
56 })
57 .and_then(|relationship| relationship.resolved_target.clone())
58 .unwrap_or_else(|| "ppt/presentation.xml".to_owned());
59 let presentation_root = parse_part(&parts, &presentation_path, &mut budget)?;
60 let presentation_relationships = relationships
61 .get(&presentation_path)
62 .map(Vec::as_slice)
63 .unwrap_or_default();
64 let presentation = parse_presentation(
65 &presentation_root,
66 &presentation_path,
67 presentation_relationships,
68 )?;
69
70 let mut has_connectors = false;
71 let mut slides = Vec::with_capacity(presentation.slides.len());
72 let mut sources = BTreeMap::new();
73 let mut notes_sources = BTreeMap::new();
74 for reference in &presentation.slides {
75 let root = parse_part(&parts, &reference.part_path, &mut budget)?;
76 let slide_relationships = relationships
77 .get(&reference.part_path)
78 .map(Vec::as_slice)
79 .unwrap_or_default();
80 has_connectors |= !root.descendants_named("cxnSp").is_empty();
81 let data = common_slide_data(
82 &root,
83 slide_relationships,
84 &reference.part_path,
85 &mut budget,
86 shape_elements,
87 )?;
88 let mut notes = String::new();
89 if let Some(notes_path) = crate::notes::slide_notes_part(slide_relationships)
90 && let Some(bytes) = parts.get(notes_path.as_str())
91 {
92 let notes_root = parse_xml(bytes, ¬es_path, &mut budget)?;
93 notes = crate::notes::notes_text(¬es_root);
94 notes_sources.insert(
95 reference.part_path.clone(),
96 crate::inventory::notes_source(¬es_root, ¬es_path),
97 );
98 }
99 sources.insert(
100 reference.part_path.clone(),
101 crate::inventory::slide_source(&root, slide_relationships, shape_elements),
102 );
103 slides.push(Slide {
104 part_path: reference.part_path.clone(),
105 name: data.name,
106 layout_part_path: relationship_by_type(
107 slide_relationships,
108 relationship_types::SLIDE_LAYOUT,
109 ),
110 show_master_shapes: bool_attribute(&root, "showMasterSp", true),
111 background: data.background,
112 background_picture: data.background_picture,
113 background_reference: data.background_reference,
114 shapes: data.shapes,
115 color_map_override: parse_color_map_override(&root),
116 notes,
117 hidden: Some(!bool_attribute(&root, "show", true)),
118 });
119 }
120
121 let master_paths = ordered_part_paths(
122 presentation.master_part_paths.clone(),
123 &source_parts,
124 "ppt/slideMasters/",
125 );
126 let mut masters = Vec::with_capacity(master_paths.len());
127 for part_path in &master_paths {
128 let root = parse_part(&parts, part_path, &mut budget)?;
129 let master_relationships = relationships
130 .get(part_path)
131 .map(Vec::as_slice)
132 .unwrap_or_default();
133 has_connectors |= !root.descendants_named("cxnSp").is_empty();
134 let data = common_slide_data(
135 &root,
136 master_relationships,
137 part_path,
138 &mut budget,
139 shape_elements,
140 )?;
141 masters.push(SlideMaster {
142 part_path: part_path.clone(),
143 name: data.name,
144 theme_part_path: relationship_by_type(master_relationships, relationship_types::THEME),
145 layout_part_paths: master_relationships
146 .iter()
147 .filter(|relationship| relationship.has_type(relationship_types::SLIDE_LAYOUT))
148 .filter_map(|relationship| relationship.resolved_target.clone())
149 .collect(),
150 background: data.background,
151 background_picture: data.background_picture,
152 background_reference: data.background_reference,
153 shapes: data.shapes,
154 text_styles: parse_text_styles(&root),
155 color_map: parse_color_map(&root),
156 });
157 }
158
159 let layout_paths = ordered_part_paths(
160 masters
161 .iter()
162 .flat_map(|master| master.layout_part_paths.iter().cloned())
163 .collect(),
164 &source_parts,
165 "ppt/slideLayouts/",
166 );
167 let mut layouts = Vec::with_capacity(layout_paths.len());
168 for part_path in &layout_paths {
169 let root = parse_part(&parts, part_path, &mut budget)?;
170 let layout_relationships = relationships
171 .get(part_path)
172 .map(Vec::as_slice)
173 .unwrap_or_default();
174 has_connectors |= !root.descendants_named("cxnSp").is_empty();
175 let data = common_slide_data(
176 &root,
177 layout_relationships,
178 part_path,
179 &mut budget,
180 shape_elements,
181 )?;
182 layouts.push(SlideLayout {
183 part_path: part_path.clone(),
184 name: root
185 .attribute("matchingName")
186 .map(str::to_owned)
187 .or(data.name),
188 layout_type: root.attribute("type").map(str::to_owned),
189 master_part_path: relationship_by_type(
190 layout_relationships,
191 relationship_types::SLIDE_MASTER,
192 ),
193 show_master_shapes: bool_attribute(&root, "showMasterSp", true),
194 background: data.background,
195 background_picture: data.background_picture,
196 background_reference: data.background_reference,
197 shapes: data.shapes,
198 color_map_override: parse_color_map_override(&root),
199 });
200 }
201
202 let theme_paths = ordered_part_paths(
203 masters
204 .iter()
205 .filter_map(|master| master.theme_part_path.clone())
206 .collect(),
207 &source_parts,
208 "ppt/theme/",
209 );
210 let mut themes = Vec::with_capacity(theme_paths.len());
211 for part_path in theme_paths {
212 let root = parse_part(&parts, &part_path, &mut budget)?;
213 let theme_relationships = relationships
214 .get(&part_path)
215 .map(Vec::as_slice)
216 .unwrap_or_default();
217 themes.push(ThemePart {
218 theme: parse_theme(&root),
219 format_scheme: parse_format_scheme(&root),
220 background_pictures: parse_background_pictures(&root, theme_relationships),
221 part_path,
222 });
223 }
224
225 let table_styles = parse_table_style_part(&parts, presentation_relationships, &mut budget)?;
226
227 let charts = parse_chart_parts(
228 &parts,
229 &ChartSources {
230 slides: &slides,
231 layouts: &layouts,
232 masters: &masters,
233 themes: &themes,
234 relationships: &relationships,
235 },
236 limits,
237 );
238
239 let deck_comments = parse_package_comments(
240 &parts,
241 &presentation,
242 presentation_relationships,
243 &relationships,
244 &mut budget,
245 )?;
246
247 let content_types = parse_content_types(&parts, &mut budget)?;
248 let diagram_drawings =
249 parse_diagram_drawings(&parts, &slides, &relationships, &mut budget, shape_elements);
250 let media = source_parts
251 .iter()
252 .filter(|(path, _)| path.starts_with("ppt/media/"))
253 .map(|(path, bytes)| MediaPart {
254 part_path: path.clone(),
255 content_type: content_type_for(path, &content_types),
256 bytes: bytes.clone(),
257 })
258 .collect();
259 let parts = source_parts
260 .into_iter()
261 .map(|(path, bytes)| PackagePart { path, bytes })
262 .collect();
263 Ok(PptxPackage {
264 presentation,
265 slides,
266 layouts,
267 masters,
268 themes,
269 charts,
270 diagram_drawings,
271 media,
272 table_styles,
273 comment_authors: deck_comments.authors,
274 comments: deck_comments.comments,
275 comment_flavor: deck_comments.flavor,
276 relationships,
277 parts,
278 source_container: ooxml_opc::SourceContainer::new(data.to_vec()),
279 shape_elements: if has_connectors {
280 shape_elements
281 } else {
282 ShapeElements::WithoutConnectors
283 },
284 sources,
285 notes_sources,
286 })
287}
288
289fn parse_package_comments(
290 parts: &HashMap<&str, &[u8]>,
291 presentation: &Presentation,
292 presentation_relationships: &[Relationship],
293 relationships: &BTreeMap<String, Vec<Relationship>>,
294 budget: &mut ParseBudget<'_>,
295) -> Result<PackageComments, PptxError> {
296 let mut found: Vec<(CommentFlavor, String, String)> = Vec::new();
297 for reference in &presentation.slides {
298 let slide_relationships = relationships
299 .get(&reference.part_path)
300 .map(Vec::as_slice)
301 .unwrap_or_default();
302 let slide_parts = slide_comment_parts(slide_relationships);
303 if let Some(part) = slide_parts.legacy {
304 found.push((CommentFlavor::Legacy, reference.part_path.clone(), part));
305 }
306 if let Some(part) = slide_parts.modern {
307 found.push((CommentFlavor::Modern, reference.part_path.clone(), part));
308 }
309 }
310 let Some(flavor) = found
311 .iter()
312 .any(|(flavor, _, _)| *flavor == CommentFlavor::Legacy)
313 .then_some(CommentFlavor::Legacy)
314 .or_else(|| (!found.is_empty()).then_some(CommentFlavor::Modern))
315 else {
316 return Ok(PackageComments::default());
317 };
318
319 let mut comments = Vec::new();
320 for (_, slide_part_path, part) in found
321 .iter()
322 .filter(|(candidate, _, _)| *candidate == flavor)
323 {
324 let Some(bytes) = parts.get(part.as_str()) else {
325 continue;
326 };
327 comments.extend(parse_comments(
328 bytes,
329 part,
330 slide_part_path,
331 flavor,
332 budget,
333 )?);
334 }
335
336 let comment_authors = match authors_part(presentation_relationships, flavor)
337 .and_then(|part| parts.get(part.as_str()).map(|bytes| (part, *bytes)))
338 {
339 Some((part, bytes)) => parse_comment_authors(bytes, &part, flavor, budget)?,
340 None => Vec::new(),
341 };
342 Ok(PackageComments {
343 authors: comment_authors,
344 comments,
345 flavor: Some(flavor),
346 })
347}
348
349#[derive(Default)]
350struct PackageComments {
351 authors: Vec<CommentAuthor>,
352 comments: Vec<Comment>,
353 flavor: Option<CommentFlavor>,
354}
355
356pub fn write_pptx(package: &PptxPackage) -> Result<Vec<u8>, PptxError> {
357 let parts = package
358 .parts
359 .iter()
360 .map(|part| (part.path.clone(), part.bytes.clone()))
361 .collect::<Vec<_>>();
362 ooxml_opc::rezip_parts_preserving(&parts, package.source_container.as_bytes())
363 .map_err(PptxError::Container)
364}
365
366fn parse_package_relationships(
367 parts: &[(String, Vec<u8>)],
368 budget: &mut ParseBudget<'_>,
369) -> Result<BTreeMap<String, Vec<Relationship>>, PptxError> {
370 let mut output = BTreeMap::new();
371 for (path, bytes) in parts {
372 let Some(source) = relationship_source(path) else {
373 continue;
374 };
375 let parsed = parse_relationships(bytes, path, &source, budget)?;
376 output.insert(source, parsed);
377 }
378 Ok(output)
379}
380
381fn relationship_source(path: &str) -> Option<String> {
382 if path == "_rels/.rels" {
383 return Some(String::new());
384 }
385 let (directory, filename) = path.rsplit_once("/_rels/")?;
386 let source_filename = filename.strip_suffix(".rels")?;
387 Some(format!("{directory}/{source_filename}"))
388}
389
390fn parse_part(
391 parts: &HashMap<&str, &[u8]>,
392 path: &str,
393 budget: &mut ParseBudget<'_>,
394) -> Result<XmlElement, PptxError> {
395 let bytes = parts
396 .get(path)
397 .ok_or_else(|| PptxError::MissingPart(path.to_owned()))?;
398 parse_xml(bytes, path, budget)
399}
400
401fn parse_table_style_part(
402 parts: &HashMap<&str, &[u8]>,
403 presentation_relationships: &[Relationship],
404 budget: &mut ParseBudget<'_>,
405) -> Result<TableStyleList, PptxError> {
406 let path = relationship_by_type(presentation_relationships, relationship_types::TABLE_STYLES)
407 .unwrap_or_else(|| "ppt/tableStyles.xml".to_owned());
408 let Some(bytes) = parts.get(path.as_str()) else {
409 return Ok(TableStyleList::default());
410 };
411 Ok(parse_table_styles(&parse_xml(bytes, &path, budget)?))
412}
413
414fn parse_presentation(
415 root: &XmlElement,
416 part_path: &str,
417 relationships: &[Relationship],
418) -> Result<Presentation, PptxError> {
419 let slide_size = root.child("sldSz");
420 let width_emu = positive_integer_attribute(slide_size, "cx").unwrap_or(12_192_000);
421 let height_emu = positive_integer_attribute(slide_size, "cy").unwrap_or(6_858_000);
422 let first_slide_num = root
423 .attribute("firstSlideNum")
424 .and_then(|value| value.parse().ok())
425 .unwrap_or(1);
426 let mut slides = Vec::new();
427 if let Some(list) = root.child("sldIdLst") {
428 for slide in list.children_named("sldId") {
429 let relationship_id = slide
430 .attribute("r:id")
431 .or_else(|| slide.attribute_local("id"))
432 .unwrap_or_default()
433 .to_owned();
434 let part_path =
435 relationship_target(relationships, &relationship_id).ok_or_else(|| {
436 PptxError::MissingPart(format!("slide relationship {relationship_id}"))
437 })?;
438 slides.push(SlideReference {
439 id: slide
440 .attribute("id")
441 .and_then(|value| value.parse().ok())
442 .unwrap_or_default(),
443 relationship_id,
444 part_path,
445 });
446 }
447 }
448 let master_part_paths = root
449 .child("sldMasterIdLst")
450 .into_iter()
451 .flat_map(|list| list.children_named("sldMasterId"))
452 .filter_map(|master| {
453 master
454 .attribute("r:id")
455 .or_else(|| master.attribute_local("id"))
456 .and_then(|id| relationship_target(relationships, id))
457 })
458 .collect();
459 Ok(Presentation {
460 part_path: part_path.to_owned(),
461 width_emu,
462 height_emu,
463 first_slide_num,
464 slides,
465 master_part_paths,
466 default_text_style: parse_style_levels(root.child("defaultTextStyle")),
467 default_text_paragraph: root
468 .child("defaultTextStyle")
469 .and_then(|style| style.child("defPPr"))
470 .map(|properties| Box::new(parse_paragraph_properties(Some(properties)))),
471 })
472}
473
474struct ChartSources<'a> {
477 slides: &'a [Slide],
478 layouts: &'a [SlideLayout],
479 masters: &'a [SlideMaster],
480 themes: &'a [ThemePart],
481 relationships: &'a BTreeMap<String, Vec<Relationship>>,
482}
483
484impl ChartSources<'_> {
485 fn chart_target(&self, source_part: &str, relationship_id: &str) -> Option<String> {
486 self.relationships
487 .get(source_part)?
488 .iter()
489 .find(|relationship| relationship.id == relationship_id)
490 .filter(|relationship| relationship.has_type(relationship_types::CHART))
491 .and_then(|relationship| relationship.resolved_target.clone())
492 }
493
494 fn theme_for(&self, source_part: &str) -> Option<&ThemePart> {
495 if let Some(slide) = self
496 .slides
497 .iter()
498 .find(|slide| slide.part_path == source_part)
499 {
500 return self.theme_via_layout(slide.layout_part_path.as_deref());
501 }
502 if self
503 .layouts
504 .iter()
505 .any(|layout| layout.part_path == source_part)
506 {
507 return self.theme_via_layout(Some(source_part));
508 }
509 if self
510 .masters
511 .iter()
512 .any(|master| master.part_path == source_part)
513 {
514 return self.theme_via_master(Some(source_part));
515 }
516 self.themes.first()
517 }
518
519 fn theme_via_layout(&self, layout_part_path: Option<&str>) -> Option<&ThemePart> {
520 let layout = layout_part_path
521 .and_then(|path| self.layouts.iter().find(|layout| layout.part_path == path))
522 .or_else(|| self.layouts.first());
523 let master_part_path = layout
524 .and_then(|layout| layout.master_part_path.as_deref())
525 .or_else(|| {
526 let layout = layout?;
527 self.masters
528 .iter()
529 .find(|master| {
530 master
531 .layout_part_paths
532 .iter()
533 .any(|path| path == &layout.part_path)
534 })
535 .map(|master| master.part_path.as_str())
536 });
537 self.theme_via_master(master_part_path)
538 }
539
540 fn theme_via_master(&self, master_part_path: Option<&str>) -> Option<&ThemePart> {
541 let master = master_part_path
542 .and_then(|path| self.masters.iter().find(|master| master.part_path == path))
543 .or_else(|| self.masters.first());
544 master
545 .and_then(|master| master.theme_part_path.as_deref())
546 .and_then(|path| self.themes.iter().find(|theme| theme.part_path == path))
547 .or_else(|| self.themes.first())
548 }
549}
550
551fn parse_chart_parts(
568 parts: &HashMap<&str, &[u8]>,
569 sources: &ChartSources<'_>,
570 limits: &ParseLimits,
571) -> Vec<ChartPart> {
572 let mut references = Vec::new();
573 for slide in sources.slides {
574 collect_chart_references(&slide.shapes, &slide.part_path, &mut references);
575 }
576 for layout in sources.layouts {
577 collect_chart_references(&layout.shapes, &layout.part_path, &mut references);
578 }
579 for master in sources.masters {
580 collect_chart_references(&master.shapes, &master.part_path, &mut references);
581 }
582 let default_theme = Theme::default();
583 let mut reads = Vec::new();
584 let mut loaded = HashSet::new();
585 let mut pending_reads: HashMap<String, usize> = HashMap::new();
586 for (source_part, relationship_id) in references {
587 let Some(part_path) = sources.chart_target(&source_part, &relationship_id) else {
588 continue;
589 };
590 let theme_part = sources.theme_for(&source_part);
591 let theme_part_path = theme_part.map(|part| part.part_path.clone());
592 if !loaded.insert((part_path.clone(), theme_part_path.clone())) {
593 continue;
594 }
595 *pending_reads.entry(part_path.clone()).or_default() += 1;
596 let theme = theme_part.map(|part| &part.theme).unwrap_or(&default_theme);
597 reads.push((part_path, theme_part_path, theme));
598 }
599
600 let mut charts = Vec::new();
601 let mut roots: HashMap<String, Option<XmlElement>> = HashMap::new();
602 let mut remaining_events = limits.max_xml_events;
603 for (part_path, theme_part_path, theme) in reads {
604 if !roots.contains_key(&part_path) {
605 let root = parts.get(part_path.as_str()).and_then(|bytes| {
606 read_chart_root(bytes, &part_path, limits, &mut remaining_events)
607 });
608 roots.insert(part_path.clone(), root);
609 }
610 let chart = roots
611 .get(&part_path)
612 .and_then(Option::as_ref)
613 .and_then(|root| parse_chart_part(root, theme));
614 if let Some(chart) = chart {
615 charts.push(ChartPart {
616 part_path: part_path.clone(),
617 theme_part_path,
618 chart,
619 });
620 }
621 if let Some(pending) = pending_reads.get_mut(&part_path) {
622 *pending -= 1;
623 if *pending == 0 {
624 roots.remove(&part_path);
625 }
626 }
627 }
628 charts
629}
630
631fn read_chart_root(
637 bytes: &[u8],
638 part_path: &str,
639 limits: &ParseLimits,
640 remaining_events: &mut usize,
641) -> Option<XmlElement> {
642 let part_limits = ParseLimits {
643 max_xml_events: *remaining_events,
644 ..limits.clone()
645 };
646 let mut budget = ParseBudget::new(&part_limits);
647 let root = parse_xml(bytes, part_path, &mut budget).ok();
648 *remaining_events = remaining_events.saturating_sub(budget.xml_events_spent());
649 root
650}
651
652fn parse_diagram_drawings(
656 parts: &HashMap<&str, &[u8]>,
657 slides: &[Slide],
658 relationships: &BTreeMap<String, Vec<Relationship>>,
659 budget: &mut ParseBudget<'_>,
660 elements: ShapeElements,
661) -> Vec<DiagramDrawing> {
662 let mut wanted: Vec<String> = Vec::new();
663 for slide in slides {
664 collect_diagram_drawings(&slide.shapes, &mut wanted);
665 }
666 wanted.sort();
667 wanted.dedup();
668 let mut drawings = Vec::new();
669 for part_path in wanted {
670 let Some(bytes) = parts.get(part_path.as_str()) else {
671 continue;
672 };
673 let Ok(root) = parse_xml(bytes, &part_path, budget) else {
674 continue;
675 };
676 let part_relationships = relationships
677 .get(&part_path)
678 .map(Vec::as_slice)
679 .unwrap_or_default();
680 if let Ok(shapes) =
681 parse_diagram_drawing(&root, part_relationships, &part_path, budget, elements)
682 && !shapes.is_empty()
683 {
684 drawings.push(DiagramDrawing { part_path, shapes });
685 }
686 }
687 drawings
688}
689
690fn collect_diagram_drawings(shapes: &[ShapeNode], output: &mut Vec<String>) {
691 for shape in shapes {
692 match shape {
693 ShapeNode::GraphicFrame(frame) => {
694 if let GraphicFrameData::Diagram {
695 drawing_part_path: Some(path),
696 ..
697 } = &frame.data
698 {
699 output.push(path.clone());
700 }
701 }
702 ShapeNode::Group(group) => collect_diagram_drawings(&group.children, output),
703 ShapeNode::Shape(_) | ShapeNode::Picture(_) => {}
704 }
705 }
706}
707
708fn collect_chart_references(
711 shapes: &[ShapeNode],
712 part_path: &str,
713 output: &mut Vec<(String, String)>,
714) {
715 for shape in shapes {
716 match shape {
717 ShapeNode::GraphicFrame(frame) => {
718 if let GraphicFrameData::Chart {
719 relationship_id, ..
720 } = &frame.data
721 {
722 output.push((part_path.to_owned(), relationship_id.clone()));
723 }
724 }
725 ShapeNode::Group(group) => collect_chart_references(&group.children, part_path, output),
726 ShapeNode::Shape(_) | ShapeNode::Picture(_) => {}
727 }
728 }
729}
730
731#[derive(Default)]
732struct ContentTypes {
733 defaults: HashMap<String, String>,
734 overrides: HashMap<String, String>,
735}
736
737fn parse_content_types(
738 parts: &HashMap<&str, &[u8]>,
739 budget: &mut ParseBudget<'_>,
740) -> Result<ContentTypes, PptxError> {
741 let root = parse_part(parts, "[Content_Types].xml", budget)?;
742 let mut content_types = ContentTypes::default();
743 for child in root.child_elements() {
744 match child.local_name() {
745 "Default" => {
746 if let (Some(extension), Some(content_type)) =
747 (child.attribute("Extension"), child.attribute("ContentType"))
748 {
749 content_types
750 .defaults
751 .insert(extension.to_ascii_lowercase(), content_type.to_owned());
752 }
753 }
754 "Override" => {
755 if let (Some(part_name), Some(content_type)) =
756 (child.attribute("PartName"), child.attribute("ContentType"))
757 {
758 content_types.overrides.insert(
759 part_name.trim_start_matches('/').to_owned(),
760 content_type.to_owned(),
761 );
762 }
763 }
764 _ => {}
765 }
766 }
767 Ok(content_types)
768}
769
770fn content_type_for(path: &str, content_types: &ContentTypes) -> String {
771 content_types
772 .overrides
773 .get(path)
774 .cloned()
775 .or_else(|| {
776 path.rsplit_once('.').and_then(|(_, extension)| {
777 content_types
778 .defaults
779 .get(&extension.to_ascii_lowercase())
780 .cloned()
781 })
782 })
783 .unwrap_or_else(|| "application/octet-stream".to_owned())
784}
785
786fn ordered_part_paths(
787 preferred: Vec<String>,
788 parts: &[(String, Vec<u8>)],
789 prefix: &str,
790) -> Vec<String> {
791 let mut seen = HashSet::new();
792 preferred
793 .into_iter()
794 .chain(
795 parts
796 .iter()
797 .map(|(path, _)| path.clone())
798 .filter(|path| path.starts_with(prefix) && path.ends_with(".xml")),
799 )
800 .filter(|path| seen.insert(path.clone()))
801 .collect()
802}
803
804fn relationship_by_type(relationships: &[Relationship], suffix: &str) -> Option<String> {
805 relationships
806 .iter()
807 .find(|relationship| relationship.has_type(suffix))
808 .and_then(|relationship| relationship.resolved_target.clone())
809}
810
811fn relationship_target(relationships: &[Relationship], id: &str) -> Option<String> {
812 relationships
813 .iter()
814 .find(|relationship| relationship.id == id)
815 .and_then(|relationship| relationship.resolved_target.clone())
816}
817
818fn positive_integer_attribute(element: Option<&XmlElement>, name: &str) -> Option<i64> {
819 let value = element?.attribute(name)?.parse::<i64>().ok()?;
820 (value > 0 && value <= 1_000_000_000_000_000).then_some(value)
821}
822
823fn bool_attribute(element: &XmlElement, name: &str, default: bool) -> bool {
824 match element.attribute(name) {
825 Some("1" | "true" | "on") => true,
826 Some("0" | "false" | "off") => false,
827 _ => default,
828 }
829}
830
831pub fn effective_color_map(
835 slide: Option<&Slide>,
836 layout: Option<&SlideLayout>,
837 master: Option<&SlideMaster>,
838) -> ColorMap {
839 slide
840 .and_then(|slide| slide.color_map_override.clone())
841 .or_else(|| layout.and_then(|layout| layout.color_map_override.clone()))
842 .or_else(|| master.map(|master| master.color_map.clone()))
843 .unwrap_or_default()
844}
845
846pub fn slide_theme(
849 package: &PptxPackage,
850 slide_part_path: Option<&str>,
851 layout_part_path: Option<&str>,
852) -> Theme {
853 let slide = slide_part_path
854 .and_then(|path| package.slides.iter().find(|slide| slide.part_path == path));
855 let layout = layout_part_path
856 .or_else(|| slide.and_then(|slide| slide.layout_part_path.as_deref()))
857 .and_then(|path| {
858 package
859 .layouts
860 .iter()
861 .find(|layout| layout.part_path == path)
862 })
863 .or_else(|| package.layouts.first());
864 let master = master_for_layout(package, layout);
865 let base = master
866 .and_then(|master| master.theme_part_path.as_deref())
867 .and_then(|path| package.themes.iter().find(|theme| theme.part_path == path))
868 .or_else(|| package.themes.first());
869 Theme {
870 color_map: effective_color_map(slide, layout, master),
871 ..base.map(|part| part.theme.clone()).unwrap_or_default()
872 }
873}
874
875pub fn master_for_layout<'a>(
877 package: &'a PptxPackage,
878 layout: Option<&SlideLayout>,
879) -> Option<&'a SlideMaster> {
880 layout
881 .and_then(|layout| layout.master_part_path.as_deref())
882 .and_then(|path| {
883 package
884 .masters
885 .iter()
886 .find(|master| master.part_path == path)
887 })
888 .or_else(|| {
889 layout.and_then(|layout| {
890 package.masters.iter().find(|master| {
891 master
892 .layout_part_paths
893 .iter()
894 .any(|path| path == &layout.part_path)
895 })
896 })
897 })
898 .or_else(|| package.masters.first())
899}
900
901fn parse_color_map(root: &XmlElement) -> ColorMap {
902 root.child("clrMap").map(color_map).unwrap_or_default()
903}
904
905fn parse_color_map_override(root: &XmlElement) -> Option<ColorMap> {
906 root.child("clrMapOvr")
907 .and_then(|element| element.child("overrideClrMapping"))
908 .map(color_map)
909}
910
911fn color_map(element: &XmlElement) -> ColorMap {
912 let mut map = ColorMap::default();
913 for (name, slot) in &element.attributes {
914 map.set(name, slot);
915 }
916 map
917}
918
919#[cfg(test)]
920mod tests {
921 use super::*;
922
923 const FIXTURE: &[u8] = include_bytes!("../../../apps/demo/public/betteroffice-demo.pptx");
924 const CHART_FIXTURE: &[u8] = include_bytes!("../tests/fixtures/chart-deck.pptx");
925 const NUMBERED_FIXTURE: &[u8] = include_bytes!("../tests/fixtures/slide-number-fields.pptx");
926 const CHART_PART: &str = "ppt/charts/chart1.xml";
927
928 #[test]
929 fn parses_betteroffice_demo_deck_surface() {
930 let package = parse_pptx(FIXTURE).unwrap();
931 assert_eq!(package.presentation.width_emu, 12_192_000);
932 assert_eq!(package.presentation.height_emu, 6_858_000);
933 assert_eq!(package.slides.len(), 3);
934 assert_eq!(package.layouts.len(), 1);
935 assert_eq!(package.masters.len(), 1);
936 assert_eq!(package.themes.len(), 1);
937 assert_eq!(package.media.len(), 1);
938 assert_eq!(package.themes[0].theme.name, "BetterOffice");
939
940 let kinds = package
941 .slides
942 .iter()
943 .flat_map(|slide| slide.shapes.iter())
944 .map(|shape| match shape {
945 ShapeNode::Shape(_) => "shape",
946 ShapeNode::Picture(_) => "picture",
947 ShapeNode::GraphicFrame(_) => "graphicFrame",
948 ShapeNode::Group(_) => "group",
949 })
950 .collect::<HashSet<_>>();
951 assert_eq!(
952 kinds,
953 HashSet::from(["shape", "picture", "graphicFrame", "group"])
954 );
955 assert!(package.slides.iter().flat_map(|slide| &slide.shapes).any(|shape| {
956 matches!(
957 shape,
958 ShapeNode::Shape(Shape {
959 text: Some(TextBody { paragraphs, .. }),
960 ..
961 }) if paragraphs.iter().flat_map(|paragraph| ¶graph.runs).any(|run| run.text.contains("Rust"))
962 )
963 }));
964 }
965
966 #[test]
967 fn first_slide_num_is_read_signed_and_defaults_to_one() {
968 assert_eq!(parse_pptx(FIXTURE).unwrap().presentation.first_slide_num, 1);
969 assert_eq!(
970 parse_pptx(NUMBERED_FIXTURE)
971 .unwrap()
972 .presentation
973 .first_slide_num,
974 10
975 );
976 for (value, expected) in [
977 ("0", 0),
978 ("-3", -3),
979 ("-2147483648", i32::MIN),
980 ("2147483647", i32::MAX),
981 ("2147483648", 1),
982 ("-2147483649", 1),
983 ("ten", 1),
984 ("", 1),
985 ] {
986 let package = parse_pptx(&demo_deck_numbered_from(value)).unwrap();
987 assert_eq!(
988 package.presentation.first_slide_num, expected,
989 "firstSlideNum={value:?}"
990 );
991 }
992 }
993
994 fn demo_deck_numbered_from(value: &str) -> Vec<u8> {
995 let mut parts = ooxml_opc::unzip_parts(FIXTURE).unwrap();
996 let slot = parts
997 .iter_mut()
998 .find(|(path, _)| path == "ppt/presentation.xml")
999 .unwrap();
1000 let xml = String::from_utf8(slot.1.clone()).unwrap();
1001 let numbered = xml.replacen(
1002 "<p:presentation ",
1003 &format!("<p:presentation firstSlideNum=\"{value}\" "),
1004 1,
1005 );
1006 assert_ne!(numbered, xml);
1007 slot.1 = numbered.into_bytes();
1008 ooxml_opc::rezip_parts(&parts).unwrap()
1009 }
1010
1011 #[test]
1012 fn untouched_save_preserves_every_part_byte_and_order() {
1013 let package = parse_pptx(FIXTURE).unwrap();
1014 let written = write_pptx(&package).unwrap();
1015 let before = ooxml_opc::unzip_parts(FIXTURE).unwrap();
1016 let after = ooxml_opc::unzip_parts(&written).unwrap();
1017 assert_eq!(after, before);
1018 }
1019
1020 #[test]
1021 fn loads_every_referenced_chart_part_against_the_deck_theme() {
1022 let package = parse_pptx(CHART_FIXTURE).unwrap();
1023 assert_eq!(
1024 package
1025 .charts
1026 .iter()
1027 .map(|part| part.part_path.as_str())
1028 .collect::<Vec<_>>(),
1029 ["ppt/charts/chart1.xml", "ppt/charts/chart2.xml"]
1030 );
1031
1032 let column = &package.charts[0].chart;
1033 assert_eq!(column.chart_type, "column");
1034 assert_eq!(column.title.as_deref(), Some("Revenue"));
1035 assert_eq!(
1036 column.legend.as_ref().unwrap().position.as_deref(),
1037 Some("right")
1038 );
1039 assert_eq!(column.series[0].color, "#6254E7");
1040 assert_eq!(column.series[1].color, "#1FA97A");
1041 assert_eq!(column.series[0].values, [12.0, 19.0, 7.0]);
1042 assert_eq!(column.plot_groups[0].gap_width, Some(150.0));
1043 assert_eq!(
1044 column.plot_groups[0].series[0]
1045 .data_labels
1046 .as_ref()
1047 .and_then(|labels| labels.show_value),
1048 Some(true)
1049 );
1050 assert!(column.plot_groups[0].data_labels.is_none());
1051 assert!(column.plot_groups[0].show_data_labels);
1052 let axes = column.axis_list.as_ref().unwrap();
1053 assert_eq!(axes[0].title.as_deref(), Some("Quarter"));
1054 assert_eq!(axes[1].title.as_deref(), Some("Millions"));
1055 assert_eq!(axes[1].max, Some(25.0));
1056
1057 let pie = &package.charts[1].chart;
1059 assert_eq!(pie.chart_type, "pie");
1060 let points = pie.plot_groups[0].series[0].points.as_ref().unwrap();
1061 assert_eq!(points[0].color, "#E7A954");
1062 assert_eq!(points[1].color, "#112233");
1063 assert_eq!(points[1].explosion, Some(10.0));
1064 }
1065
1066 struct SharedChartDeck {
1068 slides: Vec<Slide>,
1069 layouts: Vec<SlideLayout>,
1070 masters: Vec<SlideMaster>,
1071 themes: Vec<ThemePart>,
1072 relationships: BTreeMap<String, Vec<Relationship>>,
1073 }
1074
1075 impl SharedChartDeck {
1076 fn sources(&self) -> ChartSources<'_> {
1077 ChartSources {
1078 slides: &self.slides,
1079 layouts: &self.layouts,
1080 masters: &self.masters,
1081 themes: &self.themes,
1082 relationships: &self.relationships,
1083 }
1084 }
1085 }
1086
1087 fn shared_chart_deck() -> SharedChartDeck {
1088 fn chart_shape(id: u32) -> ShapeNode {
1089 ShapeNode::GraphicFrame(GraphicFrame {
1090 base: ShapeBase {
1091 id,
1092 name: format!("Chart {id}"),
1093 description: None,
1094 hidden: false,
1095 placeholder: None,
1096 transform: ShapeTransform::default(),
1097 },
1098 data: GraphicFrameData::Chart {
1099 relationship_id: "rIdChart".to_owned(),
1100 part_path: Some("ppt/charts/chart1.xml".to_owned()),
1101 },
1102 })
1103 }
1104
1105 fn slide(path: &str, layout: &str, id: u32) -> Slide {
1106 Slide {
1107 part_path: path.to_owned(),
1108 name: None,
1109 layout_part_path: Some(layout.to_owned()),
1110 show_master_shapes: true,
1111 background: None,
1112 background_picture: None,
1113 background_reference: None,
1114 shapes: vec![chart_shape(id)],
1115 color_map_override: None,
1116 notes: String::new(),
1117 hidden: Some(false),
1118 }
1119 }
1120
1121 fn chart_relationships() -> Vec<Relationship> {
1122 vec![Relationship {
1123 id: "rIdChart".to_owned(),
1124 relationship_type: relationship_types::CHART.to_owned(),
1125 target: "../charts/chart1.xml".to_owned(),
1126 target_mode: Default::default(),
1127 resolved_target: Some("ppt/charts/chart1.xml".to_owned()),
1128 }]
1129 }
1130
1131 let slides = vec![
1132 slide("ppt/slides/slide1.xml", "ppt/slideLayouts/layout1.xml", 1),
1133 slide("ppt/slides/slide2.xml", "ppt/slideLayouts/layout2.xml", 2),
1134 slide("ppt/slides/slide3.xml", "ppt/slideLayouts/layout1.xml", 3),
1135 ];
1136 let layouts = vec![
1137 SlideLayout {
1138 part_path: "ppt/slideLayouts/layout1.xml".to_owned(),
1139 name: None,
1140 layout_type: None,
1141 master_part_path: Some("ppt/slideMasters/master1.xml".to_owned()),
1142 show_master_shapes: true,
1143 background: None,
1144 background_picture: None,
1145 background_reference: None,
1146 shapes: Vec::new(),
1147 color_map_override: None,
1148 },
1149 SlideLayout {
1150 part_path: "ppt/slideLayouts/layout2.xml".to_owned(),
1151 name: None,
1152 layout_type: None,
1153 master_part_path: Some("ppt/slideMasters/master2.xml".to_owned()),
1154 show_master_shapes: true,
1155 background: None,
1156 background_picture: None,
1157 background_reference: None,
1158 shapes: Vec::new(),
1159 color_map_override: None,
1160 },
1161 ];
1162 let masters = vec![
1163 SlideMaster {
1164 part_path: "ppt/slideMasters/master1.xml".to_owned(),
1165 name: None,
1166 theme_part_path: Some("ppt/theme/theme1.xml".to_owned()),
1167 layout_part_paths: vec!["ppt/slideLayouts/layout1.xml".to_owned()],
1168 background: None,
1169 background_picture: None,
1170 background_reference: None,
1171 shapes: Vec::new(),
1172 text_styles: TextStyleSet::default(),
1173 color_map: ColorMap::default(),
1174 },
1175 SlideMaster {
1176 part_path: "ppt/slideMasters/master2.xml".to_owned(),
1177 name: None,
1178 theme_part_path: Some("ppt/theme/theme2.xml".to_owned()),
1179 layout_part_paths: vec!["ppt/slideLayouts/layout2.xml".to_owned()],
1180 background: None,
1181 background_picture: None,
1182 background_reference: None,
1183 shapes: Vec::new(),
1184 text_styles: TextStyleSet::default(),
1185 color_map: ColorMap::default(),
1186 },
1187 ];
1188 let mut first_theme = Theme::default();
1189 first_theme.color_scheme.accent1 = "112233".to_owned();
1190 let mut second_theme = Theme::default();
1191 second_theme.color_scheme.accent1 = "AABBCC".to_owned();
1192 let themes = vec![
1193 ThemePart {
1194 part_path: "ppt/theme/theme1.xml".to_owned(),
1195 theme: first_theme,
1196 format_scheme: Default::default(),
1197 background_pictures: Vec::new(),
1198 },
1199 ThemePart {
1200 part_path: "ppt/theme/theme2.xml".to_owned(),
1201 theme: second_theme,
1202 format_scheme: Default::default(),
1203 background_pictures: Vec::new(),
1204 },
1205 ];
1206 let relationships = BTreeMap::from([
1207 ("ppt/slides/slide1.xml".to_owned(), chart_relationships()),
1208 ("ppt/slides/slide2.xml".to_owned(), chart_relationships()),
1209 ("ppt/slides/slide3.xml".to_owned(), chart_relationships()),
1210 ]);
1211 SharedChartDeck {
1212 slides,
1213 layouts,
1214 masters,
1215 themes,
1216 relationships,
1217 }
1218 }
1219
1220 #[test]
1221 fn a_shared_chart_is_resolved_once_per_referencing_theme() {
1222 let deck = shared_chart_deck();
1223 let chart_xml = br#"<c:chartSpace xmlns:c="c" xmlns:a="a"><c:chart><c:plotArea><c:barChart><c:barDir val="col"/><c:ser><c:spPr><a:solidFill><a:schemeClr val="accent1"/></a:solidFill></c:spPr></c:ser></c:barChart></c:plotArea></c:chart></c:chartSpace>"#;
1224 let parts: HashMap<&str, &[u8]> =
1225 HashMap::from([("ppt/charts/chart1.xml", chart_xml.as_slice())]);
1226
1227 let charts = parse_chart_parts(&parts, &deck.sources(), &ParseLimits::default());
1228
1229 assert_eq!(charts.len(), 2);
1230 assert_eq!(
1231 charts
1232 .iter()
1233 .map(|chart| chart.theme_part_path.as_deref())
1234 .collect::<Vec<_>>(),
1235 [Some("ppt/theme/theme1.xml"), Some("ppt/theme/theme2.xml")]
1236 );
1237 assert_eq!(charts[0].chart.series[0].color, "#112233");
1238 assert_eq!(charts[1].chart.series[0].color, "#AABBCC");
1239 }
1240
1241 #[test]
1245 fn a_shared_chart_part_is_read_once_however_many_themes_reference_it() {
1246 let deck = shared_chart_deck();
1247 let chart_xml = cached_chart(1_000);
1248 let parts: HashMap<&str, &[u8]> =
1249 HashMap::from([("ppt/charts/chart1.xml", chart_xml.as_slice())]);
1250 let limits = ParseLimits {
1251 max_xml_events: 6_000,
1252 ..ParseLimits::default()
1253 };
1254
1255 let charts = parse_chart_parts(&parts, &deck.sources(), &limits);
1256
1257 assert_eq!(charts.len(), 2);
1258 assert!(
1259 charts
1260 .iter()
1261 .all(|part| part.chart.series[0].values.len() == 1_000)
1262 );
1263 }
1264
1265 #[test]
1266 fn a_deck_without_charts_loads_no_chart_parts() {
1267 assert!(parse_pptx(FIXTURE).unwrap().charts.is_empty());
1268 }
1269
1270 fn chart_deck_with(part: &str, bytes: &[u8]) -> Vec<u8> {
1272 let mut parts = ooxml_opc::unzip_parts(CHART_FIXTURE).unwrap();
1273 let slot = parts
1274 .iter_mut()
1275 .find(|(path, _)| path == part)
1276 .unwrap_or_else(|| panic!("{part} is not in the fixture"));
1277 slot.1 = bytes.to_vec();
1278 ooxml_opc::rezip_parts(&parts).unwrap()
1279 }
1280
1281 fn assert_damaged_chart_is_dropped(bytes: &[u8]) {
1284 let package = parse_pptx(&chart_deck_with(CHART_PART, bytes)).unwrap();
1285 assert_eq!(package.slides.len(), 2);
1286 assert_eq!(package.layouts.len(), 1);
1287 assert_eq!(package.masters.len(), 1);
1288 assert_eq!(package.themes.len(), 1);
1289 assert_eq!(
1290 package
1291 .slides
1292 .iter()
1293 .map(|slide| slide.name.as_deref())
1294 .collect::<Vec<_>>(),
1295 [Some("Charts"), Some("Grouped")]
1296 );
1297 assert_eq!(
1298 package.slides[0]
1299 .shapes
1300 .iter()
1301 .filter(|shape| matches!(shape, ShapeNode::GraphicFrame(_)))
1302 .count(),
1303 2
1304 );
1305 assert_eq!(
1306 package
1307 .charts
1308 .iter()
1309 .map(|part| part.part_path.as_str())
1310 .collect::<Vec<_>>(),
1311 ["ppt/charts/chart2.xml"]
1312 );
1313 }
1314
1315 #[test]
1316 fn a_malformed_chart_part_drops_only_that_chart() {
1317 assert_damaged_chart_is_dropped(b"<c:chartSpace><c:chart></c:chartSpace>");
1318 }
1319
1320 #[test]
1321 fn an_empty_chart_part_drops_only_that_chart() {
1322 assert_damaged_chart_is_dropped(b"");
1323 }
1324
1325 #[test]
1326 fn a_doctype_bearing_chart_part_drops_only_that_chart() {
1327 assert_damaged_chart_is_dropped(
1328 br#"<?xml version="1.0"?><!DOCTYPE c:chartSpace [<!ENTITY x "y">]><c:chartSpace xmlns:c="c"/>"#,
1329 );
1330 }
1331
1332 #[test]
1333 fn a_truncated_chart_part_drops_only_that_chart() {
1334 let source = ooxml_opc::unzip_parts(CHART_FIXTURE)
1335 .unwrap()
1336 .into_iter()
1337 .find(|(path, _)| path == CHART_PART)
1338 .map(|(_, bytes)| bytes)
1339 .unwrap();
1340 assert_damaged_chart_is_dropped(&source[..source.len() / 2]);
1341 }
1342
1343 #[test]
1344 fn a_binary_chart_part_drops_only_that_chart() {
1345 assert_damaged_chart_is_dropped(&[0x00, 0xff, 0xfe, 0x01, 0x80, 0x7f]);
1346 }
1347
1348 #[test]
1349 fn an_undeclared_entity_in_a_chart_part_drops_only_that_chart() {
1350 assert_damaged_chart_is_dropped(
1351 br#"<c:chartSpace xmlns:c="c"><c:chart><c:title> </c:title></c:chart></c:chartSpace>"#,
1352 );
1353 }
1354
1355 #[test]
1358 fn an_unreadable_chart_part_survives_a_save_byte_for_byte() {
1359 const DAMAGED: &[u8] = b"<c:chartSpace><c:chart></c:chartSpace>";
1360 let deck = chart_deck_with(CHART_PART, DAMAGED);
1361 let written = write_pptx(&parse_pptx(&deck).unwrap()).unwrap();
1362 assert_eq!(
1363 ooxml_opc::unzip_parts(&written).unwrap(),
1364 ooxml_opc::unzip_parts(&deck).unwrap()
1365 );
1366 assert_eq!(
1367 ooxml_opc::unzip_parts(&written)
1368 .unwrap()
1369 .into_iter()
1370 .find(|(path, _)| path == CHART_PART)
1371 .map(|(_, bytes)| bytes),
1372 Some(DAMAGED.to_vec())
1373 );
1374 }
1375
1376 fn cached_chart(points: usize) -> Vec<u8> {
1379 let mut xml = String::from(
1380 r#"<c:chartSpace xmlns:c="c" xmlns:a="a"><c:chart><c:plotArea><c:barChart><c:barDir val="col"/><c:ser><c:val><c:numRef><c:numCache>"#,
1381 );
1382 for point in 0..points {
1383 xml.push_str(&format!("<c:pt><c:v>{point}</c:v></c:pt>"));
1384 }
1385 xml.push_str("</c:numCache></c:numRef></c:val></c:ser></c:barChart></c:plotArea></c:chart></c:chartSpace>");
1386 xml.into_bytes()
1387 }
1388
1389 fn cached_chart_deck(points: usize, shapes: usize) -> Vec<u8> {
1393 let crowd = (0..shapes)
1394 .map(|shape| {
1395 format!(
1396 r#"<p:sp><p:nvSpPr><p:cNvPr id="{}" name=""/><p:cNvSpPr/><p:nvPr/></p:nvSpPr><p:spPr/></p:sp>"#,
1397 shape + 100
1398 )
1399 })
1400 .collect::<String>();
1401 let mut parts = ooxml_opc::unzip_parts(CHART_FIXTURE).unwrap();
1402 for (path, bytes) in &mut parts {
1403 if path.starts_with("ppt/charts/") {
1404 *bytes = cached_chart(points);
1405 }
1406 if path == "ppt/slides/slide2.xml" {
1407 *bytes = String::from_utf8(bytes.clone())
1408 .unwrap()
1409 .replace("</p:spTree>", &format!("{crowd}</p:spTree>"))
1410 .into_bytes();
1411 }
1412 }
1413 ooxml_opc::rezip_parts(&parts).unwrap()
1414 }
1415
1416 #[test]
1420 fn a_deck_whose_charts_outgrow_the_slides_budget_opens_with_every_chart() {
1421 let deck = cached_chart_deck(2_000, 1_500);
1422 let limits = ParseLimits {
1423 max_xml_events: 30_000,
1424 ..ParseLimits::default()
1425 };
1426
1427 let package = parse_pptx_with_limits(&deck, &limits).unwrap();
1428
1429 assert_eq!(package.slides.len(), 2);
1430 assert_eq!(
1431 package
1432 .slides
1433 .iter()
1434 .map(|slide| slide.name.as_deref())
1435 .collect::<Vec<_>>(),
1436 [Some("Charts"), Some("Grouped")]
1437 );
1438 assert_eq!(package.slides[1].shapes.len(), 1_501);
1439 assert_eq!(package.charts.len(), 2);
1440 assert!(
1441 package
1442 .charts
1443 .iter()
1444 .all(|part| part.chart.series[0].values.len() == 2_000)
1445 );
1446 }
1447
1448 #[test]
1452 fn a_deck_whose_charts_outgrow_the_chart_budget_opens_with_the_charts_that_fit() {
1453 let deck = cached_chart_deck(4_000, 1_500);
1454 let limits = ParseLimits {
1455 max_xml_events: 30_000,
1456 ..ParseLimits::default()
1457 };
1458
1459 let package = parse_pptx_with_limits(&deck, &limits).unwrap();
1460
1461 assert_eq!(package.slides.len(), 2);
1462 assert_eq!(package.slides[1].shapes.len(), 1_501);
1463 assert_eq!(
1464 package.slides[0]
1465 .shapes
1466 .iter()
1467 .filter(|shape| matches!(shape, ShapeNode::GraphicFrame(_)))
1468 .count(),
1469 2
1470 );
1471 assert_eq!(
1472 package
1473 .charts
1474 .iter()
1475 .map(|part| part.part_path.as_str())
1476 .collect::<Vec<_>>(),
1477 ["ppt/charts/chart1.xml"]
1478 );
1479 assert_eq!(package.charts[0].chart.series[0].values.len(), 4_000);
1480 }
1481
1482 fn smartart_deck(drawings: usize, shapes: usize) -> Vec<u8> {
1484 let mut parts = ooxml_opc::unzip_parts(FIXTURE).unwrap();
1485 let frames = (0..drawings)
1486 .map(|index| {
1487 format!(
1488 r#"<p:graphicFrame><p:nvGraphicFramePr><p:cNvPr id="{}" name="SmartArt {index}"/><p:cNvGraphicFramePr/><p:nvPr/></p:nvGraphicFramePr><p:xfrm><a:off x="0" y="0"/><a:ext cx="914400" cy="914400"/></p:xfrm><a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/diagram"><dgm:relIds xmlns:dgm="http://schemas.openxmlformats.org/drawingml/2006/diagram" r:dm="rIdData{index}" r:lo="" r:qs="" r:cs=""/></a:graphicData></a:graphic></p:graphicFrame>"#,
1489 index + 900
1490 )
1491 })
1492 .collect::<String>();
1493 let links = (0..drawings)
1494 .map(|index| {
1495 format!(
1496 r#"<Relationship Id="rIdData{index}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramData" Target="../diagrams/data{index}.xml"/><Relationship Id="rIdDrawing{index}" Type="http://schemas.microsoft.com/office/2007/relationships/diagramDrawing" Target="../diagrams/drawing{index}.xml"/>"#
1497 )
1498 })
1499 .collect::<String>();
1500 let crowd = (0..shapes)
1501 .map(|_| r#"<dsp:sp modelId="{0}"><dsp:nvSpPr><dsp:cNvPr id="0" name=""/><dsp:cNvSpPr/></dsp:nvSpPr><dsp:spPr/></dsp:sp>"#)
1502 .collect::<String>();
1503 let drawing = format!(
1504 r#"<dsp:drawing xmlns:dsp="http://schemas.microsoft.com/office/drawing/2008/diagram" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><dsp:spTree><dsp:nvGrpSpPr><dsp:cNvPr id="0" name=""/><dsp:cNvGrpSpPr/></dsp:nvGrpSpPr><dsp:grpSpPr/>{crowd}</dsp:spTree></dsp:drawing>"#
1505 );
1506 for (path, bytes) in &mut parts {
1507 let edited = match path.as_str() {
1508 "ppt/slides/slide1.xml" => String::from_utf8(bytes.clone())
1509 .unwrap()
1510 .replace("</p:spTree>", &format!("{frames}</p:spTree>")),
1511 "ppt/slides/_rels/slide1.xml.rels" => String::from_utf8(bytes.clone())
1512 .unwrap()
1513 .replace("</Relationships>", &format!("{links}</Relationships>")),
1514 _ => continue,
1515 };
1516 *bytes = edited.into_bytes();
1517 }
1518 for index in 0..drawings {
1519 parts.push((
1520 format!("ppt/diagrams/drawing{index}.xml"),
1521 drawing.clone().into_bytes(),
1522 ));
1523 }
1524 ooxml_opc::rezip_parts(&parts).unwrap()
1525 }
1526
1527 #[test]
1531 fn smartart_drawings_share_the_package_budget() {
1532 let whole = parse_pptx(&smartart_deck(3, 40)).unwrap();
1533 assert_eq!(whole.diagram_drawings.len(), 3);
1534 let spent = whole
1535 .slides
1536 .iter()
1537 .map(|slide| count_shapes(&slide.shapes))
1538 .sum::<usize>()
1539 + whole
1540 .layouts
1541 .iter()
1542 .map(|layout| count_shapes(&layout.shapes))
1543 .sum::<usize>()
1544 + whole
1545 .masters
1546 .iter()
1547 .map(|master| count_shapes(&master.shapes))
1548 .sum::<usize>();
1549 let limits = ParseLimits {
1550 max_shapes: spent + 100,
1551 ..ParseLimits::default()
1552 };
1553
1554 let package = parse_pptx_with_limits(&smartart_deck(3, 40), &limits).unwrap();
1555
1556 assert_eq!(package.slides.len(), whole.slides.len());
1557 assert_eq!(
1558 package
1559 .diagram_drawings
1560 .iter()
1561 .map(|drawing| drawing.shapes.len())
1562 .collect::<Vec<_>>(),
1563 [40, 40]
1564 );
1565 }
1566
1567 fn count_shapes(shapes: &[ShapeNode]) -> usize {
1568 shapes
1569 .iter()
1570 .map(|shape| match shape {
1571 ShapeNode::Group(group) => 1 + count_shapes(&group.children),
1572 _ => 1,
1573 })
1574 .sum()
1575 }
1576
1577 #[test]
1578 fn package_limits_apply_across_xml_parts() {
1579 let limits = ParseLimits {
1580 max_xml_bytes: 100,
1581 ..ParseLimits::default()
1582 };
1583 assert!(matches!(
1584 parse_pptx_with_limits(FIXTURE, &limits),
1585 Err(PptxError::ResourceLimit {
1586 kind: "xmlBytes",
1587 ..
1588 })
1589 ));
1590 }
1591}