Skip to main content

rust_pptx/
model.rs

1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(rename_all = "lowercase")]
7pub enum PresentationFormat {
8    Pptx,
9    Ppt,
10}
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13#[serde(rename_all = "camelCase")]
14pub struct PresentationSize {
15    pub width_emu: u64,
16    pub height_emu: u64,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(rename_all = "camelCase")]
21pub struct PresentationWarning {
22    pub code: String,
23    pub message: String,
24    pub severity: String,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub slide_index: Option<usize>,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub node_id: Option<String>,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub part_name: Option<String>,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub feature: Option<String>,
33}
34
35impl PresentationWarning {
36    pub fn warning(code: &str, message: impl Into<String>) -> Self {
37        Self {
38            code: code.into(),
39            message: message.into(),
40            severity: "warning".into(),
41            slide_index: None,
42            node_id: None,
43            part_name: None,
44            feature: None,
45        }
46    }
47}
48
49#[derive(Debug, Clone, Default, Serialize, Deserialize)]
50#[serde(rename_all = "camelCase")]
51pub struct PresentationMetadata {
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub title: Option<String>,
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub subject: Option<String>,
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub creator: Option<String>,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub company: Option<String>,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
63#[serde(rename_all = "camelCase")]
64pub struct PresentationAsset {
65    pub id: String,
66    pub content_type: String,
67    pub byte_length: usize,
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub file_name: Option<String>,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub data: Option<serde_bytes::ByteBuf>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
75#[serde(rename_all = "camelCase")]
76pub struct PresentationEmbeddedFont {
77    pub family: String,
78    pub asset_id: String,
79    pub style: String,
80    pub weight: String,
81}
82
83#[derive(Debug, Clone, Default, Serialize, Deserialize)]
84#[serde(rename_all = "camelCase")]
85pub struct Transform {
86    pub x: i64,
87    pub y: i64,
88    pub width: i64,
89    pub height: i64,
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub rotation: Option<f64>,
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub flip_horizontal: Option<bool>,
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub flip_vertical: Option<bool>,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
99#[serde(
100    tag = "type",
101    rename_all = "kebab-case",
102    rename_all_fields = "camelCase"
103)]
104#[allow(clippy::large_enum_variant)]
105pub enum SlideNode {
106    Shape {
107        id: String,
108        name: String,
109        transform: Transform,
110        geometry: ShapeGeometry,
111        #[serde(skip_serializing_if = "Option::is_none")]
112        fill: Option<FillStyle>,
113        #[serde(skip_serializing_if = "Option::is_none")]
114        line: Option<LineStyle>,
115        paragraphs: Vec<TextParagraph>,
116        #[serde(skip_serializing_if = "Option::is_none")]
117        vertical_alignment: Option<VerticalAlignment>,
118        #[serde(skip_serializing_if = "Option::is_none")]
119        text_insets: Option<TextInsets>,
120        #[serde(skip_serializing_if = "Option::is_none")]
121        autofit: Option<TextAutofit>,
122        #[serde(skip_serializing_if = "Option::is_none")]
123        text_rotation: Option<f64>,
124        #[serde(skip_serializing_if = "Option::is_none")]
125        vertical_text: Option<String>,
126        #[serde(skip_serializing_if = "Option::is_none")]
127        horizontal_overflow: Option<String>,
128        #[serde(skip_serializing_if = "Option::is_none")]
129        vertical_overflow: Option<String>,
130        #[serde(skip_serializing_if = "Option::is_none")]
131        text_wrap: Option<String>,
132        #[serde(skip_serializing_if = "Option::is_none")]
133        column_count: Option<usize>,
134        #[serde(skip_serializing_if = "Option::is_none")]
135        column_spacing: Option<i64>,
136        #[serde(skip_serializing_if = "Option::is_none")]
137        right_to_left_columns: Option<bool>,
138        #[serde(skip_serializing_if = "Option::is_none")]
139        space_first_last_paragraph: Option<bool>,
140    },
141    Image {
142        id: String,
143        name: String,
144        transform: Transform,
145        asset_id: String,
146        #[serde(skip_serializing_if = "Option::is_none")]
147        crop: Option<ImageCrop>,
148        #[serde(skip_serializing_if = "Option::is_none")]
149        opacity: Option<f64>,
150        preserve_aspect_ratio: bool,
151        #[serde(skip_serializing_if = "Option::is_none")]
152        effects: Option<ImageEffects>,
153    },
154    Group {
155        id: String,
156        name: String,
157        transform: Transform,
158        children: Vec<SlideNode>,
159        #[serde(skip_serializing_if = "Option::is_none")]
160        child_transform: Option<Transform>,
161    },
162    Table {
163        id: String,
164        name: String,
165        transform: Transform,
166        rows: Vec<Vec<TableCell>>,
167        #[serde(skip_serializing_if = "Vec::is_empty")]
168        column_widths: Vec<i64>,
169        #[serde(skip_serializing_if = "Vec::is_empty")]
170        row_heights: Vec<i64>,
171    },
172    Chart {
173        id: String,
174        name: String,
175        transform: Transform,
176        chart_type: String,
177        #[serde(skip_serializing_if = "Option::is_none")]
178        title: Option<String>,
179        series: Vec<ChartSeries>,
180        #[serde(skip_serializing_if = "Option::is_none")]
181        has_legend: Option<bool>,
182        /// Raw DrawingML chart part XML (`c:chartSpace` or `cx:chartSpace`) so
183        /// viewers can render with full PowerPoint fidelity.
184        #[serde(skip_serializing_if = "Option::is_none")]
185        chart_xml: Option<String>,
186        /// Chart artwork at the dimensions of the slide frame.
187        #[serde(skip_serializing_if = "Option::is_none")]
188        chart_svg: Option<String>,
189        /// Optional companion chart style definition.
190        #[serde(skip_serializing_if = "Option::is_none")]
191        chart_style_xml: Option<String>,
192        /// Optional companion chart color style definition.
193        #[serde(skip_serializing_if = "Option::is_none")]
194        chart_colors_xml: Option<String>,
195    },
196    Unknown {
197        id: String,
198        name: String,
199        transform: Transform,
200        feature: String,
201    },
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
205#[serde(rename_all = "camelCase")]
206pub struct TableCell {
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub row_span: Option<usize>,
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub col_span: Option<usize>,
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub fill: Option<FillStyle>,
213    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
214    pub borders: BTreeMap<String, LineStyle>,
215    pub paragraphs: Vec<TextParagraph>,
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub text_insets: Option<TextInsets>,
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub vertical_alignment: Option<VerticalAlignment>,
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub text_rotation: Option<f64>,
222}
223
224#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
225#[serde(rename_all = "camelCase")]
226pub struct TextInsets {
227    pub top: i64,
228    pub right: i64,
229    pub bottom: i64,
230    pub left: i64,
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
234#[serde(rename_all = "camelCase")]
235pub struct ImageCrop {
236    pub top: f64,
237    pub right: f64,
238    pub bottom: f64,
239    pub left: f64,
240}
241
242/// DrawingML picture color effects applied to an `a:blip`.
243#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
244#[serde(rename_all = "camelCase")]
245pub struct ImageEffects {
246    /// `a:biLevel` black/white threshold, normalized to 0..1.
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub bi_level_threshold: Option<f64>,
249    /// `a:grayscl` grayscale recolor.
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub grayscale: Option<bool>,
252    /// `a:duotone` two-color ramp, dark then light.
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub duotone: Option<Vec<ColorValue>>,
255    /// `a:lum` brightness, normalized to -1..1.
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub brightness: Option<f64>,
258    /// `a:lum` contrast, normalized to -1..1.
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub contrast: Option<f64>,
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
264#[serde(rename_all = "camelCase")]
265pub struct ChartSeries {
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub name: Option<String>,
268    #[serde(skip_serializing_if = "Option::is_none")]
269    pub categories: Option<Vec<serde_json::Value>>,
270    pub values: Vec<Option<f64>>,
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub color: Option<ColorValue>,
273}
274
275#[derive(Debug, Clone, Default, Serialize, Deserialize)]
276pub struct ShapeGeometry {
277    #[serde(skip_serializing_if = "Option::is_none")]
278    pub preset: Option<String>,
279    /// SVG path data normalized to a 0..1 view box for DrawingML custom geometry.
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub path: Option<String>,
282}
283
284#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
285#[serde(rename_all = "camelCase")]
286pub struct ColorValue {
287    pub value: String,
288    #[serde(skip_serializing_if = "Option::is_none")]
289    pub alpha: Option<f64>,
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize)]
293#[serde(rename_all = "camelCase")]
294pub struct GradientStop {
295    pub position: f64,
296    pub color: ColorValue,
297}
298
299#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
300#[serde(rename_all = "lowercase")]
301pub enum FillImageMode {
302    Stretch,
303    Tile,
304}
305
306#[derive(Debug, Clone, Serialize, Deserialize)]
307#[serde(
308    tag = "type",
309    rename_all = "lowercase",
310    rename_all_fields = "camelCase"
311)]
312pub enum FillStyle {
313    None,
314    Solid {
315        color: ColorValue,
316    },
317    Gradient {
318        #[serde(skip_serializing_if = "Option::is_none")]
319        angle: Option<f64>,
320        stops: Vec<GradientStop>,
321    },
322    Pattern {
323        preset: String,
324        foreground: ColorValue,
325        background: ColorValue,
326    },
327    Image {
328        asset_id: String,
329        mode: FillImageMode,
330        #[serde(skip_serializing_if = "Option::is_none")]
331        crop: Option<ImageCrop>,
332        #[serde(skip_serializing_if = "Option::is_none")]
333        opacity: Option<f64>,
334        #[serde(skip_serializing_if = "Option::is_none")]
335        effects: Option<ImageEffects>,
336    },
337}
338
339#[derive(Debug, Clone, Default, Serialize, Deserialize)]
340#[serde(rename_all = "camelCase")]
341pub struct LineStyle {
342    #[serde(skip_serializing_if = "Option::is_none")]
343    pub color: Option<ColorValue>,
344    #[serde(skip_serializing_if = "Option::is_none")]
345    pub width: Option<f64>,
346    #[serde(skip_serializing_if = "Option::is_none")]
347    pub dash: Option<String>,
348    #[serde(skip_serializing_if = "Option::is_none")]
349    pub start_arrow: Option<String>,
350    #[serde(skip_serializing_if = "Option::is_none")]
351    pub end_arrow: Option<String>,
352}
353
354#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
355#[serde(rename_all = "lowercase")]
356pub enum VerticalAlignment {
357    Top,
358    Middle,
359    Bottom,
360}
361
362#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
363#[serde(rename_all = "lowercase")]
364pub enum TextAutoFitMode {
365    None,
366    Normal,
367    Shape,
368}
369
370#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
371#[serde(rename_all = "camelCase")]
372pub struct TextAutofit {
373    pub mode: TextAutoFitMode,
374    #[serde(skip_serializing_if = "Option::is_none")]
375    pub font_scale: Option<f64>,
376    #[serde(skip_serializing_if = "Option::is_none")]
377    pub line_spacing_reduction: Option<f64>,
378}
379
380#[derive(Debug, Clone, Default, Serialize, Deserialize)]
381#[serde(rename_all = "camelCase")]
382pub struct TextParagraph {
383    pub runs: Vec<TextRun>,
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub alignment: Option<String>,
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub level: Option<usize>,
388    #[serde(skip_serializing_if = "Option::is_none")]
389    pub bullet: Option<TextBullet>,
390    #[serde(skip_serializing_if = "Option::is_none")]
391    pub line_spacing: Option<TextSpacing>,
392    #[serde(skip_serializing_if = "Option::is_none")]
393    pub space_before: Option<TextSpacing>,
394    #[serde(skip_serializing_if = "Option::is_none")]
395    pub space_after: Option<TextSpacing>,
396    #[serde(skip_serializing_if = "Option::is_none")]
397    pub rtl: Option<bool>,
398    #[serde(skip_serializing_if = "Option::is_none")]
399    pub margin_left_emu: Option<i64>,
400    #[serde(skip_serializing_if = "Option::is_none")]
401    pub indent_emu: Option<i64>,
402    #[serde(skip_serializing_if = "Option::is_none")]
403    pub default_tab_size_emu: Option<i64>,
404    #[serde(skip_serializing_if = "Option::is_none")]
405    pub tab_stops: Option<Vec<TextTabStop>>,
406    #[serde(skip_serializing_if = "Option::is_none")]
407    pub east_asian_line_break: Option<bool>,
408    #[serde(skip_serializing_if = "Option::is_none")]
409    pub latin_line_break: Option<bool>,
410    #[serde(skip_serializing_if = "Option::is_none")]
411    pub hanging_punctuation: Option<bool>,
412}
413
414#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
415#[serde(rename_all = "lowercase")]
416pub enum TextTabAlignment {
417    Left,
418    Center,
419    Right,
420    Decimal,
421}
422
423#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
424#[serde(rename_all = "camelCase")]
425pub struct TextTabStop {
426    pub position_emu: i64,
427    pub alignment: TextTabAlignment,
428}
429
430#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
431#[serde(rename_all = "camelCase")]
432pub struct TextSpacing {
433    pub value: f64,
434    pub unit: TextSpacingUnit,
435}
436
437#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
438#[serde(rename_all = "lowercase")]
439pub enum TextSpacingUnit {
440    Points,
441    Percent,
442}
443
444#[derive(Debug, Clone, Serialize, Deserialize)]
445#[serde(rename_all = "camelCase")]
446pub struct TextBullet {
447    pub kind: String,
448    #[serde(skip_serializing_if = "Option::is_none")]
449    pub value: Option<String>,
450    #[serde(skip_serializing_if = "Option::is_none")]
451    pub font_family: Option<String>,
452    #[serde(skip_serializing_if = "Option::is_none")]
453    pub font_size_pt: Option<f64>,
454    #[serde(skip_serializing_if = "Option::is_none")]
455    pub size_percent: Option<f64>,
456    #[serde(skip_serializing_if = "Option::is_none")]
457    pub start_at: Option<usize>,
458}
459
460#[derive(Debug, Clone, Default, Serialize, Deserialize)]
461#[serde(rename_all = "camelCase")]
462pub struct TextRun {
463    pub text: String,
464    #[serde(skip_serializing_if = "Option::is_none")]
465    pub font_family: Option<String>,
466    #[serde(skip_serializing_if = "Option::is_none")]
467    pub east_asian_font_family: Option<String>,
468    #[serde(skip_serializing_if = "Option::is_none")]
469    pub complex_script_font_family: Option<String>,
470    #[serde(skip_serializing_if = "Option::is_none")]
471    pub symbol_font_family: Option<String>,
472    #[serde(skip_serializing_if = "Option::is_none")]
473    pub font_size_pt: Option<f64>,
474    #[serde(skip_serializing_if = "Option::is_none")]
475    pub bold: Option<bool>,
476    #[serde(skip_serializing_if = "Option::is_none")]
477    pub italic: Option<bool>,
478    #[serde(skip_serializing_if = "Option::is_none")]
479    pub underline: Option<bool>,
480    #[serde(skip_serializing_if = "Option::is_none")]
481    pub strike: Option<bool>,
482    #[serde(skip_serializing_if = "Option::is_none")]
483    pub color: Option<ColorValue>,
484    #[serde(skip_serializing_if = "Option::is_none")]
485    pub baseline: Option<f64>,
486    #[serde(skip_serializing_if = "Option::is_none")]
487    pub language: Option<String>,
488    #[serde(skip_serializing_if = "Option::is_none")]
489    pub alternative_language: Option<String>,
490    #[serde(skip_serializing_if = "Option::is_none")]
491    pub right_to_left: Option<bool>,
492    #[serde(skip_serializing_if = "Option::is_none")]
493    pub hyperlink: Option<String>,
494    #[serde(skip_serializing_if = "Option::is_none")]
495    pub character_spacing_pt: Option<f64>,
496    #[serde(skip_serializing_if = "Option::is_none")]
497    pub kerning_threshold_pt: Option<f64>,
498}
499
500#[derive(Debug, Clone, Serialize, Deserialize)]
501#[serde(rename_all = "camelCase")]
502pub struct PresentationTheme {
503    pub id: String,
504    #[serde(skip_serializing_if = "Option::is_none")]
505    pub name: Option<String>,
506    pub colors: BTreeMap<String, String>,
507    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
508    pub major_fonts: BTreeMap<String, String>,
509    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
510    pub minor_fonts: BTreeMap<String, String>,
511}
512
513#[derive(Debug, Clone, Serialize, Deserialize)]
514#[serde(rename_all = "camelCase")]
515pub struct SlideMaster {
516    pub id: String,
517    #[serde(skip_serializing_if = "Option::is_none")]
518    pub name: Option<String>,
519    #[serde(skip_serializing_if = "Option::is_none")]
520    pub theme_id: Option<String>,
521    pub nodes: Vec<SlideNode>,
522}
523
524#[derive(Debug, Clone, Serialize, Deserialize)]
525#[serde(rename_all = "camelCase")]
526pub struct SlideLayout {
527    pub id: String,
528    #[serde(skip_serializing_if = "Option::is_none")]
529    pub name: Option<String>,
530    #[serde(skip_serializing_if = "Option::is_none")]
531    pub master_id: Option<String>,
532    pub nodes: Vec<SlideNode>,
533}
534
535#[derive(Debug, Clone, Serialize, Deserialize)]
536#[serde(rename_all = "camelCase")]
537pub struct PresentationSlide {
538    pub id: String,
539    pub index: usize,
540    #[serde(skip_serializing_if = "Option::is_none")]
541    pub name: Option<String>,
542    #[serde(skip_serializing_if = "Option::is_none")]
543    pub hidden: Option<bool>,
544    #[serde(skip_serializing_if = "Option::is_none")]
545    pub master_id: Option<String>,
546    #[serde(skip_serializing_if = "Option::is_none")]
547    pub layout_id: Option<String>,
548    #[serde(skip_serializing_if = "Option::is_none")]
549    pub background: Option<FillStyle>,
550    pub nodes: Vec<SlideNode>,
551    #[serde(skip_serializing_if = "Vec::is_empty")]
552    pub notes: Vec<SlideNote>,
553    #[serde(skip_serializing_if = "Vec::is_empty")]
554    pub comments: Vec<SlideComment>,
555    #[serde(skip_serializing_if = "Option::is_none")]
556    pub source_part: Option<String>,
557    pub warnings: Vec<PresentationWarning>,
558}
559
560#[derive(Debug, Clone, Serialize, Deserialize)]
561pub struct SlideNote {
562    pub text: String,
563}
564
565#[derive(Debug, Clone, Serialize, Deserialize)]
566#[serde(rename_all = "camelCase")]
567pub struct SlideComment {
568    pub id: String,
569    pub text: String,
570    #[serde(skip_serializing_if = "Option::is_none")]
571    pub author: Option<String>,
572    #[serde(skip_serializing_if = "Option::is_none")]
573    pub created_at: Option<String>,
574    #[serde(skip_serializing_if = "Option::is_none")]
575    pub x: Option<f64>,
576    #[serde(skip_serializing_if = "Option::is_none")]
577    pub y: Option<f64>,
578}
579
580#[derive(Debug, Clone, Serialize, Deserialize)]
581#[serde(rename_all = "camelCase")]
582pub struct PresentationDocument {
583    pub format: PresentationFormat,
584    pub size: PresentationSize,
585    pub slides: Vec<PresentationSlide>,
586    pub masters: Vec<SlideMaster>,
587    pub layouts: Vec<SlideLayout>,
588    pub themes: Vec<PresentationTheme>,
589    pub assets: BTreeMap<String, PresentationAsset>,
590    #[serde(default, skip_serializing_if = "Vec::is_empty")]
591    pub embedded_fonts: Vec<PresentationEmbeddedFont>,
592    pub warnings: Vec<PresentationWarning>,
593    #[serde(skip_serializing_if = "Option::is_none")]
594    pub metadata: Option<PresentationMetadata>,
595}