Skip to main content

pptx_edit/
model.rs

1use ooxml_drawingml::{ShapeFill, ShapeOutline};
2use pptx_parse::{GraphicFrameData, Placeholder};
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Serialize, Deserialize)]
7#[serde(rename_all = "camelCase")]
8pub enum EditOrigin {
9    #[default]
10    Local,
11    Agent,
12    Remote,
13    System,
14}
15
16#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub struct EditCtx {
19    pub origin: EditOrigin,
20    pub author: String,
21}
22
23impl EditCtx {
24    pub fn local(author: impl Into<String>) -> Self {
25        Self {
26            origin: EditOrigin::Local,
27            author: author.into(),
28        }
29    }
30}
31
32#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
33#[serde(rename_all = "camelCase")]
34pub struct TextStyle {
35    pub bold: Option<bool>,
36    pub italic: Option<bool>,
37    pub font_size_pt: Option<f64>,
38    pub color: Option<String>,
39    pub font_family: Option<String>,
40    pub underline: Option<String>,
41}
42
43#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
44#[serde(rename_all = "camelCase")]
45pub struct TextStylePatch {
46    pub bold: Option<bool>,
47    pub italic: Option<bool>,
48    pub font_size_pt: Option<f64>,
49    pub color: Option<String>,
50    pub font_family: Option<String>,
51    pub underline: Option<String>,
52}
53
54#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
55#[serde(rename_all = "camelCase")]
56pub struct TextRunSnapshot {
57    pub text: String,
58    pub style: TextStyle,
59}
60
61#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
62#[serde(rename_all = "camelCase")]
63pub struct ParagraphSnapshot {
64    pub id: String,
65    pub alignment: Option<String>,
66    pub level: u32,
67    pub bullet_json: Option<String>,
68    pub runs: Vec<TextRunSnapshot>,
69}
70
71#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase")]
73pub struct StorySnapshot {
74    pub id: String,
75    pub length: u32,
76    pub paragraphs: Vec<ParagraphSnapshot>,
77}
78
79impl StorySnapshot {
80    pub fn plain_text(&self) -> String {
81        self.paragraphs
82            .iter()
83            .map(|paragraph| {
84                paragraph
85                    .runs
86                    .iter()
87                    .map(|run| run.text.as_str())
88                    .collect::<String>()
89            })
90            .collect::<Vec<_>>()
91            .join("\n")
92    }
93}
94
95#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
96#[serde(rename_all = "camelCase")]
97pub struct ShapeSnapshot {
98    pub id: String,
99    pub source_id: u32,
100    pub kind: ShapeKind,
101    pub name: String,
102    pub x: i64,
103    pub y: i64,
104    pub width: i64,
105    pub height: i64,
106    pub rotation_deg: f64,
107    pub flip_h: bool,
108    pub flip_v: bool,
109    pub geometry: String,
110    pub placeholder: Option<Placeholder>,
111    pub fill: Option<ShapeFill>,
112    pub outline: Option<ShapeOutline>,
113    pub media_part_path: Option<String>,
114    pub graphic: Option<GraphicFrameData>,
115    pub text_stories: Vec<StorySnapshot>,
116    pub children: Vec<ShapeSnapshot>,
117}
118
119#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
120#[serde(rename_all = "camelCase")]
121pub enum ShapeKind {
122    Shape,
123    Picture,
124    GraphicFrame,
125    Group,
126}
127
128#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
129#[serde(rename_all = "camelCase")]
130pub struct SlideSnapshot {
131    pub id: String,
132    pub source_part_path: Option<String>,
133    pub layout_part_path: Option<String>,
134    pub name: Option<String>,
135    pub shapes: Vec<ShapeSnapshot>,
136}
137
138#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
139#[serde(rename_all = "camelCase")]
140pub struct DeckSnapshot {
141    pub width_emu: i64,
142    pub height_emu: i64,
143    pub slides: Vec<SlideSnapshot>,
144}
145
146#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
147#[serde(rename_all = "camelCase")]
148pub struct SlideReceipt {
149    pub slide_id: String,
150    pub from_index: Option<u32>,
151    pub to_index: Option<u32>,
152}
153
154#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
155#[serde(rename_all = "camelCase")]
156pub struct ShapeReceipt {
157    pub slide_id: String,
158    pub shape_id: String,
159    pub index: u32,
160}
161
162#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
163#[serde(rename_all = "camelCase")]
164pub struct TransformReceipt {
165    pub slide_id: String,
166    pub shape_id: String,
167    pub before: ShapeRect,
168    pub after: ShapeRect,
169}
170
171#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
172pub struct ShapeRect {
173    pub x: i64,
174    pub y: i64,
175    pub width: i64,
176    pub height: i64,
177}
178
179#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
180#[serde(rename_all = "camelCase")]
181pub struct TextReceipt {
182    pub story_id: String,
183    pub start: u32,
184    pub end: u32,
185    pub text: String,
186}
187
188#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
189#[serde(rename_all = "camelCase")]
190pub struct ShapeDraft {
191    pub name: String,
192    pub rect: ShapeRect,
193    pub text: String,
194    pub style: TextStyle,
195}
196
197#[derive(Clone, Copy, Debug, Eq, PartialEq)]
198pub enum UpdateOrigin {
199    Local,
200    Remote,
201}
202
203#[derive(Clone, Debug, Eq, PartialEq)]
204pub struct UpdateEvent {
205    pub update: Vec<u8>,
206    pub origin: UpdateOrigin,
207}
208
209#[derive(Debug, Error)]
210pub enum EditError {
211    #[error("invalid client ID {0}")]
212    InvalidClientId(u64),
213    #[error("could not parse PPTX: {0}")]
214    Parse(String),
215    #[error("invalid deck state: {0}")]
216    InvalidState(String),
217    #[error("invalid yrs update: {0}")]
218    InvalidUpdate(String),
219    #[error("invalid yrs state vector: {0}")]
220    InvalidStateVector(String),
221    #[error("slide {0:?} was not found")]
222    SlideNotFound(String),
223    #[error("shape {0:?} was not found")]
224    ShapeNotFound(String),
225    #[error("story {0:?} was not found")]
226    StoryNotFound(String),
227    #[error("index {index} is outside length {length}")]
228    OutOfBounds { index: u32, length: u32 },
229    #[error("text range {start}..{end} crosses a paragraph boundary")]
230    ParagraphBoundary { start: u32, end: u32 },
231    #[error("invalid shape geometry: {0}")]
232    InvalidGeometry(String),
233    #[error("update observer failed: {0}")]
234    Observer(String),
235    #[error("JSON boundary error: {0}")]
236    Json(String),
237}
238
239pub type EditResult<T> = Result<T, EditError>;