1#![deny(
8 clippy::unwrap_used,
9 clippy::expect_used,
10 clippy::panic,
11 clippy::unimplemented,
12 clippy::todo,
13 clippy::unreachable
14)]
15#![forbid(unsafe_code)]
16
17pub mod anim;
18pub mod chart;
19pub mod defaults;
20pub mod edit;
21pub mod resolve;
22pub mod style;
23pub mod table;
24pub mod text;
25pub mod theme;
26pub mod words;
27
28use std::sync::Arc;
29
30pub use deckcraft_color::{ColorScheme, ColorTransform, Rgba, SchemeSlot};
31pub use deckcraft_geom::{Rect, Size, Xfrm};
32use serde::{Deserialize, Serialize};
33
34pub use anim::{AnimClass, AnimStart, Animation, Transition};
35pub use chart::Chart;
36pub use style::{ColorBase, ColorRef, Effects, Fill, Line, ShapeStyle};
37pub use table::{Cell, Table};
38pub use text::{Paragraph, Run, RunProps, TextBody};
39pub use theme::Theme;
40
41pub(crate) fn yes() -> bool {
42 true
43}
44
45macro_rules! id_type {
46 ($name:ident) => {
47 #[derive(
48 Clone,
49 Copy,
50 Debug,
51 Default,
52 PartialEq,
53 Eq,
54 PartialOrd,
55 Ord,
56 Hash,
57 Serialize,
58 Deserialize,
59 )]
60 #[serde(transparent)]
61 pub struct $name(pub u32);
62 impl std::fmt::Display for $name {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 write!(f, "{}", self.0)
65 }
66 }
67 };
68}
69id_type!(SlideId);
70id_type!(ShapeId);
71id_type!(LayoutId);
72id_type!(MasterId);
73id_type!(MediaId);
74
75#[derive(Debug, thiserror::Error)]
76pub enum ModelError {
77 #[error("no slide {0}")]
78 NoSlide(String),
79 #[error("no shape {0}")]
80 NoShape(ShapeId),
81 #[error("no layout {0}")]
82 NoLayout(LayoutId),
83 #[error("{0}")]
84 Invalid(String),
85}
86
87pub type Result<T> = std::result::Result<T, ModelError>;
88
89#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
91#[serde(rename_all = "camelCase")]
92pub enum PhType {
93 Title,
94 CtrTitle,
95 SubTitle,
96 #[default]
97 Body,
98 Obj,
99 Chart,
100 Table,
101 ClipArt,
102 Diagram,
103 Media,
104 Picture,
105 Date,
106 Footer,
107 SlideNum,
108 Header,
109 SlideImage,
110}
111
112impl PhType {
113 pub fn xml(self) -> &'static str {
114 match self {
115 PhType::Title => "title",
116 PhType::CtrTitle => "ctrTitle",
117 PhType::SubTitle => "subTitle",
118 PhType::Body => "body",
119 PhType::Obj => "obj",
120 PhType::Chart => "chart",
121 PhType::Table => "tbl",
122 PhType::ClipArt => "clipArt",
123 PhType::Diagram => "dgm",
124 PhType::Media => "media",
125 PhType::Picture => "pic",
126 PhType::Date => "dt",
127 PhType::Footer => "ftr",
128 PhType::SlideNum => "sldNum",
129 PhType::Header => "hdr",
130 PhType::SlideImage => "sldImg",
131 }
132 }
133 pub fn from_xml(s: &str) -> Self {
134 match s {
135 "title" => PhType::Title,
136 "ctrTitle" => PhType::CtrTitle,
137 "subTitle" => PhType::SubTitle,
138 "obj" => PhType::Obj,
139 "chart" => PhType::Chart,
140 "tbl" => PhType::Table,
141 "clipArt" => PhType::ClipArt,
142 "dgm" => PhType::Diagram,
143 "media" => PhType::Media,
144 "pic" => PhType::Picture,
145 "dt" => PhType::Date,
146 "ftr" => PhType::Footer,
147 "sldNum" => PhType::SlideNum,
148 "hdr" => PhType::Header,
149 "sldImg" => PhType::SlideImage,
150 _ => PhType::Body,
151 }
152 }
153 pub fn is_title(self) -> bool {
154 matches!(self, PhType::Title | PhType::CtrTitle)
155 }
156 pub fn prompt(self) -> &'static str {
158 match self {
159 PhType::Title | PhType::CtrTitle => "Click to add title",
160 PhType::SubTitle => "Click to add subtitle",
161 PhType::Body | PhType::Obj => "Click to add text",
162 PhType::Chart => "Click icon to add chart",
163 PhType::Table => "Click icon to add table",
164 PhType::Diagram => "Click icon to add SmartArt graphic",
165 PhType::Media => "Click icon to add media",
166 PhType::Picture | PhType::ClipArt => "Click icon to add picture",
167 PhType::Date => "Date",
168 PhType::Footer => "Footer",
169 PhType::SlideNum => "‹#›",
170 PhType::Header => "Header",
171 PhType::SlideImage => "",
172 }
173 }
174 pub fn is_footer_kind(self) -> bool {
176 matches!(
177 self,
178 PhType::Date | PhType::Footer | PhType::SlideNum | PhType::Header
179 )
180 }
181}
182
183#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
184#[serde(default, rename_all = "camelCase")]
185pub struct Placeholder {
186 pub kind: PhType,
187 pub idx: u32,
188 pub vertical: bool,
190 pub size: Option<String>,
192 pub has_custom_prompt: bool,
193}
194
195#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
197#[serde(rename_all = "camelCase", tag = "kind")]
198pub enum Geom {
199 Preset {
200 name: String,
201 #[serde(default, skip_serializing_if = "Vec::is_empty")]
202 adj: Vec<f64>,
203 },
204 Custom { paths: Vec<CustomPath> },
206}
207
208impl Default for Geom {
209 fn default() -> Self {
210 Geom::Preset {
211 name: "rect".into(),
212 adj: vec![],
213 }
214 }
215}
216
217impl Geom {
218 pub fn preset(name: &str) -> Self {
219 Geom::Preset {
220 name: name.into(),
221 adj: vec![],
222 }
223 }
224 pub fn preset_name(&self) -> Option<&str> {
225 match self {
226 Geom::Preset { name, .. } => Some(name),
227 Geom::Custom { .. } => None,
228 }
229 }
230}
231
232#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
234#[serde(default, rename_all = "camelCase")]
235pub struct CustomPath {
236 pub w: f64,
237 pub h: f64,
238 pub d: String,
240 pub fill: deckcraft_geom::preset::FillMode,
241 pub stroke: bool,
242}
243
244impl Default for CustomPath {
245 fn default() -> Self {
246 CustomPath {
247 w: 0.0,
248 h: 0.0,
249 d: String::new(),
250 fill: deckcraft_geom::preset::FillMode::Norm,
251 stroke: true,
252 }
253 }
254}
255
256#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
258#[serde(default, rename_all = "camelCase")]
259pub struct MediaClip {
260 pub media: MediaId,
261 pub video: bool,
262 pub poster: Option<MediaId>,
264 pub trim_start_ms: u32,
265 pub trim_end_ms: u32,
266 pub fade_in_ms: u32,
267 pub fade_out_ms: u32,
268 pub volume: f64,
270 pub autoplay: bool,
271 pub loop_play: bool,
272 pub rewind: bool,
273 pub play_across_slides: bool,
274 pub hide_while_not_playing: bool,
275 pub full_screen: bool,
276 pub bookmarks: Vec<(String, u32)>,
278 pub duration_ms: u32,
280 pub width: u32,
282 pub height: u32,
283}
284
285#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
287#[serde(default, rename_all = "camelCase")]
288pub struct InkStroke {
289 pub points: Vec<(f64, f64, f32)>,
290 pub color: Rgba,
291 pub width: f64,
292 pub highlighter: bool,
293}
294
295#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
296#[serde(rename_all = "camelCase", tag = "type")]
297pub enum ShapeKind {
298 Shape,
300 Picture {
301 fill: style::PictureFill,
302 },
303 Group {
304 children: Vec<Shape>,
305 child: Xfrm,
307 },
308 Connector {
309 start: Option<(ShapeId, u32)>,
310 end: Option<(ShapeId, u32)>,
311 },
312 Table(Table),
313 Chart(Box<Chart>),
314 Media(MediaClip),
315 Ink {
316 strokes: Vec<InkStroke>,
317 },
318 Opaque {
321 xml: String,
322 preview: Option<MediaId>,
323 label: String,
324 },
325}
326
327#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
328#[serde(default, rename_all = "camelCase")]
329pub struct Shape {
330 pub id: ShapeId,
331 pub name: String,
332 pub xfrm: Option<Xfrm>,
334 pub kind: ShapeKind,
335 pub geom: Geom,
336 pub ph: Option<Placeholder>,
337 pub fill: Option<Fill>,
339 pub line: Option<Line>,
340 pub effects: Option<Effects>,
341 pub style: Option<ShapeStyle>,
342 pub text: Option<TextBody>,
343 pub hidden: bool,
344 pub locked: bool,
345 pub text_box: bool,
347 pub descr: String,
349 pub title: String,
350 pub decorative: bool,
351 pub click: Option<text::Hyperlink>,
352 pub hover: Option<text::Hyperlink>,
353 #[serde(skip_serializing_if = "Option::is_none")]
355 pub raw_ext: Option<String>,
356}
357
358impl Default for Shape {
359 fn default() -> Self {
360 Shape {
361 id: ShapeId(0),
362 name: String::new(),
363 xfrm: None,
364 kind: ShapeKind::Shape,
365 geom: Geom::default(),
366 ph: None,
367 fill: None,
368 line: None,
369 effects: None,
370 style: None,
371 text: None,
372 hidden: false,
373 locked: false,
374 text_box: false,
375 descr: String::new(),
376 title: String::new(),
377 decorative: false,
378 click: None,
379 hover: None,
380 raw_ext: None,
381 }
382 }
383}
384
385impl Shape {
386 pub fn is_placeholder(&self) -> bool {
387 self.ph.is_some()
388 }
389 pub fn ph_type(&self) -> Option<PhType> {
390 self.ph.as_ref().map(|p| p.kind)
391 }
392 pub fn is_group(&self) -> bool {
393 matches!(self.kind, ShapeKind::Group { .. })
394 }
395 pub fn children(&self) -> &[Shape] {
396 match &self.kind {
397 ShapeKind::Group { children, .. } => children,
398 _ => &[],
399 }
400 }
401 pub fn children_mut(&mut self) -> Option<&mut Vec<Shape>> {
402 match &mut self.kind {
403 ShapeKind::Group { children, .. } => Some(children),
404 _ => None,
405 }
406 }
407 pub fn is_line(&self) -> bool {
409 matches!(self.kind, ShapeKind::Connector { .. })
410 || self
411 .geom
412 .preset_name()
413 .is_some_and(deckcraft_geom::preset::is_line_like)
414 }
415 pub fn kind_name(&self) -> &'static str {
416 match &self.kind {
417 ShapeKind::Shape if self.ph.is_some() => "placeholder",
418 ShapeKind::Shape if self.text_box => "textBox",
419 ShapeKind::Shape => "shape",
420 ShapeKind::Picture { .. } => "picture",
421 ShapeKind::Group { .. } => "group",
422 ShapeKind::Connector { .. } => "connector",
423 ShapeKind::Table(_) => "table",
424 ShapeKind::Chart(_) => "chart",
425 ShapeKind::Media(m) if m.video => "video",
426 ShapeKind::Media(_) => "audio",
427 ShapeKind::Ink { .. } => "ink",
428 ShapeKind::Opaque { .. } => "object",
429 }
430 }
431}
432
433pub fn walk<'a>(shapes: &'a [Shape], f: &mut dyn FnMut(&'a Shape, usize)) {
435 fn rec<'a>(shapes: &'a [Shape], depth: usize, f: &mut dyn FnMut(&'a Shape, usize)) {
436 if depth > 64 {
437 return;
438 }
439 for s in shapes {
440 f(s, depth);
441 rec(s.children(), depth + 1, f);
442 }
443 }
444 rec(shapes, 0, f);
445}
446
447pub fn find_shape(shapes: &[Shape], id: ShapeId) -> Option<&Shape> {
448 fn rec(shapes: &[Shape], id: ShapeId, depth: usize) -> Option<&Shape> {
449 if depth > 64 {
450 return None;
451 }
452 for s in shapes {
453 if s.id == id {
454 return Some(s);
455 }
456 if let Some(f) = rec(s.children(), id, depth + 1) {
457 return Some(f);
458 }
459 }
460 None
461 }
462 rec(shapes, id, 0)
463}
464
465pub fn find_shape_mut(shapes: &mut [Shape], id: ShapeId) -> Option<&mut Shape> {
466 fn rec(shapes: &mut [Shape], id: ShapeId, depth: usize) -> Option<&mut Shape> {
467 if depth > 64 {
468 return None;
469 }
470 for s in shapes.iter_mut() {
471 if s.id == id {
472 return Some(s);
473 }
474 if let ShapeKind::Group { children, .. } = &mut s.kind
475 && let Some(f) = rec(children, id, depth + 1)
476 {
477 return Some(f);
478 }
479 }
480 None
481 }
482 rec(shapes, id, 0)
483}
484
485pub fn sibling_list_mut(shapes: &mut Vec<Shape>, id: ShapeId) -> Option<&mut Vec<Shape>> {
487 fn rec(shapes: &mut Vec<Shape>, id: ShapeId, depth: usize) -> Option<&mut Vec<Shape>> {
488 if depth > 64 {
489 return None;
490 }
491 if shapes.iter().any(|s| s.id == id) {
492 return Some(shapes);
493 }
494 for s in shapes.iter_mut() {
495 if let ShapeKind::Group { children, .. } = &mut s.kind
496 && let Some(f) = rec(children, id, depth + 1)
497 {
498 return Some(f);
499 }
500 }
501 None
502 }
503 rec(shapes, id, 0)
504}
505
506pub fn parent_of(shapes: &[Shape], id: ShapeId) -> Option<Option<ShapeId>> {
508 fn rec(
509 shapes: &[Shape],
510 parent: Option<ShapeId>,
511 id: ShapeId,
512 depth: usize,
513 ) -> Option<Option<ShapeId>> {
514 if depth > 64 {
515 return None;
516 }
517 for s in shapes {
518 if s.id == id {
519 return Some(parent);
520 }
521 if let Some(f) = rec(s.children(), Some(s.id), id, depth + 1) {
522 return Some(f);
523 }
524 }
525 None
526 }
527 rec(shapes, None, id, 0)
528}
529
530#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
532#[serde(rename_all = "camelCase", tag = "kind")]
533pub enum Background {
534 Fill {
535 fill: Fill,
536 },
537 Ref {
539 idx: u32,
540 color: ColorRef,
541 },
542}
543
544#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
545#[serde(default, rename_all = "camelCase")]
546pub struct Comment {
547 pub author: String,
548 pub initials: String,
549 pub text: String,
550 pub date: String,
552 pub x: f64,
553 pub y: f64,
554 pub resolved: bool,
555 pub replies: Vec<Comment>,
556 pub shape: Option<ShapeId>,
558}
559
560#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
561#[serde(default, rename_all = "camelCase")]
562pub struct Slide {
563 pub id: SlideId,
564 pub layout: LayoutId,
565 pub name: String,
566 pub shapes: Vec<Shape>,
567 pub background: Option<Background>,
568 pub hidden: bool,
569 pub show_master_shapes: bool,
570 pub transition: Option<Transition>,
571 pub animations: Vec<Animation>,
572 pub notes: TextBody,
574 pub comments: Vec<Comment>,
575 #[serde(skip_serializing_if = "Option::is_none")]
578 pub color_map: Option<Vec<(String, String)>>,
579 #[serde(skip_serializing_if = "Option::is_none")]
581 pub raw_ext: Option<String>,
582}
583
584impl Default for Slide {
585 fn default() -> Self {
586 Slide {
587 id: SlideId(0),
588 layout: LayoutId(0),
589 name: String::new(),
590 shapes: vec![],
591 background: None,
592 hidden: false,
593 show_master_shapes: true,
594 transition: None,
595 animations: vec![],
596 notes: TextBody::default(),
597 comments: vec![],
598 color_map: None,
599 raw_ext: None,
600 }
601 }
602}
603
604impl Slide {
605 pub fn shape(&self, id: ShapeId) -> Option<&Shape> {
606 find_shape(&self.shapes, id)
607 }
608 pub fn shape_mut(&mut self, id: ShapeId) -> Option<&mut Shape> {
609 find_shape_mut(&mut self.shapes, id)
610 }
611 pub fn title(&self) -> String {
613 self.shapes
614 .iter()
615 .find(|s| s.ph_type().is_some_and(PhType::is_title))
616 .and_then(|s| s.text.as_ref())
617 .map(|t| t.text().replace('\u{b}', " "))
618 .unwrap_or_default()
619 }
620 pub fn notes_text(&self) -> String {
621 self.notes.text()
622 }
623}
624
625#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
626#[serde(rename_all = "camelCase")]
627pub enum LayoutType {
628 Title,
629 #[default]
630 TitleAndContent,
631 SectionHeader,
632 TwoContent,
633 Comparison,
634 TitleOnly,
635 Blank,
636 ContentWithCaption,
637 PictureWithCaption,
638 TitleAndVerticalText,
639 VerticalTitleAndText,
640 Custom,
641}
642
643impl LayoutType {
644 pub fn xml(self) -> &'static str {
645 match self {
646 LayoutType::Title => "title",
647 LayoutType::TitleAndContent => "obj",
648 LayoutType::SectionHeader => "secHead",
649 LayoutType::TwoContent => "twoObj",
650 LayoutType::Comparison => "twoTxTwoObj",
651 LayoutType::TitleOnly => "titleOnly",
652 LayoutType::Blank => "blank",
653 LayoutType::ContentWithCaption => "objTx",
654 LayoutType::PictureWithCaption => "picTx",
655 LayoutType::TitleAndVerticalText => "vertTx",
656 LayoutType::VerticalTitleAndText => "vertTitleAndTx",
657 LayoutType::Custom => "cust",
658 }
659 }
660 pub fn from_xml(s: &str) -> Self {
661 match s {
662 "title" => LayoutType::Title,
663 "obj" | "tx" => LayoutType::TitleAndContent,
664 "secHead" => LayoutType::SectionHeader,
665 "twoObj" | "twoColTx" => LayoutType::TwoContent,
666 "twoTxTwoObj" => LayoutType::Comparison,
667 "titleOnly" => LayoutType::TitleOnly,
668 "blank" => LayoutType::Blank,
669 "objTx" => LayoutType::ContentWithCaption,
670 "picTx" => LayoutType::PictureWithCaption,
671 "vertTx" => LayoutType::TitleAndVerticalText,
672 "vertTitleAndTx" => LayoutType::VerticalTitleAndText,
673 _ => LayoutType::Custom,
674 }
675 }
676}
677
678#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
679#[serde(default, rename_all = "camelCase")]
680pub struct Layout {
681 pub id: LayoutId,
682 pub name: String,
683 pub kind: LayoutType,
684 pub shapes: Vec<Shape>,
685 pub background: Option<Background>,
686 pub show_master_shapes: bool,
687 pub preserve: bool,
688 #[serde(skip_serializing_if = "Option::is_none")]
691 pub color_map: Option<Vec<(String, String)>>,
692 #[serde(skip_serializing_if = "Option::is_none")]
693 pub raw_ext: Option<String>,
694}
695
696#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
698#[serde(default, rename_all = "camelCase")]
699pub struct HeaderFooter {
700 pub date: bool,
701 pub slide_number: bool,
702 pub footer: bool,
703 pub header: bool,
704 pub footer_text: String,
705 pub date_text: String,
707 pub date_format: String,
709 pub hide_on_title: bool,
710}
711
712#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
713#[serde(default, rename_all = "camelCase")]
714pub struct Master {
715 pub id: MasterId,
716 pub name: String,
717 pub theme: Theme,
718 pub shapes: Vec<Shape>,
719 pub background: Option<Background>,
720 pub title_style: text::ListStyle,
721 pub body_style: text::ListStyle,
722 pub other_style: text::ListStyle,
723 pub layouts: Vec<Layout>,
724 pub color_map: Vec<(String, String)>,
726 pub preserve: bool,
727 #[serde(skip_serializing_if = "Option::is_none")]
728 pub raw_ext: Option<String>,
729}
730
731impl Master {
732 pub fn layout(&self, id: LayoutId) -> Option<&Layout> {
733 self.layouts.iter().find(|l| l.id == id)
734 }
735 pub fn map_slot(&self, slot: SchemeSlot) -> SchemeSlot {
737 map_slot(&self.color_map, slot)
738 }
739 pub fn scheme(&self) -> ColorScheme {
741 self.scheme_with(&self.color_map)
742 }
743 pub fn scheme_with(&self, map: &[(String, String)]) -> ColorScheme {
746 let mut s = self.theme.colors.clone();
747 if map.is_empty() {
748 return s;
749 }
750 let base = self.theme.colors.clone();
751 for (alias, slot) in [
752 (SchemeSlot::Bg1, SchemeSlot::Lt1),
753 (SchemeSlot::Tx1, SchemeSlot::Dk1),
754 (SchemeSlot::Bg2, SchemeSlot::Lt2),
755 (SchemeSlot::Tx2, SchemeSlot::Dk2),
756 ] {
757 s.set(slot, base.get(map_slot(map, alias)));
758 }
759 s
760 }
761}
762
763pub fn map_slot(map: &[(String, String)], slot: SchemeSlot) -> SchemeSlot {
766 let key = match slot {
767 SchemeSlot::Bg1 => "bg1",
768 SchemeSlot::Tx1 => "tx1",
769 SchemeSlot::Bg2 => "bg2",
770 SchemeSlot::Tx2 => "tx2",
771 other => return other,
772 };
773 map.iter()
774 .find(|(k, _)| k == key)
775 .and_then(|(_, v)| SchemeSlot::from_xml(v))
776 .unwrap_or(match slot {
777 SchemeSlot::Bg1 => SchemeSlot::Lt1,
778 SchemeSlot::Tx1 => SchemeSlot::Dk1,
779 SchemeSlot::Bg2 => SchemeSlot::Lt2,
780 _ => SchemeSlot::Dk2,
781 })
782}
783
784#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
785#[serde(default, rename_all = "camelCase")]
786pub struct Section {
787 pub name: String,
788 pub slides: Vec<SlideId>,
789}
790
791#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
792#[serde(default, rename_all = "camelCase")]
793pub struct CustomShow {
794 pub name: String,
795 pub slides: Vec<SlideId>,
796}
797
798#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
800#[serde(default, rename_all = "camelCase")]
801pub struct ShowSettings {
802 pub show_type: String,
804 pub loop_until_esc: bool,
805 pub without_narration: bool,
806 pub without_animation: bool,
807 pub use_timings: bool,
808 pub range: Option<(u32, u32)>,
810 pub custom_show: Option<String>,
811 pub pen_color: Rgba,
812 pub laser_color: Rgba,
813 pub show_media_controls: bool,
814}
815
816impl Default for ShowSettings {
817 fn default() -> Self {
818 ShowSettings {
819 show_type: "speaker".into(),
820 loop_until_esc: false,
821 without_narration: false,
822 without_animation: false,
823 use_timings: true,
824 range: None,
825 custom_show: None,
826 pen_color: Rgba::rgb(0xFF, 0x00, 0x00),
827 laser_color: Rgba::rgb(0xFF, 0x00, 0x00),
828 show_media_controls: true,
829 }
830 }
831}
832
833#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
835#[serde(default, rename_all = "camelCase")]
836pub struct Properties {
837 pub title: String,
838 pub subject: String,
839 pub author: String,
840 pub keywords: String,
841 pub comments: String,
842 pub category: String,
843 pub company: String,
844 pub created: String,
845 pub modified: String,
846 pub last_modified_by: String,
847 pub revision: u32,
848}
849
850#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
852#[serde(default, rename_all = "camelCase")]
853pub struct MediaItem {
854 pub id: MediaId,
855 pub name: String,
857 pub content_type: String,
858 #[serde(skip)]
860 pub data: Arc<Vec<u8>>,
861 pub link: Option<String>,
863}
864
865#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
866#[serde(default, rename_all = "camelCase")]
867pub struct Presentation {
868 pub slide_size: Size,
870 pub notes_size: Size,
871 pub slides: Vec<Arc<Slide>>,
872 pub masters: Vec<Arc<Master>>,
873 pub notes_master: Option<Arc<Master>>,
874 pub handout_master: Option<Arc<Master>>,
875 pub sections: Vec<Section>,
876 pub custom_shows: Vec<CustomShow>,
877 pub show: ShowSettings,
878 pub header_footer: HeaderFooter,
879 pub props: Properties,
880 pub default_text_style: text::ListStyle,
882 pub media: Vec<MediaItem>,
883 pub first_slide_number: u32,
884 pub next_id: u32,
886 pub embedded_fonts: Vec<(String, MediaId)>,
888 #[serde(skip_serializing_if = "Vec::is_empty")]
890 pub table_styles: Vec<table::TableStyleDef>,
891 #[serde(skip_serializing_if = "Option::is_none")]
893 pub raw_ext: Option<String>,
894}
895
896impl Default for Presentation {
897 fn default() -> Self {
898 defaults::new_presentation(None)
899 }
900}
901
902impl Presentation {
903 pub fn alloc_id(&mut self) -> u32 {
905 self.next_id = self.next_id.max(1).saturating_add(1);
906 self.next_id
907 }
908 pub fn fix_next_id(&mut self) {
910 let mut m = self.next_id;
911 for s in &self.slides {
912 m = m.max(s.id.0);
913 walk(&s.shapes, &mut |sh, _| m = m.max(sh.id.0));
914 }
915 for ma in &self.masters {
916 m = m.max(ma.id.0);
917 walk(&ma.shapes, &mut |sh, _| m = m.max(sh.id.0));
918 for l in &ma.layouts {
919 m = m.max(l.id.0);
920 walk(&l.shapes, &mut |sh, _| m = m.max(sh.id.0));
921 }
922 }
923 for md in &self.media {
924 m = m.max(md.id.0);
925 }
926 self.next_id = m;
927 }
928 pub fn slide_index(&self, id: SlideId) -> Option<usize> {
929 self.slides.iter().position(|s| s.id == id)
930 }
931 pub fn slide(&self, id: SlideId) -> Option<&Slide> {
932 self.slides.iter().find(|s| s.id == id).map(|s| s.as_ref())
933 }
934 pub fn slide_mut(&mut self, id: SlideId) -> Option<&mut Slide> {
935 self.slides
936 .iter_mut()
937 .find(|s| s.id == id)
938 .map(Arc::make_mut)
939 }
940 pub fn slide_at(&self, i: usize) -> Option<&Slide> {
941 self.slides.get(i).map(|s| s.as_ref())
942 }
943 pub fn layout(&self, id: LayoutId) -> Option<(&Master, &Layout)> {
945 self.masters
946 .iter()
947 .find_map(|m| m.layout(id).map(|l| (m.as_ref(), l)))
948 }
949 pub fn master_for(&self, slide: &Slide) -> Option<(&Master, Option<&Layout>)> {
951 if let Some((m, l)) = self.layout(slide.layout) {
952 return Some((m, Some(l)));
953 }
954 self.masters
955 .first()
956 .map(|m| (m.as_ref(), m.layouts.first()))
957 }
958 pub fn first_master(&self) -> Option<&Master> {
959 self.masters.first().map(|m| m.as_ref())
960 }
961 pub fn media(&self, id: MediaId) -> Option<&MediaItem> {
962 self.media.iter().find(|m| m.id == id)
963 }
964 pub fn add_media(&mut self, name: &str, content_type: &str, data: Vec<u8>) -> MediaId {
966 if let Some(m) = self
967 .media
968 .iter()
969 .find(|m| m.data.len() == data.len() && *m.data == data)
970 {
971 return m.id;
972 }
973 let id = MediaId(self.alloc_id());
974 self.media.push(MediaItem {
975 id,
976 name: name.into(),
977 content_type: content_type.into(),
978 data: Arc::new(data),
979 link: None,
980 });
981 id
982 }
983 pub fn section_of(&self, slide: SlideId) -> Option<usize> {
985 self.sections.iter().position(|s| s.slides.contains(&slide))
986 }
987 pub fn slide_number(&self, index: usize) -> u32 {
989 self.first_slide_number
990 .saturating_add(u32::try_from(index).unwrap_or(u32::MAX))
991 }
992 pub fn validate(&self) -> Vec<String> {
994 let mut out = vec![];
995 let mut seen = std::collections::HashSet::new();
996 for s in &self.slides {
997 if !seen.insert(s.id) {
998 out.push(format!("duplicate slide id {}", s.id));
999 }
1000 if self.layout(s.layout).is_none() {
1001 out.push(format!("slide {} uses missing layout {}", s.id, s.layout));
1002 }
1003 let mut ids = std::collections::HashSet::new();
1004 walk(&s.shapes, &mut |sh, _| {
1005 if !ids.insert(sh.id) {
1006 out.push(format!("slide {}: duplicate shape id {}", s.id, sh.id));
1007 }
1008 });
1009 }
1010 out
1011 }
1012}
1013
1014#[cfg(test)]
1015mod tests;