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