1use std::collections::{BTreeMap, HashMap, HashSet};
2
3use crate::drawing::{common_slide_data, parse_text_styles};
4use crate::model::*;
5use crate::relationships::{Relationship, parse_relationships, relationship_types};
6use crate::theme::parse_theme;
7use crate::xml::{ParseBudget, XmlElement, parse_xml};
8use crate::{ParseLimits, PptxError};
9
10pub fn parse_pptx(data: &[u8]) -> Result<PptxPackage, PptxError> {
11 parse_pptx_with_limits(data, &ParseLimits::default())
12}
13
14pub fn parse_pptx_with_limits(data: &[u8], limits: &ParseLimits) -> Result<PptxPackage, PptxError> {
15 let source_parts = ooxml_opc::unzip_parts(data).map_err(PptxError::Container)?;
16 let parts: HashMap<&str, &[u8]> = source_parts
17 .iter()
18 .map(|(path, bytes)| (path.as_str(), bytes.as_slice()))
19 .collect();
20 let mut budget = ParseBudget::new(limits);
21 let relationships = parse_package_relationships(&source_parts, &mut budget)?;
22 let presentation_path = relationships
23 .get("")
24 .and_then(|entries| {
25 entries
26 .iter()
27 .find(|relationship| relationship.has_type("/officeDocument"))
28 })
29 .and_then(|relationship| relationship.resolved_target.clone())
30 .unwrap_or_else(|| "ppt/presentation.xml".to_owned());
31 let presentation_root = parse_part(&parts, &presentation_path, &mut budget)?;
32 let presentation_relationships = relationships
33 .get(&presentation_path)
34 .map(Vec::as_slice)
35 .unwrap_or_default();
36 let presentation = parse_presentation(
37 &presentation_root,
38 &presentation_path,
39 presentation_relationships,
40 )?;
41
42 let mut slides = Vec::with_capacity(presentation.slides.len());
43 for reference in &presentation.slides {
44 let root = parse_part(&parts, &reference.part_path, &mut budget)?;
45 let slide_relationships = relationships
46 .get(&reference.part_path)
47 .map(Vec::as_slice)
48 .unwrap_or_default();
49 let data = common_slide_data(
50 &root,
51 slide_relationships,
52 &reference.part_path,
53 &mut budget,
54 )?;
55 slides.push(Slide {
56 part_path: reference.part_path.clone(),
57 name: data.name,
58 layout_part_path: relationship_by_type(
59 slide_relationships,
60 relationship_types::SLIDE_LAYOUT,
61 ),
62 show_master_shapes: bool_attribute(&root, "showMasterSp", true),
63 background: data.background,
64 shapes: data.shapes,
65 });
66 }
67
68 let master_paths = ordered_part_paths(
69 presentation.master_part_paths.clone(),
70 &source_parts,
71 "ppt/slideMasters/",
72 );
73 let mut masters = Vec::with_capacity(master_paths.len());
74 for part_path in &master_paths {
75 let root = parse_part(&parts, part_path, &mut budget)?;
76 let master_relationships = relationships
77 .get(part_path)
78 .map(Vec::as_slice)
79 .unwrap_or_default();
80 let data = common_slide_data(&root, master_relationships, part_path, &mut budget)?;
81 masters.push(SlideMaster {
82 part_path: part_path.clone(),
83 name: data.name,
84 theme_part_path: relationship_by_type(master_relationships, relationship_types::THEME),
85 layout_part_paths: master_relationships
86 .iter()
87 .filter(|relationship| relationship.has_type(relationship_types::SLIDE_LAYOUT))
88 .filter_map(|relationship| relationship.resolved_target.clone())
89 .collect(),
90 background: data.background,
91 shapes: data.shapes,
92 text_styles: parse_text_styles(&root),
93 });
94 }
95
96 let layout_paths = ordered_part_paths(
97 masters
98 .iter()
99 .flat_map(|master| master.layout_part_paths.iter().cloned())
100 .collect(),
101 &source_parts,
102 "ppt/slideLayouts/",
103 );
104 let mut layouts = Vec::with_capacity(layout_paths.len());
105 for part_path in &layout_paths {
106 let root = parse_part(&parts, part_path, &mut budget)?;
107 let layout_relationships = relationships
108 .get(part_path)
109 .map(Vec::as_slice)
110 .unwrap_or_default();
111 let data = common_slide_data(&root, layout_relationships, part_path, &mut budget)?;
112 layouts.push(SlideLayout {
113 part_path: part_path.clone(),
114 name: root
115 .attribute("matchingName")
116 .map(str::to_owned)
117 .or(data.name),
118 layout_type: root.attribute("type").map(str::to_owned),
119 master_part_path: relationship_by_type(
120 layout_relationships,
121 relationship_types::SLIDE_MASTER,
122 ),
123 show_master_shapes: bool_attribute(&root, "showMasterSp", true),
124 background: data.background,
125 shapes: data.shapes,
126 });
127 }
128
129 let theme_paths = ordered_part_paths(
130 masters
131 .iter()
132 .filter_map(|master| master.theme_part_path.clone())
133 .collect(),
134 &source_parts,
135 "ppt/theme/",
136 );
137 let mut themes = Vec::with_capacity(theme_paths.len());
138 for part_path in theme_paths {
139 let root = parse_part(&parts, &part_path, &mut budget)?;
140 themes.push(ThemePart {
141 part_path,
142 theme: parse_theme(&root),
143 });
144 }
145
146 let content_types = parse_content_types(&parts, &mut budget)?;
147 let media = source_parts
148 .iter()
149 .filter(|(path, _)| path.starts_with("ppt/media/"))
150 .map(|(path, bytes)| MediaPart {
151 part_path: path.clone(),
152 content_type: content_type_for(path, &content_types),
153 bytes: bytes.clone(),
154 })
155 .collect();
156 let parts = source_parts
157 .into_iter()
158 .map(|(path, bytes)| PackagePart { path, bytes })
159 .collect();
160 Ok(PptxPackage {
161 presentation,
162 slides,
163 layouts,
164 masters,
165 themes,
166 media,
167 relationships,
168 parts,
169 })
170}
171
172pub fn write_pptx(package: &PptxPackage) -> Result<Vec<u8>, PptxError> {
173 let parts = package
174 .parts
175 .iter()
176 .map(|part| (part.path.clone(), part.bytes.clone()))
177 .collect::<Vec<_>>();
178 ooxml_opc::rezip_parts(&parts).map_err(PptxError::Container)
179}
180
181fn parse_package_relationships(
182 parts: &[(String, Vec<u8>)],
183 budget: &mut ParseBudget<'_>,
184) -> Result<BTreeMap<String, Vec<Relationship>>, PptxError> {
185 let mut output = BTreeMap::new();
186 for (path, bytes) in parts {
187 let Some(source) = relationship_source(path) else {
188 continue;
189 };
190 let parsed = parse_relationships(bytes, path, &source, budget)?;
191 output.insert(source, parsed);
192 }
193 Ok(output)
194}
195
196fn relationship_source(path: &str) -> Option<String> {
197 if path == "_rels/.rels" {
198 return Some(String::new());
199 }
200 let (directory, filename) = path.rsplit_once("/_rels/")?;
201 let source_filename = filename.strip_suffix(".rels")?;
202 Some(format!("{directory}/{source_filename}"))
203}
204
205fn parse_part(
206 parts: &HashMap<&str, &[u8]>,
207 path: &str,
208 budget: &mut ParseBudget<'_>,
209) -> Result<XmlElement, PptxError> {
210 let bytes = parts
211 .get(path)
212 .ok_or_else(|| PptxError::MissingPart(path.to_owned()))?;
213 parse_xml(bytes, path, budget)
214}
215
216fn parse_presentation(
217 root: &XmlElement,
218 part_path: &str,
219 relationships: &[Relationship],
220) -> Result<Presentation, PptxError> {
221 let slide_size = root.child("sldSz");
222 let width_emu = positive_integer_attribute(slide_size, "cx").unwrap_or(12_192_000);
223 let height_emu = positive_integer_attribute(slide_size, "cy").unwrap_or(6_858_000);
224 let mut slides = Vec::new();
225 if let Some(list) = root.child("sldIdLst") {
226 for slide in list.children_named("sldId") {
227 let relationship_id = slide
228 .attribute("r:id")
229 .or_else(|| slide.attribute_local("id"))
230 .unwrap_or_default()
231 .to_owned();
232 let part_path =
233 relationship_target(relationships, &relationship_id).ok_or_else(|| {
234 PptxError::MissingPart(format!("slide relationship {relationship_id}"))
235 })?;
236 slides.push(SlideReference {
237 id: slide
238 .attribute("id")
239 .and_then(|value| value.parse().ok())
240 .unwrap_or_default(),
241 relationship_id,
242 part_path,
243 });
244 }
245 }
246 let master_part_paths = root
247 .child("sldMasterIdLst")
248 .into_iter()
249 .flat_map(|list| list.children_named("sldMasterId"))
250 .filter_map(|master| {
251 master
252 .attribute("r:id")
253 .or_else(|| master.attribute_local("id"))
254 .and_then(|id| relationship_target(relationships, id))
255 })
256 .collect();
257 Ok(Presentation {
258 part_path: part_path.to_owned(),
259 width_emu,
260 height_emu,
261 slides,
262 master_part_paths,
263 })
264}
265
266#[derive(Default)]
267struct ContentTypes {
268 defaults: HashMap<String, String>,
269 overrides: HashMap<String, String>,
270}
271
272fn parse_content_types(
273 parts: &HashMap<&str, &[u8]>,
274 budget: &mut ParseBudget<'_>,
275) -> Result<ContentTypes, PptxError> {
276 let root = parse_part(parts, "[Content_Types].xml", budget)?;
277 let mut content_types = ContentTypes::default();
278 for child in root.child_elements() {
279 match child.local_name() {
280 "Default" => {
281 if let (Some(extension), Some(content_type)) =
282 (child.attribute("Extension"), child.attribute("ContentType"))
283 {
284 content_types
285 .defaults
286 .insert(extension.to_ascii_lowercase(), content_type.to_owned());
287 }
288 }
289 "Override" => {
290 if let (Some(part_name), Some(content_type)) =
291 (child.attribute("PartName"), child.attribute("ContentType"))
292 {
293 content_types.overrides.insert(
294 part_name.trim_start_matches('/').to_owned(),
295 content_type.to_owned(),
296 );
297 }
298 }
299 _ => {}
300 }
301 }
302 Ok(content_types)
303}
304
305fn content_type_for(path: &str, content_types: &ContentTypes) -> String {
306 content_types
307 .overrides
308 .get(path)
309 .cloned()
310 .or_else(|| {
311 path.rsplit_once('.').and_then(|(_, extension)| {
312 content_types
313 .defaults
314 .get(&extension.to_ascii_lowercase())
315 .cloned()
316 })
317 })
318 .unwrap_or_else(|| "application/octet-stream".to_owned())
319}
320
321fn ordered_part_paths(
322 preferred: Vec<String>,
323 parts: &[(String, Vec<u8>)],
324 prefix: &str,
325) -> Vec<String> {
326 let mut seen = HashSet::new();
327 preferred
328 .into_iter()
329 .chain(
330 parts
331 .iter()
332 .map(|(path, _)| path.clone())
333 .filter(|path| path.starts_with(prefix) && path.ends_with(".xml")),
334 )
335 .filter(|path| seen.insert(path.clone()))
336 .collect()
337}
338
339fn relationship_by_type(relationships: &[Relationship], suffix: &str) -> Option<String> {
340 relationships
341 .iter()
342 .find(|relationship| relationship.has_type(suffix))
343 .and_then(|relationship| relationship.resolved_target.clone())
344}
345
346fn relationship_target(relationships: &[Relationship], id: &str) -> Option<String> {
347 relationships
348 .iter()
349 .find(|relationship| relationship.id == id)
350 .and_then(|relationship| relationship.resolved_target.clone())
351}
352
353fn positive_integer_attribute(element: Option<&XmlElement>, name: &str) -> Option<i64> {
354 let value = element?.attribute(name)?.parse::<i64>().ok()?;
355 (value > 0 && value <= 1_000_000_000_000_000).then_some(value)
356}
357
358fn bool_attribute(element: &XmlElement, name: &str, default: bool) -> bool {
359 match element.attribute(name) {
360 Some("1" | "true" | "on") => true,
361 Some("0" | "false" | "off") => false,
362 _ => default,
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369
370 const FIXTURE: &[u8] = include_bytes!("../../../apps/demo/public/betteroffice-demo.pptx");
371
372 #[test]
373 fn parses_betteroffice_demo_deck_surface() {
374 let package = parse_pptx(FIXTURE).unwrap();
375 assert_eq!(package.presentation.width_emu, 12_192_000);
376 assert_eq!(package.presentation.height_emu, 6_858_000);
377 assert_eq!(package.slides.len(), 3);
378 assert_eq!(package.layouts.len(), 1);
379 assert_eq!(package.masters.len(), 1);
380 assert_eq!(package.themes.len(), 1);
381 assert_eq!(package.media.len(), 1);
382 assert_eq!(package.themes[0].theme.name, "BetterOffice");
383
384 let kinds = package
385 .slides
386 .iter()
387 .flat_map(|slide| slide.shapes.iter())
388 .map(|shape| match shape {
389 ShapeNode::Shape(_) => "shape",
390 ShapeNode::Picture(_) => "picture",
391 ShapeNode::GraphicFrame(_) => "graphicFrame",
392 ShapeNode::Group(_) => "group",
393 })
394 .collect::<HashSet<_>>();
395 assert_eq!(
396 kinds,
397 HashSet::from(["shape", "picture", "graphicFrame", "group"])
398 );
399 assert!(package.slides.iter().flat_map(|slide| &slide.shapes).any(|shape| {
400 matches!(
401 shape,
402 ShapeNode::Shape(Shape {
403 text: Some(TextBody { paragraphs, .. }),
404 ..
405 }) if paragraphs.iter().flat_map(|paragraph| ¶graph.runs).any(|run| run.text.contains("Rust"))
406 )
407 }));
408 }
409
410 #[test]
411 fn untouched_save_preserves_every_part_byte_and_order() {
412 let package = parse_pptx(FIXTURE).unwrap();
413 let written = write_pptx(&package).unwrap();
414 let before = ooxml_opc::unzip_parts(FIXTURE).unwrap();
415 let after = ooxml_opc::unzip_parts(&written).unwrap();
416 assert_eq!(after, before);
417 }
418
419 #[test]
420 fn package_limits_apply_across_xml_parts() {
421 let limits = ParseLimits {
422 max_xml_bytes: 100,
423 ..ParseLimits::default()
424 };
425 assert!(matches!(
426 parse_pptx_with_limits(FIXTURE, &limits),
427 Err(PptxError::ResourceLimit {
428 kind: "xmlBytes",
429 ..
430 })
431 ));
432 }
433}