Skip to main content

pptxboss_core/
model.rs

1//! The slide content model: the shape tree of a slide-family part with its
2//! text bodies, pictures, tables and groups (ECMA-376 Part 1, 19.3 and
3//! 21.1). Only what a reader needs to extract content and structure is
4//! kept; formatting beyond run-level emphasis is not modelled.
5
6/// A length in English Metric Units: 914400 per inch, 12700 per point.
7pub type Emu = i64;
8
9/// Position and size of a shape (20.1.7.6).
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
11pub struct Transform {
12    pub x: Emu,
13    pub y: Emu,
14    pub cx: Emu,
15    pub cy: Emu,
16    /// Rotation in 60,000ths of a degree, clockwise.
17    pub rot: i64,
18    pub flip_h: bool,
19    pub flip_v: bool,
20}
21
22/// A group's child coordinate space (20.1.7.5).
23#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
24pub struct ChildSpace {
25    pub x: Emu,
26    pub y: Emu,
27    pub cx: Emu,
28    pub cy: Emu,
29}
30
31/// A placeholder declaration (19.3.1.36).
32#[derive(Clone, Debug, Default, PartialEq, Eq)]
33pub struct Placeholder {
34    /// `type`; `obj` when omitted.
35    pub kind: PlaceholderKind,
36    /// `idx`; 0 when omitted.
37    pub idx: u32,
38}
39
40/// `ST_PlaceholderType` (19.7.10).
41#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
42pub enum PlaceholderKind {
43    Title,
44    Body,
45    CenterTitle,
46    Subtitle,
47    DateTime,
48    SlideNumber,
49    Footer,
50    Header,
51    #[default]
52    Object,
53    Chart,
54    Table,
55    ClipArt,
56    Diagram,
57    Media,
58    Picture,
59    SlideImage,
60    Other,
61}
62
63impl PlaceholderKind {
64    pub fn parse(value: &[u8]) -> PlaceholderKind {
65        match value {
66            b"title" => PlaceholderKind::Title,
67            b"body" => PlaceholderKind::Body,
68            b"ctrTitle" => PlaceholderKind::CenterTitle,
69            b"subTitle" => PlaceholderKind::Subtitle,
70            b"dt" => PlaceholderKind::DateTime,
71            b"sldNum" => PlaceholderKind::SlideNumber,
72            b"ftr" => PlaceholderKind::Footer,
73            b"hdr" => PlaceholderKind::Header,
74            b"obj" => PlaceholderKind::Object,
75            b"chart" => PlaceholderKind::Chart,
76            b"tbl" => PlaceholderKind::Table,
77            b"clipArt" => PlaceholderKind::ClipArt,
78            b"dgm" => PlaceholderKind::Diagram,
79            b"media" => PlaceholderKind::Media,
80            b"pic" => PlaceholderKind::Picture,
81            b"sldImg" => PlaceholderKind::SlideImage,
82            _ => PlaceholderKind::Other,
83        }
84    }
85
86    /// True for the title and centered title placeholders.
87    pub fn is_title(self) -> bool {
88        matches!(self, PlaceholderKind::Title | PlaceholderKind::CenterTitle)
89    }
90
91    /// True for the date, footer and slide number placeholders, whose text
92    /// is presentation furniture rather than slide content.
93    pub fn is_furniture(self) -> bool {
94        matches!(
95            self,
96            PlaceholderKind::DateTime
97                | PlaceholderKind::SlideNumber
98                | PlaceholderKind::Footer
99                | PlaceholderKind::Header
100        )
101    }
102
103    pub fn as_str(self) -> &'static str {
104        match self {
105            PlaceholderKind::Title => "title",
106            PlaceholderKind::Body => "body",
107            PlaceholderKind::CenterTitle => "ctrTitle",
108            PlaceholderKind::Subtitle => "subTitle",
109            PlaceholderKind::DateTime => "dt",
110            PlaceholderKind::SlideNumber => "sldNum",
111            PlaceholderKind::Footer => "ftr",
112            PlaceholderKind::Header => "hdr",
113            PlaceholderKind::Object => "obj",
114            PlaceholderKind::Chart => "chart",
115            PlaceholderKind::Table => "tbl",
116            PlaceholderKind::ClipArt => "clipArt",
117            PlaceholderKind::Diagram => "dgm",
118            PlaceholderKind::Media => "media",
119            PlaceholderKind::Picture => "pic",
120            PlaceholderKind::SlideImage => "sldImg",
121            PlaceholderKind::Other => "other",
122        }
123    }
124}
125
126/// Bullet setting of a paragraph (21.1.2.4).
127#[derive(Clone, Debug, Default, PartialEq, Eq)]
128pub enum Bullet {
129    /// Nothing specified on the paragraph; inherited from the list style.
130    #[default]
131    Inherited,
132    None,
133    Char(String),
134    AutoNumber {
135        scheme: String,
136        start_at: u32,
137    },
138    Picture,
139}
140
141/// Run-level character properties that survive extraction (21.1.2.3.9).
142#[derive(Clone, Debug, Default, PartialEq, Eq)]
143pub struct RunProps {
144    pub bold: Option<bool>,
145    pub italic: Option<bool>,
146    pub underline: Option<bool>,
147    pub strike: Option<bool>,
148    /// Font size in hundredths of a point.
149    pub size: Option<u32>,
150    /// Relationship id of a click hyperlink.
151    pub hyperlink: Option<String>,
152    pub lang: Option<String>,
153    pub typeface: Option<String>,
154}
155
156/// What a run is (21.1.2.3.8, 21.1.2.2.4, 21.1.2.2.1).
157#[derive(Clone, Debug, PartialEq, Eq)]
158pub enum RunKind {
159    Text,
160    /// `a:br`: a line break within the paragraph.
161    LineBreak,
162    /// `a:fld` with its `type`; the text is the cached value.
163    Field(String),
164}
165
166#[derive(Clone, Debug, PartialEq, Eq)]
167pub struct Run {
168    pub kind: RunKind,
169    pub text: String,
170    pub props: RunProps,
171}
172
173impl Run {
174    pub fn text(text: impl Into<String>) -> Self {
175        Self {
176            kind: RunKind::Text,
177            text: text.into(),
178            props: RunProps::default(),
179        }
180    }
181}
182
183/// One `a:p` (21.1.2.2.6).
184#[derive(Clone, Debug, Default, PartialEq, Eq)]
185pub struct Paragraph {
186    /// Indent level 0 to 8.
187    pub level: u8,
188    pub bullet: Bullet,
189    pub runs: Vec<Run>,
190}
191
192impl Paragraph {
193    /// The paragraph's text with line breaks as `\n`.
194    pub fn text(&self) -> String {
195        let mut out = String::new();
196        self.write_text(&mut out);
197        out
198    }
199
200    pub fn write_text(&self, out: &mut String) {
201        for run in &self.runs {
202            match run.kind {
203                RunKind::LineBreak => out.push('\n'),
204                _ => out.push_str(&run.text),
205            }
206        }
207    }
208
209    pub fn is_empty(&self) -> bool {
210        self.runs
211            .iter()
212            .all(|run| run.text.is_empty() && run.kind != RunKind::LineBreak)
213    }
214}
215
216/// A text body (21.1.2.1).
217#[derive(Clone, Debug, Default, PartialEq, Eq)]
218pub struct TextBody {
219    pub paragraphs: Vec<Paragraph>,
220}
221
222impl TextBody {
223    /// Paragraph texts joined by `\n`.
224    pub fn text(&self) -> String {
225        let mut out = String::new();
226        self.write_text(&mut out);
227        out
228    }
229
230    pub fn write_text(&self, out: &mut String) {
231        for (i, paragraph) in self.paragraphs.iter().enumerate() {
232            if i > 0 {
233                out.push('\n');
234            }
235            paragraph.write_text(out);
236        }
237    }
238
239    pub fn is_empty(&self) -> bool {
240        self.paragraphs.iter().all(Paragraph::is_empty)
241    }
242}
243
244/// A table cell (21.1.3.16).
245#[derive(Clone, Debug, Default, PartialEq, Eq)]
246pub struct Cell {
247    pub body: TextBody,
248    pub grid_span: u32,
249    pub row_span: u32,
250    /// Merged into the cell to its left; carries no content of its own.
251    pub h_merge: bool,
252    /// Merged into the cell above it.
253    pub v_merge: bool,
254}
255
256impl Cell {
257    /// False for cells merged away into another cell.
258    pub fn is_origin(&self) -> bool {
259        !self.h_merge && !self.v_merge
260    }
261}
262
263#[derive(Clone, Debug, Default, PartialEq, Eq)]
264pub struct Row {
265    pub height: Emu,
266    pub cells: Vec<Cell>,
267}
268
269/// A table (21.1.3.13).
270#[derive(Clone, Debug, Default, PartialEq, Eq)]
271pub struct Table {
272    pub column_widths: Vec<Emu>,
273    pub rows: Vec<Row>,
274}
275
276/// A picture's image reference (20.1.8.13).
277#[derive(Clone, Debug, Default, PartialEq, Eq)]
278pub struct Picture {
279    /// `r:embed`: relationship id of an image part in the package.
280    pub embed: Option<String>,
281    /// `r:link`: relationship id of an external image.
282    pub link: Option<String>,
283    /// Relationship id of an attached audio or video clip.
284    pub media: Option<String>,
285}
286
287/// An embedded object frame (19.3.2.4).
288#[derive(Clone, Debug, Default, PartialEq, Eq)]
289pub struct OleObject {
290    pub prog_id: Option<String>,
291    pub rel_id: Option<String>,
292    pub preview: Option<Picture>,
293}
294
295/// What a shape holds.
296#[derive(Clone, Debug, PartialEq, Eq)]
297pub enum Content {
298    /// `p:sp`: possibly with a text body; empty when the shape has none.
299    Text(TextBody),
300    /// `p:pic`.
301    Picture(Picture),
302    /// `p:grpSp`: children in z-order, and the group's child coordinate space.
303    Group(Vec<Shape>, Option<ChildSpace>),
304    /// `p:graphicFrame` holding `a:tbl`.
305    Table(Table),
306    /// `p:graphicFrame` holding `c:chart`, with its relationship id.
307    Chart(Option<String>),
308    /// `p:graphicFrame` holding a diagram, with the diagram data relationship id.
309    Diagram(Option<String>),
310    /// `p:graphicFrame` holding `p:oleObj`.
311    Ole(OleObject),
312    /// `p:cxnSp`: a connector; no content.
313    Connector,
314    /// `p:contentPart` with its relationship id.
315    ContentPart(Option<String>),
316    /// A `p:graphicFrame` whose `graphicData` URI the reader does not know.
317    UnknownGraphic(String),
318}
319
320/// One node of the shape tree (19.3.1.45).
321#[derive(Clone, Debug, PartialEq, Eq)]
322pub struct Shape {
323    /// `cNvPr/@id`.
324    pub id: u32,
325    /// `cNvPr/@name`.
326    pub name: String,
327    /// `cNvPr/@hidden`.
328    pub hidden: bool,
329    /// `cNvPr/@descr`, the alternative text.
330    pub description: Option<String>,
331    /// `cNvPr/hlinkClick/@r:id`.
332    pub hyperlink: Option<String>,
333    pub placeholder: Option<Placeholder>,
334    pub transform: Option<Transform>,
335    /// `cNvSpPr/@txBox`: the shape is a text box.
336    pub text_box: bool,
337    pub content: Content,
338}
339
340impl Shape {
341    /// The shape's own text body, if it is a text shape.
342    pub fn text_body(&self) -> Option<&TextBody> {
343        match &self.content {
344            Content::Text(body) => Some(body),
345            _ => None,
346        }
347    }
348
349    /// True when the placeholder is a title or centered title.
350    pub fn is_title(&self) -> bool {
351        self.placeholder
352            .as_ref()
353            .is_some_and(|ph| ph.kind.is_title())
354    }
355}
356
357/// The root element kinds that carry a shape tree.
358#[derive(Clone, Copy, Debug, PartialEq, Eq)]
359pub enum SlideKind {
360    Slide,
361    Layout,
362    Master,
363    Notes,
364    NotesMaster,
365    HandoutMaster,
366}
367
368impl SlideKind {
369    /// The root element name for this kind.
370    pub fn root_name(self) -> &'static str {
371        match self {
372            SlideKind::Slide => "sld",
373            SlideKind::Layout => "sldLayout",
374            SlideKind::Master => "sldMaster",
375            SlideKind::Notes => "notes",
376            SlideKind::NotesMaster => "notesMaster",
377            SlideKind::HandoutMaster => "handoutMaster",
378        }
379    }
380
381    pub fn from_root(local: &[u8]) -> Option<SlideKind> {
382        match local {
383            b"sld" => Some(SlideKind::Slide),
384            b"sldLayout" => Some(SlideKind::Layout),
385            b"sldMaster" => Some(SlideKind::Master),
386            b"notes" => Some(SlideKind::Notes),
387            b"notesMaster" => Some(SlideKind::NotesMaster),
388            b"handoutMaster" => Some(SlideKind::HandoutMaster),
389            _ => None,
390        }
391    }
392}
393
394/// The parsed content of one slide-family part.
395#[derive(Clone, Debug, PartialEq, Eq)]
396pub struct SlideContent {
397    pub kind: SlideKind,
398    /// `cSld/@name`.
399    pub name: Option<String>,
400    /// `sld/@show`; hidden slides are `false`.
401    pub show: bool,
402    /// Shapes in z-order, which is also document and reading order.
403    pub shapes: Vec<Shape>,
404}
405
406impl SlideContent {
407    /// Every shape in document order, descending into groups.
408    pub fn walk(&self) -> impl Iterator<Item = &Shape> {
409        let mut stack: Vec<&Shape> = self.shapes.iter().rev().collect();
410        std::iter::from_fn(move || {
411            let shape = stack.pop()?;
412            if let Content::Group(children, _) = &shape.content {
413                stack.extend(children.iter().rev());
414            }
415            Some(shape)
416        })
417    }
418
419    /// The first title placeholder's text, if any.
420    pub fn title(&self) -> Option<String> {
421        self.walk()
422            .find(|shape| shape.is_title())
423            .and_then(Shape::text_body)
424            .map(TextBody::text)
425            .filter(|text| !text.trim().is_empty())
426    }
427}