1pub type Emu = i64;
8
9#[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 pub rot: i64,
18 pub flip_h: bool,
19 pub flip_v: bool,
20}
21
22#[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#[derive(Clone, Debug, Default, PartialEq, Eq)]
33pub struct Placeholder {
34 pub kind: PlaceholderKind,
36 pub idx: u32,
38}
39
40#[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 pub fn is_title(self) -> bool {
88 matches!(self, PlaceholderKind::Title | PlaceholderKind::CenterTitle)
89 }
90
91 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#[derive(Clone, Debug, Default, PartialEq, Eq)]
128pub enum Bullet {
129 #[default]
131 Inherited,
132 None,
133 Char(String),
134 AutoNumber {
135 scheme: String,
136 start_at: u32,
137 },
138 Picture,
139}
140
141#[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 pub size: Option<u32>,
150 pub hyperlink: Option<String>,
152 pub lang: Option<String>,
153 pub typeface: Option<String>,
154}
155
156#[derive(Clone, Debug, PartialEq, Eq)]
158pub enum RunKind {
159 Text,
160 LineBreak,
162 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#[derive(Clone, Debug, Default, PartialEq, Eq)]
185pub struct Paragraph {
186 pub level: u8,
188 pub bullet: Bullet,
189 pub runs: Vec<Run>,
190}
191
192impl Paragraph {
193 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#[derive(Clone, Debug, Default, PartialEq, Eq)]
218pub struct TextBody {
219 pub paragraphs: Vec<Paragraph>,
220}
221
222impl TextBody {
223 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#[derive(Clone, Debug, Default, PartialEq, Eq)]
246pub struct Cell {
247 pub body: TextBody,
248 pub grid_span: u32,
249 pub row_span: u32,
250 pub h_merge: bool,
252 pub v_merge: bool,
254}
255
256impl Cell {
257 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#[derive(Clone, Debug, Default, PartialEq, Eq)]
271pub struct Table {
272 pub column_widths: Vec<Emu>,
273 pub rows: Vec<Row>,
274}
275
276#[derive(Clone, Debug, Default, PartialEq, Eq)]
278pub struct Picture {
279 pub embed: Option<String>,
281 pub link: Option<String>,
283 pub media: Option<String>,
285}
286
287#[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#[derive(Clone, Debug, PartialEq, Eq)]
297pub enum Content {
298 Text(TextBody),
300 Picture(Picture),
302 Group(Vec<Shape>, Option<ChildSpace>),
304 Table(Table),
306 Chart(Option<String>),
308 Diagram(Option<String>),
310 Ole(OleObject),
312 Connector,
314 ContentPart(Option<String>),
316 UnknownGraphic(String),
318}
319
320#[derive(Clone, Debug, PartialEq, Eq)]
322pub struct Shape {
323 pub id: u32,
325 pub name: String,
327 pub hidden: bool,
329 pub description: Option<String>,
331 pub hyperlink: Option<String>,
333 pub placeholder: Option<Placeholder>,
334 pub transform: Option<Transform>,
335 pub text_box: bool,
337 pub content: Content,
338}
339
340impl Shape {
341 pub fn text_body(&self) -> Option<&TextBody> {
343 match &self.content {
344 Content::Text(body) => Some(body),
345 _ => None,
346 }
347 }
348
349 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#[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 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#[derive(Clone, Debug, PartialEq, Eq)]
396pub struct SlideContent {
397 pub kind: SlideKind,
398 pub name: Option<String>,
400 pub show: bool,
402 pub shapes: Vec<Shape>,
404}
405
406impl SlideContent {
407 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 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}