Skip to main content

pptxboss_core/
presentation.rs

1//! The presentation part (`p:presentation`, ECMA-376 Part 1, 19.2.1.26):
2//! the slide and master id lists that give the deck its order, and the
3//! slide and notes sizes.
4
5use crate::mce::children;
6use crate::model::Emu;
7use crate::opc::Defect;
8use crate::slide::parse_coordinate;
9use crate::xml::{unescape_attr, Event, Ns, Reader, Start, XmlError};
10
11/// One `p:sldId` (19.2.1.33).
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct SlideId {
14    /// `id`; `None` when absent or not a number.
15    pub id: Option<u32>,
16    /// `r:id` naming the Slide part relationship.
17    pub rel_id: String,
18    pub offset: usize,
19}
20
21/// One `p:sldMasterId` (19.2.1.36).
22#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct MasterId {
24    pub id: Option<u32>,
25    pub rel_id: String,
26    pub offset: usize,
27}
28
29/// `p:sldSz` (19.2.1.39).
30#[derive(Clone, Debug, PartialEq, Eq)]
31pub struct SlideSize {
32    pub cx: Emu,
33    pub cy: Emu,
34    /// `type`, e.g. `screen16x9`; `None` means `custom`.
35    pub kind: Option<String>,
36}
37
38/// A section of the slide list (`p14:section`, PowerPoint 2010 and later).
39#[derive(Clone, Debug, Default, PartialEq, Eq)]
40pub struct Section {
41    pub name: String,
42    /// `@id`, a GUID.
43    pub id: Option<String>,
44    /// `p14:sldId/@id` values: slide ids from `sldIdLst`, in section order.
45    pub slide_ids: Vec<u32>,
46}
47
48/// The parsed presentation part.
49#[derive(Clone, Debug, Default, PartialEq, Eq)]
50pub struct Presentation {
51    pub slides: Vec<SlideId>,
52    /// Sections from the `p14:sectionLst` extension, in order; empty when none.
53    pub sections: Vec<Section>,
54    pub masters: Vec<MasterId>,
55    /// `r:id` of the notes master, if listed.
56    pub notes_master: Option<String>,
57    /// `r:id` of the handout master, if listed.
58    pub handout_master: Option<String>,
59    pub slide_size: Option<SlideSize>,
60    pub notes_size: Option<(Emu, Emu)>,
61    /// `firstSlideNum`; 1 when omitted.
62    pub first_slide_num: i32,
63    pub rtl: bool,
64    /// False when the root is not `p:presentation`.
65    pub root_ok: bool,
66    pub defects: Vec<Defect>,
67}
68
69impl Presentation {
70    pub fn parse(xml: &[u8]) -> Result<Self, XmlError> {
71        let mut reader = Reader::new(xml);
72        let root = loop {
73            match reader.next()? {
74                Event::Start(start) => break start,
75                Event::Eof => {
76                    return Err(XmlError {
77                        offset: xml.len(),
78                        msg: "no root element",
79                    })
80                }
81                _ => {}
82            }
83        };
84        let mut presentation = Presentation {
85            first_slide_num: 1,
86            root_ok: root.name.is(Ns::Pml, b"presentation"),
87            ..Presentation::default()
88        };
89        presentation.first_slide_num = attr_i32(&reader, &root, b"firstSlideNum").unwrap_or(1);
90        presentation.rtl = matches!(reader.attr(&root, Ns::None, b"rtl"), Some(b"1" | b"true"));
91        children(&mut reader, &mut |reader, child| {
92            if child.name.ns != Ns::Pml {
93                return Ok(());
94            }
95            match child.name.local {
96                b"sldIdLst" => children(reader, &mut |reader, item| {
97                    if !item.name.is(Ns::Pml, b"sldId") {
98                        return Ok(());
99                    }
100                    match reader
101                        .attr(&item, Ns::Rel, b"id")
102                        .map(unescape_attr)
103                        .filter(|id| !id.is_empty())
104                    {
105                        Some(rel_id) => presentation.slides.push(SlideId {
106                            id: attr_u32(reader, &item, b"id"),
107                            rel_id,
108                            offset: item.offset,
109                        }),
110                        None => presentation.defects.push(Defect {
111                            offset: item.offset,
112                            msg: "sldId without r:id",
113                        }),
114                    }
115                    Ok(())
116                }),
117                b"sldMasterIdLst" => children(reader, &mut |reader, item| {
118                    if !item.name.is(Ns::Pml, b"sldMasterId") {
119                        return Ok(());
120                    }
121                    match reader
122                        .attr(&item, Ns::Rel, b"id")
123                        .map(unescape_attr)
124                        .filter(|id| !id.is_empty())
125                    {
126                        Some(rel_id) => presentation.masters.push(MasterId {
127                            id: attr_u32(reader, &item, b"id"),
128                            rel_id,
129                            offset: item.offset,
130                        }),
131                        None => presentation.defects.push(Defect {
132                            offset: item.offset,
133                            msg: "sldMasterId without r:id",
134                        }),
135                    }
136                    Ok(())
137                }),
138                b"notesMasterIdLst" => children(reader, &mut |reader, item| {
139                    if item.name.is(Ns::Pml, b"notesMasterId")
140                        && presentation.notes_master.is_none()
141                    {
142                        presentation.notes_master =
143                            reader.attr(&item, Ns::Rel, b"id").map(unescape_attr);
144                    }
145                    Ok(())
146                }),
147                b"handoutMasterIdLst" => children(reader, &mut |reader, item| {
148                    if item.name.is(Ns::Pml, b"handoutMasterId")
149                        && presentation.handout_master.is_none()
150                    {
151                        presentation.handout_master =
152                            reader.attr(&item, Ns::Rel, b"id").map(unescape_attr);
153                    }
154                    Ok(())
155                }),
156                b"sldSz" => {
157                    let cx = reader
158                        .attr(&child, Ns::None, b"cx")
159                        .and_then(parse_coordinate);
160                    let cy = reader
161                        .attr(&child, Ns::None, b"cy")
162                        .and_then(parse_coordinate);
163                    match (cx, cy) {
164                        (Some(cx), Some(cy)) => {
165                            presentation.slide_size = Some(SlideSize {
166                                cx,
167                                cy,
168                                kind: reader.attr(&child, Ns::None, b"type").map(unescape_attr),
169                            });
170                        }
171                        _ => presentation.defects.push(Defect {
172                            offset: child.offset,
173                            msg: "sldSz without numeric cx and cy",
174                        }),
175                    }
176                    Ok(())
177                }
178                b"notesSz" => {
179                    let cx = reader
180                        .attr(&child, Ns::None, b"cx")
181                        .and_then(parse_coordinate);
182                    let cy = reader
183                        .attr(&child, Ns::None, b"cy")
184                        .and_then(parse_coordinate);
185                    match (cx, cy) {
186                        (Some(cx), Some(cy)) => presentation.notes_size = Some((cx, cy)),
187                        _ => presentation.defects.push(Defect {
188                            offset: child.offset,
189                            msg: "notesSz without numeric cx and cy",
190                        }),
191                    }
192                    Ok(())
193                }
194                b"extLst" => children(reader, &mut |reader, _| {
195                    children(reader, &mut |reader, inner| {
196                        if !inner.name.is(Ns::P14, b"sectionLst") {
197                            return Ok(());
198                        }
199                        parse_sections(reader, &mut presentation.sections)
200                    })
201                }),
202                _ => Ok(()),
203            }
204        })?;
205        Ok(presentation)
206    }
207}
208
209/// Reads `p14:section` children of a `p14:sectionLst` into `sections`.
210fn parse_sections<'a>(
211    reader: &mut Reader<'a>,
212    sections: &mut Vec<Section>,
213) -> Result<(), XmlError> {
214    children(reader, &mut |reader, section| {
215        if !section.name.is(Ns::P14, b"section") {
216            return Ok(());
217        }
218        let mut item = Section {
219            name: reader
220                .attr(&section, Ns::None, b"name")
221                .map(unescape_attr)
222                .unwrap_or_default(),
223            id: reader.attr(&section, Ns::None, b"id").map(unescape_attr),
224            slide_ids: Vec::new(),
225        };
226        children(reader, &mut |reader, list| {
227            if !list.name.is(Ns::P14, b"sldIdLst") {
228                return Ok(());
229            }
230            children(reader, &mut |reader, slide| {
231                if slide.name.is(Ns::P14, b"sldId") {
232                    if let Some(id) = attr_u32(reader, &slide, b"id") {
233                        item.slide_ids.push(id);
234                    }
235                }
236                Ok(())
237            })
238        })?;
239        sections.push(item);
240        Ok(())
241    })
242}
243
244fn attr_u32(reader: &Reader<'_>, start: &Start<'_>, local: &[u8]) -> Option<u32> {
245    std::str::from_utf8(reader.attr(start, Ns::None, local)?)
246        .ok()?
247        .trim()
248        .parse()
249        .ok()
250}
251
252fn attr_i32(reader: &Reader<'_>, start: &Start<'_>, local: &[u8]) -> Option<i32> {
253    std::str::from_utf8(reader.attr(start, Ns::None, local)?)
254        .ok()?
255        .trim()
256        .parse()
257        .ok()
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263
264    const XML: &[u8] = br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
265<p:presentation xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" firstSlideNum="5" rtl="1" saveSubsetFonts="1">
266<p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId1"/></p:sldMasterIdLst>
267<p:notesMasterIdLst><p:notesMasterId r:id="rId6"/></p:notesMasterIdLst>
268<p:handoutMasterIdLst><p:handoutMasterId r:id="rId7"/></p:handoutMasterIdLst>
269<p:sldIdLst><p:sldId id="256" r:id="rId2"/><p:sldId id="257" r:id="rId3"/><p:sldId id="x" r:id="rId4"/><p:sldId id="259"/></p:sldIdLst>
270<p:sldSz cx="12192000" cy="6858000" type="screen16x9"/><p:notesSz cx="6858000" cy="9144000"/>
271<p:defaultTextStyle><a:defPPr/></p:defaultTextStyle>
272</p:presentation>"#;
273
274    #[test]
275    fn slide_order_masters_and_sizes_are_read() {
276        let presentation = Presentation::parse(XML).unwrap();
277        assert!(presentation.root_ok);
278        assert_eq!(presentation.first_slide_num, 5);
279        assert!(presentation.rtl);
280        assert_eq!(presentation.slides.len(), 3);
281        assert_eq!(presentation.slides[0].id, Some(256));
282        assert_eq!(presentation.slides[0].rel_id, "rId2");
283        assert_eq!(presentation.slides[2].id, None);
284        assert_eq!(presentation.slides[2].rel_id, "rId4");
285        assert_eq!(presentation.defects.len(), 1);
286        assert_eq!(presentation.defects[0].msg, "sldId without r:id");
287        assert_eq!(presentation.masters.len(), 1);
288        assert_eq!(presentation.masters[0].id, Some(2147483648));
289        assert_eq!(presentation.notes_master.as_deref(), Some("rId6"));
290        assert_eq!(presentation.handout_master.as_deref(), Some("rId7"));
291        assert_eq!(
292            presentation.slide_size,
293            Some(SlideSize {
294                cx: 12192000,
295                cy: 6858000,
296                kind: Some("screen16x9".into())
297            })
298        );
299        assert_eq!(presentation.notes_size, Some((6858000, 9144000)));
300    }
301
302    #[test]
303    fn a_minimal_presentation_and_a_wrong_root_parse() {
304        let minimal = br#"<p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:notesSz cx="913607" cy="913607"/></p:presentation>"#;
305        let presentation = Presentation::parse(minimal).unwrap();
306        assert!(presentation.slides.is_empty());
307        assert_eq!(presentation.first_slide_num, 1);
308        assert_eq!(presentation.notes_size, Some((913607, 913607)));
309        let wrong = Presentation::parse(b"<x/>").unwrap();
310        assert!(!wrong.root_ok);
311        assert!(Presentation::parse(b"").is_err());
312    }
313}