Skip to main content

odox_core/doc/
pres.rs

1//! A presentation: `.odp`.
2//!
3//! A presentation's body is a sequence of `draw:page` elements, each naming the
4//! master page it draws its background and its placeholder geometry from. The
5//! shapes on a page carry their own position and size in `svg:` attributes, in
6//! the page's coordinate space, which is what makes a slide drawable without
7//! laying anything out: a renderer scales the page to the space it has and puts
8//! each shape where the document says.
9//
10// Author: David M. Anderson
11// Built with AI assistance (Claude, Anthropic)
12
13use super::Document;
14use crate::style::{Family, Fill, PageLayout};
15use crate::xml::{Element, Ns};
16use crate::{Error, media_type};
17
18/// An `OpenDocument` presentation.
19pub struct Presentation {
20    /// The package and everything shared with the other two formats.
21    pub document: Document,
22}
23
24/// One slide.
25pub struct Slide<'a> {
26    /// The `draw:page` element and everything on it.
27    pub element: &'a Element,
28    /// The slide's name, where it has one. An application generates `page1` and
29    /// so on for slides nobody has named.
30    pub name: Option<&'a str>,
31    /// The master page it takes its background and placeholders from.
32    pub master_page: Option<&'a str>,
33}
34
35impl Slide<'_> {
36    /// The drawing-page style the slide names, which carries its background and
37    /// the switches over what its master gives it.
38    pub fn style_name(&self) -> Option<&str> {
39        self.element.attr(&Ns::Draw, "style-name")
40    }
41
42    /// The speaker's notes, which ODF keeps in a `presentation:notes` element on
43    /// the page rather than in a part of its own.
44    pub fn notes(&self) -> Option<&Element> {
45        self.element.child(&Ns::Presentation, "notes")
46    }
47
48    /// The shapes on the slide: every child that is not the notes.
49    ///
50    /// Order is drawing order, back to front, which is the order ODF writes them
51    /// in and the order they have to be drawn in.
52    pub fn shapes(&self) -> impl Iterator<Item = &Element> {
53        self.element
54            .elements()
55            .filter(|e| !e.is(&Ns::Presentation, "notes") && !e.is(&Ns::Office, "forms"))
56    }
57}
58
59impl Presentation {
60    /// Read a `.odp` package.
61    ///
62    /// # Errors
63    ///
64    /// The bytes are not a presentation, or its `content.xml` cannot be read.
65    pub fn read(bytes: &[u8]) -> Result<Self, Error> {
66        let document = Document::read(bytes, media_type::PRESENTATION_ANY)?;
67        Ok(Self { document })
68    }
69
70    /// The slides, in the order they are presented.
71    pub fn slides(&self) -> Vec<Slide<'_>> {
72        let Some(body) = self.document.body_of("presentation") else {
73            return Vec::new();
74        };
75        body.elements()
76            .filter(|e| e.is(&Ns::Draw, "page"))
77            .map(|element| Slide {
78                element,
79                name: element.attr(&Ns::Draw, "name"),
80                master_page: element.attr(&Ns::Draw, "master-page-name"),
81            })
82            .collect()
83    }
84
85    /// The master page a slide names.
86    pub fn master(&self, slide: &Slide<'_>) -> Option<&Element> {
87        self.document.styles.master_page(slide.master_page?)
88    }
89
90    /// What fills the ground behind a slide.
91    ///
92    /// The slide's own drawing-page style first, then its master's, which is
93    /// where a template puts the colour or gradient every slide shares. A slide
94    /// that turns `presentation:background-visible` off gets neither.
95    pub fn background(&self, slide: &Slide<'_>) -> Fill {
96        let own = slide
97            .style_name()
98            .map(|name| self.document.styles.resolve(&Family::DrawingPage, name));
99        if own
100            .as_ref()
101            .and_then(|properties| properties.background_visible)
102            == Some(false)
103        {
104            return Fill::None;
105        }
106        if let Some(fill) = own.map(|properties| properties.graphic.fill())
107            && fill != Fill::None
108        {
109            return fill;
110        }
111        self.master(slide)
112            .and_then(|master| master.attr(&Ns::Draw, "style-name"))
113            .map(|name| self.document.styles.resolve(&Family::DrawingPage, name))
114            .map_or(Fill::None, |properties| properties.graphic.fill())
115    }
116
117    /// The master page's decorations: what is drawn behind a slide before
118    /// anything on the slide itself.
119    ///
120    /// **A child carrying a `presentation:class` is left out**, because that
121    /// attribute is what makes a frame a slot rather than a decoration: the
122    /// slide's own frame of that class takes its place, and what the master
123    /// holds is either a prompt or a field. Left in, a slide gains the words
124    /// *Click to edit Master title style* and a literal `<number>`.
125    ///
126    /// The class and not `presentation:placeholder`, which would be the obvious
127    /// test and is not written reliably: the title frame on `deck.odp`'s master
128    /// carries the prompt text and no such attribute. Measured, not read.
129    ///
130    /// Empty for a slide whose style turns `presentation:background-objects-visible`
131    /// off, which is how a template offers a plain slide.
132    pub fn background_objects(&self, slide: &Slide<'_>) -> Vec<&Element> {
133        let shown = slide
134            .style_name()
135            .map(|name| self.document.styles.resolve(&Family::DrawingPage, name))
136            .and_then(|properties| properties.background_objects_visible);
137        if shown == Some(false) {
138            return Vec::new();
139        }
140        let Some(master) = self.master(slide) else {
141            return Vec::new();
142        };
143        master
144            .elements()
145            .filter(|e| e.attr(&Ns::Draw, "layer") == Some("backgroundobjects"))
146            .filter(|e| e.attr(&Ns::Presentation, "class").is_none())
147            .collect()
148    }
149
150    /// The page geometry a slide is drawn in: the size of its master page's
151    /// layout.
152    ///
153    /// Every shape's position is in this space, so a renderer needs it before it
154    /// can place anything.
155    pub fn page_layout(&self, slide: &Slide<'_>) -> PageLayout {
156        slide
157            .master_page
158            .and_then(|name| self.document.styles.page_layout_of(name))
159            .cloned()
160            .unwrap_or_else(presentation_default)
161    }
162}
163
164/// The slide size of a presentation that declares no page layout.
165///
166/// A presentation's default is landscape where a text document's is portrait, and
167/// ODF's own default for one is the same paper turned on its side.
168fn presentation_default() -> PageLayout {
169    let portrait = PageLayout::default();
170    PageLayout {
171        width: portrait.height,
172        height: portrait.width,
173        margin: portrait.margin,
174    }
175}