1mod animation_model;
2mod legacy;
3mod legacy_cfb;
4mod model;
5mod pptx;
6
7pub use animation_model::*;
8pub use model::*;
9pub use rust_office_art::ParseOptions;
10
11use thiserror::Error;
12
13#[derive(Debug, Error)]
14pub enum ParseError {
15 #[error("input is empty")]
16 Empty,
17 #[error("input exceeds the configured {0} byte limit")]
18 InputLimit(usize),
19 #[error("unsupported presentation format")]
20 UnsupportedFormat,
21 #[error("encrypted presentations are not supported")]
22 Encrypted,
23 #[error("corrupt presentation: {0}")]
24 Corrupt(String),
25 #[error("resource limit exceeded: {0}")]
26 ResourceLimit(String),
27}
28
29#[derive(Debug, Clone)]
30pub struct ParseLimits {
31 pub max_input_bytes: usize,
32 pub max_entries: usize,
33 pub max_entry_bytes: usize,
34 pub max_total_uncompressed_bytes: usize,
35 pub max_xml_depth: usize,
36 pub max_xml_nodes: usize,
37 pub max_xml_attributes: usize,
38 pub max_xml_text_bytes: usize,
39 pub max_xml_attribute_bytes: usize,
40}
41
42impl Default for ParseLimits {
43 fn default() -> Self {
44 Self {
45 max_input_bytes: usize::MAX,
46 max_entries: 20_000,
47 max_entry_bytes: usize::MAX,
48 max_total_uncompressed_bytes: usize::MAX,
49 max_xml_depth: 256,
50 max_xml_nodes: 500_000,
51 max_xml_attributes: 1_000_000,
52 max_xml_text_bytes: 64 * 1024 * 1024,
53 max_xml_attribute_bytes: 32 * 1024 * 1024,
54 }
55 }
56}
57
58pub fn detect_format(bytes: &[u8]) -> Result<PresentationFormat, ParseError> {
59 if bytes.is_empty() {
60 return Err(ParseError::Empty);
61 }
62 if bytes.starts_with(b"PK\x03\x04")
63 || bytes.starts_with(b"PK\x05\x06")
64 || bytes.starts_with(b"PK\x07\x08")
65 {
66 return Ok(PresentationFormat::Pptx);
67 }
68 const OLE_MAGIC: &[u8; 8] = b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1";
69 if bytes.starts_with(OLE_MAGIC) {
70 return Ok(PresentationFormat::Ppt);
71 }
72 Err(ParseError::UnsupportedFormat)
73}
74
75pub fn parse_presentation(
76 bytes: &[u8],
77 limits: &ParseLimits,
78) -> Result<PresentationDocument, ParseError> {
79 parse_presentation_with_options(bytes, limits, &ParseOptions::default())
80}
81
82pub fn parse_presentation_with_options(
83 bytes: &[u8],
84 limits: &ParseLimits,
85 options: &ParseOptions,
86) -> Result<PresentationDocument, ParseError> {
87 if bytes.len() > limits.max_input_bytes {
88 return Err(ParseError::InputLimit(limits.max_input_bytes));
89 }
90 match detect_format(bytes)? {
91 PresentationFormat::Pptx => pptx::parse(bytes, limits, options),
92 PresentationFormat::Ppt => legacy::parse(bytes, limits),
93 }
94}
95
96#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
102#[serde(rename_all = "camelCase", default)]
103pub struct ArtworkTarget {
104 pub slide_index: Option<usize>,
105 pub layout_id: Option<String>,
106 pub master_id: Option<String>,
107 pub node_id: String,
108}
109
110#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
112#[serde(rename_all = "camelCase")]
113pub struct NodeArtwork {
114 pub svg: String,
115 #[serde(default, skip_serializing_if = "Option::is_none")]
116 pub paint_bounds: Option<rust_office_art::PaintBounds>,
117 #[serde(default, skip_serializing_if = "Option::is_none")]
118 pub fallback_picture: Option<Box<PictureArtwork>>,
119}
120
121pub fn render_node_artwork(
126 bytes: &[u8],
127 limits: &ParseLimits,
128 options: &ParseOptions,
129 target: &ArtworkTarget,
130) -> Result<Option<NodeArtwork>, ParseError> {
131 if bytes.len() > limits.max_input_bytes {
132 return Err(ParseError::InputLimit(limits.max_input_bytes));
133 }
134 let document = match detect_format(bytes)? {
135 PresentationFormat::Pptx => pptx::parse_with_artwork_target(bytes, limits, options, Some(target))?,
136 PresentationFormat::Ppt => return Ok(None),
137 };
138 let nodes = if let Some(index) = target.slide_index {
139 match document.slides.get(index) {
140 Some(slide) => &slide.nodes,
141 None => return Ok(None),
142 }
143 } else if let Some(id) = &target.layout_id {
144 match document.layouts.iter().find(|layout| &layout.id == id) {
145 Some(layout) => &layout.nodes,
146 None => return Ok(None),
147 }
148 } else if let Some(id) = &target.master_id {
149 match document.masters.iter().find(|master| &master.id == id) {
150 Some(master) => &master.nodes,
151 None => return Ok(None),
152 }
153 } else {
154 return Ok(None);
155 };
156 Ok(find_node(nodes, &target.node_id).and_then(node_artwork))
157}
158
159pub struct ParsedPresentation {
171 document: PresentationDocument,
172 retained: Option<pptx::RetainedParse>,
173}
174
175impl ParsedPresentation {
176 pub fn document(&self) -> &PresentationDocument {
179 &self.document
180 }
181
182 pub fn into_document(self) -> PresentationDocument {
184 self.document
185 }
186
187 pub fn render_node_artwork(
191 &self,
192 target: &ArtworkTarget,
193 ) -> Result<Option<NodeArtwork>, ParseError> {
194 match &self.retained {
195 Some(retained) => retained.render_node_artwork(&self.document.assets, target),
196 None => Ok(None),
197 }
198 }
199}
200
201pub fn parse_presentation_retained(
205 bytes: &[u8],
206 limits: &ParseLimits,
207 options: &ParseOptions,
208) -> Result<ParsedPresentation, ParseError> {
209 if bytes.len() > limits.max_input_bytes {
210 return Err(ParseError::InputLimit(limits.max_input_bytes));
211 }
212 match detect_format(bytes)? {
213 PresentationFormat::Pptx => {
214 let (document, retained) = pptx::parse_retained(bytes, limits, options)?;
215 Ok(ParsedPresentation {
216 document,
217 retained: Some(retained),
218 })
219 }
220 PresentationFormat::Ppt => Ok(ParsedPresentation {
221 document: legacy::parse(bytes, limits)?,
222 retained: None,
223 }),
224 }
225}
226
227pub(crate) fn find_node<'a>(nodes: &'a [SlideNode], id: &str) -> Option<&'a SlideNode> {
228 for node in nodes {
229 let node_id = match node {
230 SlideNode::Shape { id, .. }
231 | SlideNode::Image { id, .. }
232 | SlideNode::Group { id, .. }
233 | SlideNode::Table { id, .. }
234 | SlideNode::Chart { id, .. }
235 | SlideNode::Media { id, .. }
236 | SlideNode::Unknown { id, .. } => id,
237 };
238 if node_id == id {
239 return Some(node);
240 }
241 if let SlideNode::Group { children, .. } = node {
242 if let Some(found) = find_node(children, id) {
243 return Some(found);
244 }
245 }
246 }
247 None
248}
249
250pub(crate) fn node_artwork(node: &SlideNode) -> Option<NodeArtwork> {
251 match node {
252 SlideNode::Shape {
253 shape_svg: Some(svg),
254 paint_bounds,
255 ..
256 } => Some(NodeArtwork {
257 svg: svg.clone(),
258 paint_bounds: paint_bounds.clone(),
259 fallback_picture: None,
260 }),
261 SlideNode::Image {
262 picture_svg: Some(svg),
263 paint_bounds,
264 fallback_picture,
265 ..
266 } => Some(NodeArtwork {
267 svg: svg.clone(),
268 paint_bounds: paint_bounds.clone(),
269 fallback_picture: fallback_picture.clone(),
270 }),
271 SlideNode::Chart {
272 chart_svg: Some(svg), ..
273 } => Some(NodeArtwork {
274 svg: svg.clone(),
275 paint_bounds: None,
276 fallback_picture: None,
277 }),
278 _ => None,
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 #[test]
287 fn detects_ooxml_and_ole_magic() {
288 assert_eq!(
289 detect_format(b"PK\x03\x04rest").unwrap(),
290 PresentationFormat::Pptx
291 );
292 assert_eq!(
293 detect_format(b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1rest").unwrap(),
294 PresentationFormat::Ppt
295 );
296 }
297
298 #[test]
299 fn rejects_unknown_input() {
300 assert!(matches!(
301 detect_format(b"not powerpoint"),
302 Err(ParseError::UnsupportedFormat)
303 ));
304 }
305}