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::edit::Refused;
15use crate::style::{Family, Fill, PageLayout};
16use crate::value::Length;
17use crate::xml::{Element, Ns};
18use crate::{Error, media_type};
19
20/// An `OpenDocument` presentation.
21pub struct Presentation {
22    /// The package and everything shared with the other two formats.
23    pub document: Document,
24}
25
26/// One slide.
27pub struct Slide<'a> {
28    /// The `draw:page` element and everything on it.
29    pub element: &'a Element,
30    /// The slide's name, where it has one. An application generates `page1` and
31    /// so on for slides nobody has named.
32    pub name: Option<&'a str>,
33    /// The master page it takes its background and placeholders from.
34    pub master_page: Option<&'a str>,
35    /// Where the `draw:page` sits among the body's children, so that the
36    /// slide can be reached again for changing without holding a reference.
37    pub position: usize,
38}
39
40impl Slide<'_> {
41    /// The drawing-page style the slide names, which carries its background and
42    /// the switches over what its master gives it.
43    pub fn style_name(&self) -> Option<&str> {
44        self.element.attr(&Ns::Draw, "style-name")
45    }
46
47    /// The speaker's notes, which ODF keeps in a `presentation:notes` element on
48    /// the page rather than in a part of its own.
49    pub fn notes(&self) -> Option<&Element> {
50        self.element.child(&Ns::Presentation, "notes")
51    }
52
53    /// The shapes on the slide: every child that is not the notes.
54    ///
55    /// Order is drawing order, back to front, which is the order ODF writes them
56    /// in and the order they have to be drawn in.
57    pub fn shapes(&self) -> impl Iterator<Item = &Element> {
58        self.shapes_indexed().map(|(_, shape)| shape)
59    }
60
61    /// The shapes with their index among the page's children, which is what
62    /// [`Presentation::set_geometry`] takes.
63    pub fn shapes_indexed(&self) -> impl Iterator<Item = (usize, &Element)> {
64        self.element
65            .elements_indexed()
66            .filter(|(_, e)| !e.is(&Ns::Presentation, "notes") && !e.is(&Ns::Office, "forms"))
67    }
68}
69
70impl Presentation {
71    /// Read a `.odp` package.
72    ///
73    /// # Errors
74    ///
75    /// The bytes are not a presentation, or its `content.xml` cannot be read.
76    pub fn read(bytes: &[u8]) -> Result<Self, Error> {
77        let document = Document::read(bytes, media_type::PRESENTATION_ANY)?;
78        Ok(Self { document })
79    }
80
81    /// The slides, in the order they are presented.
82    pub fn slides(&self) -> Vec<Slide<'_>> {
83        let Some(body) = self.document.body_of("presentation") else {
84            return Vec::new();
85        };
86        body.elements_indexed()
87            .filter(|(_, e)| e.is(&Ns::Draw, "page"))
88            .map(|(position, element)| Slide {
89                element,
90                name: element.attr(&Ns::Draw, "name"),
91                master_page: element.attr(&Ns::Draw, "master-page-name"),
92                position,
93            })
94            .collect()
95    }
96
97    /// Move and size a shape: `svg:x`, `svg:y`, `svg:width` and `svg:height`,
98    /// each written in the unit it was read in, in centimetres where it was
99    /// absent. The shape is named by its slide's position among the body's
100    /// children and its own among the page's, as [`Slide::shapes_indexed`]
101    /// gives them.
102    ///
103    /// A shape placed by `draw:transform` states no corner and is not moved
104    /// this way; asking is refused rather than answered wrongly.
105    ///
106    /// # Errors
107    ///
108    /// There is no such slide or shape, or the shape is placed by a transform.
109    pub fn set_geometry(
110        &mut self,
111        slide: usize,
112        shape: usize,
113        x: Length,
114        y: Length,
115        width: Length,
116        height: Length,
117    ) -> Result<(), Refused> {
118        let names = [
119            (self.document.name(&Ns::Svg, "x"), x),
120            (self.document.name(&Ns::Svg, "y"), y),
121            (self.document.name(&Ns::Svg, "width"), width),
122            (self.document.name(&Ns::Svg, "height"), height),
123        ];
124        let element = self
125            .document
126            .content
127            .child_mut(&Ns::Office, "body")
128            .and_then(|body| body.child_mut(&Ns::Office, "presentation"))
129            .and_then(|body| body.at_mut(&[slide, shape]))
130            .ok_or(Refused::NotFound)?;
131        if element.attr(&Ns::Draw, "transform").is_some() {
132            return Err(Refused::NotFound);
133        }
134        for (name, length) in names {
135            let unit = element
136                .attr(&name.ns, &name.local)
137                .map_or("cm", Length::unit_of)
138                .to_owned();
139            element.set_attr(name, length.write(&unit));
140        }
141        Ok(())
142    }
143
144    /// The `draw:page` element at a position among the body's children, for
145    /// changing what is on it.
146    pub fn page_mut(&mut self, position: usize) -> Option<&mut Element> {
147        self.document
148            .content
149            .child_mut(&Ns::Office, "body")?
150            .child_mut(&Ns::Office, "presentation")?
151            .at_mut(&[position])
152    }
153
154    /// Where the slide at a position among the body's children is under the
155    /// content root, as a path of child indices: what an editor addresses its
156    /// labels' paragraphs from.
157    pub fn page_path(&self, position: usize) -> Option<Vec<usize>> {
158        let mut path = self.document.body_path("presentation")?;
159        path.push(position);
160        Some(path)
161    }
162
163    /// The master page a slide names.
164    pub fn master(&self, slide: &Slide<'_>) -> Option<&Element> {
165        self.document.styles.master_page(slide.master_page?)
166    }
167
168    /// What fills the ground behind a slide.
169    ///
170    /// The slide's own drawing-page style first, then its master's, which is
171    /// where a template puts the colour or gradient every slide shares. A slide
172    /// that turns `presentation:background-visible` off gets neither.
173    pub fn background(&self, slide: &Slide<'_>) -> Fill {
174        let own = slide
175            .style_name()
176            .map(|name| self.document.styles.resolve(&Family::DrawingPage, name));
177        if own
178            .as_ref()
179            .and_then(|properties| properties.background_visible)
180            == Some(false)
181        {
182            return Fill::None;
183        }
184        if let Some(fill) = own.map(|properties| properties.graphic.fill())
185            && fill != Fill::None
186        {
187            return fill;
188        }
189        self.master(slide)
190            .and_then(|master| master.attr(&Ns::Draw, "style-name"))
191            .map(|name| self.document.styles.resolve(&Family::DrawingPage, name))
192            .map_or(Fill::None, |properties| properties.graphic.fill())
193    }
194
195    /// The master page's decorations: what is drawn behind a slide before
196    /// anything on the slide itself.
197    ///
198    /// **A child carrying a `presentation:class` is left out**, because that
199    /// attribute is what makes a frame a slot rather than a decoration: the
200    /// slide's own frame of that class takes its place, and what the master
201    /// holds is either a prompt or a field. Left in, a slide gains the words
202    /// *Click to edit Master title style* and a literal `<number>`.
203    ///
204    /// The class and not `presentation:placeholder`, which would be the obvious
205    /// test and is not written reliably: the title frame on `deck.odp`'s master
206    /// carries the prompt text and no such attribute. Measured, not read.
207    ///
208    /// Empty for a slide whose style turns `presentation:background-objects-visible`
209    /// off, which is how a template offers a plain slide.
210    pub fn background_objects(&self, slide: &Slide<'_>) -> Vec<&Element> {
211        let shown = slide
212            .style_name()
213            .map(|name| self.document.styles.resolve(&Family::DrawingPage, name))
214            .and_then(|properties| properties.background_objects_visible);
215        if shown == Some(false) {
216            return Vec::new();
217        }
218        let Some(master) = self.master(slide) else {
219            return Vec::new();
220        };
221        master
222            .elements()
223            .filter(|e| e.attr(&Ns::Draw, "layer") == Some("backgroundobjects"))
224            .filter(|e| e.attr(&Ns::Presentation, "class").is_none())
225            .collect()
226    }
227
228    /// The page geometry a slide is drawn in: the size of its master page's
229    /// layout.
230    ///
231    /// Every shape's position is in this space, so a renderer needs it before it
232    /// can place anything.
233    pub fn page_layout(&self, slide: &Slide<'_>) -> PageLayout {
234        slide
235            .master_page
236            .and_then(|name| self.document.styles.page_layout_of(name))
237            .cloned()
238            .unwrap_or_else(presentation_default)
239    }
240}
241
242/// The slide size of a presentation that declares no page layout.
243///
244/// A presentation's default is landscape where a text document's is portrait, and
245/// ODF's own default for one is the same paper turned on its side.
246fn presentation_default() -> PageLayout {
247    let portrait = PageLayout::default();
248    PageLayout {
249        width: portrait.height,
250        height: portrait.width,
251        margin: portrait.margin,
252    }
253}