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