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 has_parts(&self) -> bool {
90 !self.parts.is_empty()
91 }
92
93 pub fn slide_source(&self, part_path: &str) -> Option<&SlideSource> {
96 self.sources.get(part_path)
97 }
98
99 pub fn notes_source(&self, slide_part_path: &str) -> Option<&NotesSource> {
101 self.notes_sources.get(slide_part_path)
102 }
103
104 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 #[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 #[serde(default, skip_serializing_if = "Vec::is_empty")]
140 pub default_text_style: Vec<ParagraphProperties>,
141 #[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 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
177 pub declares_background: bool,
178 pub shapes: Vec<ShapeNode>,
179 #[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 #[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 #[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 #[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 #[serde(default, skip_serializing_if = "ThemeFormatScheme::is_empty")]
234 pub format_scheme: ThemeFormatScheme,
235 #[serde(default, skip_serializing_if = "Vec::is_empty")]
237 pub background_pictures: Vec<Option<PictureFill>>,
238}
239
240#[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
261fn 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
270fn 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
353const SINGLETON_PLACEHOLDERS: [&str; 5] = ["title", "sldNum", "dt", "ftr", "hdr"];
357
358impl Placeholder {
359 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 #[serde(default, skip_serializing_if = "Vec::is_empty")]
397 pub paths: Vec<CustomGeometryPath>,
398 #[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 #[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#[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 #[serde(default, skip_serializing_if = "PictureCrop::is_whole")]
427 pub fill_rect: PictureCrop,
428 #[serde(default, skip_serializing_if = "Option::is_none")]
430 pub tile: Option<PictureTile>,
431 #[serde(default, skip_serializing_if = "Vec::is_empty")]
433 pub effects: Vec<BlipEffect>,
434}
435
436#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
439#[serde(rename_all = "camelCase")]
440pub struct PictureTile {
441 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 #[serde(default, skip_serializing_if = "Vec::is_empty")]
468 pub effects: Vec<BlipEffect>,
469 #[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#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
502#[serde(tag = "type", rename_all = "camelCase")]
503pub enum BlipEffect {
504 BiLevel { threshold: f64 },
506 Grayscale,
508 Alpha { amount: f64 },
510 Luminance { brightness: f64, contrast: f64 },
512 Duotone {
514 shadow: Option<ColorValue>,
515 highlight: Option<ColorValue>,
516 },
517 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 #[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#[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#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
607#[serde(rename_all = "camelCase")]
608pub struct Table {
609 #[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#[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 #[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#[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 #[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 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#[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
738fn 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
757fn 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 #[serde(default, skip_serializing_if = "Option::is_none")]
794 pub compat_line_spacing: Option<bool>,
795 #[serde(default, skip_serializing_if = "Option::is_none")]
798 pub space_first_last_para: Option<bool>,
799 pub autofit: Option<TextAutofit>,
800 #[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 #[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#[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 #[serde(default, skip_serializing_if = "Option::is_none")]
887 pub default_tab_size: Option<i64>,
888 #[serde(default, skip_serializing_if = "Option::is_none")]
891 pub tab_stops: Option<Vec<i64>>,
892 #[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#[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 #[serde(default, skip_serializing_if = "Option::is_none")]
986 pub baseline_pct: Option<f64>,
987 #[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 #[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}