Skip to main content

pptx_parse/
model.rs

1use std::collections::BTreeMap;
2
3pub use ooxml_drawingml::ShapeStyle;
4use ooxml_drawingml::{
5    ColorValue, GeometryPathCommand, ShapeEffects, ShapeFill, ShapeOutline, TableStyleList, Theme,
6    ThemeFormatScheme,
7};
8use serde::{Deserialize, Serialize};
9
10use crate::comments::{Comment, CommentAuthor, CommentFlavor};
11use crate::relationships::Relationship;
12
13pub use ooxml_drawingml::chart::{
14    ChartAxes, ChartAxis, ChartDataLabels, ChartLegend, ChartMarker, ChartPlotGroup, ChartPoint,
15    ChartPointLabel, ChartSeries, ChartSpace, ChartTextProperties,
16};
17
18/// Shape elements counted by source ordinals.
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "camelCase")]
21pub(crate) enum ShapeElements {
22    WithConnectors,
23    #[default]
24    WithoutConnectors,
25}
26
27impl ShapeElements {
28    fn is_legacy(&self) -> bool {
29        *self == Self::WithoutConnectors
30    }
31
32    pub(crate) fn contains(self, local: &str) -> bool {
33        matches!(local, "sp" | "pic" | "graphicFrame" | "grpSp")
34            || (self == Self::WithConnectors && local == "cxnSp")
35    }
36}
37
38#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
39#[serde(rename_all = "camelCase")]
40pub struct PptxPackage {
41    pub presentation: Presentation,
42    pub slides: Vec<Slide>,
43    pub layouts: Vec<SlideLayout>,
44    pub masters: Vec<SlideMaster>,
45    pub themes: Vec<ThemePart>,
46    /// Absent from packages serialized before charts were parsed.
47    #[serde(default)]
48    pub charts: Vec<ChartPart>,
49    pub media: Vec<MediaPart>,
50    /// Absent from packages serialized before table styles were parsed.
51    #[serde(default, skip_serializing_if = "TableStyleList::is_empty")]
52    pub table_styles: TableStyleList,
53    #[serde(default, skip_serializing_if = "Vec::is_empty")]
54    pub comment_authors: Vec<CommentAuthor>,
55    #[serde(default, skip_serializing_if = "Vec::is_empty")]
56    pub comments: Vec<Comment>,
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub comment_flavor: Option<CommentFlavor>,
59    pub relationships: BTreeMap<String, Vec<Relationship>>,
60    #[serde(skip)]
61    pub(crate) parts: Vec<PackagePart>,
62    #[serde(default, skip_serializing_if = "ShapeElements::is_legacy")]
63    pub(crate) shape_elements: ShapeElements,
64}
65
66impl PptxPackage {
67    pub fn part_bytes(&self, path: &str) -> Option<&[u8]> {
68        self.parts
69            .iter()
70            .find(|part| part.path == path)
71            .map(|part| part.bytes.as_slice())
72    }
73
74    /// False for packages recovered from a collaboration update, which carry
75    /// the parsed model but not the raw part bytes.
76    pub fn has_parts(&self) -> bool {
77        !self.parts.is_empty()
78    }
79
80    /// Whether source ordinals include connectors.
81    pub fn models_connectors(&self) -> bool {
82        self.shape_elements == ShapeElements::WithConnectors
83    }
84
85    pub fn replace_part(&mut self, path: &str, bytes: Vec<u8>) -> bool {
86        let Some(part) = self.parts.iter_mut().find(|part| part.path == path) else {
87            return false;
88        };
89        part.bytes = bytes;
90        true
91    }
92}
93
94#[derive(Clone, Debug, PartialEq)]
95pub(crate) struct PackagePart {
96    pub path: String,
97    pub bytes: Vec<u8>,
98}
99
100#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
101#[serde(rename_all = "camelCase")]
102pub struct Presentation {
103    pub part_path: String,
104    pub width_emu: i64,
105    pub height_emu: i64,
106    /// `p:presentation/@firstSlideNum`.
107    #[serde(
108        default = "default_first_slide_num",
109        skip_serializing_if = "is_default_first_slide_num"
110    )]
111    pub first_slide_num: i32,
112    pub slides: Vec<SlideReference>,
113    pub master_part_paths: Vec<String>,
114}
115
116fn default_first_slide_num() -> i32 {
117    1
118}
119
120fn is_default_first_slide_num(value: &i32) -> bool {
121    *value == default_first_slide_num()
122}
123
124#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
125#[serde(rename_all = "camelCase")]
126pub struct SlideReference {
127    pub id: u32,
128    pub relationship_id: String,
129    pub part_path: String,
130}
131
132#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
133#[serde(rename_all = "camelCase")]
134pub struct Slide {
135    pub part_path: String,
136    pub name: Option<String>,
137    pub layout_part_path: Option<String>,
138    pub show_master_shapes: bool,
139    pub background: Option<ShapeFill>,
140    pub shapes: Vec<ShapeNode>,
141    #[serde(default, skip_serializing_if = "String::is_empty")]
142    pub notes: String,
143}
144
145#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
146#[serde(rename_all = "camelCase")]
147pub struct SlideLayout {
148    pub part_path: String,
149    pub name: Option<String>,
150    pub layout_type: Option<String>,
151    pub master_part_path: Option<String>,
152    pub show_master_shapes: bool,
153    pub background: Option<ShapeFill>,
154    pub shapes: Vec<ShapeNode>,
155}
156
157#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
158#[serde(rename_all = "camelCase")]
159pub struct SlideMaster {
160    pub part_path: String,
161    pub name: Option<String>,
162    pub theme_part_path: Option<String>,
163    pub layout_part_paths: Vec<String>,
164    pub background: Option<ShapeFill>,
165    pub shapes: Vec<ShapeNode>,
166    pub text_styles: TextStyleSet,
167}
168
169#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
170#[serde(rename_all = "camelCase")]
171pub struct ThemePart {
172    pub part_path: String,
173    pub theme: Theme,
174    /// Absent from packages serialized before `a:fmtScheme` was parsed.
175    #[serde(default, skip_serializing_if = "ThemeFormatScheme::is_empty")]
176    pub format_scheme: ThemeFormatScheme,
177}
178
179/// A chart part resolved against one referenced presentation theme.
180#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
181#[serde(rename_all = "camelCase")]
182pub struct ChartPart {
183    pub part_path: String,
184    pub theme_part_path: Option<String>,
185    pub chart: ChartSpace,
186}
187
188#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
189#[serde(rename_all = "camelCase")]
190pub struct MediaPart {
191    pub part_path: String,
192    pub content_type: String,
193    pub bytes: Vec<u8>,
194}
195
196#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
197#[serde(
198    tag = "kind",
199    rename_all = "camelCase",
200    rename_all_fields = "camelCase"
201)]
202pub enum ShapeNode {
203    Shape(Shape),
204    Picture(Picture),
205    GraphicFrame(GraphicFrame),
206    Group(GroupShape),
207}
208
209impl ShapeNode {
210    pub fn id(&self) -> u32 {
211        match self {
212            Self::Shape(shape) => shape.base.id,
213            Self::Picture(picture) => picture.base.id,
214            Self::GraphicFrame(frame) => frame.base.id,
215            Self::Group(group) => group.base.id,
216        }
217    }
218}
219
220#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
221#[serde(rename_all = "camelCase")]
222pub struct ShapeBase {
223    pub id: u32,
224    pub name: String,
225    pub description: Option<String>,
226    pub hidden: bool,
227    pub placeholder: Option<Placeholder>,
228    pub transform: ShapeTransform,
229}
230
231#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
232#[serde(rename_all = "camelCase")]
233pub struct ShapeTransform {
234    pub x: i64,
235    pub y: i64,
236    pub width: i64,
237    pub height: i64,
238    pub rotation_deg: f64,
239    pub flip_h: bool,
240    pub flip_v: bool,
241    pub child_x: Option<i64>,
242    pub child_y: Option<i64>,
243    pub child_width: Option<i64>,
244    pub child_height: Option<i64>,
245}
246
247#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
248#[serde(rename_all = "camelCase")]
249pub struct Placeholder {
250    pub placeholder_type: Option<String>,
251    pub index: Option<u32>,
252    pub orientation: Option<String>,
253    pub size: Option<String>,
254}
255
256#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
257#[serde(rename_all = "camelCase")]
258pub struct Shape {
259    #[serde(flatten)]
260    pub base: ShapeBase,
261    pub geometry: String,
262    #[serde(
263        default = "has_preset_geometry_default",
264        skip_serializing_if = "is_preset_geometry"
265    )]
266    pub has_preset_geometry: bool,
267    /// Custom paths in shape-relative coordinates.
268    #[serde(default, skip_serializing_if = "Vec::is_empty")]
269    pub paths: Vec<CustomGeometryPath>,
270    /// Theme formatting and text defaults.
271    #[serde(default, skip_serializing_if = "Option::is_none")]
272    pub style: Option<Box<ShapeStyle>>,
273    #[serde(default)]
274    pub adjust_values: BTreeMap<String, f64>,
275    pub fill: Option<ShapeFill>,
276    /// The image behind an `a:blipFill`, when the fill is a stretched picture.
277    #[serde(default, skip_serializing_if = "Option::is_none")]
278    pub picture_fill: Option<Box<PictureFill>>,
279    pub outline: Option<ShapeOutline>,
280    #[serde(default, skip_serializing_if = "Option::is_none")]
281    pub effects: Option<ShapeEffects>,
282    pub text: Option<TextBody>,
283}
284
285/// An `a:blipFill` on a shape: the image, and the box it stretches into.
286#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
287#[serde(rename_all = "camelCase")]
288pub struct PictureFill {
289    #[serde(default, skip_serializing_if = "Option::is_none")]
290    pub relationship_id: Option<String>,
291    #[serde(default, skip_serializing_if = "Option::is_none")]
292    pub media_part_path: Option<String>,
293    #[serde(default, skip_serializing_if = "PictureCrop::is_whole")]
294    pub crop: PictureCrop,
295    /// `a:stretch/a:fillRect` insets, in thousandths of a percent of the box.
296    #[serde(default, skip_serializing_if = "PictureCrop::is_whole")]
297    pub fill_rect: PictureCrop,
298}
299
300#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
301#[serde(rename_all = "camelCase")]
302pub struct CustomGeometryPath {
303    pub commands: Vec<GeometryPathCommand>,
304    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
305    pub no_fill: bool,
306    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
307    pub no_stroke: bool,
308}
309
310#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
311#[serde(rename_all = "camelCase")]
312pub struct Picture {
313    #[serde(flatten)]
314    pub base: ShapeBase,
315    pub relationship_id: Option<String>,
316    pub media_part_path: Option<String>,
317    pub crop: PictureCrop,
318    /// Bitmap effects in document order.
319    #[serde(default, skip_serializing_if = "Vec::is_empty")]
320    pub effects: Vec<BlipEffect>,
321    /// Preset mask; defaults to the frame rectangle.
322    #[serde(default = "rect_geometry", skip_serializing_if = "is_rect")]
323    pub geometry: String,
324    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
325    pub adjust_values: BTreeMap<String, f64>,
326    pub fill: Option<ShapeFill>,
327    pub outline: Option<ShapeOutline>,
328    #[serde(default, skip_serializing_if = "Option::is_none")]
329    pub shape_effects: Option<ShapeEffects>,
330    #[serde(default, skip_serializing_if = "Option::is_none")]
331    pub style: Option<Box<ShapeStyle>>,
332}
333
334fn rect_geometry() -> String {
335    "rect".to_owned()
336}
337
338fn is_rect(geometry: &str) -> bool {
339    geometry == "rect"
340}
341
342fn has_preset_geometry_default() -> bool {
343    true
344}
345
346fn is_preset_geometry(value: &bool) -> bool {
347    *value
348}
349
350/// Bitmap effects with unresolved colours.
351#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
352#[serde(tag = "type", rename_all = "camelCase")]
353pub enum BlipEffect {
354    /// `a:biLevel`: luminance below `threshold` becomes black, the rest white.
355    BiLevel { threshold: f64 },
356    /// `a:grayscl`.
357    Grayscale,
358    /// `a:lum`: brightness and contrast, each a fraction in `-1.0..=1.0`.
359    Luminance { brightness: f64, contrast: f64 },
360    /// `a:duotone`: luminance interpolates between the two colours.
361    Duotone {
362        shadow: Option<ColorValue>,
363        highlight: Option<ColorValue>,
364    },
365    /// Exact colour replacement.
366    ColorChange {
367        from: Option<ColorValue>,
368        to: Option<ColorValue>,
369        #[serde(
370            default = "default_use_alpha",
371            skip_serializing_if = "use_alpha_is_default",
372            rename = "useAlpha"
373        )]
374        use_alpha: bool,
375    },
376}
377
378fn default_use_alpha() -> bool {
379    true
380}
381
382fn use_alpha_is_default(value: &bool) -> bool {
383    *value
384}
385
386#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
387pub struct PictureCrop {
388    pub left: i32,
389    pub top: i32,
390    pub right: i32,
391    pub bottom: i32,
392}
393
394impl PictureCrop {
395    pub fn is_whole(&self) -> bool {
396        *self == Self::default()
397    }
398}
399
400#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
401#[serde(rename_all = "camelCase")]
402pub struct GraphicFrame {
403    #[serde(flatten)]
404    pub base: ShapeBase,
405    pub data: GraphicFrameData,
406}
407
408#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
409#[serde(
410    tag = "type",
411    rename_all = "camelCase",
412    rename_all_fields = "camelCase"
413)]
414pub enum GraphicFrameData {
415    Table(Table),
416    Chart {
417        relationship_id: String,
418        part_path: Option<String>,
419    },
420    Diagram {
421        relationship_ids: Vec<String>,
422    },
423    Unknown {
424        uri: Option<String>,
425        #[serde(default, skip_serializing_if = "Option::is_none")]
426        picture: Option<Box<Picture>>,
427    },
428}
429
430#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
431#[serde(rename_all = "camelCase")]
432pub struct GroupShape {
433    #[serde(flatten)]
434    pub base: ShapeBase,
435    pub children: Vec<ShapeNode>,
436}
437
438/// An `a:tbl`: its column grid, table-wide properties and rows.
439#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
440#[serde(rename_all = "camelCase")]
441pub struct Table {
442    /// `a:gridCol/@w`, in EMU.
443    #[serde(default, skip_serializing_if = "Vec::is_empty")]
444    pub grid: Vec<i64>,
445    #[serde(default, skip_serializing_if = "TableProperties::is_default")]
446    pub properties: TableProperties,
447    #[serde(
448        default,
449        serialize_with = "serialize_table_rows",
450        deserialize_with = "deserialize_table_rows"
451    )]
452    pub rows: Vec<TableRow>,
453}
454
455/// `a:tblPr`: which style parts apply, and the style they come from.
456#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
457#[serde(rename_all = "camelCase", default)]
458pub struct TableProperties {
459    pub first_row: bool,
460    pub last_row: bool,
461    pub first_col: bool,
462    pub last_col: bool,
463    pub band_row: bool,
464    pub band_col: bool,
465    #[serde(skip_serializing_if = "Option::is_none")]
466    pub style_id: Option<String>,
467}
468
469impl TableProperties {
470    pub fn is_default(&self) -> bool {
471        *self == Self::default()
472    }
473}
474
475#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
476#[serde(rename_all = "camelCase")]
477pub struct TableRow {
478    /// `a:tr/@h`, in EMU, a minimum rather than a fixed height.
479    #[serde(default)]
480    pub height: i64,
481    pub cells: Vec<TableCell>,
482}
483
484impl TableRow {
485    fn is_text_only(&self) -> bool {
486        self.height == 0 && self.cells.iter().all(TableCell::is_text_only)
487    }
488}
489
490/// An `a:tc`, with its `a:tcPr` anchoring and margins folded into `text`.
491#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
492#[serde(rename_all = "camelCase")]
493pub struct TableCell {
494    pub text: TextBody,
495    #[serde(default = "unit_span", skip_serializing_if = "is_unit_span")]
496    pub grid_span: u32,
497    #[serde(default = "unit_span", skip_serializing_if = "is_unit_span")]
498    pub row_span: u32,
499    /// An `hMerge`/`vMerge` continuation: covered by an earlier origin cell.
500    #[serde(default, skip_serializing_if = "is_false")]
501    pub merged: bool,
502    #[serde(default, skip_serializing_if = "Option::is_none")]
503    pub fill: Option<ShapeFill>,
504    #[serde(default, skip_serializing_if = "TableCellBorders::is_empty")]
505    pub borders: TableCellBorders,
506}
507
508impl TableCell {
509    pub fn from_text(text: TextBody) -> Self {
510        Self {
511            text,
512            ..Self::default()
513        }
514    }
515
516    /// Compared against a cell built from this text alone, so a field added
517    /// later cannot be silently dropped by the released encoding.
518    fn is_text_only(&self) -> bool {
519        *self == Self::from_text(self.text.clone())
520    }
521}
522
523impl Default for TableCell {
524    fn default() -> Self {
525        Self {
526            text: TextBody::default(),
527            grid_span: 1,
528            row_span: 1,
529            merged: false,
530            fill: None,
531            borders: TableCellBorders::default(),
532        }
533    }
534}
535
536/// `a:tcPr/a:lnL`, `a:lnT`, `a:lnR` and `a:lnB`.
537#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
538#[serde(rename_all = "camelCase", default)]
539pub struct TableCellBorders {
540    #[serde(skip_serializing_if = "Option::is_none")]
541    pub left: Option<ShapeOutline>,
542    #[serde(skip_serializing_if = "Option::is_none")]
543    pub top: Option<ShapeOutline>,
544    #[serde(skip_serializing_if = "Option::is_none")]
545    pub right: Option<ShapeOutline>,
546    #[serde(skip_serializing_if = "Option::is_none")]
547    pub bottom: Option<ShapeOutline>,
548}
549
550impl TableCellBorders {
551    pub fn is_empty(&self) -> bool {
552        *self == Self::default()
553    }
554}
555
556fn unit_span() -> u32 {
557    1
558}
559
560fn is_unit_span(span: &u32) -> bool {
561    *span == 1
562}
563
564fn is_false(value: &bool) -> bool {
565    !*value
566}
567
568/// Writes rows that hold nothing but cell text in the released encoding, so a
569/// stored package keeps its bytes until a table gains geometry.
570fn serialize_table_rows<S>(rows: &[TableRow], serializer: S) -> Result<S::Ok, S::Error>
571where
572    S: serde::Serializer,
573{
574    use serde::ser::SerializeSeq;
575
576    if !rows.iter().all(TableRow::is_text_only) {
577        return rows.serialize(serializer);
578    }
579    let mut sequence = serializer.serialize_seq(Some(rows.len()))?;
580    for row in rows {
581        let cells: Vec<_> = row.cells.iter().map(|cell| &cell.text).collect();
582        sequence.serialize_element(&cells)?;
583    }
584    sequence.end()
585}
586
587/// Also accepts the released encoding's rows, which were bare `a:txBody` lists.
588fn deserialize_table_rows<'de, D>(deserializer: D) -> Result<Vec<TableRow>, D::Error>
589where
590    D: serde::Deserializer<'de>,
591{
592    #[derive(Deserialize)]
593    #[serde(untagged)]
594    enum Row {
595        Modern(TableRow),
596        Legacy(Vec<TextBody>),
597    }
598
599    Ok(Vec::<Row>::deserialize(deserializer)?
600        .into_iter()
601        .map(|row| match row {
602            Row::Modern(row) => row,
603            Row::Legacy(cells) => TableRow {
604                height: 0,
605                cells: cells.into_iter().map(TableCell::from_text).collect(),
606            },
607        })
608        .collect())
609}
610
611#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
612#[serde(rename_all = "camelCase")]
613pub struct TextBody {
614    pub anchor: Option<String>,
615    pub vertical: Option<String>,
616    /// Use a 1.2 em percentage pitch.
617    #[serde(default, skip_serializing_if = "Option::is_none")]
618    pub compat_line_spacing: Option<bool>,
619    pub autofit: Option<TextAutofit>,
620    #[serde(default, skip_serializing_if = "Option::is_none")]
621    pub vertical_overflow: Option<TextOverflow>,
622    #[serde(default, skip_serializing_if = "Option::is_none")]
623    pub horizontal_overflow: Option<TextOverflow>,
624    pub inset_left: Option<i64>,
625    pub inset_top: Option<i64>,
626    pub inset_right: Option<i64>,
627    pub inset_bottom: Option<i64>,
628    /// List properties by outline level.
629    #[serde(default, skip_serializing_if = "Vec::is_empty")]
630    pub list_style: Vec<ParagraphProperties>,
631    #[serde(default, skip_serializing_if = "Option::is_none")]
632    pub default_list_style: Option<Box<ParagraphProperties>>,
633    pub paragraphs: Vec<TextParagraph>,
634}
635
636#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
637#[serde(rename_all = "camelCase")]
638pub enum TextOverflow {
639    Overflow,
640    Clip,
641    Ellipsis,
642}
643
644#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
645#[serde(
646    tag = "type",
647    rename_all = "camelCase",
648    rename_all_fields = "camelCase"
649)]
650pub enum TextAutofit {
651    None,
652    Shape,
653    Normal {
654        font_scale: Option<f64>,
655        line_space_reduction: Option<f64>,
656    },
657}
658
659#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
660#[serde(rename_all = "camelCase")]
661pub struct TextParagraph {
662    pub properties: ParagraphProperties,
663    pub runs: Vec<TextRun>,
664    pub end_properties: Option<RunProperties>,
665}
666
667/// A spacing height, as a share of the text size or in points.
668#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
669#[serde(
670    tag = "type",
671    rename_all = "camelCase",
672    rename_all_fields = "camelCase"
673)]
674pub enum LineSpacing {
675    Percent { value: f64 },
676    Points { value: f64 },
677}
678
679#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
680#[serde(rename_all = "camelCase")]
681pub struct ParagraphProperties {
682    pub alignment: Option<String>,
683    pub level: u32,
684    pub margin_left: Option<i64>,
685    #[serde(default, skip_serializing_if = "Option::is_none")]
686    pub margin_right: Option<i64>,
687    pub indent: Option<i64>,
688    pub bullet: Option<Bullet>,
689    #[serde(default, skip_serializing_if = "Option::is_none")]
690    pub line_spacing: Option<LineSpacing>,
691    #[serde(default, skip_serializing_if = "Option::is_none")]
692    pub space_before: Option<LineSpacing>,
693    #[serde(default, skip_serializing_if = "Option::is_none")]
694    pub space_after: Option<LineSpacing>,
695    #[serde(default, skip_serializing_if = "Option::is_none")]
696    pub bullet_font: Option<BulletFont>,
697    #[serde(default, skip_serializing_if = "Option::is_none")]
698    pub bullet_color: Option<BulletColor>,
699    #[serde(default, skip_serializing_if = "Option::is_none")]
700    pub bullet_size: Option<BulletSize>,
701    pub default_run: Option<RunProperties>,
702}
703
704#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
705#[serde(tag = "type", content = "value", rename_all = "camelCase")]
706pub enum BulletFont {
707    FollowText,
708    Typeface(String),
709}
710
711#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
712#[serde(tag = "type", content = "value", rename_all = "camelCase")]
713pub enum BulletColor {
714    FollowText,
715    Color(ColorValue),
716}
717
718#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
719#[serde(tag = "type", content = "value", rename_all = "camelCase")]
720pub enum BulletSize {
721    FollowText,
722    Percent(f64),
723    Points(f64),
724}
725
726#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
727#[serde(
728    tag = "type",
729    rename_all = "camelCase",
730    rename_all_fields = "camelCase"
731)]
732pub enum Bullet {
733    Character {
734        value: String,
735    },
736    AutoNumber {
737        scheme: String,
738        start_at: u32,
739        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
740        restart: bool,
741    },
742    None,
743}
744
745#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
746#[serde(rename_all = "camelCase")]
747pub struct TextRun {
748    pub text: String,
749    pub properties: RunProperties,
750    pub field_id: Option<String>,
751    pub field_type: Option<String>,
752    pub line_break: bool,
753}
754
755#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
756#[serde(rename_all = "camelCase")]
757pub struct RunProperties {
758    pub font_size_pt: Option<f64>,
759    #[serde(default, skip_serializing_if = "Option::is_none")]
760    pub spacing_pt: Option<f64>,
761    /// Baseline shift as a percentage of the font size; negative is subscript.
762    #[serde(default, skip_serializing_if = "Option::is_none")]
763    pub baseline_pct: Option<f64>,
764    pub bold: Option<bool>,
765    pub italic: Option<bool>,
766    pub underline: Option<String>,
767    pub font_family: Option<String>,
768    pub color: Option<ColorValue>,
769    pub language: Option<String>,
770    pub hyperlink_relationship_id: Option<String>,
771}
772
773#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
774#[serde(rename_all = "camelCase")]
775pub struct TextStyleSet {
776    pub title: Vec<ParagraphProperties>,
777    pub body: Vec<ParagraphProperties>,
778    pub other: Vec<ParagraphProperties>,
779}