#![deny(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::unimplemented,
clippy::todo,
clippy::unreachable
)]
#![forbid(unsafe_code)]
pub mod anim;
pub mod chart;
pub mod defaults;
pub mod edit;
pub mod resolve;
pub mod style;
pub mod table;
pub mod text;
pub mod theme;
pub mod words;
use std::sync::Arc;
pub use deckcraft_color::{ColorScheme, ColorTransform, Rgba, SchemeSlot};
pub use deckcraft_geom::{Rect, Size, Xfrm};
use serde::{Deserialize, Serialize};
pub use anim::{AnimClass, AnimStart, Animation, Transition};
pub use chart::Chart;
pub use style::{ColorBase, ColorRef, Effects, Fill, Line, ShapeStyle};
pub use table::{Cell, Table};
pub use text::{Paragraph, Run, RunProps, TextBody};
pub use theme::Theme;
pub(crate) fn yes() -> bool {
true
}
macro_rules! id_type {
($name:ident) => {
#[derive(
Clone,
Copy,
Debug,
Default,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
)]
#[serde(transparent)]
pub struct $name(pub u32);
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
};
}
id_type!(SlideId);
id_type!(ShapeId);
id_type!(LayoutId);
id_type!(MasterId);
id_type!(MediaId);
#[derive(Debug, thiserror::Error)]
pub enum ModelError {
#[error("no slide {0}")]
NoSlide(String),
#[error("no shape {0}")]
NoShape(ShapeId),
#[error("no layout {0}")]
NoLayout(LayoutId),
#[error("{0}")]
Invalid(String),
}
pub type Result<T> = std::result::Result<T, ModelError>;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum PhType {
Title,
CtrTitle,
SubTitle,
#[default]
Body,
Obj,
Chart,
Table,
ClipArt,
Diagram,
Media,
Picture,
Date,
Footer,
SlideNum,
Header,
SlideImage,
}
impl PhType {
pub fn xml(self) -> &'static str {
match self {
PhType::Title => "title",
PhType::CtrTitle => "ctrTitle",
PhType::SubTitle => "subTitle",
PhType::Body => "body",
PhType::Obj => "obj",
PhType::Chart => "chart",
PhType::Table => "tbl",
PhType::ClipArt => "clipArt",
PhType::Diagram => "dgm",
PhType::Media => "media",
PhType::Picture => "pic",
PhType::Date => "dt",
PhType::Footer => "ftr",
PhType::SlideNum => "sldNum",
PhType::Header => "hdr",
PhType::SlideImage => "sldImg",
}
}
pub fn from_xml(s: &str) -> Self {
match s {
"title" => PhType::Title,
"ctrTitle" => PhType::CtrTitle,
"subTitle" => PhType::SubTitle,
"obj" => PhType::Obj,
"chart" => PhType::Chart,
"tbl" => PhType::Table,
"clipArt" => PhType::ClipArt,
"dgm" => PhType::Diagram,
"media" => PhType::Media,
"pic" => PhType::Picture,
"dt" => PhType::Date,
"ftr" => PhType::Footer,
"sldNum" => PhType::SlideNum,
"hdr" => PhType::Header,
"sldImg" => PhType::SlideImage,
_ => PhType::Body,
}
}
pub fn is_title(self) -> bool {
matches!(self, PhType::Title | PhType::CtrTitle)
}
pub fn prompt(self) -> &'static str {
match self {
PhType::Title | PhType::CtrTitle => "Click to add title",
PhType::SubTitle => "Click to add subtitle",
PhType::Body | PhType::Obj => "Click to add text",
PhType::Chart => "Click icon to add chart",
PhType::Table => "Click icon to add table",
PhType::Diagram => "Click icon to add SmartArt graphic",
PhType::Media => "Click icon to add media",
PhType::Picture | PhType::ClipArt => "Click icon to add picture",
PhType::Date => "Date",
PhType::Footer => "Footer",
PhType::SlideNum => "‹#›",
PhType::Header => "Header",
PhType::SlideImage => "",
}
}
pub fn is_footer_kind(self) -> bool {
matches!(
self,
PhType::Date | PhType::Footer | PhType::SlideNum | PhType::Header
)
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Placeholder {
pub kind: PhType,
pub idx: u32,
pub vertical: bool,
pub size: Option<String>,
pub has_custom_prompt: bool,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "kind")]
pub enum Geom {
Preset {
name: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
adj: Vec<f64>,
},
Custom { paths: Vec<CustomPath> },
}
impl Default for Geom {
fn default() -> Self {
Geom::Preset {
name: "rect".into(),
adj: vec![],
}
}
}
impl Geom {
pub fn preset(name: &str) -> Self {
Geom::Preset {
name: name.into(),
adj: vec![],
}
}
pub fn preset_name(&self) -> Option<&str> {
match self {
Geom::Preset { name, .. } => Some(name),
Geom::Custom { .. } => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct CustomPath {
pub w: f64,
pub h: f64,
pub d: String,
pub fill: deckcraft_geom::preset::FillMode,
pub stroke: bool,
}
impl Default for CustomPath {
fn default() -> Self {
CustomPath {
w: 0.0,
h: 0.0,
d: String::new(),
fill: deckcraft_geom::preset::FillMode::Norm,
stroke: true,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct MediaClip {
pub media: MediaId,
pub video: bool,
pub poster: Option<MediaId>,
pub trim_start_ms: u32,
pub trim_end_ms: u32,
pub fade_in_ms: u32,
pub fade_out_ms: u32,
pub volume: f64,
pub autoplay: bool,
pub loop_play: bool,
pub rewind: bool,
pub play_across_slides: bool,
pub hide_while_not_playing: bool,
pub full_screen: bool,
pub bookmarks: Vec<(String, u32)>,
pub duration_ms: u32,
pub width: u32,
pub height: u32,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct InkStroke {
pub points: Vec<(f64, f64, f32)>,
pub color: Rgba,
pub width: f64,
pub highlighter: bool,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum ShapeKind {
Shape,
Picture {
fill: style::PictureFill,
},
Group {
children: Vec<Shape>,
child: Xfrm,
},
Connector {
start: Option<(ShapeId, u32)>,
end: Option<(ShapeId, u32)>,
},
Table(Table),
Chart(Box<Chart>),
Media(MediaClip),
Ink {
strokes: Vec<InkStroke>,
},
Opaque {
xml: String,
preview: Option<MediaId>,
label: String,
},
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Shape {
pub id: ShapeId,
pub name: String,
pub xfrm: Option<Xfrm>,
pub kind: ShapeKind,
pub geom: Geom,
pub ph: Option<Placeholder>,
pub fill: Option<Fill>,
pub line: Option<Line>,
pub effects: Option<Effects>,
pub style: Option<ShapeStyle>,
pub text: Option<TextBody>,
pub hidden: bool,
pub locked: bool,
pub text_box: bool,
pub descr: String,
pub title: String,
pub decorative: bool,
pub click: Option<text::Hyperlink>,
pub hover: Option<text::Hyperlink>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_ext: Option<String>,
}
impl Default for Shape {
fn default() -> Self {
Shape {
id: ShapeId(0),
name: String::new(),
xfrm: None,
kind: ShapeKind::Shape,
geom: Geom::default(),
ph: None,
fill: None,
line: None,
effects: None,
style: None,
text: None,
hidden: false,
locked: false,
text_box: false,
descr: String::new(),
title: String::new(),
decorative: false,
click: None,
hover: None,
raw_ext: None,
}
}
}
impl Shape {
pub fn is_placeholder(&self) -> bool {
self.ph.is_some()
}
pub fn ph_type(&self) -> Option<PhType> {
self.ph.as_ref().map(|p| p.kind)
}
pub fn is_group(&self) -> bool {
matches!(self.kind, ShapeKind::Group { .. })
}
pub fn children(&self) -> &[Shape] {
match &self.kind {
ShapeKind::Group { children, .. } => children,
_ => &[],
}
}
pub fn children_mut(&mut self) -> Option<&mut Vec<Shape>> {
match &mut self.kind {
ShapeKind::Group { children, .. } => Some(children),
_ => None,
}
}
pub fn is_line(&self) -> bool {
matches!(self.kind, ShapeKind::Connector { .. })
|| self
.geom
.preset_name()
.is_some_and(deckcraft_geom::preset::is_line_like)
}
pub fn kind_name(&self) -> &'static str {
match &self.kind {
ShapeKind::Shape if self.ph.is_some() => "placeholder",
ShapeKind::Shape if self.text_box => "textBox",
ShapeKind::Shape => "shape",
ShapeKind::Picture { .. } => "picture",
ShapeKind::Group { .. } => "group",
ShapeKind::Connector { .. } => "connector",
ShapeKind::Table(_) => "table",
ShapeKind::Chart(_) => "chart",
ShapeKind::Media(m) if m.video => "video",
ShapeKind::Media(_) => "audio",
ShapeKind::Ink { .. } => "ink",
ShapeKind::Opaque { .. } => "object",
}
}
}
pub fn walk<'a>(shapes: &'a [Shape], f: &mut dyn FnMut(&'a Shape, usize)) {
fn rec<'a>(shapes: &'a [Shape], depth: usize, f: &mut dyn FnMut(&'a Shape, usize)) {
if depth > 64 {
return;
}
for s in shapes {
f(s, depth);
rec(s.children(), depth + 1, f);
}
}
rec(shapes, 0, f);
}
pub fn find_shape(shapes: &[Shape], id: ShapeId) -> Option<&Shape> {
fn rec(shapes: &[Shape], id: ShapeId, depth: usize) -> Option<&Shape> {
if depth > 64 {
return None;
}
for s in shapes {
if s.id == id {
return Some(s);
}
if let Some(f) = rec(s.children(), id, depth + 1) {
return Some(f);
}
}
None
}
rec(shapes, id, 0)
}
pub fn find_shape_mut(shapes: &mut [Shape], id: ShapeId) -> Option<&mut Shape> {
fn rec(shapes: &mut [Shape], id: ShapeId, depth: usize) -> Option<&mut Shape> {
if depth > 64 {
return None;
}
for s in shapes.iter_mut() {
if s.id == id {
return Some(s);
}
if let ShapeKind::Group { children, .. } = &mut s.kind
&& let Some(f) = rec(children, id, depth + 1)
{
return Some(f);
}
}
None
}
rec(shapes, id, 0)
}
pub fn sibling_list_mut(shapes: &mut Vec<Shape>, id: ShapeId) -> Option<&mut Vec<Shape>> {
fn rec(shapes: &mut Vec<Shape>, id: ShapeId, depth: usize) -> Option<&mut Vec<Shape>> {
if depth > 64 {
return None;
}
if shapes.iter().any(|s| s.id == id) {
return Some(shapes);
}
for s in shapes.iter_mut() {
if let ShapeKind::Group { children, .. } = &mut s.kind
&& let Some(f) = rec(children, id, depth + 1)
{
return Some(f);
}
}
None
}
rec(shapes, id, 0)
}
pub fn parent_of(shapes: &[Shape], id: ShapeId) -> Option<Option<ShapeId>> {
fn rec(
shapes: &[Shape],
parent: Option<ShapeId>,
id: ShapeId,
depth: usize,
) -> Option<Option<ShapeId>> {
if depth > 64 {
return None;
}
for s in shapes {
if s.id == id {
return Some(parent);
}
if let Some(f) = rec(s.children(), Some(s.id), id, depth + 1) {
return Some(f);
}
}
None
}
rec(shapes, None, id, 0)
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "kind")]
pub enum Background {
Fill {
fill: Fill,
},
Ref {
idx: u32,
color: ColorRef,
},
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Comment {
pub author: String,
pub initials: String,
pub text: String,
pub date: String,
pub x: f64,
pub y: f64,
pub resolved: bool,
pub replies: Vec<Comment>,
pub shape: Option<ShapeId>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Slide {
pub id: SlideId,
pub layout: LayoutId,
pub name: String,
pub shapes: Vec<Shape>,
pub background: Option<Background>,
pub hidden: bool,
pub show_master_shapes: bool,
pub transition: Option<Transition>,
pub animations: Vec<Animation>,
pub notes: TextBody,
pub comments: Vec<Comment>,
#[serde(skip_serializing_if = "Option::is_none")]
pub color_map: Option<Vec<(String, String)>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_ext: Option<String>,
}
impl Default for Slide {
fn default() -> Self {
Slide {
id: SlideId(0),
layout: LayoutId(0),
name: String::new(),
shapes: vec![],
background: None,
hidden: false,
show_master_shapes: true,
transition: None,
animations: vec![],
notes: TextBody::default(),
comments: vec![],
color_map: None,
raw_ext: None,
}
}
}
impl Slide {
pub fn shape(&self, id: ShapeId) -> Option<&Shape> {
find_shape(&self.shapes, id)
}
pub fn shape_mut(&mut self, id: ShapeId) -> Option<&mut Shape> {
find_shape_mut(&mut self.shapes, id)
}
pub fn title(&self) -> String {
self.shapes
.iter()
.find(|s| s.ph_type().is_some_and(PhType::is_title))
.and_then(|s| s.text.as_ref())
.map(|t| t.text().replace('\u{b}', " "))
.unwrap_or_default()
}
pub fn notes_text(&self) -> String {
self.notes.text()
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum LayoutType {
Title,
#[default]
TitleAndContent,
SectionHeader,
TwoContent,
Comparison,
TitleOnly,
Blank,
ContentWithCaption,
PictureWithCaption,
TitleAndVerticalText,
VerticalTitleAndText,
Custom,
}
impl LayoutType {
pub fn xml(self) -> &'static str {
match self {
LayoutType::Title => "title",
LayoutType::TitleAndContent => "obj",
LayoutType::SectionHeader => "secHead",
LayoutType::TwoContent => "twoObj",
LayoutType::Comparison => "twoTxTwoObj",
LayoutType::TitleOnly => "titleOnly",
LayoutType::Blank => "blank",
LayoutType::ContentWithCaption => "objTx",
LayoutType::PictureWithCaption => "picTx",
LayoutType::TitleAndVerticalText => "vertTx",
LayoutType::VerticalTitleAndText => "vertTitleAndTx",
LayoutType::Custom => "cust",
}
}
pub fn from_xml(s: &str) -> Self {
match s {
"title" => LayoutType::Title,
"obj" | "tx" => LayoutType::TitleAndContent,
"secHead" => LayoutType::SectionHeader,
"twoObj" | "twoColTx" => LayoutType::TwoContent,
"twoTxTwoObj" => LayoutType::Comparison,
"titleOnly" => LayoutType::TitleOnly,
"blank" => LayoutType::Blank,
"objTx" => LayoutType::ContentWithCaption,
"picTx" => LayoutType::PictureWithCaption,
"vertTx" => LayoutType::TitleAndVerticalText,
"vertTitleAndTx" => LayoutType::VerticalTitleAndText,
_ => LayoutType::Custom,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Layout {
pub id: LayoutId,
pub name: String,
pub kind: LayoutType,
pub shapes: Vec<Shape>,
pub background: Option<Background>,
pub show_master_shapes: bool,
pub preserve: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub color_map: Option<Vec<(String, String)>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_ext: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct HeaderFooter {
pub date: bool,
pub slide_number: bool,
pub footer: bool,
pub header: bool,
pub footer_text: String,
pub date_text: String,
pub date_format: String,
pub hide_on_title: bool,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Master {
pub id: MasterId,
pub name: String,
pub theme: Theme,
pub shapes: Vec<Shape>,
pub background: Option<Background>,
pub title_style: text::ListStyle,
pub body_style: text::ListStyle,
pub other_style: text::ListStyle,
pub layouts: Vec<Layout>,
pub color_map: Vec<(String, String)>,
pub preserve: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_ext: Option<String>,
}
impl Master {
pub fn layout(&self, id: LayoutId) -> Option<&Layout> {
self.layouts.iter().find(|l| l.id == id)
}
pub fn map_slot(&self, slot: SchemeSlot) -> SchemeSlot {
map_slot(&self.color_map, slot)
}
pub fn scheme(&self) -> ColorScheme {
self.scheme_with(&self.color_map)
}
pub fn scheme_with(&self, map: &[(String, String)]) -> ColorScheme {
let mut s = self.theme.colors.clone();
if map.is_empty() {
return s;
}
let base = self.theme.colors.clone();
for (alias, slot) in [
(SchemeSlot::Bg1, SchemeSlot::Lt1),
(SchemeSlot::Tx1, SchemeSlot::Dk1),
(SchemeSlot::Bg2, SchemeSlot::Lt2),
(SchemeSlot::Tx2, SchemeSlot::Dk2),
] {
s.set(slot, base.get(map_slot(map, alias)));
}
s
}
}
pub fn map_slot(map: &[(String, String)], slot: SchemeSlot) -> SchemeSlot {
let key = match slot {
SchemeSlot::Bg1 => "bg1",
SchemeSlot::Tx1 => "tx1",
SchemeSlot::Bg2 => "bg2",
SchemeSlot::Tx2 => "tx2",
other => return other,
};
map.iter()
.find(|(k, _)| k == key)
.and_then(|(_, v)| SchemeSlot::from_xml(v))
.unwrap_or(match slot {
SchemeSlot::Bg1 => SchemeSlot::Lt1,
SchemeSlot::Tx1 => SchemeSlot::Dk1,
SchemeSlot::Bg2 => SchemeSlot::Lt2,
_ => SchemeSlot::Dk2,
})
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Section {
pub name: String,
pub slides: Vec<SlideId>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct CustomShow {
pub name: String,
pub slides: Vec<SlideId>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct ShowSettings {
pub show_type: String,
pub loop_until_esc: bool,
pub without_narration: bool,
pub without_animation: bool,
pub use_timings: bool,
pub range: Option<(u32, u32)>,
pub custom_show: Option<String>,
pub pen_color: Rgba,
pub laser_color: Rgba,
pub show_media_controls: bool,
}
impl Default for ShowSettings {
fn default() -> Self {
ShowSettings {
show_type: "speaker".into(),
loop_until_esc: false,
without_narration: false,
without_animation: false,
use_timings: true,
range: None,
custom_show: None,
pen_color: Rgba::rgb(0xFF, 0x00, 0x00),
laser_color: Rgba::rgb(0xFF, 0x00, 0x00),
show_media_controls: true,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Properties {
pub title: String,
pub subject: String,
pub author: String,
pub keywords: String,
pub comments: String,
pub category: String,
pub company: String,
pub created: String,
pub modified: String,
pub last_modified_by: String,
pub revision: u32,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct MediaItem {
pub id: MediaId,
pub name: String,
pub content_type: String,
#[serde(skip)]
pub data: Arc<Vec<u8>>,
pub link: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Presentation {
pub slide_size: Size,
pub notes_size: Size,
pub slides: Vec<Arc<Slide>>,
pub masters: Vec<Arc<Master>>,
pub notes_master: Option<Arc<Master>>,
pub handout_master: Option<Arc<Master>>,
pub sections: Vec<Section>,
pub custom_shows: Vec<CustomShow>,
pub show: ShowSettings,
pub header_footer: HeaderFooter,
pub props: Properties,
pub default_text_style: text::ListStyle,
pub media: Vec<MediaItem>,
pub first_slide_number: u32,
pub next_id: u32,
pub embedded_fonts: Vec<(String, MediaId)>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub table_styles: Vec<table::TableStyleDef>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_ext: Option<String>,
}
impl Default for Presentation {
fn default() -> Self {
defaults::new_presentation(None)
}
}
impl Presentation {
pub fn alloc_id(&mut self) -> u32 {
self.next_id = self.next_id.max(1).saturating_add(1);
self.next_id
}
pub fn fix_next_id(&mut self) {
let mut m = self.next_id;
for s in &self.slides {
m = m.max(s.id.0);
walk(&s.shapes, &mut |sh, _| m = m.max(sh.id.0));
}
for ma in &self.masters {
m = m.max(ma.id.0);
walk(&ma.shapes, &mut |sh, _| m = m.max(sh.id.0));
for l in &ma.layouts {
m = m.max(l.id.0);
walk(&l.shapes, &mut |sh, _| m = m.max(sh.id.0));
}
}
for md in &self.media {
m = m.max(md.id.0);
}
self.next_id = m;
}
pub fn slide_index(&self, id: SlideId) -> Option<usize> {
self.slides.iter().position(|s| s.id == id)
}
pub fn slide(&self, id: SlideId) -> Option<&Slide> {
self.slides.iter().find(|s| s.id == id).map(|s| s.as_ref())
}
pub fn slide_mut(&mut self, id: SlideId) -> Option<&mut Slide> {
self.slides
.iter_mut()
.find(|s| s.id == id)
.map(Arc::make_mut)
}
pub fn slide_at(&self, i: usize) -> Option<&Slide> {
self.slides.get(i).map(|s| s.as_ref())
}
pub fn layout(&self, id: LayoutId) -> Option<(&Master, &Layout)> {
self.masters
.iter()
.find_map(|m| m.layout(id).map(|l| (m.as_ref(), l)))
}
pub fn master_for(&self, slide: &Slide) -> Option<(&Master, Option<&Layout>)> {
if let Some((m, l)) = self.layout(slide.layout) {
return Some((m, Some(l)));
}
self.masters
.first()
.map(|m| (m.as_ref(), m.layouts.first()))
}
pub fn first_master(&self) -> Option<&Master> {
self.masters.first().map(|m| m.as_ref())
}
pub fn media(&self, id: MediaId) -> Option<&MediaItem> {
self.media.iter().find(|m| m.id == id)
}
pub fn add_media(&mut self, name: &str, content_type: &str, data: Vec<u8>) -> MediaId {
if let Some(m) = self
.media
.iter()
.find(|m| m.data.len() == data.len() && *m.data == data)
{
return m.id;
}
let id = MediaId(self.alloc_id());
self.media.push(MediaItem {
id,
name: name.into(),
content_type: content_type.into(),
data: Arc::new(data),
link: None,
});
id
}
pub fn section_of(&self, slide: SlideId) -> Option<usize> {
self.sections.iter().position(|s| s.slides.contains(&slide))
}
pub fn slide_number(&self, index: usize) -> u32 {
self.first_slide_number
.saturating_add(u32::try_from(index).unwrap_or(u32::MAX))
}
pub fn validate(&self) -> Vec<String> {
let mut out = vec![];
let mut seen = std::collections::HashSet::new();
for s in &self.slides {
if !seen.insert(s.id) {
out.push(format!("duplicate slide id {}", s.id));
}
if self.layout(s.layout).is_none() {
out.push(format!("slide {} uses missing layout {}", s.id, s.layout));
}
let mut ids = std::collections::HashSet::new();
walk(&s.shapes, &mut |sh, _| {
if !ids.insert(sh.id) {
out.push(format!("slide {}: duplicate shape id {}", s.id, sh.id));
}
});
}
out
}
}
#[cfg(test)]
mod tests;