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}