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 pub shapes: Vec<ShapeNode>,
175 #[serde(default, skip_serializing_if = "Option::is_none")]
177 pub color_map_override: Option<ColorMap>,
178 #[serde(default, skip_serializing_if = "String::is_empty")]
179 pub notes: String,
180 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub hidden: Option<bool>,
183}
184
185#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
186#[serde(rename_all = "camelCase")]
187pub struct SlideLayout {
188 pub part_path: String,
189 pub name: Option<String>,
190 pub layout_type: Option<String>,
191 pub master_part_path: Option<String>,
192 pub show_master_shapes: bool,
193 pub background: Option<ShapeFill>,
194 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub background_picture: Option<Box<PictureFill>>,
196 #[serde(default, skip_serializing_if = "Option::is_none")]
197 pub background_reference: Option<StyleReference>,
198 pub shapes: Vec<ShapeNode>,
199 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub color_map_override: Option<ColorMap>,
202}
203
204#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
205#[serde(rename_all = "camelCase")]
206pub struct SlideMaster {
207 pub part_path: String,
208 pub name: Option<String>,
209 pub theme_part_path: Option<String>,
210 pub layout_part_paths: Vec<String>,
211 pub background: Option<ShapeFill>,
212 #[serde(default, skip_serializing_if = "Option::is_none")]
213 pub background_picture: Option<Box<PictureFill>>,
214 #[serde(default, skip_serializing_if = "Option::is_none")]
215 pub background_reference: Option<StyleReference>,
216 pub shapes: Vec<ShapeNode>,
217 pub text_styles: TextStyleSet,
218 #[serde(default, skip_serializing_if = "ColorMap::is_identity")]
220 pub color_map: ColorMap,
221}
222
223#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
224#[serde(rename_all = "camelCase")]
225pub struct ThemePart {
226 pub part_path: String,
227 pub theme: Theme,
228 #[serde(default, skip_serializing_if = "ThemeFormatScheme::is_empty")]
230 pub format_scheme: ThemeFormatScheme,
231 #[serde(default, skip_serializing_if = "Vec::is_empty")]
233 pub background_pictures: Vec<Option<PictureFill>>,
234}
235
236#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
238#[serde(rename_all = "camelCase")]
239pub struct ChartPart {
240 pub part_path: String,
241 pub theme_part_path: Option<String>,
242 pub chart: ChartSpace,
243}
244
245#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
246#[serde(rename_all = "camelCase")]
247pub struct MediaPart {
248 pub part_path: String,
249 pub content_type: String,
250 #[serde(
251 serialize_with = "serialize_media_bytes",
252 deserialize_with = "deserialize_media_bytes"
253 )]
254 pub bytes: Vec<u8>,
255}
256
257fn serialize_media_bytes<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
259where
260 S: serde::Serializer,
261{
262 use base64::Engine as _;
263 serializer.serialize_str(&base64::engine::general_purpose::STANDARD.encode(bytes))
264}
265
266fn deserialize_media_bytes<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
268where
269 D: serde::Deserializer<'de>,
270{
271 use base64::Engine as _;
272 use serde::de::Error as _;
273
274 #[derive(Deserialize)]
275 #[serde(untagged)]
276 enum Bytes {
277 Base64(String),
278 Integers(Vec<u8>),
279 }
280
281 match Bytes::deserialize(deserializer)? {
282 Bytes::Base64(encoded) => base64::engine::general_purpose::STANDARD
283 .decode(encoded)
284 .map_err(D::Error::custom),
285 Bytes::Integers(bytes) => Ok(bytes),
286 }
287}
288
289#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
290#[serde(
291 tag = "kind",
292 rename_all = "camelCase",
293 rename_all_fields = "camelCase"
294)]
295pub enum ShapeNode {
296 Shape(Shape),
297 Picture(Picture),
298 GraphicFrame(GraphicFrame),
299 Group(GroupShape),
300}
301
302impl ShapeNode {
303 pub fn id(&self) -> u32 {
304 match self {
305 Self::Shape(shape) => shape.base.id,
306 Self::Picture(picture) => picture.base.id,
307 Self::GraphicFrame(frame) => frame.base.id,
308 Self::Group(group) => group.base.id,
309 }
310 }
311}
312
313#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
314#[serde(rename_all = "camelCase")]
315pub struct ShapeBase {
316 pub id: u32,
317 pub name: String,
318 pub description: Option<String>,
319 pub hidden: bool,
320 pub placeholder: Option<Placeholder>,
321 pub transform: ShapeTransform,
322}
323
324#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
325#[serde(rename_all = "camelCase")]
326pub struct ShapeTransform {
327 pub x: i64,
328 pub y: i64,
329 pub width: i64,
330 pub height: i64,
331 pub rotation_deg: f64,
332 pub flip_h: bool,
333 pub flip_v: bool,
334 pub child_x: Option<i64>,
335 pub child_y: Option<i64>,
336 pub child_width: Option<i64>,
337 pub child_height: Option<i64>,
338}
339
340#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
341#[serde(rename_all = "camelCase")]
342pub struct Placeholder {
343 pub placeholder_type: Option<String>,
344 pub index: Option<u32>,
345 pub orientation: Option<String>,
346 pub size: Option<String>,
347}
348
349const SINGLETON_PLACEHOLDERS: [&str; 5] = ["title", "sldNum", "dt", "ftr", "hdr"];
353
354impl Placeholder {
355 pub fn matches(&self, other: &Placeholder) -> bool {
358 let left_type = normalize_placeholder_type(self.placeholder_type.as_deref());
359 let right_type = normalize_placeholder_type(other.placeholder_type.as_deref());
360 if SINGLETON_PLACEHOLDERS.contains(&left_type)
361 || SINGLETON_PLACEHOLDERS.contains(&right_type)
362 {
363 return left_type == right_type;
364 }
365 match (self.index, other.index) {
366 (Some(left), Some(right)) => left == right,
367 _ => left_type == right_type,
368 }
369 }
370}
371
372fn normalize_placeholder_type(value: Option<&str>) -> &str {
373 match value.unwrap_or("body") {
374 "ctrTitle" => "title",
375 "obj" => "body",
376 value => value,
377 }
378}
379
380#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
381#[serde(rename_all = "camelCase")]
382pub struct Shape {
383 #[serde(flatten)]
384 pub base: ShapeBase,
385 pub geometry: String,
386 #[serde(
387 default = "has_preset_geometry_default",
388 skip_serializing_if = "is_preset_geometry"
389 )]
390 pub has_preset_geometry: bool,
391 #[serde(default, skip_serializing_if = "Vec::is_empty")]
393 pub paths: Vec<CustomGeometryPath>,
394 #[serde(default, skip_serializing_if = "Option::is_none")]
396 pub style: Option<Box<ShapeStyle>>,
397 #[serde(default)]
398 pub adjust_values: BTreeMap<String, f64>,
399 pub fill: Option<ShapeFill>,
400 #[serde(default, skip_serializing_if = "Option::is_none")]
402 pub picture_fill: Option<Box<PictureFill>>,
403 pub outline: Option<ShapeOutline>,
404 #[serde(default, skip_serializing_if = "Option::is_none")]
405 pub effects: Option<ShapeEffects>,
406 pub text: Option<TextBody>,
407}
408
409#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
411#[serde(rename_all = "camelCase")]
412pub struct PictureFill {
413 #[serde(default, skip_serializing_if = "Option::is_none")]
414 pub relationship_id: Option<String>,
415 #[serde(default, skip_serializing_if = "Option::is_none")]
416 pub media_part_path: Option<String>,
417 #[serde(default, skip_serializing_if = "PictureCrop::is_whole")]
418 pub crop: PictureCrop,
419 #[serde(default, skip_serializing_if = "PictureCrop::is_whole")]
421 pub fill_rect: PictureCrop,
422 #[serde(default, skip_serializing_if = "Option::is_none")]
424 pub tile: Option<PictureTile>,
425}
426
427#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
430#[serde(rename_all = "camelCase")]
431pub struct PictureTile {
432 pub scale_x: f64,
434 pub scale_y: f64,
435}
436
437#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
438#[serde(rename_all = "camelCase")]
439pub struct CustomGeometryPath {
440 pub commands: Vec<GeometryPathCommand>,
441 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
442 pub no_fill: bool,
443 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
444 pub no_stroke: bool,
445}
446
447#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
448#[serde(rename_all = "camelCase")]
449pub struct Picture {
450 #[serde(flatten)]
451 pub base: ShapeBase,
452 pub relationship_id: Option<String>,
453 pub media_part_path: Option<String>,
454 pub crop: PictureCrop,
455 #[serde(default, skip_serializing_if = "Vec::is_empty")]
457 pub effects: Vec<BlipEffect>,
458 #[serde(default = "rect_geometry", skip_serializing_if = "is_rect")]
460 pub geometry: String,
461 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
462 pub adjust_values: BTreeMap<String, f64>,
463 pub fill: Option<ShapeFill>,
464 pub outline: Option<ShapeOutline>,
465 #[serde(default, skip_serializing_if = "Option::is_none")]
466 pub shape_effects: Option<ShapeEffects>,
467 #[serde(default, skip_serializing_if = "Option::is_none")]
468 pub style: Option<Box<ShapeStyle>>,
469}
470
471fn rect_geometry() -> String {
472 "rect".to_owned()
473}
474
475fn is_rect(geometry: &str) -> bool {
476 geometry == "rect"
477}
478
479fn has_preset_geometry_default() -> bool {
480 true
481}
482
483fn is_preset_geometry(value: &bool) -> bool {
484 *value
485}
486
487#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
489#[serde(tag = "type", rename_all = "camelCase")]
490pub enum BlipEffect {
491 BiLevel { threshold: f64 },
493 Grayscale,
495 Alpha { amount: f64 },
497 Luminance { brightness: f64, contrast: f64 },
499 Duotone {
501 shadow: Option<ColorValue>,
502 highlight: Option<ColorValue>,
503 },
504 ColorChange {
506 from: Option<ColorValue>,
507 to: Option<ColorValue>,
508 #[serde(
509 default = "default_use_alpha",
510 skip_serializing_if = "use_alpha_is_default",
511 rename = "useAlpha"
512 )]
513 use_alpha: bool,
514 },
515}
516
517fn default_use_alpha() -> bool {
518 true
519}
520
521fn use_alpha_is_default(value: &bool) -> bool {
522 *value
523}
524
525#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
526pub struct PictureCrop {
527 pub left: i32,
528 pub top: i32,
529 pub right: i32,
530 pub bottom: i32,
531}
532
533impl PictureCrop {
534 pub fn is_whole(&self) -> bool {
535 *self == Self::default()
536 }
537}
538
539#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
540#[serde(rename_all = "camelCase")]
541pub struct GraphicFrame {
542 #[serde(flatten)]
543 pub base: ShapeBase,
544 pub data: GraphicFrameData,
545}
546
547#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
548#[serde(
549 tag = "type",
550 rename_all = "camelCase",
551 rename_all_fields = "camelCase"
552)]
553pub enum GraphicFrameData {
554 Table(Table),
555 Chart {
556 relationship_id: String,
557 part_path: Option<String>,
558 },
559 Diagram {
560 relationship_ids: Vec<String>,
561 #[serde(default, skip_serializing_if = "Option::is_none")]
565 drawing_part_path: Option<String>,
566 },
567 Unknown {
568 uri: Option<String>,
569 #[serde(default, skip_serializing_if = "Option::is_none")]
570 picture: Option<Box<Picture>>,
571 },
572}
573
574#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
576#[serde(rename_all = "camelCase")]
577pub struct DiagramDrawing {
578 pub part_path: String,
579 pub shapes: Vec<ShapeNode>,
580}
581
582#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
583#[serde(rename_all = "camelCase")]
584pub struct GroupShape {
585 #[serde(flatten)]
586 pub base: ShapeBase,
587 pub children: Vec<ShapeNode>,
588}
589
590#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
592#[serde(rename_all = "camelCase")]
593pub struct Table {
594 #[serde(default, skip_serializing_if = "Vec::is_empty")]
596 pub grid: Vec<i64>,
597 #[serde(default, skip_serializing_if = "TableProperties::is_default")]
598 pub properties: TableProperties,
599 #[serde(
600 default,
601 serialize_with = "serialize_table_rows",
602 deserialize_with = "deserialize_table_rows"
603 )]
604 pub rows: Vec<TableRow>,
605}
606
607#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
609#[serde(rename_all = "camelCase", default)]
610pub struct TableProperties {
611 pub first_row: bool,
612 pub last_row: bool,
613 pub first_col: bool,
614 pub last_col: bool,
615 pub band_row: bool,
616 pub band_col: bool,
617 #[serde(skip_serializing_if = "Option::is_none")]
618 pub style_id: Option<String>,
619}
620
621impl TableProperties {
622 pub fn is_default(&self) -> bool {
623 *self == Self::default()
624 }
625}
626
627#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
628#[serde(rename_all = "camelCase")]
629pub struct TableRow {
630 #[serde(default)]
632 pub height: i64,
633 pub cells: Vec<TableCell>,
634}
635
636impl TableRow {
637 fn is_text_only(&self) -> bool {
638 self.height == 0 && self.cells.iter().all(TableCell::is_text_only)
639 }
640}
641
642#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
644#[serde(rename_all = "camelCase")]
645pub struct TableCell {
646 pub text: TextBody,
647 #[serde(default = "unit_span", skip_serializing_if = "is_unit_span")]
648 pub grid_span: u32,
649 #[serde(default = "unit_span", skip_serializing_if = "is_unit_span")]
650 pub row_span: u32,
651 #[serde(default, skip_serializing_if = "is_false")]
653 pub merged: bool,
654 #[serde(default, skip_serializing_if = "Option::is_none")]
655 pub fill: Option<ShapeFill>,
656 #[serde(default, skip_serializing_if = "TableCellBorders::is_empty")]
657 pub borders: TableCellBorders,
658}
659
660impl TableCell {
661 pub fn from_text(text: TextBody) -> Self {
662 Self {
663 text,
664 ..Self::default()
665 }
666 }
667
668 fn is_text_only(&self) -> bool {
671 *self == Self::from_text(self.text.clone())
672 }
673}
674
675impl Default for TableCell {
676 fn default() -> Self {
677 Self {
678 text: TextBody::default(),
679 grid_span: 1,
680 row_span: 1,
681 merged: false,
682 fill: None,
683 borders: TableCellBorders::default(),
684 }
685 }
686}
687
688#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
690#[serde(rename_all = "camelCase", default)]
691pub struct TableCellBorders {
692 #[serde(skip_serializing_if = "Option::is_none")]
693 pub left: Option<ShapeOutline>,
694 #[serde(skip_serializing_if = "Option::is_none")]
695 pub top: Option<ShapeOutline>,
696 #[serde(skip_serializing_if = "Option::is_none")]
697 pub right: Option<ShapeOutline>,
698 #[serde(skip_serializing_if = "Option::is_none")]
699 pub bottom: Option<ShapeOutline>,
700}
701
702impl TableCellBorders {
703 pub fn is_empty(&self) -> bool {
704 *self == Self::default()
705 }
706}
707
708fn unit_span() -> u32 {
709 1
710}
711
712fn is_unit_span(span: &u32) -> bool {
713 *span == 1
714}
715
716fn is_false(value: &bool) -> bool {
717 !*value
718}
719
720fn serialize_table_rows<S>(rows: &[TableRow], serializer: S) -> Result<S::Ok, S::Error>
723where
724 S: serde::Serializer,
725{
726 use serde::ser::SerializeSeq;
727
728 if !rows.iter().all(TableRow::is_text_only) {
729 return rows.serialize(serializer);
730 }
731 let mut sequence = serializer.serialize_seq(Some(rows.len()))?;
732 for row in rows {
733 let cells: Vec<_> = row.cells.iter().map(|cell| &cell.text).collect();
734 sequence.serialize_element(&cells)?;
735 }
736 sequence.end()
737}
738
739fn deserialize_table_rows<'de, D>(deserializer: D) -> Result<Vec<TableRow>, D::Error>
741where
742 D: serde::Deserializer<'de>,
743{
744 #[derive(Deserialize)]
745 #[serde(untagged)]
746 enum Row {
747 Modern(TableRow),
748 Legacy(Vec<TextBody>),
749 }
750
751 Ok(Vec::<Row>::deserialize(deserializer)?
752 .into_iter()
753 .map(|row| match row {
754 Row::Modern(row) => row,
755 Row::Legacy(cells) => TableRow {
756 height: 0,
757 cells: cells.into_iter().map(TableCell::from_text).collect(),
758 },
759 })
760 .collect())
761}
762
763#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
764#[serde(rename_all = "camelCase")]
765pub struct TextBody {
766 pub anchor: Option<String>,
767 pub vertical: Option<String>,
768 #[serde(default, skip_serializing_if = "Option::is_none")]
770 pub compat_line_spacing: Option<bool>,
771 #[serde(default, skip_serializing_if = "Option::is_none")]
774 pub space_first_last_para: Option<bool>,
775 pub autofit: Option<TextAutofit>,
776 #[serde(default, skip_serializing_if = "Option::is_none")]
779 pub wrap: Option<bool>,
780 #[serde(default, skip_serializing_if = "Option::is_none")]
781 pub vertical_overflow: Option<TextOverflow>,
782 #[serde(default, skip_serializing_if = "Option::is_none")]
783 pub horizontal_overflow: Option<TextOverflow>,
784 pub inset_left: Option<i64>,
785 pub inset_top: Option<i64>,
786 pub inset_right: Option<i64>,
787 pub inset_bottom: Option<i64>,
788 #[serde(default, skip_serializing_if = "Vec::is_empty")]
790 pub list_style: Vec<ParagraphProperties>,
791 #[serde(default, skip_serializing_if = "Option::is_none")]
792 pub default_list_style: Option<Box<ParagraphProperties>>,
793 pub paragraphs: Vec<TextParagraph>,
794}
795
796#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
797#[serde(rename_all = "camelCase")]
798pub enum TextOverflow {
799 Overflow,
800 Clip,
801 Ellipsis,
802}
803
804#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
805#[serde(
806 tag = "type",
807 rename_all = "camelCase",
808 rename_all_fields = "camelCase"
809)]
810pub enum TextAutofit {
811 None,
812 Shape,
813 Normal {
814 font_scale: Option<f64>,
815 line_space_reduction: Option<f64>,
816 },
817}
818
819#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
820#[serde(rename_all = "camelCase")]
821pub struct TextParagraph {
822 pub properties: ParagraphProperties,
823 pub runs: Vec<TextRun>,
824 pub end_properties: Option<RunProperties>,
825}
826
827#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
829#[serde(
830 tag = "type",
831 rename_all = "camelCase",
832 rename_all_fields = "camelCase"
833)]
834pub enum LineSpacing {
835 Percent { value: f64 },
836 Points { value: f64 },
837}
838
839#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
840#[serde(rename_all = "camelCase")]
841pub struct ParagraphProperties {
842 pub alignment: Option<String>,
843 pub level: u32,
844 pub margin_left: Option<i64>,
845 #[serde(default, skip_serializing_if = "Option::is_none")]
846 pub margin_right: Option<i64>,
847 pub indent: Option<i64>,
848 pub bullet: Option<Bullet>,
849 #[serde(default, skip_serializing_if = "Option::is_none")]
850 pub line_spacing: Option<LineSpacing>,
851 #[serde(default, skip_serializing_if = "Option::is_none")]
852 pub space_before: Option<LineSpacing>,
853 #[serde(default, skip_serializing_if = "Option::is_none")]
854 pub space_after: Option<LineSpacing>,
855 #[serde(default, skip_serializing_if = "Option::is_none")]
856 pub bullet_font: Option<BulletFont>,
857 #[serde(default, skip_serializing_if = "Option::is_none")]
858 pub bullet_color: Option<BulletColor>,
859 #[serde(default, skip_serializing_if = "Option::is_none")]
860 pub bullet_size: Option<BulletSize>,
861 #[serde(default, skip_serializing_if = "Option::is_none")]
863 pub default_tab_size: Option<i64>,
864 #[serde(default, skip_serializing_if = "Option::is_none")]
867 pub tab_stops: Option<Vec<i64>>,
868 #[serde(default, skip_serializing_if = "Option::is_none")]
870 pub rtl: Option<bool>,
871 pub default_run: Option<RunProperties>,
872}
873
874#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
875#[serde(tag = "type", content = "value", rename_all = "camelCase")]
876pub enum BulletFont {
877 FollowText,
878 Typeface(String),
879}
880
881#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
882#[serde(tag = "type", content = "value", rename_all = "camelCase")]
883pub enum BulletColor {
884 FollowText,
885 Color(ColorValue),
886}
887
888#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
889#[serde(tag = "type", content = "value", rename_all = "camelCase")]
890pub enum BulletSize {
891 FollowText,
892 Percent(f64),
893 Points(f64),
894}
895
896#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
897#[serde(
898 tag = "type",
899 rename_all = "camelCase",
900 rename_all_fields = "camelCase"
901)]
902pub enum Bullet {
903 Character {
904 value: String,
905 },
906 AutoNumber {
907 scheme: String,
908 start_at: u32,
909 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
910 restart: bool,
911 },
912 None,
913}
914
915#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
916#[serde(rename_all = "camelCase")]
917pub struct TextRun {
918 pub text: String,
919 pub properties: RunProperties,
920 pub field_id: Option<String>,
921 pub field_type: Option<String>,
922 pub line_break: bool,
923}
924
925#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
928#[serde(rename_all = "camelCase")]
929pub enum TextCaps {
930 None,
931 Small,
932 All,
933}
934
935impl TextCaps {
936 pub fn from_attribute(value: &str) -> Option<Self> {
937 match value {
938 "none" => Some(Self::None),
939 "small" => Some(Self::Small),
940 "all" => Some(Self::All),
941 _ => None,
942 }
943 }
944
945 pub fn as_attribute(self) -> &'static str {
946 match self {
947 Self::None => "none",
948 Self::Small => "small",
949 Self::All => "all",
950 }
951 }
952}
953
954#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
955#[serde(rename_all = "camelCase")]
956pub struct RunProperties {
957 pub font_size_pt: Option<f64>,
958 #[serde(default, skip_serializing_if = "Option::is_none")]
959 pub spacing_pt: Option<f64>,
960 #[serde(default, skip_serializing_if = "Option::is_none")]
962 pub baseline_pct: Option<f64>,
963 pub bold: Option<bool>,
964 pub italic: Option<bool>,
965 pub underline: Option<String>,
966 #[serde(default, skip_serializing_if = "Option::is_none")]
967 pub caps: Option<TextCaps>,
968 pub font_family: Option<String>,
969 pub color: Option<ColorValue>,
970 pub language: Option<String>,
971 pub hyperlink_relationship_id: Option<String>,
972 #[serde(default, skip_serializing_if = "Option::is_none")]
974 pub effects: Option<ShapeEffects>,
975}
976
977#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
978#[serde(rename_all = "camelCase")]
979pub struct TextStyleSet {
980 pub title: Vec<ParagraphProperties>,
981 pub body: Vec<ParagraphProperties>,
982 pub other: Vec<ParagraphProperties>,
983}