Skip to main content

pptx_parse/
model.rs

1use std::collections::BTreeMap;
2
3use ooxml_drawingml::{ColorValue, ShapeFill, ShapeOutline, Theme};
4use serde::{Deserialize, Serialize};
5
6use crate::relationships::Relationship;
7
8pub use ooxml_drawingml::chart::{
9    ChartAxes, ChartAxis, ChartDataLabels, ChartLegend, ChartMarker, ChartPlotGroup, ChartPoint,
10    ChartPointLabel, ChartSeries, ChartSpace, ChartTextProperties,
11};
12
13#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub struct PptxPackage {
16    pub presentation: Presentation,
17    pub slides: Vec<Slide>,
18    pub layouts: Vec<SlideLayout>,
19    pub masters: Vec<SlideMaster>,
20    pub themes: Vec<ThemePart>,
21    /// Absent from packages serialized before charts were parsed.
22    #[serde(default)]
23    pub charts: Vec<ChartPart>,
24    pub media: Vec<MediaPart>,
25    pub relationships: BTreeMap<String, Vec<Relationship>>,
26    #[serde(skip)]
27    pub(crate) parts: Vec<PackagePart>,
28}
29
30impl PptxPackage {
31    pub fn part_bytes(&self, path: &str) -> Option<&[u8]> {
32        self.parts
33            .iter()
34            .find(|part| part.path == path)
35            .map(|part| part.bytes.as_slice())
36    }
37
38    /// False for packages recovered from a collaboration update, which carry
39    /// the parsed model but not the raw part bytes.
40    pub fn has_parts(&self) -> bool {
41        !self.parts.is_empty()
42    }
43
44    pub fn replace_part(&mut self, path: &str, bytes: Vec<u8>) -> bool {
45        let Some(part) = self.parts.iter_mut().find(|part| part.path == path) else {
46            return false;
47        };
48        part.bytes = bytes;
49        true
50    }
51}
52
53#[derive(Clone, Debug, PartialEq)]
54pub(crate) struct PackagePart {
55    pub path: String,
56    pub bytes: Vec<u8>,
57}
58
59#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
60#[serde(rename_all = "camelCase")]
61pub struct Presentation {
62    pub part_path: String,
63    pub width_emu: i64,
64    pub height_emu: i64,
65    pub slides: Vec<SlideReference>,
66    pub master_part_paths: Vec<String>,
67}
68
69#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
70#[serde(rename_all = "camelCase")]
71pub struct SlideReference {
72    pub id: u32,
73    pub relationship_id: String,
74    pub part_path: String,
75}
76
77#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
78#[serde(rename_all = "camelCase")]
79pub struct Slide {
80    pub part_path: String,
81    pub name: Option<String>,
82    pub layout_part_path: Option<String>,
83    pub show_master_shapes: bool,
84    pub background: Option<ShapeFill>,
85    pub shapes: Vec<ShapeNode>,
86}
87
88#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
89#[serde(rename_all = "camelCase")]
90pub struct SlideLayout {
91    pub part_path: String,
92    pub name: Option<String>,
93    pub layout_type: Option<String>,
94    pub master_part_path: Option<String>,
95    pub show_master_shapes: bool,
96    pub background: Option<ShapeFill>,
97    pub shapes: Vec<ShapeNode>,
98}
99
100#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
101#[serde(rename_all = "camelCase")]
102pub struct SlideMaster {
103    pub part_path: String,
104    pub name: Option<String>,
105    pub theme_part_path: Option<String>,
106    pub layout_part_paths: Vec<String>,
107    pub background: Option<ShapeFill>,
108    pub shapes: Vec<ShapeNode>,
109    pub text_styles: TextStyleSet,
110}
111
112#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
113#[serde(rename_all = "camelCase")]
114pub struct ThemePart {
115    pub part_path: String,
116    pub theme: Theme,
117}
118
119/// A chart part resolved against one referenced presentation theme.
120#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
121#[serde(rename_all = "camelCase")]
122pub struct ChartPart {
123    pub part_path: String,
124    pub theme_part_path: Option<String>,
125    pub chart: ChartSpace,
126}
127
128#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
129#[serde(rename_all = "camelCase")]
130pub struct MediaPart {
131    pub part_path: String,
132    pub content_type: String,
133    pub bytes: Vec<u8>,
134}
135
136#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
137#[serde(
138    tag = "kind",
139    rename_all = "camelCase",
140    rename_all_fields = "camelCase"
141)]
142pub enum ShapeNode {
143    Shape(Shape),
144    Picture(Picture),
145    GraphicFrame(GraphicFrame),
146    Group(GroupShape),
147}
148
149impl ShapeNode {
150    pub fn id(&self) -> u32 {
151        match self {
152            Self::Shape(shape) => shape.base.id,
153            Self::Picture(picture) => picture.base.id,
154            Self::GraphicFrame(frame) => frame.base.id,
155            Self::Group(group) => group.base.id,
156        }
157    }
158}
159
160#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
161#[serde(rename_all = "camelCase")]
162pub struct ShapeBase {
163    pub id: u32,
164    pub name: String,
165    pub description: Option<String>,
166    pub hidden: bool,
167    pub placeholder: Option<Placeholder>,
168    pub transform: ShapeTransform,
169}
170
171#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct ShapeTransform {
174    pub x: i64,
175    pub y: i64,
176    pub width: i64,
177    pub height: i64,
178    pub rotation_deg: f64,
179    pub flip_h: bool,
180    pub flip_v: bool,
181    pub child_x: Option<i64>,
182    pub child_y: Option<i64>,
183    pub child_width: Option<i64>,
184    pub child_height: Option<i64>,
185}
186
187#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
188#[serde(rename_all = "camelCase")]
189pub struct Placeholder {
190    pub placeholder_type: Option<String>,
191    pub index: Option<u32>,
192    pub orientation: Option<String>,
193    pub size: Option<String>,
194}
195
196#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
197#[serde(rename_all = "camelCase")]
198pub struct Shape {
199    #[serde(flatten)]
200    pub base: ShapeBase,
201    pub geometry: String,
202    #[serde(default)]
203    pub adjust_values: BTreeMap<String, f64>,
204    pub fill: Option<ShapeFill>,
205    pub outline: Option<ShapeOutline>,
206    pub text: Option<TextBody>,
207}
208
209#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
210#[serde(rename_all = "camelCase")]
211pub struct Picture {
212    #[serde(flatten)]
213    pub base: ShapeBase,
214    pub relationship_id: Option<String>,
215    pub media_part_path: Option<String>,
216    pub crop: PictureCrop,
217    pub fill: Option<ShapeFill>,
218    pub outline: Option<ShapeOutline>,
219}
220
221#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
222pub struct PictureCrop {
223    pub left: i32,
224    pub top: i32,
225    pub right: i32,
226    pub bottom: i32,
227}
228
229#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
230#[serde(rename_all = "camelCase")]
231pub struct GraphicFrame {
232    #[serde(flatten)]
233    pub base: ShapeBase,
234    pub data: GraphicFrameData,
235}
236
237#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
238#[serde(
239    tag = "type",
240    rename_all = "camelCase",
241    rename_all_fields = "camelCase"
242)]
243pub enum GraphicFrameData {
244    Table {
245        rows: Vec<Vec<TextBody>>,
246    },
247    Chart {
248        relationship_id: String,
249        part_path: Option<String>,
250    },
251    Diagram {
252        relationship_ids: Vec<String>,
253    },
254    Unknown {
255        uri: Option<String>,
256    },
257}
258
259#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
260#[serde(rename_all = "camelCase")]
261pub struct GroupShape {
262    #[serde(flatten)]
263    pub base: ShapeBase,
264    pub children: Vec<ShapeNode>,
265}
266
267#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
268#[serde(rename_all = "camelCase")]
269pub struct TextBody {
270    pub anchor: Option<String>,
271    pub vertical: Option<String>,
272    pub autofit: Option<TextAutofit>,
273    pub inset_left: Option<i64>,
274    pub inset_top: Option<i64>,
275    pub inset_right: Option<i64>,
276    pub inset_bottom: Option<i64>,
277    pub paragraphs: Vec<TextParagraph>,
278}
279
280#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
281#[serde(
282    tag = "type",
283    rename_all = "camelCase",
284    rename_all_fields = "camelCase"
285)]
286pub enum TextAutofit {
287    None,
288    Shape,
289    Normal {
290        font_scale: Option<f64>,
291        line_space_reduction: Option<f64>,
292    },
293}
294
295#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
296#[serde(rename_all = "camelCase")]
297pub struct TextParagraph {
298    pub properties: ParagraphProperties,
299    pub runs: Vec<TextRun>,
300    pub end_properties: Option<RunProperties>,
301}
302
303#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
304#[serde(rename_all = "camelCase")]
305pub struct ParagraphProperties {
306    pub alignment: Option<String>,
307    pub level: u32,
308    pub margin_left: Option<i64>,
309    pub indent: Option<i64>,
310    pub bullet: Option<Bullet>,
311    pub default_run: Option<RunProperties>,
312}
313
314#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
315#[serde(
316    tag = "type",
317    rename_all = "camelCase",
318    rename_all_fields = "camelCase"
319)]
320pub enum Bullet {
321    Character { value: String },
322    AutoNumber { scheme: String, start_at: u32 },
323    None,
324}
325
326#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
327#[serde(rename_all = "camelCase")]
328pub struct TextRun {
329    pub text: String,
330    pub properties: RunProperties,
331    pub field_id: Option<String>,
332    pub field_type: Option<String>,
333    pub line_break: bool,
334}
335
336#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
337#[serde(rename_all = "camelCase")]
338pub struct RunProperties {
339    pub font_size_pt: Option<f64>,
340    pub bold: Option<bool>,
341    pub italic: Option<bool>,
342    pub underline: Option<String>,
343    pub font_family: Option<String>,
344    pub color: Option<ColorValue>,
345    pub language: Option<String>,
346    pub hyperlink_relationship_id: Option<String>,
347}
348
349#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
350#[serde(rename_all = "camelCase")]
351pub struct TextStyleSet {
352    pub title: Vec<ParagraphProperties>,
353    pub body: Vec<ParagraphProperties>,
354    pub other: Vec<ParagraphProperties>,
355}