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}