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