Skip to main content

pptx_edit/
model.rs

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