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