Skip to main content

pptx_parse/
model.rs

1use std::collections::BTreeMap;
2
3pub use ooxml_drawingml::ShapeStyle;
4use ooxml_drawingml::{
5    ColorMap, ColorValue, GeometryPathCommand, ShapeEffects, ShapeFill, ShapeOutline,
6    StyleReference, TableStyleList, Theme, ThemeFormatScheme,
7};
8use serde::{Deserialize, Serialize};
9
10use crate::comments::{Comment, CommentAuthor, CommentFlavor};
11use crate::inventory::{NotesSource, SlideSource};
12use crate::relationships::Relationship;
13
14pub use ooxml_drawingml::chart::{
15    ChartAxes, ChartAxis, ChartDataLabels, ChartLegend, ChartMarker, ChartPlotGroup, ChartPoint,
16    ChartPointLabel, ChartSeries, ChartSpace, ChartTextProperties,
17};
18
19/// Shape elements counted by source ordinals.
20#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22pub(crate) enum ShapeElements {
23    WithConnectors,
24    #[default]
25    WithoutConnectors,
26}
27
28impl ShapeElements {
29    fn is_legacy(&self) -> bool {
30        *self == Self::WithoutConnectors
31    }
32
33    pub(crate) fn contains(self, local: &str) -> bool {
34        matches!(local, "sp" | "pic" | "graphicFrame" | "grpSp")
35            || (self == Self::WithConnectors && local == "cxnSp")
36    }
37}
38
39#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
40#[serde(rename_all = "camelCase")]
41pub struct PptxPackage {
42    pub presentation: Presentation,
43    pub slides: Vec<Slide>,
44    pub layouts: Vec<SlideLayout>,
45    pub masters: Vec<SlideMaster>,
46    pub themes: Vec<ThemePart>,
47    /// Absent from packages serialized before charts were parsed.
48    #[serde(default)]
49    pub charts: Vec<ChartPart>,
50    /// The drawing PowerPoint saves beside each SmartArt graphic.
51    #[serde(default, skip_serializing_if = "Vec::is_empty")]
52    pub diagram_drawings: Vec<DiagramDrawing>,
53    pub media: Vec<MediaPart>,
54    /// Absent from packages serialized before table styles were parsed.
55    #[serde(default, skip_serializing_if = "TableStyleList::is_empty")]
56    pub table_styles: TableStyleList,
57    #[serde(default, skip_serializing_if = "Vec::is_empty")]
58    pub comment_authors: Vec<CommentAuthor>,
59    #[serde(default, skip_serializing_if = "Vec::is_empty")]
60    pub comments: Vec<Comment>,
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub comment_flavor: Option<CommentFlavor>,
63    pub relationships: BTreeMap<String, Vec<Relationship>>,
64    #[serde(skip)]
65    pub(crate) parts: Vec<PackagePart>,
66    /// Source bytes for verbatim member passthrough on save.
67    #[serde(skip)]
68    pub(crate) source_container: ooxml_opc::SourceContainer,
69    #[serde(default, skip_serializing_if = "ShapeElements::is_legacy")]
70    pub(crate) shape_elements: ShapeElements,
71    /// Source inventories of the slide parts, by part path, found while parsing.
72    #[serde(skip)]
73    pub(crate) sources: BTreeMap<String, SlideSource>,
74    /// Notes pages, by the part path of the slide they belong to.
75    #[serde(skip)]
76    pub(crate) notes_sources: BTreeMap<String, NotesSource>,
77}
78
79impl PptxPackage {
80    pub fn part_bytes(&self, path: &str) -> Option<&[u8]> {
81        self.parts
82            .iter()
83            .find(|part| part.path == path)
84            .map(|part| part.bytes.as_slice())
85    }
86
87    /// False for packages recovered from a collaboration update, which carry
88    /// the parsed model but not the raw part bytes.
89    pub fn has_parts(&self) -> bool {
90        !self.parts.is_empty()
91    }
92
93    /// Where a slide part's shapes sit in its XML and what the model leaves out; `None` for
94    /// packages not parsed from file bytes.
95    pub fn slide_source(&self, part_path: &str) -> Option<&SlideSource> {
96        self.sources.get(part_path)
97    }
98
99    /// The notes page of the slide at `slide_part_path`, when the package was parsed with one.
100    pub fn notes_source(&self, slide_part_path: &str) -> Option<&NotesSource> {
101        self.notes_sources.get(slide_part_path)
102    }
103
104    /// Whether source ordinals include connectors.
105    pub fn models_connectors(&self) -> bool {
106        self.shape_elements == ShapeElements::WithConnectors
107    }
108
109    pub fn replace_part(&mut self, path: &str, bytes: Vec<u8>) -> bool {
110        let Some(part) = self.parts.iter_mut().find(|part| part.path == path) else {
111            return false;
112        };
113        part.bytes = bytes;
114        true
115    }
116}
117
118#[derive(Clone, Debug, PartialEq)]
119pub(crate) struct PackagePart {
120    pub path: String,
121    pub bytes: Vec<u8>,
122}
123
124#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
125#[serde(rename_all = "camelCase")]
126pub struct Presentation {
127    pub part_path: String,
128    pub width_emu: i64,
129    pub height_emu: i64,
130    /// `p:presentation/@firstSlideNum`.
131    #[serde(
132        default = "default_first_slide_num",
133        skip_serializing_if = "is_default_first_slide_num"
134    )]
135    pub first_slide_num: i32,
136    pub slides: Vec<SlideReference>,
137    pub master_part_paths: Vec<String>,
138    /// `p:defaultTextStyle`, one entry per list level.
139    #[serde(default, skip_serializing_if = "Vec::is_empty")]
140    pub default_text_style: Vec<ParagraphProperties>,
141    /// `p:defaultTextStyle/a:defPPr`, under every level.
142    #[serde(default, skip_serializing_if = "Option::is_none")]
143    pub default_text_paragraph: Option<Box<ParagraphProperties>>,
144}
145
146fn default_first_slide_num() -> i32 {
147    1
148}
149
150fn is_default_first_slide_num(value: &i32) -> bool {
151    *value == default_first_slide_num()
152}
153
154#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
155#[serde(rename_all = "camelCase")]
156pub struct SlideReference {
157    pub id: u32,
158    pub relationship_id: String,
159    pub part_path: String,
160}
161
162#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
163#[serde(rename_all = "camelCase")]
164pub struct Slide {
165    pub part_path: String,
166    pub name: Option<String>,
167    pub layout_part_path: Option<String>,
168    pub show_master_shapes: bool,
169    pub background: Option<ShapeFill>,
170    #[serde(default, skip_serializing_if = "Option::is_none")]
171    pub background_picture: Option<Box<PictureFill>>,
172    #[serde(default, skip_serializing_if = "Option::is_none")]
173    pub background_reference: Option<StyleReference>,
174    pub shapes: Vec<ShapeNode>,
175    /// `p:clrMapOvr/a:overrideClrMapping`; absent when the parent map applies.
176    #[serde(default, skip_serializing_if = "Option::is_none")]
177    pub color_map_override: Option<ColorMap>,
178    #[serde(default, skip_serializing_if = "String::is_empty")]
179    pub notes: String,
180    /// `p:sld/@show` negated; `None` in packages stored before slide visibility was read.
181    #[serde(default, skip_serializing_if = "Option::is_none")]
182    pub hidden: Option<bool>,
183}
184
185#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
186#[serde(rename_all = "camelCase")]
187pub struct SlideLayout {
188    pub part_path: String,
189    pub name: Option<String>,
190    pub layout_type: Option<String>,
191    pub master_part_path: Option<String>,
192    pub show_master_shapes: bool,
193    pub background: Option<ShapeFill>,
194    #[serde(default, skip_serializing_if = "Option::is_none")]
195    pub background_picture: Option<Box<PictureFill>>,
196    #[serde(default, skip_serializing_if = "Option::is_none")]
197    pub background_reference: Option<StyleReference>,
198    pub shapes: Vec<ShapeNode>,
199    /// `p:clrMapOvr/a:overrideClrMapping`; absent when the master map applies.
200    #[serde(default, skip_serializing_if = "Option::is_none")]
201    pub color_map_override: Option<ColorMap>,
202}
203
204#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
205#[serde(rename_all = "camelCase")]
206pub struct SlideMaster {
207    pub part_path: String,
208    pub name: Option<String>,
209    pub theme_part_path: Option<String>,
210    pub layout_part_paths: Vec<String>,
211    pub background: Option<ShapeFill>,
212    #[serde(default, skip_serializing_if = "Option::is_none")]
213    pub background_picture: Option<Box<PictureFill>>,
214    #[serde(default, skip_serializing_if = "Option::is_none")]
215    pub background_reference: Option<StyleReference>,
216    pub shapes: Vec<ShapeNode>,
217    pub text_styles: TextStyleSet,
218    /// `p:clrMap`; absent from packages serialized before it was parsed.
219    #[serde(default, skip_serializing_if = "ColorMap::is_identity")]
220    pub color_map: ColorMap,
221}
222
223#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
224#[serde(rename_all = "camelCase")]
225pub struct ThemePart {
226    pub part_path: String,
227    pub theme: Theme,
228    /// Absent from packages serialized before `a:fmtScheme` was parsed.
229    #[serde(default, skip_serializing_if = "ThemeFormatScheme::is_empty")]
230    pub format_scheme: ThemeFormatScheme,
231    /// `a:bgFillStyleLst` picture entries, indexed as `p:bgRef` names them.
232    #[serde(default, skip_serializing_if = "Vec::is_empty")]
233    pub background_pictures: Vec<Option<PictureFill>>,
234}
235
236/// A chart part resolved against one referenced presentation theme.
237#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
238#[serde(rename_all = "camelCase")]
239pub struct ChartPart {
240    pub part_path: String,
241    pub theme_part_path: Option<String>,
242    pub chart: ChartSpace,
243}
244
245#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
246#[serde(rename_all = "camelCase")]
247pub struct MediaPart {
248    pub part_path: String,
249    pub content_type: String,
250    #[serde(
251        serialize_with = "serialize_media_bytes",
252        deserialize_with = "deserialize_media_bytes"
253    )]
254    pub bytes: Vec<u8>,
255}
256
257/// Writes base64: a JSON integer array inflates the payload about fourfold.
258fn serialize_media_bytes<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
259where
260    S: serde::Serializer,
261{
262    use base64::Engine as _;
263    serializer.serialize_str(&base64::engine::general_purpose::STANDARD.encode(bytes))
264}
265
266/// Also accepts the integer arrays written before schema 2.2.
267fn deserialize_media_bytes<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
268where
269    D: serde::Deserializer<'de>,
270{
271    use base64::Engine as _;
272    use serde::de::Error as _;
273
274    #[derive(Deserialize)]
275    #[serde(untagged)]
276    enum Bytes {
277        Base64(String),
278        Integers(Vec<u8>),
279    }
280
281    match Bytes::deserialize(deserializer)? {
282        Bytes::Base64(encoded) => base64::engine::general_purpose::STANDARD
283            .decode(encoded)
284            .map_err(D::Error::custom),
285        Bytes::Integers(bytes) => Ok(bytes),
286    }
287}
288
289#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
290#[serde(
291    tag = "kind",
292    rename_all = "camelCase",
293    rename_all_fields = "camelCase"
294)]
295pub enum ShapeNode {
296    Shape(Shape),
297    Picture(Picture),
298    GraphicFrame(GraphicFrame),
299    Group(GroupShape),
300}
301
302impl ShapeNode {
303    pub fn id(&self) -> u32 {
304        match self {
305            Self::Shape(shape) => shape.base.id,
306            Self::Picture(picture) => picture.base.id,
307            Self::GraphicFrame(frame) => frame.base.id,
308            Self::Group(group) => group.base.id,
309        }
310    }
311}
312
313#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
314#[serde(rename_all = "camelCase")]
315pub struct ShapeBase {
316    pub id: u32,
317    pub name: String,
318    pub description: Option<String>,
319    pub hidden: bool,
320    pub placeholder: Option<Placeholder>,
321    pub transform: ShapeTransform,
322}
323
324#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
325#[serde(rename_all = "camelCase")]
326pub struct ShapeTransform {
327    pub x: i64,
328    pub y: i64,
329    pub width: i64,
330    pub height: i64,
331    pub rotation_deg: f64,
332    pub flip_h: bool,
333    pub flip_v: bool,
334    pub child_x: Option<i64>,
335    pub child_y: Option<i64>,
336    pub child_width: Option<i64>,
337    pub child_height: Option<i64>,
338}
339
340#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
341#[serde(rename_all = "camelCase")]
342pub struct Placeholder {
343    pub placeholder_type: Option<String>,
344    pub index: Option<u32>,
345    pub orientation: Option<String>,
346    pub size: Option<String>,
347}
348
349/// A slide holds one of each of these, so they inherit by type: PowerPoint
350/// writes a slide number as `idx="12"` over a master's `idx="4"` and still
351/// draws it where the master put it (#797).
352const SINGLETON_PLACEHOLDERS: [&str; 5] = ["title", "sldNum", "dt", "ftr", "hdr"];
353
354impl Placeholder {
355    /// Whether this placeholder and `other` fill the same slot, so one
356    /// inherits from the other.
357    pub fn matches(&self, other: &Placeholder) -> bool {
358        let left_type = normalize_placeholder_type(self.placeholder_type.as_deref());
359        let right_type = normalize_placeholder_type(other.placeholder_type.as_deref());
360        if SINGLETON_PLACEHOLDERS.contains(&left_type)
361            || SINGLETON_PLACEHOLDERS.contains(&right_type)
362        {
363            return left_type == right_type;
364        }
365        match (self.index, other.index) {
366            (Some(left), Some(right)) => left == right,
367            _ => left_type == right_type,
368        }
369    }
370}
371
372fn normalize_placeholder_type(value: Option<&str>) -> &str {
373    match value.unwrap_or("body") {
374        "ctrTitle" => "title",
375        "obj" => "body",
376        value => value,
377    }
378}
379
380#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
381#[serde(rename_all = "camelCase")]
382pub struct Shape {
383    #[serde(flatten)]
384    pub base: ShapeBase,
385    pub geometry: String,
386    #[serde(
387        default = "has_preset_geometry_default",
388        skip_serializing_if = "is_preset_geometry"
389    )]
390    pub has_preset_geometry: bool,
391    /// Custom paths in shape-relative coordinates.
392    #[serde(default, skip_serializing_if = "Vec::is_empty")]
393    pub paths: Vec<CustomGeometryPath>,
394    /// Theme formatting and text defaults.
395    #[serde(default, skip_serializing_if = "Option::is_none")]
396    pub style: Option<Box<ShapeStyle>>,
397    #[serde(default)]
398    pub adjust_values: BTreeMap<String, f64>,
399    pub fill: Option<ShapeFill>,
400    /// The image behind an `a:blipFill`, when the fill is a stretched picture.
401    #[serde(default, skip_serializing_if = "Option::is_none")]
402    pub picture_fill: Option<Box<PictureFill>>,
403    pub outline: Option<ShapeOutline>,
404    #[serde(default, skip_serializing_if = "Option::is_none")]
405    pub effects: Option<ShapeEffects>,
406    pub text: Option<TextBody>,
407}
408
409/// An `a:blipFill` on a shape: the image, and the box it stretches into.
410#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
411#[serde(rename_all = "camelCase")]
412pub struct PictureFill {
413    #[serde(default, skip_serializing_if = "Option::is_none")]
414    pub relationship_id: Option<String>,
415    #[serde(default, skip_serializing_if = "Option::is_none")]
416    pub media_part_path: Option<String>,
417    #[serde(default, skip_serializing_if = "PictureCrop::is_whole")]
418    pub crop: PictureCrop,
419    /// `a:stretch/a:fillRect` insets, in thousandths of a percent of the box.
420    #[serde(default, skip_serializing_if = "PictureCrop::is_whole")]
421    pub fill_rect: PictureCrop,
422    /// `a:tile`: the picture repeats at its own size instead of stretching.
423    #[serde(default, skip_serializing_if = "Option::is_none")]
424    pub tile: Option<PictureTile>,
425}
426
427/// `a:blipFill/a:tile`. The offset, alignment and flip it can also carry are
428/// not read: every tile in the corpus starts at the top left, unflipped.
429#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
430#[serde(rename_all = "camelCase")]
431pub struct PictureTile {
432    /// `@sx` and `@sy` as a fraction, 1.0 for the picture's own size.
433    pub scale_x: f64,
434    pub scale_y: f64,
435}
436
437#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
438#[serde(rename_all = "camelCase")]
439pub struct CustomGeometryPath {
440    pub commands: Vec<GeometryPathCommand>,
441    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
442    pub no_fill: bool,
443    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
444    pub no_stroke: bool,
445}
446
447#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
448#[serde(rename_all = "camelCase")]
449pub struct Picture {
450    #[serde(flatten)]
451    pub base: ShapeBase,
452    pub relationship_id: Option<String>,
453    pub media_part_path: Option<String>,
454    pub crop: PictureCrop,
455    /// Bitmap effects in document order.
456    #[serde(default, skip_serializing_if = "Vec::is_empty")]
457    pub effects: Vec<BlipEffect>,
458    /// Preset mask; defaults to the frame rectangle.
459    #[serde(default = "rect_geometry", skip_serializing_if = "is_rect")]
460    pub geometry: String,
461    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
462    pub adjust_values: BTreeMap<String, f64>,
463    pub fill: Option<ShapeFill>,
464    pub outline: Option<ShapeOutline>,
465    #[serde(default, skip_serializing_if = "Option::is_none")]
466    pub shape_effects: Option<ShapeEffects>,
467    #[serde(default, skip_serializing_if = "Option::is_none")]
468    pub style: Option<Box<ShapeStyle>>,
469}
470
471fn rect_geometry() -> String {
472    "rect".to_owned()
473}
474
475fn is_rect(geometry: &str) -> bool {
476    geometry == "rect"
477}
478
479fn has_preset_geometry_default() -> bool {
480    true
481}
482
483fn is_preset_geometry(value: &bool) -> bool {
484    *value
485}
486
487/// Bitmap effects with unresolved colours.
488#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
489#[serde(tag = "type", rename_all = "camelCase")]
490pub enum BlipEffect {
491    /// `a:biLevel`: luminance below `threshold` becomes black, the rest white.
492    BiLevel { threshold: f64 },
493    /// `a:grayscl`.
494    Grayscale,
495    /// `a:alphaModFix`: the whole bitmap drawn at `amount` opacity.
496    Alpha { amount: f64 },
497    /// `a:lum`: brightness and contrast, each a fraction in `-1.0..=1.0`.
498    Luminance { brightness: f64, contrast: f64 },
499    /// `a:duotone`: luminance interpolates between the two colours.
500    Duotone {
501        shadow: Option<ColorValue>,
502        highlight: Option<ColorValue>,
503    },
504    /// Exact colour replacement.
505    ColorChange {
506        from: Option<ColorValue>,
507        to: Option<ColorValue>,
508        #[serde(
509            default = "default_use_alpha",
510            skip_serializing_if = "use_alpha_is_default",
511            rename = "useAlpha"
512        )]
513        use_alpha: bool,
514    },
515}
516
517fn default_use_alpha() -> bool {
518    true
519}
520
521fn use_alpha_is_default(value: &bool) -> bool {
522    *value
523}
524
525#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
526pub struct PictureCrop {
527    pub left: i32,
528    pub top: i32,
529    pub right: i32,
530    pub bottom: i32,
531}
532
533impl PictureCrop {
534    pub fn is_whole(&self) -> bool {
535        *self == Self::default()
536    }
537}
538
539#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
540#[serde(rename_all = "camelCase")]
541pub struct GraphicFrame {
542    #[serde(flatten)]
543    pub base: ShapeBase,
544    pub data: GraphicFrameData,
545}
546
547#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
548#[serde(
549    tag = "type",
550    rename_all = "camelCase",
551    rename_all_fields = "camelCase"
552)]
553pub enum GraphicFrameData {
554    Table(Table),
555    Chart {
556        relationship_id: String,
557        part_path: Option<String>,
558    },
559    Diagram {
560        relationship_ids: Vec<String>,
561        /// `ppt/diagrams/drawing#.xml`, the shapes PowerPoint saves beside a
562        /// SmartArt graphic so a reader that cannot lay the diagram out can
563        /// still draw it.
564        #[serde(default, skip_serializing_if = "Option::is_none")]
565        drawing_part_path: Option<String>,
566    },
567    Unknown {
568        uri: Option<String>,
569        #[serde(default, skip_serializing_if = "Option::is_none")]
570        picture: Option<Box<Picture>>,
571    },
572}
573
574/// One `ppt/diagrams/drawing#.xml`: the shapes a SmartArt graphic resolves to.
575#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
576#[serde(rename_all = "camelCase")]
577pub struct DiagramDrawing {
578    pub part_path: String,
579    pub shapes: Vec<ShapeNode>,
580}
581
582#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
583#[serde(rename_all = "camelCase")]
584pub struct GroupShape {
585    #[serde(flatten)]
586    pub base: ShapeBase,
587    pub children: Vec<ShapeNode>,
588}
589
590/// An `a:tbl`: its column grid, table-wide properties and rows.
591#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
592#[serde(rename_all = "camelCase")]
593pub struct Table {
594    /// `a:gridCol/@w`, in EMU.
595    #[serde(default, skip_serializing_if = "Vec::is_empty")]
596    pub grid: Vec<i64>,
597    #[serde(default, skip_serializing_if = "TableProperties::is_default")]
598    pub properties: TableProperties,
599    #[serde(
600        default,
601        serialize_with = "serialize_table_rows",
602        deserialize_with = "deserialize_table_rows"
603    )]
604    pub rows: Vec<TableRow>,
605}
606
607/// `a:tblPr`: which style parts apply, and the style they come from.
608#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
609#[serde(rename_all = "camelCase", default)]
610pub struct TableProperties {
611    pub first_row: bool,
612    pub last_row: bool,
613    pub first_col: bool,
614    pub last_col: bool,
615    pub band_row: bool,
616    pub band_col: bool,
617    #[serde(skip_serializing_if = "Option::is_none")]
618    pub style_id: Option<String>,
619}
620
621impl TableProperties {
622    pub fn is_default(&self) -> bool {
623        *self == Self::default()
624    }
625}
626
627#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
628#[serde(rename_all = "camelCase")]
629pub struct TableRow {
630    /// `a:tr/@h`, in EMU, a minimum rather than a fixed height.
631    #[serde(default)]
632    pub height: i64,
633    pub cells: Vec<TableCell>,
634}
635
636impl TableRow {
637    fn is_text_only(&self) -> bool {
638        self.height == 0 && self.cells.iter().all(TableCell::is_text_only)
639    }
640}
641
642/// An `a:tc`, with its `a:tcPr` anchoring and margins folded into `text`.
643#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
644#[serde(rename_all = "camelCase")]
645pub struct TableCell {
646    pub text: TextBody,
647    #[serde(default = "unit_span", skip_serializing_if = "is_unit_span")]
648    pub grid_span: u32,
649    #[serde(default = "unit_span", skip_serializing_if = "is_unit_span")]
650    pub row_span: u32,
651    /// An `hMerge`/`vMerge` continuation: covered by an earlier origin cell.
652    #[serde(default, skip_serializing_if = "is_false")]
653    pub merged: bool,
654    #[serde(default, skip_serializing_if = "Option::is_none")]
655    pub fill: Option<ShapeFill>,
656    #[serde(default, skip_serializing_if = "TableCellBorders::is_empty")]
657    pub borders: TableCellBorders,
658}
659
660impl TableCell {
661    pub fn from_text(text: TextBody) -> Self {
662        Self {
663            text,
664            ..Self::default()
665        }
666    }
667
668    /// Compared against a cell built from this text alone, so a field added
669    /// later cannot be silently dropped by the released encoding.
670    fn is_text_only(&self) -> bool {
671        *self == Self::from_text(self.text.clone())
672    }
673}
674
675impl Default for TableCell {
676    fn default() -> Self {
677        Self {
678            text: TextBody::default(),
679            grid_span: 1,
680            row_span: 1,
681            merged: false,
682            fill: None,
683            borders: TableCellBorders::default(),
684        }
685    }
686}
687
688/// `a:tcPr/a:lnL`, `a:lnT`, `a:lnR` and `a:lnB`.
689#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
690#[serde(rename_all = "camelCase", default)]
691pub struct TableCellBorders {
692    #[serde(skip_serializing_if = "Option::is_none")]
693    pub left: Option<ShapeOutline>,
694    #[serde(skip_serializing_if = "Option::is_none")]
695    pub top: Option<ShapeOutline>,
696    #[serde(skip_serializing_if = "Option::is_none")]
697    pub right: Option<ShapeOutline>,
698    #[serde(skip_serializing_if = "Option::is_none")]
699    pub bottom: Option<ShapeOutline>,
700}
701
702impl TableCellBorders {
703    pub fn is_empty(&self) -> bool {
704        *self == Self::default()
705    }
706}
707
708fn unit_span() -> u32 {
709    1
710}
711
712fn is_unit_span(span: &u32) -> bool {
713    *span == 1
714}
715
716fn is_false(value: &bool) -> bool {
717    !*value
718}
719
720/// Writes rows that hold nothing but cell text in the released encoding, so a
721/// stored package keeps its bytes until a table gains geometry.
722fn serialize_table_rows<S>(rows: &[TableRow], serializer: S) -> Result<S::Ok, S::Error>
723where
724    S: serde::Serializer,
725{
726    use serde::ser::SerializeSeq;
727
728    if !rows.iter().all(TableRow::is_text_only) {
729        return rows.serialize(serializer);
730    }
731    let mut sequence = serializer.serialize_seq(Some(rows.len()))?;
732    for row in rows {
733        let cells: Vec<_> = row.cells.iter().map(|cell| &cell.text).collect();
734        sequence.serialize_element(&cells)?;
735    }
736    sequence.end()
737}
738
739/// Also accepts the released encoding's rows, which were bare `a:txBody` lists.
740fn deserialize_table_rows<'de, D>(deserializer: D) -> Result<Vec<TableRow>, D::Error>
741where
742    D: serde::Deserializer<'de>,
743{
744    #[derive(Deserialize)]
745    #[serde(untagged)]
746    enum Row {
747        Modern(TableRow),
748        Legacy(Vec<TextBody>),
749    }
750
751    Ok(Vec::<Row>::deserialize(deserializer)?
752        .into_iter()
753        .map(|row| match row {
754            Row::Modern(row) => row,
755            Row::Legacy(cells) => TableRow {
756                height: 0,
757                cells: cells.into_iter().map(TableCell::from_text).collect(),
758            },
759        })
760        .collect())
761}
762
763#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
764#[serde(rename_all = "camelCase")]
765pub struct TextBody {
766    pub anchor: Option<String>,
767    pub vertical: Option<String>,
768    /// Use a 1.2 em percentage pitch.
769    #[serde(default, skip_serializing_if = "Option::is_none")]
770    pub compat_line_spacing: Option<bool>,
771    /// `a:bodyPr/@spcFirstLastPara`: honour the first paragraph's space-before
772    /// and the last one's space-after.
773    #[serde(default, skip_serializing_if = "Option::is_none")]
774    pub space_first_last_para: Option<bool>,
775    pub autofit: Option<TextAutofit>,
776    /// `a:bodyPr/@wrap`: `false` for `none`, which lays every paragraph on one
777    /// line and lets it run past the shape.
778    #[serde(default, skip_serializing_if = "Option::is_none")]
779    pub wrap: Option<bool>,
780    #[serde(default, skip_serializing_if = "Option::is_none")]
781    pub vertical_overflow: Option<TextOverflow>,
782    #[serde(default, skip_serializing_if = "Option::is_none")]
783    pub horizontal_overflow: Option<TextOverflow>,
784    pub inset_left: Option<i64>,
785    pub inset_top: Option<i64>,
786    pub inset_right: Option<i64>,
787    pub inset_bottom: Option<i64>,
788    /// List properties by outline level.
789    #[serde(default, skip_serializing_if = "Vec::is_empty")]
790    pub list_style: Vec<ParagraphProperties>,
791    #[serde(default, skip_serializing_if = "Option::is_none")]
792    pub default_list_style: Option<Box<ParagraphProperties>>,
793    pub paragraphs: Vec<TextParagraph>,
794}
795
796#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
797#[serde(rename_all = "camelCase")]
798pub enum TextOverflow {
799    Overflow,
800    Clip,
801    Ellipsis,
802}
803
804#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
805#[serde(
806    tag = "type",
807    rename_all = "camelCase",
808    rename_all_fields = "camelCase"
809)]
810pub enum TextAutofit {
811    None,
812    Shape,
813    Normal {
814        font_scale: Option<f64>,
815        line_space_reduction: Option<f64>,
816    },
817}
818
819#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
820#[serde(rename_all = "camelCase")]
821pub struct TextParagraph {
822    pub properties: ParagraphProperties,
823    pub runs: Vec<TextRun>,
824    pub end_properties: Option<RunProperties>,
825}
826
827/// A spacing height, as a share of the text size or in points.
828#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
829#[serde(
830    tag = "type",
831    rename_all = "camelCase",
832    rename_all_fields = "camelCase"
833)]
834pub enum LineSpacing {
835    Percent { value: f64 },
836    Points { value: f64 },
837}
838
839#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
840#[serde(rename_all = "camelCase")]
841pub struct ParagraphProperties {
842    pub alignment: Option<String>,
843    pub level: u32,
844    pub margin_left: Option<i64>,
845    #[serde(default, skip_serializing_if = "Option::is_none")]
846    pub margin_right: Option<i64>,
847    pub indent: Option<i64>,
848    pub bullet: Option<Bullet>,
849    #[serde(default, skip_serializing_if = "Option::is_none")]
850    pub line_spacing: Option<LineSpacing>,
851    #[serde(default, skip_serializing_if = "Option::is_none")]
852    pub space_before: Option<LineSpacing>,
853    #[serde(default, skip_serializing_if = "Option::is_none")]
854    pub space_after: Option<LineSpacing>,
855    #[serde(default, skip_serializing_if = "Option::is_none")]
856    pub bullet_font: Option<BulletFont>,
857    #[serde(default, skip_serializing_if = "Option::is_none")]
858    pub bullet_color: Option<BulletColor>,
859    #[serde(default, skip_serializing_if = "Option::is_none")]
860    pub bullet_size: Option<BulletSize>,
861    /// `a:pPr/@defTabSz` in EMU: the pitch of the implicit tab stops.
862    #[serde(default, skip_serializing_if = "Option::is_none")]
863    pub default_tab_size: Option<i64>,
864    /// `a:pPr/a:tabLst` positions in EMU. A declared empty list clears the
865    /// stops the list style would otherwise contribute.
866    #[serde(default, skip_serializing_if = "Option::is_none")]
867    pub tab_stops: Option<Vec<i64>>,
868    /// `a:pPr/@rtl`: the paragraph reads right to left.
869    #[serde(default, skip_serializing_if = "Option::is_none")]
870    pub rtl: Option<bool>,
871    pub default_run: Option<RunProperties>,
872}
873
874#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
875#[serde(tag = "type", content = "value", rename_all = "camelCase")]
876pub enum BulletFont {
877    FollowText,
878    Typeface(String),
879}
880
881#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
882#[serde(tag = "type", content = "value", rename_all = "camelCase")]
883pub enum BulletColor {
884    FollowText,
885    Color(ColorValue),
886}
887
888#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
889#[serde(tag = "type", content = "value", rename_all = "camelCase")]
890pub enum BulletSize {
891    FollowText,
892    Percent(f64),
893    Points(f64),
894}
895
896#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
897#[serde(
898    tag = "type",
899    rename_all = "camelCase",
900    rename_all_fields = "camelCase"
901)]
902pub enum Bullet {
903    Character {
904        value: String,
905    },
906    AutoNumber {
907        scheme: String,
908        start_at: u32,
909        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
910        restart: bool,
911    },
912    None,
913}
914
915#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
916#[serde(rename_all = "camelCase")]
917pub struct TextRun {
918    pub text: String,
919    pub properties: RunProperties,
920    pub field_id: Option<String>,
921    pub field_type: Option<String>,
922    pub line_break: bool,
923}
924
925/// `a:rPr/@cap`: how a run is cased when drawn. Display only — the stored text
926/// keeps the author's casing.
927#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
928#[serde(rename_all = "camelCase")]
929pub enum TextCaps {
930    None,
931    Small,
932    All,
933}
934
935impl TextCaps {
936    pub fn from_attribute(value: &str) -> Option<Self> {
937        match value {
938            "none" => Some(Self::None),
939            "small" => Some(Self::Small),
940            "all" => Some(Self::All),
941            _ => None,
942        }
943    }
944
945    pub fn as_attribute(self) -> &'static str {
946        match self {
947            Self::None => "none",
948            Self::Small => "small",
949            Self::All => "all",
950        }
951    }
952}
953
954#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
955#[serde(rename_all = "camelCase")]
956pub struct RunProperties {
957    pub font_size_pt: Option<f64>,
958    #[serde(default, skip_serializing_if = "Option::is_none")]
959    pub spacing_pt: Option<f64>,
960    /// Baseline shift as a percentage of the font size; negative is subscript.
961    #[serde(default, skip_serializing_if = "Option::is_none")]
962    pub baseline_pct: Option<f64>,
963    pub bold: Option<bool>,
964    pub italic: Option<bool>,
965    pub underline: Option<String>,
966    #[serde(default, skip_serializing_if = "Option::is_none")]
967    pub caps: Option<TextCaps>,
968    pub font_family: Option<String>,
969    pub color: Option<ColorValue>,
970    pub language: Option<String>,
971    pub hyperlink_relationship_id: Option<String>,
972    /// `a:rPr/a:effectLst`: the shadow PowerPoint draws behind the glyphs.
973    #[serde(default, skip_serializing_if = "Option::is_none")]
974    pub effects: Option<ShapeEffects>,
975}
976
977#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
978#[serde(rename_all = "camelCase")]
979pub struct TextStyleSet {
980    pub title: Vec<ParagraphProperties>,
981    pub body: Vec<ParagraphProperties>,
982    pub other: Vec<ParagraphProperties>,
983}