Skip to main content

rust_pptx/
lib.rs

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/// One node of a presentation, addressed for on-demand artwork rendering.
97///
98/// Exactly one of `slide_index`, `layout_id` or `master_id` selects the node
99/// list; `node_id` is the node's `id` in the parsed model, including the
100/// `layout:` or `master:` prefix of inherited decorative nodes.
101#[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/// Generated artwork for one node, as the eager parse would have stored it.
111#[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
121/// Renders the artwork of one node. The presentation is parsed again with
122/// every other node's artwork deferred, so the cost is one parse without
123/// rendering plus the one render. The result equals the eager parse's artwork
124/// for that node, or `None` when the node has none or does not exist.
125pub 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
159/// A presentation parsed once with artwork deferred, kept for rendering many
160/// nodes on demand.
161///
162/// Besides the deferred model, the handle retains the inflated slide, layout,
163/// master and theme parts with their XML trees, relationships, color maps and
164/// related chart, diagram, ink and control parts, so a render never reads the
165/// archive again. Each render parses only the node list that can hold the
166/// target and renders the one node; the result equals the eager parse's
167/// artwork for that node and what [`render_node_artwork`] returns. The handle
168/// is larger than the model alone and is not `Sync`; drop it when the viewer
169/// closes the presentation.
170pub struct ParsedPresentation {
171    document: PresentationDocument,
172    retained: Option<pptx::RetainedParse>,
173}
174
175impl ParsedPresentation {
176    /// The model, as `parse_presentation_with_options` returns it with
177    /// `defer_artwork` set.
178    pub fn document(&self) -> &PresentationDocument {
179        &self.document
180    }
181
182    /// Gives up the retained state and keeps the model.
183    pub fn into_document(self) -> PresentationDocument {
184        self.document
185    }
186
187    /// Renders the artwork of one node from the retained state, or `None` when
188    /// the node has none or does not exist. Legacy binary presentations have no
189    /// generated artwork and always yield `None`.
190    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
201/// Parses a presentation with artwork deferred and keeps what later renders
202/// need. `options.defer_artwork` is treated as set; the other options apply as
203/// in [`parse_presentation_with_options`].
204pub 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}