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#[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 #[serde(default)]
49 pub charts: Vec<ChartPart>,
50 #[serde(default, skip_serializing_if = "Vec::is_empty")]
52 pub diagram_drawings: Vec<DiagramDrawing>,
53 pub media: Vec<MediaPart>,
54 #[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 #[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 #[serde(skip)]
73 pub(crate) sources: BTreeMap<String, SlideSource>,
74 #[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 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 pub fn has_parts(&self) -> bool {
118 !self.parts.is_empty()
119 }
120
121 pub fn slide_source(&self, part_path: &str) -> Option<&SlideSource> {
124 self.sources.get(part_path)
125 }
126
127 pub fn notes_source(&self, slide_part_path: &str) -> Option<&NotesSource> {
129 self.notes_sources.get(slide_part_path)
130 }
131
132 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 #[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 #[serde(default, skip_serializing_if = "Vec::is_empty")]
168 pub default_text_style: Vec<ParagraphProperties>,
169 #[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 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
205 pub declares_background: bool,
206 pub shapes: Vec<ShapeNode>,
207 #[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 #[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 #[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 #[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 #[serde(default, skip_serializing_if = "ThemeFormatScheme::is_empty")]
262 pub format_scheme: ThemeFormatScheme,
263 #[serde(default, skip_serializing_if = "Vec::is_empty")]
265 pub background_pictures: Vec<Option<PictureFill>>,
266}
267
268#[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
289fn 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
298fn 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
381const SINGLETON_PLACEHOLDERS: [&str; 5] = ["title", "sldNum", "dt", "ftr", "hdr"];
385
386impl Placeholder {
387 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 #[serde(default, skip_serializing_if = "Vec::is_empty")]
425 pub paths: Vec<CustomGeometryPath>,
426 #[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 #[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#[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 #[serde(default, skip_serializing_if = "PictureCrop::is_whole")]
455 pub fill_rect: PictureCrop,
456 #[serde(default, skip_serializing_if = "Option::is_none")]
458 pub tile: Option<PictureTile>,
459 #[serde(default, skip_serializing_if = "Vec::is_empty")]
461 pub effects: Vec<BlipEffect>,
462}
463
464#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
467#[serde(rename_all = "camelCase")]
468pub struct PictureTile {
469 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 #[serde(default, skip_serializing_if = "Vec::is_empty")]
496 pub effects: Vec<BlipEffect>,
497 #[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#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
530#[serde(tag = "type", rename_all = "camelCase")]
531pub enum BlipEffect {
532 BiLevel { threshold: f64 },
534 Grayscale,
536 Alpha { amount: f64 },
538 Luminance { brightness: f64, contrast: f64 },
540 Duotone {
542 shadow: Option<ColorValue>,
543 highlight: Option<ColorValue>,
544 },
545 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 #[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#[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#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
635#[serde(rename_all = "camelCase")]
636pub struct Table {
637 #[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#[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 #[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#[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 #[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 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#[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
766fn 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
785fn 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 #[serde(default, skip_serializing_if = "Option::is_none")]
822 pub compat_line_spacing: Option<bool>,
823 #[serde(default, skip_serializing_if = "Option::is_none")]
826 pub space_first_last_para: Option<bool>,
827 pub autofit: Option<TextAutofit>,
828 #[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 #[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#[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 #[serde(default, skip_serializing_if = "Option::is_none")]
915 pub default_tab_size: Option<i64>,
916 #[serde(default, skip_serializing_if = "Option::is_none")]
919 pub tab_stops: Option<Vec<i64>>,
920 #[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#[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 #[serde(default, skip_serializing_if = "Option::is_none")]
1014 pub baseline_pct: Option<f64>,
1015 #[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 #[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}