1use crate::error::Result;
20use crate::generated_serializers::ToXml;
21use crate::types;
22use ooxml_opc::PackageWriter;
23use std::collections::HashMap;
24use std::fs::File;
25use std::io::{BufWriter, Seek, Write};
26use std::path::Path;
27
28const CT_WORKBOOK: &str =
30 "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml";
31const CT_WORKSHEET: &str =
32 "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml";
33const CT_SHARED_STRINGS: &str =
34 "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml";
35const CT_STYLES: &str = "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml";
36const CT_COMMENTS: &str =
37 "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml";
38const CT_RELATIONSHIPS: &str = "application/vnd.openxmlformats-package.relationships+xml";
39const CT_XML: &str = "application/xml";
40#[cfg(feature = "sml-charts")]
41const CT_DRAWING: &str = "application/vnd.openxmlformats-officedocument.drawing+xml";
42#[cfg(feature = "sml-charts")]
43const CT_CHART: &str = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml";
44#[cfg(feature = "sml-pivot")]
45const CT_PIVOT_TABLE: &str =
46 "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml";
47#[cfg(feature = "sml-pivot")]
48const CT_PIVOT_CACHE_DEF: &str =
49 "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml";
50#[cfg(feature = "sml-pivot")]
51const CT_PIVOT_CACHE_REC: &str =
52 "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml";
53
54const REL_OFFICE_DOCUMENT: &str =
56 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
57const REL_WORKSHEET: &str =
58 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet";
59const REL_SHARED_STRINGS: &str =
60 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings";
61const REL_STYLES: &str =
62 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles";
63const REL_COMMENTS: &str =
64 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";
65const REL_HYPERLINK: &str =
66 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink";
67#[cfg(feature = "sml-charts")]
68const REL_DRAWING: &str =
69 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing";
70#[cfg(feature = "sml-charts")]
71const REL_CHART: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart";
72#[cfg(feature = "sml-pivot")]
73const REL_PIVOT_TABLE: &str =
74 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable";
75#[cfg(feature = "sml-pivot")]
76const REL_PIVOT_CACHE_DEF: &str =
77 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition";
78#[cfg(feature = "sml-pivot")]
79const REL_PIVOT_CACHE_REC: &str =
80 "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheRecords";
81
82const NS_SPREADSHEET: &str = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
84const NS_RELATIONSHIPS: &str =
85 "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
86
87#[derive(Debug, Clone)]
89pub enum WriteCellValue {
90 String(String),
92 Number(f64),
94 Boolean(bool),
96 Formula(String),
98 Empty,
100}
101
102impl From<&str> for WriteCellValue {
103 fn from(s: &str) -> Self {
104 WriteCellValue::String(s.to_string())
105 }
106}
107
108impl From<String> for WriteCellValue {
109 fn from(s: String) -> Self {
110 WriteCellValue::String(s)
111 }
112}
113
114impl From<f64> for WriteCellValue {
115 fn from(n: f64) -> Self {
116 WriteCellValue::Number(n)
117 }
118}
119
120impl From<i32> for WriteCellValue {
121 fn from(n: i32) -> Self {
122 WriteCellValue::Number(n as f64)
123 }
124}
125
126impl From<i64> for WriteCellValue {
127 fn from(n: i64) -> Self {
128 WriteCellValue::Number(n as f64)
129 }
130}
131
132impl From<bool> for WriteCellValue {
133 fn from(b: bool) -> Self {
134 WriteCellValue::Boolean(b)
135 }
136}
137
138#[derive(Debug, Clone, Default, PartialEq)]
142pub struct CellStyle {
143 pub font: Option<FontStyle>,
145 pub fill: Option<FillStyle>,
147 pub border: Option<BorderStyle>,
149 pub number_format: Option<String>,
151 pub horizontal_alignment: Option<HorizontalAlignment>,
153 pub vertical_alignment: Option<VerticalAlignment>,
155 pub wrap_text: bool,
157}
158
159impl CellStyle {
160 pub fn new() -> Self {
162 Self::default()
163 }
164
165 pub fn with_font(mut self, font: FontStyle) -> Self {
167 self.font = Some(font);
168 self
169 }
170
171 pub fn with_fill(mut self, fill: FillStyle) -> Self {
173 self.fill = Some(fill);
174 self
175 }
176
177 pub fn with_border(mut self, border: BorderStyle) -> Self {
179 self.border = Some(border);
180 self
181 }
182
183 pub fn with_number_format(mut self, format: impl Into<String>) -> Self {
185 self.number_format = Some(format.into());
186 self
187 }
188
189 pub fn with_horizontal_alignment(mut self, align: HorizontalAlignment) -> Self {
191 self.horizontal_alignment = Some(align);
192 self
193 }
194
195 pub fn with_vertical_alignment(mut self, align: VerticalAlignment) -> Self {
197 self.vertical_alignment = Some(align);
198 self
199 }
200
201 pub fn with_wrap_text(mut self, wrap: bool) -> Self {
203 self.wrap_text = wrap;
204 self
205 }
206}
207
208#[derive(Debug, Clone, Default, PartialEq)]
210pub struct FontStyle {
211 pub name: Option<String>,
213 pub size: Option<f64>,
215 pub bold: bool,
217 pub italic: bool,
219 pub underline: Option<UnderlineStyle>,
221 pub strikethrough: bool,
223 pub color: Option<String>,
225}
226
227impl FontStyle {
228 pub fn new() -> Self {
230 Self::default()
231 }
232
233 pub fn with_name(mut self, name: impl Into<String>) -> Self {
235 self.name = Some(name.into());
236 self
237 }
238
239 pub fn with_size(mut self, size: f64) -> Self {
241 self.size = Some(size);
242 self
243 }
244
245 pub fn bold(mut self) -> Self {
247 self.bold = true;
248 self
249 }
250
251 pub fn italic(mut self) -> Self {
253 self.italic = true;
254 self
255 }
256
257 pub fn underline(mut self, style: UnderlineStyle) -> Self {
259 self.underline = Some(style);
260 self
261 }
262
263 pub fn strikethrough(mut self) -> Self {
265 self.strikethrough = true;
266 self
267 }
268
269 pub fn with_color(mut self, color: impl Into<String>) -> Self {
271 self.color = Some(color.into());
272 self
273 }
274}
275
276#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
280pub enum UnderlineStyle {
281 #[default]
283 Single,
284 Double,
286 SingleAccounting,
288 DoubleAccounting,
290}
291
292impl UnderlineStyle {
293 fn to_xml_value(self) -> &'static str {
294 match self {
295 UnderlineStyle::Single => "single",
296 UnderlineStyle::Double => "double",
297 UnderlineStyle::SingleAccounting => "singleAccounting",
298 UnderlineStyle::DoubleAccounting => "doubleAccounting",
299 }
300 }
301}
302
303#[derive(Debug, Clone, Default, PartialEq)]
305pub struct FillStyle {
306 pub pattern: FillPattern,
308 pub fg_color: Option<String>,
310 pub bg_color: Option<String>,
312}
313
314impl FillStyle {
315 pub fn new() -> Self {
317 Self::default()
318 }
319
320 pub fn solid(color: impl Into<String>) -> Self {
322 Self {
323 pattern: FillPattern::Solid,
324 fg_color: Some(color.into()),
325 bg_color: None,
326 }
327 }
328
329 pub fn with_pattern(mut self, pattern: FillPattern) -> Self {
331 self.pattern = pattern;
332 self
333 }
334
335 pub fn with_fg_color(mut self, color: impl Into<String>) -> Self {
337 self.fg_color = Some(color.into());
338 self
339 }
340
341 pub fn with_bg_color(mut self, color: impl Into<String>) -> Self {
343 self.bg_color = Some(color.into());
344 self
345 }
346}
347
348#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
352pub enum FillPattern {
353 #[default]
355 None,
356 Solid,
358 MediumGray,
360 DarkGray,
362 LightGray,
364 DarkHorizontal,
366 DarkVertical,
368 DarkDown,
370 DarkUp,
372 DarkGrid,
374 DarkTrellis,
376 LightHorizontal,
378 LightVertical,
380 LightDown,
382 LightUp,
384 LightGrid,
386 LightTrellis,
388 Gray125,
390 Gray0625,
392}
393
394impl FillPattern {
395 fn to_xml_value(self) -> &'static str {
396 match self {
397 FillPattern::None => "none",
398 FillPattern::Solid => "solid",
399 FillPattern::MediumGray => "mediumGray",
400 FillPattern::DarkGray => "darkGray",
401 FillPattern::LightGray => "lightGray",
402 FillPattern::DarkHorizontal => "darkHorizontal",
403 FillPattern::DarkVertical => "darkVertical",
404 FillPattern::DarkDown => "darkDown",
405 FillPattern::DarkUp => "darkUp",
406 FillPattern::DarkGrid => "darkGrid",
407 FillPattern::DarkTrellis => "darkTrellis",
408 FillPattern::LightHorizontal => "lightHorizontal",
409 FillPattern::LightVertical => "lightVertical",
410 FillPattern::LightDown => "lightDown",
411 FillPattern::LightUp => "lightUp",
412 FillPattern::LightGrid => "lightGrid",
413 FillPattern::LightTrellis => "lightTrellis",
414 FillPattern::Gray125 => "gray125",
415 FillPattern::Gray0625 => "gray0625",
416 }
417 }
418}
419
420#[derive(Debug, Clone, Default, PartialEq)]
422pub struct BorderStyle {
423 pub left: Option<BorderSideStyle>,
425 pub right: Option<BorderSideStyle>,
427 pub top: Option<BorderSideStyle>,
429 pub bottom: Option<BorderSideStyle>,
431 pub diagonal: Option<BorderSideStyle>,
433 pub diagonal_up: bool,
435 pub diagonal_down: bool,
437}
438
439impl BorderStyle {
440 pub fn new() -> Self {
442 Self::default()
443 }
444
445 pub fn all(style: BorderLineStyle, color: Option<String>) -> Self {
447 let side = BorderSideStyle { style, color };
448 Self {
449 left: Some(side.clone()),
450 right: Some(side.clone()),
451 top: Some(side.clone()),
452 bottom: Some(side),
453 diagonal: None,
454 diagonal_up: false,
455 diagonal_down: false,
456 }
457 }
458
459 pub fn with_left(mut self, style: BorderLineStyle, color: Option<String>) -> Self {
461 self.left = Some(BorderSideStyle { style, color });
462 self
463 }
464
465 pub fn with_right(mut self, style: BorderLineStyle, color: Option<String>) -> Self {
467 self.right = Some(BorderSideStyle { style, color });
468 self
469 }
470
471 pub fn with_top(mut self, style: BorderLineStyle, color: Option<String>) -> Self {
473 self.top = Some(BorderSideStyle { style, color });
474 self
475 }
476
477 pub fn with_bottom(mut self, style: BorderLineStyle, color: Option<String>) -> Self {
479 self.bottom = Some(BorderSideStyle { style, color });
480 self
481 }
482}
483
484#[derive(Debug, Clone, Default, PartialEq)]
486pub struct BorderSideStyle {
487 pub style: BorderLineStyle,
489 pub color: Option<String>,
491}
492
493#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
497pub enum BorderLineStyle {
498 #[default]
500 None,
501 Thin,
503 Medium,
505 Dashed,
507 Dotted,
509 Thick,
511 Double,
513 Hair,
515 MediumDashed,
517 DashDot,
519 MediumDashDot,
521 DashDotDot,
523 MediumDashDotDot,
525 SlantDashDot,
527}
528
529impl BorderLineStyle {
530 fn to_xml_value(self) -> &'static str {
531 match self {
532 BorderLineStyle::None => "none",
533 BorderLineStyle::Thin => "thin",
534 BorderLineStyle::Medium => "medium",
535 BorderLineStyle::Dashed => "dashed",
536 BorderLineStyle::Dotted => "dotted",
537 BorderLineStyle::Thick => "thick",
538 BorderLineStyle::Double => "double",
539 BorderLineStyle::Hair => "hair",
540 BorderLineStyle::MediumDashed => "mediumDashed",
541 BorderLineStyle::DashDot => "dashDot",
542 BorderLineStyle::MediumDashDot => "mediumDashDot",
543 BorderLineStyle::DashDotDot => "dashDotDot",
544 BorderLineStyle::MediumDashDotDot => "mediumDashDotDot",
545 BorderLineStyle::SlantDashDot => "slantDashDot",
546 }
547 }
548}
549
550#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
554pub enum HorizontalAlignment {
555 #[default]
557 General,
558 Left,
560 Center,
562 Right,
564 Fill,
566 Justify,
568 CenterContinuous,
570 Distributed,
572}
573
574impl HorizontalAlignment {
575 fn to_xml_value(self) -> &'static str {
576 match self {
577 HorizontalAlignment::General => "general",
578 HorizontalAlignment::Left => "left",
579 HorizontalAlignment::Center => "center",
580 HorizontalAlignment::Right => "right",
581 HorizontalAlignment::Fill => "fill",
582 HorizontalAlignment::Justify => "justify",
583 HorizontalAlignment::CenterContinuous => "centerContinuous",
584 HorizontalAlignment::Distributed => "distributed",
585 }
586 }
587}
588
589#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
593pub enum VerticalAlignment {
594 Top,
596 Center,
598 #[default]
600 Bottom,
601 Justify,
603 Distributed,
605}
606
607impl VerticalAlignment {
608 fn to_xml_value(self) -> &'static str {
609 match self {
610 VerticalAlignment::Top => "top",
611 VerticalAlignment::Center => "center",
612 VerticalAlignment::Bottom => "bottom",
613 VerticalAlignment::Justify => "justify",
614 VerticalAlignment::Distributed => "distributed",
615 }
616 }
617}
618
619#[derive(Debug, Clone)]
621struct BuilderCell {
622 value: WriteCellValue,
623 style: Option<CellStyle>,
624}
625
626#[derive(Debug, Clone)]
628pub struct ConditionalFormat {
629 pub range: String,
631 pub rules: Vec<ConditionalFormatRule>,
633}
634
635#[derive(Debug, Clone)]
637pub struct ConditionalFormatRule {
638 pub rule_type: crate::ConditionalRuleType,
640 pub priority: u32,
642 pub dxf_id: Option<u32>,
644 pub operator: Option<String>,
646 pub formulas: Vec<String>,
648 pub text: Option<String>,
650}
651
652impl ConditionalFormat {
653 pub fn new(range: impl Into<String>) -> Self {
655 Self {
656 range: range.into(),
657 rules: Vec::new(),
658 }
659 }
660
661 pub fn add_cell_is_rule(
663 mut self,
664 operator: &str,
665 formula: impl Into<String>,
666 priority: u32,
667 dxf_id: Option<u32>,
668 ) -> Self {
669 self.rules.push(ConditionalFormatRule {
670 rule_type: crate::ConditionalRuleType::CellIs,
671 priority,
672 dxf_id,
673 operator: Some(operator.to_string()),
674 formulas: vec![formula.into()],
675 text: None,
676 });
677 self
678 }
679
680 pub fn add_expression_rule(
682 mut self,
683 formula: impl Into<String>,
684 priority: u32,
685 dxf_id: Option<u32>,
686 ) -> Self {
687 self.rules.push(ConditionalFormatRule {
688 rule_type: crate::ConditionalRuleType::Expression,
689 priority,
690 dxf_id,
691 operator: None,
692 formulas: vec![formula.into()],
693 text: None,
694 });
695 self
696 }
697
698 pub fn add_duplicate_values_rule(mut self, priority: u32, dxf_id: Option<u32>) -> Self {
700 self.rules.push(ConditionalFormatRule {
701 rule_type: crate::ConditionalRuleType::DuplicateValues,
702 priority,
703 dxf_id,
704 operator: None,
705 formulas: Vec::new(),
706 text: None,
707 });
708 self
709 }
710
711 pub fn add_contains_text_rule(
713 mut self,
714 text: impl Into<String>,
715 priority: u32,
716 dxf_id: Option<u32>,
717 ) -> Self {
718 let text = text.into();
719 self.rules.push(ConditionalFormatRule {
720 rule_type: crate::ConditionalRuleType::ContainsText,
721 priority,
722 dxf_id,
723 operator: Some("containsText".to_string()),
724 formulas: Vec::new(),
725 text: Some(text),
726 });
727 self
728 }
729}
730
731#[derive(Debug, Clone)]
733pub struct DataValidationBuilder {
734 pub range: String,
736 pub validation_type: crate::DataValidationType,
738 pub operator: crate::DataValidationOperator,
740 pub formula1: Option<String>,
742 pub formula2: Option<String>,
744 pub allow_blank: bool,
746 pub show_input_message: bool,
748 pub show_error_message: bool,
750 pub error_style: crate::DataValidationErrorStyle,
752 pub error_title: Option<String>,
754 pub error_message: Option<String>,
756 pub prompt_title: Option<String>,
758 pub prompt_message: Option<String>,
760}
761
762impl DataValidationBuilder {
763 pub fn new(range: impl Into<String>) -> Self {
765 Self {
766 range: range.into(),
767 validation_type: crate::DataValidationType::None,
768 operator: crate::DataValidationOperator::Between,
769 formula1: None,
770 formula2: None,
771 allow_blank: true,
772 show_input_message: true,
773 show_error_message: true,
774 error_style: crate::DataValidationErrorStyle::Stop,
775 error_title: None,
776 error_message: None,
777 prompt_title: None,
778 prompt_message: None,
779 }
780 }
781
782 pub fn list(range: impl Into<String>, source: impl Into<String>) -> Self {
784 Self {
785 range: range.into(),
786 validation_type: crate::DataValidationType::List,
787 operator: crate::DataValidationOperator::Between,
788 formula1: Some(source.into()),
789 formula2: None,
790 allow_blank: true,
791 show_input_message: true,
792 show_error_message: true,
793 error_style: crate::DataValidationErrorStyle::Stop,
794 error_title: None,
795 error_message: None,
796 prompt_title: None,
797 prompt_message: None,
798 }
799 }
800
801 pub fn whole_number(
803 range: impl Into<String>,
804 operator: crate::DataValidationOperator,
805 value1: impl Into<String>,
806 ) -> Self {
807 Self {
808 range: range.into(),
809 validation_type: crate::DataValidationType::Whole,
810 operator,
811 formula1: Some(value1.into()),
812 formula2: None,
813 allow_blank: true,
814 show_input_message: true,
815 show_error_message: true,
816 error_style: crate::DataValidationErrorStyle::Stop,
817 error_title: None,
818 error_message: None,
819 prompt_title: None,
820 prompt_message: None,
821 }
822 }
823
824 pub fn decimal(
826 range: impl Into<String>,
827 operator: crate::DataValidationOperator,
828 value1: impl Into<String>,
829 ) -> Self {
830 Self {
831 range: range.into(),
832 validation_type: crate::DataValidationType::Decimal,
833 operator,
834 formula1: Some(value1.into()),
835 formula2: None,
836 allow_blank: true,
837 show_input_message: true,
838 show_error_message: true,
839 error_style: crate::DataValidationErrorStyle::Stop,
840 error_title: None,
841 error_message: None,
842 prompt_title: None,
843 prompt_message: None,
844 }
845 }
846
847 pub fn with_formula2(mut self, formula2: impl Into<String>) -> Self {
849 self.formula2 = Some(formula2.into());
850 self
851 }
852
853 pub fn with_allow_blank(mut self, allow: bool) -> Self {
855 self.allow_blank = allow;
856 self
857 }
858
859 pub fn with_error_style(mut self, style: crate::DataValidationErrorStyle) -> Self {
861 self.error_style = style;
862 self
863 }
864
865 pub fn with_error(mut self, title: impl Into<String>, message: impl Into<String>) -> Self {
867 self.error_title = Some(title.into());
868 self.error_message = Some(message.into());
869 self
870 }
871
872 pub fn with_prompt(mut self, title: impl Into<String>, message: impl Into<String>) -> Self {
874 self.prompt_title = Some(title.into());
875 self.prompt_message = Some(message.into());
876 self.show_input_message = true;
877 self
878 }
879}
880
881#[derive(Debug, Clone)]
896pub struct DefinedNameBuilder {
897 pub name: String,
899 pub reference: String,
901 pub local_sheet_id: Option<u32>,
903 pub comment: Option<String>,
905 pub hidden: bool,
907}
908
909impl DefinedNameBuilder {
910 pub fn new(name: impl Into<String>, reference: impl Into<String>) -> Self {
912 Self {
913 name: name.into(),
914 reference: reference.into(),
915 local_sheet_id: None,
916 comment: None,
917 hidden: false,
918 }
919 }
920
921 pub fn with_sheet_scope(
923 name: impl Into<String>,
924 reference: impl Into<String>,
925 sheet_index: u32,
926 ) -> Self {
927 Self {
928 name: name.into(),
929 reference: reference.into(),
930 local_sheet_id: Some(sheet_index),
931 comment: None,
932 hidden: false,
933 }
934 }
935
936 pub fn with_comment(mut self, comment: impl Into<String>) -> Self {
938 self.comment = Some(comment.into());
939 self
940 }
941
942 pub fn hidden(mut self) -> Self {
944 self.hidden = true;
945 self
946 }
947
948 pub fn print_area(sheet_index: u32, reference: impl Into<String>) -> Self {
957 Self {
958 name: "_xlnm.Print_Area".to_string(),
959 reference: reference.into(),
960 local_sheet_id: Some(sheet_index),
961 comment: None,
962 hidden: false,
963 }
964 }
965
966 pub fn print_titles(sheet_index: u32, reference: impl Into<String>) -> Self {
976 Self {
977 name: "_xlnm.Print_Titles".to_string(),
978 reference: reference.into(),
979 local_sheet_id: Some(sheet_index),
980 comment: None,
981 hidden: false,
982 }
983 }
984}
985
986#[derive(Debug, Clone, Default)]
991pub struct CommentRun {
992 pub text: String,
994 pub bold: bool,
996 pub italic: bool,
998 pub color: Option<String>,
1000 pub font_size: Option<f64>,
1002}
1003
1004impl CommentRun {
1005 pub fn set_bold(&mut self, bold: bool) -> &mut Self {
1007 self.bold = bold;
1008 self
1009 }
1010
1011 pub fn set_italic(&mut self, italic: bool) -> &mut Self {
1013 self.italic = italic;
1014 self
1015 }
1016
1017 pub fn set_color(&mut self, rgb: &str) -> &mut Self {
1019 self.color = Some(rgb.to_string());
1020 self
1021 }
1022
1023 pub fn set_font_size(&mut self, pt: f64) -> &mut Self {
1025 self.font_size = Some(pt);
1026 self
1027 }
1028}
1029
1030#[derive(Debug, Clone)]
1049pub struct CommentBuilder {
1050 pub reference: String,
1052 pub text: String,
1054 pub author: Option<String>,
1056 pub runs: Vec<CommentRun>,
1058}
1059
1060impl CommentBuilder {
1061 pub fn new(reference: impl Into<String>, text: impl Into<String>) -> Self {
1063 Self {
1064 reference: reference.into(),
1065 text: text.into(),
1066 author: None,
1067 runs: Vec::new(),
1068 }
1069 }
1070
1071 pub fn new_rich(reference: impl Into<String>) -> Self {
1075 Self {
1076 reference: reference.into(),
1077 text: String::new(),
1078 author: None,
1079 runs: Vec::new(),
1080 }
1081 }
1082
1083 pub fn with_author(
1085 reference: impl Into<String>,
1086 text: impl Into<String>,
1087 author: impl Into<String>,
1088 ) -> Self {
1089 Self {
1090 reference: reference.into(),
1091 text: text.into(),
1092 author: Some(author.into()),
1093 runs: Vec::new(),
1094 }
1095 }
1096
1097 pub fn author(mut self, author: impl Into<String>) -> Self {
1099 self.author = Some(author.into());
1100 self
1101 }
1102
1103 pub fn add_run(&mut self, text: &str) -> &mut CommentRun {
1115 self.runs.push(CommentRun {
1116 text: text.to_string(),
1117 ..Default::default()
1118 });
1119 self.runs.last_mut().unwrap()
1120 }
1121}
1122
1123#[cfg(feature = "sml-protection")]
1143#[derive(Debug, Clone, Default)]
1144pub struct SheetProtectionOptions {
1145 pub password: Option<String>,
1150 pub sheet: bool,
1153 pub select_locked_cells: bool,
1155 pub select_unlocked_cells: bool,
1157 pub format_cells: bool,
1159 pub format_columns: bool,
1161 pub format_rows: bool,
1163 pub insert_columns: bool,
1165 pub insert_rows: bool,
1167 pub delete_columns: bool,
1169 pub delete_rows: bool,
1171 pub sort: bool,
1173 pub auto_filter: bool,
1175 pub pivot_tables: bool,
1177}
1178
1179#[cfg(feature = "sml-protection")]
1185fn ooxml_xor_hash(password: &str) -> Vec<u8> {
1186 if password.is_empty() {
1187 return vec![0x00, 0x00];
1188 }
1189
1190 let chars: Vec<u8> = password.chars().map(|c| c as u8).collect();
1199 let mut hash: u16 = 0;
1200
1201 for &ch in chars.iter().rev() {
1202 hash = ((hash << 1) | (hash >> 14)) & 0x7FFF;
1204 hash ^= ch as u16;
1205 }
1206
1207 hash = ((hash << 1) | (hash >> 14)) & 0x7FFF;
1209 hash ^= chars.len() as u16;
1210 hash ^= 0xCE4B;
1211
1212 vec![(hash >> 8) as u8, (hash & 0xFF) as u8]
1213}
1214
1215#[derive(Debug, Clone)]
1217enum HyperlinkDest {
1218 External(String),
1220 Internal(String),
1222}
1223
1224#[derive(Debug, Clone)]
1226struct HyperlinkEntry {
1227 reference: String,
1229 dest: HyperlinkDest,
1230 tooltip: Option<String>,
1231 display: Option<String>,
1232}
1233
1234#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1236pub enum PageOrientation {
1237 #[default]
1239 Portrait,
1240 Landscape,
1242}
1243
1244#[derive(Debug, Clone, Default)]
1248pub struct PageSetupOptions {
1249 pub orientation: Option<PageOrientation>,
1251 pub paper_size: Option<u32>,
1253 pub scale: Option<u32>,
1255 pub fit_to_width: Option<u32>,
1257 pub fit_to_height: Option<u32>,
1259}
1260
1261impl PageSetupOptions {
1262 pub fn new() -> Self {
1264 Self::default()
1265 }
1266
1267 pub fn with_orientation(mut self, orientation: PageOrientation) -> Self {
1269 self.orientation = Some(orientation);
1270 self
1271 }
1272
1273 pub fn with_paper_size(mut self, size: u32) -> Self {
1275 self.paper_size = Some(size);
1276 self
1277 }
1278
1279 pub fn with_scale(mut self, scale: u32) -> Self {
1281 self.scale = Some(scale);
1282 self
1283 }
1284
1285 pub fn with_fit_to(mut self, width: u32, height: u32) -> Self {
1287 self.fit_to_width = Some(width);
1288 self.fit_to_height = Some(height);
1289 self
1290 }
1291}
1292
1293#[cfg(feature = "sml-charts")]
1301#[derive(Debug)]
1302struct ChartEntry {
1303 chart_xml: Vec<u8>,
1305 x: u32,
1307 y: u32,
1309 width: u32,
1311 height: u32,
1313}
1314
1315#[cfg(feature = "sml-pivot")]
1336#[derive(Debug, Clone)]
1337pub struct PivotTableOptions {
1338 pub name: String,
1340 pub source_ref: String,
1342 pub dest_ref: String,
1344 pub row_fields: Vec<String>,
1346 pub col_fields: Vec<String>,
1348 pub data_fields: Vec<String>,
1350}
1351
1352#[cfg(feature = "sml-pivot")]
1354#[derive(Debug)]
1355struct PivotEntry {
1356 opts: PivotTableOptions,
1357}
1358
1359#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1364pub enum IgnoredErrorType {
1365 NumberStoredAsText,
1367 Formula,
1369 TwoDigitTextYear,
1371 EvalError,
1373 FormulaRange,
1375 UnlockedFormula,
1377 EmptyCellReference,
1379 ListDataValidation,
1381 CalculatedColumn,
1383}
1384
1385#[derive(Debug)]
1393pub struct SheetBuilder {
1394 name: String,
1395 cells: HashMap<(u32, u32), BuilderCell>,
1397 row_heights: HashMap<u32, f64>,
1399 row_outline_levels: HashMap<u32, u8>,
1401 row_collapsed: HashMap<u32, bool>,
1403 col_outline_levels: HashMap<u32, u8>,
1405 col_collapsed: HashMap<u32, bool>,
1407 comments: Vec<CommentBuilder>,
1409 hyperlinks: Vec<HyperlinkEntry>,
1411 #[cfg(feature = "sml-charts")]
1413 charts: Vec<ChartEntry>,
1414 #[cfg(feature = "sml-pivot")]
1416 pivot_tables: Vec<PivotEntry>,
1417 show_gridlines: Option<bool>,
1419 show_row_col_headers: Option<bool>,
1421 worksheet: types::Worksheet,
1423}
1424
1425fn init_worksheet() -> types::Worksheet {
1427 types::Worksheet {
1428 #[cfg(feature = "sml-styling")]
1429 sheet_properties: None,
1430 dimension: None,
1431 sheet_views: None,
1432 #[cfg(feature = "sml-styling")]
1433 sheet_format: None,
1434 #[cfg(feature = "sml-styling")]
1435 cols: Vec::new(),
1436 sheet_data: Box::new(types::SheetData {
1437 row: Vec::new(),
1438 #[cfg(feature = "extra-children")]
1439 extra_children: Vec::new(),
1440 }),
1441 #[cfg(feature = "sml-formulas")]
1442 sheet_calc_pr: None,
1443 #[cfg(feature = "sml-protection")]
1444 sheet_protection: None,
1445 #[cfg(feature = "sml-protection")]
1446 protected_ranges: None,
1447 #[cfg(feature = "sml-formulas-advanced")]
1448 scenarios: None,
1449 #[cfg(feature = "sml-filtering")]
1450 auto_filter: None,
1451 #[cfg(feature = "sml-filtering")]
1452 sort_state: None,
1453 #[cfg(feature = "sml-formulas-advanced")]
1454 data_consolidate: None,
1455 #[cfg(feature = "sml-structure")]
1456 custom_sheet_views: None,
1457 merged_cells: None,
1458 #[cfg(feature = "sml-i18n")]
1459 phonetic_pr: None,
1460 #[cfg(feature = "sml-styling")]
1461 conditional_formatting: Vec::new(),
1462 #[cfg(feature = "sml-validation")]
1463 data_validations: None,
1464 #[cfg(feature = "sml-hyperlinks")]
1465 hyperlinks: None,
1466 #[cfg(feature = "sml-layout")]
1467 print_options: None,
1468 #[cfg(feature = "sml-layout")]
1469 page_margins: None,
1470 #[cfg(feature = "sml-layout")]
1471 page_setup: None,
1472 #[cfg(feature = "sml-layout")]
1473 header_footer: None,
1474 #[cfg(feature = "sml-layout")]
1475 row_breaks: None,
1476 #[cfg(feature = "sml-layout")]
1477 col_breaks: None,
1478 #[cfg(feature = "sml-metadata")]
1479 custom_properties: None,
1480 #[cfg(feature = "sml-formulas-advanced")]
1481 cell_watches: None,
1482 #[cfg(feature = "sml-validation")]
1483 ignored_errors: None,
1484 #[cfg(feature = "sml-metadata")]
1485 smart_tags: None,
1486 #[cfg(feature = "sml-drawings")]
1487 drawing: None,
1488 #[cfg(feature = "sml-comments")]
1489 legacy_drawing: None,
1490 #[cfg(feature = "sml-layout")]
1491 legacy_drawing_h_f: None,
1492 #[cfg(feature = "sml-drawings")]
1493 drawing_h_f: None,
1494 #[cfg(feature = "sml-drawings")]
1495 picture: None,
1496 #[cfg(feature = "sml-external")]
1497 ole_objects: None,
1498 #[cfg(feature = "sml-external")]
1499 controls: None,
1500 #[cfg(feature = "sml-external")]
1501 web_publish_items: None,
1502 #[cfg(feature = "sml-tables")]
1503 table_parts: None,
1504 #[cfg(feature = "sml-extensions")]
1505 extension_list: None,
1506 #[cfg(feature = "extra-children")]
1507 extra_children: Vec::new(),
1508 }
1509}
1510
1511impl SheetBuilder {
1512 fn new(name: impl Into<String>) -> Self {
1513 Self {
1514 name: name.into(),
1515 cells: HashMap::new(),
1516 row_heights: HashMap::new(),
1517 row_outline_levels: HashMap::new(),
1518 row_collapsed: HashMap::new(),
1519 col_outline_levels: HashMap::new(),
1520 col_collapsed: HashMap::new(),
1521 comments: Vec::new(),
1522 hyperlinks: Vec::new(),
1523 #[cfg(feature = "sml-charts")]
1524 charts: Vec::new(),
1525 #[cfg(feature = "sml-pivot")]
1526 pivot_tables: Vec::new(),
1527 show_gridlines: None,
1528 show_row_col_headers: None,
1529 worksheet: init_worksheet(),
1530 }
1531 }
1532
1533 pub fn set_cell(&mut self, reference: &str, value: impl Into<WriteCellValue>) {
1535 if let Some((row, col)) = parse_cell_reference(reference) {
1536 self.cells.insert(
1537 (row, col),
1538 BuilderCell {
1539 value: value.into(),
1540 style: None,
1541 },
1542 );
1543 }
1544 }
1545
1546 pub fn set_cell_styled(
1548 &mut self,
1549 reference: &str,
1550 value: impl Into<WriteCellValue>,
1551 style: CellStyle,
1552 ) {
1553 if let Some((row, col)) = parse_cell_reference(reference) {
1554 self.cells.insert(
1555 (row, col),
1556 BuilderCell {
1557 value: value.into(),
1558 style: Some(style),
1559 },
1560 );
1561 }
1562 }
1563
1564 pub fn set_cell_at(&mut self, row: u32, col: u32, value: impl Into<WriteCellValue>) {
1566 self.cells.insert(
1567 (row, col),
1568 BuilderCell {
1569 value: value.into(),
1570 style: None,
1571 },
1572 );
1573 }
1574
1575 pub fn set_cell_at_styled(
1577 &mut self,
1578 row: u32,
1579 col: u32,
1580 value: impl Into<WriteCellValue>,
1581 style: CellStyle,
1582 ) {
1583 self.cells.insert(
1584 (row, col),
1585 BuilderCell {
1586 value: value.into(),
1587 style: Some(style),
1588 },
1589 );
1590 }
1591
1592 pub fn set_cell_style(&mut self, reference: &str, style: CellStyle) {
1594 if let Some((row, col)) = parse_cell_reference(reference)
1595 && let Some(cell) = self.cells.get_mut(&(row, col))
1596 {
1597 cell.style = Some(style);
1598 }
1599 }
1600
1601 pub fn set_formula(&mut self, reference: &str, formula: impl Into<String>) {
1603 if let Some((row, col)) = parse_cell_reference(reference) {
1604 self.cells.insert(
1605 (row, col),
1606 BuilderCell {
1607 value: WriteCellValue::Formula(formula.into()),
1608 style: None,
1609 },
1610 );
1611 }
1612 }
1613
1614 pub fn set_formula_styled(
1616 &mut self,
1617 reference: &str,
1618 formula: impl Into<String>,
1619 style: CellStyle,
1620 ) {
1621 if let Some((row, col)) = parse_cell_reference(reference) {
1622 self.cells.insert(
1623 (row, col),
1624 BuilderCell {
1625 value: WriteCellValue::Formula(formula.into()),
1626 style: Some(style),
1627 },
1628 );
1629 }
1630 }
1631
1632 pub fn merge_cells(&mut self, range: &str) {
1636 let mc = self.worksheet.merged_cells.get_or_insert_with(Box::default);
1637 mc.merge_cell.push(types::MergedCell {
1638 reference: range.to_string(),
1639 #[cfg(feature = "extra-attrs")]
1640 extra_attrs: Default::default(),
1641 });
1642 mc.count = Some(mc.merge_cell.len() as u32);
1643 }
1644
1645 pub fn set_column_width(&mut self, col: &str, width: f64) {
1649 if let Some(col_num) = column_letter_to_number(col) {
1650 self.push_column(col_num, col_num, width);
1651 }
1652 }
1653
1654 pub fn set_column_width_range(&mut self, start_col: &str, end_col: &str, width: f64) {
1658 if let (Some(min), Some(max)) = (
1659 column_letter_to_number(start_col),
1660 column_letter_to_number(end_col),
1661 ) {
1662 self.push_column(min, max, width);
1663 }
1664 }
1665
1666 fn push_column(&mut self, min: u32, max: u32, width: f64) {
1668 #[cfg(feature = "sml-styling")]
1669 {
1670 let col = types::Column {
1671 #[cfg(feature = "sml-styling")]
1672 start_column: min,
1673 #[cfg(feature = "sml-styling")]
1674 end_column: max,
1675 #[cfg(feature = "sml-styling")]
1676 width: Some(width),
1677 #[cfg(feature = "sml-styling")]
1678 style: None,
1679 #[cfg(feature = "sml-structure")]
1680 hidden: None,
1681 #[cfg(feature = "sml-styling")]
1682 best_fit: None,
1683 #[cfg(feature = "sml-styling")]
1684 custom_width: Some(true),
1685 #[cfg(feature = "sml-i18n")]
1686 phonetic: None,
1687 #[cfg(feature = "sml-structure")]
1688 outline_level: None,
1689 #[cfg(feature = "sml-structure")]
1690 collapsed: None,
1691 #[cfg(feature = "extra-attrs")]
1692 extra_attrs: Default::default(),
1693 };
1694 if let Some(cols_list) = self.worksheet.cols.first_mut() {
1695 cols_list.col.push(col);
1696 } else {
1697 self.worksheet.cols.push(types::Columns {
1698 col: vec![col],
1699 #[cfg(feature = "extra-children")]
1700 extra_children: Vec::new(),
1701 });
1702 }
1703 }
1704 #[cfg(not(feature = "sml-styling"))]
1705 {
1706 let _ = (min, max, width);
1707 }
1708 }
1709
1710 pub fn set_row_height(&mut self, row: u32, height: f64) {
1712 self.row_heights.insert(row, height);
1713 }
1714
1715 pub fn set_freeze_pane(&mut self, rows: u32, cols: u32) {
1732 self.apply_freeze_pane(rows, cols);
1733 }
1734
1735 pub fn freeze_rows(&mut self, n: u32) {
1739 let (_, c) = self.current_freeze_pane();
1740 self.apply_freeze_pane(n, c);
1741 }
1742
1743 pub fn freeze_cols(&mut self, n: u32) {
1747 let (r, _) = self.current_freeze_pane();
1748 self.apply_freeze_pane(r, n);
1749 }
1750
1751 fn current_freeze_pane(&self) -> (u32, u32) {
1753 #[cfg(feature = "sml-structure")]
1754 {
1755 let pane = self
1756 .worksheet
1757 .sheet_views
1758 .as_deref()
1759 .and_then(|v| v.sheet_view.first())
1760 .and_then(|sv| sv.pane.as_deref());
1761 if let Some(p) = pane {
1762 return (
1763 p.y_split.map(|y| y as u32).unwrap_or(0),
1764 p.x_split.map(|x| x as u32).unwrap_or(0),
1765 );
1766 }
1767 }
1768 (0, 0)
1769 }
1770
1771 fn apply_freeze_pane(&mut self, frozen_rows: u32, frozen_cols: u32) {
1773 #[cfg(feature = "sml-structure")]
1774 {
1775 if frozen_rows == 0 && frozen_cols == 0 {
1776 self.worksheet.sheet_views = None;
1777 return;
1778 }
1779 let active_pane = match (frozen_rows > 0, frozen_cols > 0) {
1781 (true, true) => types::PaneType::BottomRight,
1782 (true, false) => types::PaneType::BottomLeft,
1783 (false, true) => types::PaneType::TopRight,
1784 (false, false) => types::PaneType::TopLeft,
1785 };
1786 let top_left_col = if frozen_cols > 0 {
1788 column_to_letter(frozen_cols + 1)
1789 } else {
1790 "A".to_string()
1791 };
1792 let top_left_row = frozen_rows + 1;
1793 let top_left_cell = format!("{}{}", top_left_col, top_left_row);
1794
1795 let pane = types::Pane {
1796 x_split: (frozen_cols > 0).then_some(frozen_cols as f64),
1797 y_split: (frozen_rows > 0).then_some(frozen_rows as f64),
1798 top_left_cell: Some(top_left_cell),
1799 active_pane: Some(active_pane),
1800 state: Some(types::PaneState::Frozen),
1801 #[cfg(feature = "extra-attrs")]
1802 extra_attrs: Default::default(),
1803 };
1804 let sheet_view = types::SheetView {
1805 #[cfg(feature = "sml-protection")]
1806 window_protection: None,
1807 #[cfg(feature = "sml-formulas")]
1808 show_formulas: None,
1809 #[cfg(feature = "sml-styling")]
1810 show_grid_lines: None,
1811 #[cfg(feature = "sml-styling")]
1812 show_row_col_headers: None,
1813 #[cfg(feature = "sml-styling")]
1814 show_zeros: None,
1815 #[cfg(feature = "sml-i18n")]
1816 right_to_left: None,
1817 tab_selected: None,
1818 #[cfg(feature = "sml-layout")]
1819 show_ruler: None,
1820 #[cfg(feature = "sml-structure")]
1821 show_outline_symbols: None,
1822 #[cfg(feature = "sml-styling")]
1823 default_grid_color: None,
1824 #[cfg(feature = "sml-layout")]
1825 show_white_space: None,
1826 view: None,
1827 top_left_cell: None,
1828 #[cfg(feature = "sml-styling")]
1829 color_id: None,
1830 zoom_scale: None,
1831 zoom_scale_normal: None,
1832 #[cfg(feature = "sml-layout")]
1833 zoom_scale_sheet_layout_view: None,
1834 #[cfg(feature = "sml-layout")]
1835 zoom_scale_page_layout_view: None,
1836 workbook_view_id: 0,
1837 #[cfg(feature = "sml-structure")]
1838 pane: Some(Box::new(pane)),
1839 selection: vec![types::Selection {
1840 pane: Some(active_pane),
1841 active_cell: None,
1842 active_cell_id: None,
1843 square_reference: None,
1844 #[cfg(feature = "extra-attrs")]
1845 extra_attrs: Default::default(),
1846 }],
1847 #[cfg(feature = "sml-pivot")]
1848 pivot_selection: Vec::new(),
1849 #[cfg(feature = "sml-extensions")]
1850 extension_list: None,
1851 #[cfg(feature = "extra-attrs")]
1852 extra_attrs: Default::default(),
1853 #[cfg(feature = "extra-children")]
1854 extra_children: Vec::new(),
1855 };
1856 self.worksheet.sheet_views = Some(Box::new(types::SheetViews {
1857 sheet_view: vec![sheet_view],
1858 extension_list: None,
1859 #[cfg(feature = "extra-children")]
1860 extra_children: Vec::new(),
1861 }));
1862 }
1863 #[cfg(not(feature = "sml-structure"))]
1864 {
1865 let _ = (frozen_rows, frozen_cols);
1866 }
1867 }
1868
1869 pub fn add_conditional_format(&mut self, cf: ConditionalFormat) {
1871 #[cfg(feature = "sml-styling")]
1872 self.worksheet
1873 .conditional_formatting
1874 .push(build_one_conditional_format(&cf));
1875 #[cfg(not(feature = "sml-styling"))]
1876 let _ = cf;
1877 }
1878
1879 pub fn add_data_validation(&mut self, dv: DataValidationBuilder) {
1881 #[cfg(feature = "sml-validation")]
1882 {
1883 let validation = build_one_data_validation(&dv);
1884 let dvs = self.worksheet.data_validations.get_or_insert_with(|| {
1885 Box::new(types::DataValidations {
1886 disable_prompts: None,
1887 x_window: None,
1888 y_window: None,
1889 count: None,
1890 data_validation: Vec::new(),
1891 #[cfg(feature = "extra-attrs")]
1892 extra_attrs: Default::default(),
1893 #[cfg(feature = "extra-children")]
1894 extra_children: Vec::new(),
1895 })
1896 });
1897 dvs.data_validation.push(validation);
1898 dvs.count = Some(dvs.data_validation.len() as u32);
1899 }
1900 #[cfg(not(feature = "sml-validation"))]
1901 let _ = dv;
1902 }
1903
1904 pub fn add_comment(&mut self, reference: impl Into<String>, text: impl Into<String>) {
1912 self.comments.push(CommentBuilder::new(reference, text));
1913 }
1914
1915 pub fn add_comment_with_author(
1923 &mut self,
1924 reference: impl Into<String>,
1925 text: impl Into<String>,
1926 author: impl Into<String>,
1927 ) {
1928 self.comments
1929 .push(CommentBuilder::with_author(reference, text, author));
1930 }
1931
1932 pub fn add_comment_builder(&mut self, comment: CommentBuilder) {
1934 self.comments.push(comment);
1935 }
1936
1937 pub fn set_auto_filter(&mut self, range: &str) {
1950 #[cfg(feature = "sml-filtering")]
1951 {
1952 self.worksheet.auto_filter = Some(Box::new(types::AutoFilter {
1953 reference: Some(range.to_string()),
1954 filter_column: Vec::new(),
1955 sort_state: None,
1956 #[cfg(feature = "sml-extensions")]
1957 extension_list: None,
1958 #[cfg(feature = "extra-attrs")]
1959 extra_attrs: Default::default(),
1960 #[cfg(feature = "extra-children")]
1961 extra_children: Vec::new(),
1962 }));
1963 }
1964 #[cfg(not(feature = "sml-filtering"))]
1965 let _ = range;
1966 }
1967
1968 pub fn add_hyperlink(&mut self, reference: &str, url: &str) {
1980 self.hyperlinks.push(HyperlinkEntry {
1981 reference: reference.to_string(),
1982 dest: HyperlinkDest::External(url.to_string()),
1983 tooltip: None,
1984 display: None,
1985 });
1986 }
1987
1988 pub fn set_last_hyperlink_tooltip(&mut self, tooltip: &str) {
1993 if let Some(h) = self.hyperlinks.last_mut() {
1994 h.tooltip = Some(tooltip.to_string());
1995 }
1996 }
1997
1998 pub fn add_internal_hyperlink(&mut self, reference: &str, location: &str) {
2010 self.hyperlinks.push(HyperlinkEntry {
2011 reference: reference.to_string(),
2012 dest: HyperlinkDest::Internal(location.to_string()),
2013 tooltip: None,
2014 display: None,
2015 });
2016 }
2017
2018 pub fn set_page_setup(&mut self, opts: PageSetupOptions) {
2032 #[cfg(feature = "sml-layout")]
2033 {
2034 let orientation = opts.orientation.map(|o| match o {
2035 PageOrientation::Portrait => types::STOrientation::Portrait,
2036 PageOrientation::Landscape => types::STOrientation::Landscape,
2037 });
2038 self.worksheet.page_setup = Some(Box::new(types::PageSetup {
2039 #[cfg(feature = "sml-layout")]
2040 paper_size: opts.paper_size,
2041 #[cfg(feature = "sml-layout")]
2042 paper_height: None,
2043 #[cfg(feature = "sml-layout")]
2044 paper_width: None,
2045 #[cfg(feature = "sml-layout")]
2046 scale: opts.scale,
2047 #[cfg(feature = "sml-layout")]
2048 first_page_number: None,
2049 #[cfg(feature = "sml-layout")]
2050 fit_to_width: opts.fit_to_width,
2051 #[cfg(feature = "sml-layout")]
2052 fit_to_height: opts.fit_to_height,
2053 #[cfg(feature = "sml-layout")]
2054 page_order: None,
2055 #[cfg(feature = "sml-layout")]
2056 orientation,
2057 #[cfg(feature = "sml-layout")]
2058 use_printer_defaults: None,
2059 #[cfg(feature = "sml-layout")]
2060 black_and_white: None,
2061 #[cfg(feature = "sml-layout")]
2062 draft: None,
2063 #[cfg(feature = "sml-layout")]
2064 cell_comments: None,
2065 #[cfg(feature = "sml-layout")]
2066 use_first_page_number: None,
2067 #[cfg(feature = "sml-layout")]
2068 errors: None,
2069 #[cfg(feature = "sml-layout")]
2070 horizontal_dpi: None,
2071 #[cfg(feature = "sml-layout")]
2072 vertical_dpi: None,
2073 #[cfg(feature = "sml-layout")]
2074 copies: None,
2075 id: None,
2076 #[cfg(feature = "extra-attrs")]
2077 extra_attrs: Default::default(),
2078 }));
2079 }
2080 #[cfg(not(feature = "sml-layout"))]
2081 let _ = opts;
2082 }
2083
2084 pub fn set_page_margins(
2094 &mut self,
2095 left: f64,
2096 right: f64,
2097 top: f64,
2098 bottom: f64,
2099 header: f64,
2100 footer: f64,
2101 ) {
2102 #[cfg(feature = "sml-layout")]
2103 {
2104 self.worksheet.page_margins = Some(Box::new(types::PageMargins {
2105 #[cfg(feature = "sml-layout")]
2106 left,
2107 #[cfg(feature = "sml-layout")]
2108 right,
2109 #[cfg(feature = "sml-layout")]
2110 top,
2111 #[cfg(feature = "sml-layout")]
2112 bottom,
2113 #[cfg(feature = "sml-layout")]
2114 header,
2115 #[cfg(feature = "sml-layout")]
2116 footer,
2117 #[cfg(feature = "extra-attrs")]
2118 extra_attrs: Default::default(),
2119 }));
2120 }
2121 #[cfg(not(feature = "sml-layout"))]
2122 let _ = (left, right, top, bottom, header, footer);
2123 }
2124
2125 pub fn set_row_outline_level(&mut self, row: u32, level: u8) {
2133 self.row_outline_levels.insert(row, level);
2134 }
2135
2136 pub fn set_row_collapsed(&mut self, row: u32, collapsed: bool) {
2138 self.row_collapsed.insert(row, collapsed);
2139 }
2140
2141 pub fn set_column_outline_level(&mut self, col: &str, level: u8) {
2145 if let Some(col_num) = column_letter_to_number(col) {
2146 self.col_outline_levels.insert(col_num, level);
2147 }
2148 }
2149
2150 pub fn set_column_collapsed(&mut self, col: &str, collapsed: bool) {
2154 if let Some(col_num) = column_letter_to_number(col) {
2155 self.col_collapsed.insert(col_num, collapsed);
2156 }
2157 }
2158
2159 pub fn add_ignored_error(&mut self, sqref: &str, error_type: IgnoredErrorType) {
2175 #[cfg(feature = "sml-validation")]
2176 {
2177 let mut entry = types::IgnoredError {
2178 square_reference: sqref.to_string(),
2179 eval_error: None,
2180 two_digit_text_year: None,
2181 number_stored_as_text: None,
2182 formula: None,
2183 formula_range: None,
2184 unlocked_formula: None,
2185 empty_cell_reference: None,
2186 list_data_validation: None,
2187 calculated_column: None,
2188 #[cfg(feature = "extra-attrs")]
2189 extra_attrs: Default::default(),
2190 };
2191 match error_type {
2192 IgnoredErrorType::NumberStoredAsText => {
2193 entry.number_stored_as_text = Some(true);
2194 }
2195 IgnoredErrorType::Formula => {
2196 entry.formula = Some(true);
2197 }
2198 IgnoredErrorType::TwoDigitTextYear => {
2199 entry.two_digit_text_year = Some(true);
2200 }
2201 IgnoredErrorType::EvalError => {
2202 entry.eval_error = Some(true);
2203 }
2204 IgnoredErrorType::FormulaRange => {
2205 entry.formula_range = Some(true);
2206 }
2207 IgnoredErrorType::UnlockedFormula => {
2208 entry.unlocked_formula = Some(true);
2209 }
2210 IgnoredErrorType::EmptyCellReference => {
2211 entry.empty_cell_reference = Some(true);
2212 }
2213 IgnoredErrorType::ListDataValidation => {
2214 entry.list_data_validation = Some(true);
2215 }
2216 IgnoredErrorType::CalculatedColumn => {
2217 entry.calculated_column = Some(true);
2218 }
2219 }
2220 let ie = self.worksheet.ignored_errors.get_or_insert_with(|| {
2221 Box::new(types::IgnoredErrors {
2222 ignored_error: Vec::new(),
2223 extension_list: None,
2224 #[cfg(feature = "extra-children")]
2225 extra_children: Vec::new(),
2226 })
2227 });
2228 ie.ignored_error.push(entry);
2229 }
2230 #[cfg(not(feature = "sml-validation"))]
2231 let _ = (sqref, error_type);
2232 }
2233
2234 #[cfg(feature = "sml-protection")]
2253 pub fn set_sheet_protection(&mut self, opts: SheetProtectionOptions) {
2254 {
2255 let password = opts
2256 .password
2257 .as_deref()
2258 .filter(|p| !p.is_empty())
2259 .map(ooxml_xor_hash);
2260
2261 self.worksheet.sheet_protection = Some(Box::new(types::SheetProtection {
2262 password,
2263 algorithm_name: None,
2264 hash_value: None,
2265 salt_value: None,
2266 spin_count: None,
2267 sheet: if opts.sheet { Some(true) } else { None },
2268 objects: None,
2269 scenarios: None,
2270 format_cells: if opts.format_cells { Some(true) } else { None },
2271 format_columns: if opts.format_columns {
2272 Some(true)
2273 } else {
2274 None
2275 },
2276 format_rows: if opts.format_rows { Some(true) } else { None },
2277 insert_columns: if opts.insert_columns {
2278 Some(true)
2279 } else {
2280 None
2281 },
2282 insert_rows: if opts.insert_rows { Some(true) } else { None },
2283 insert_hyperlinks: None,
2284 delete_columns: if opts.delete_columns {
2285 Some(true)
2286 } else {
2287 None
2288 },
2289 delete_rows: if opts.delete_rows { Some(true) } else { None },
2290 select_locked_cells: if opts.select_locked_cells {
2291 Some(true)
2292 } else {
2293 None
2294 },
2295 sort: if opts.sort { Some(true) } else { None },
2296 auto_filter: if opts.auto_filter { Some(true) } else { None },
2297 pivot_tables: if opts.pivot_tables { Some(true) } else { None },
2298 select_unlocked_cells: if opts.select_unlocked_cells {
2299 Some(true)
2300 } else {
2301 None
2302 },
2303 #[cfg(feature = "extra-attrs")]
2304 extra_attrs: Default::default(),
2305 }));
2306 }
2307 }
2308
2309 pub fn set_tab_color(&mut self, rgb: &str) {
2324 #[cfg(feature = "sml-styling")]
2325 {
2326 let color = Box::new(types::Color {
2327 auto: None,
2328 indexed: None,
2329 rgb: Some(hex_color_to_bytes(rgb)),
2330 theme: None,
2331 tint: None,
2332 #[cfg(feature = "extra-attrs")]
2333 extra_attrs: Default::default(),
2334 });
2335 let props = self
2336 .worksheet
2337 .sheet_properties
2338 .get_or_insert_with(|| Box::new(types::SheetProperties::default()));
2339 props.tab_color = Some(color);
2340 }
2341 #[cfg(not(feature = "sml-styling"))]
2342 let _ = rgb;
2343 }
2344
2345 pub fn set_show_gridlines(&mut self, show: bool) {
2354 self.show_gridlines = Some(show);
2355 }
2356
2357 pub fn set_show_row_col_headers(&mut self, show: bool) {
2363 self.show_row_col_headers = Some(show);
2364 }
2365
2366 pub fn embed_chart(
2383 &mut self,
2384 chart_xml: &[u8],
2385 x: u32,
2386 y: u32,
2387 width: u32,
2388 height: u32,
2389 ) -> &mut Self {
2390 #[cfg(feature = "sml-charts")]
2391 self.charts.push(ChartEntry {
2392 chart_xml: chart_xml.to_vec(),
2393 x,
2394 y,
2395 width,
2396 height,
2397 });
2398 #[cfg(not(feature = "sml-charts"))]
2399 let _ = (chart_xml, x, y, width, height);
2400 self
2401 }
2402
2403 #[cfg(feature = "sml-pivot")]
2419 pub fn add_pivot_table(&mut self, opts: PivotTableOptions) -> &mut Self {
2420 self.pivot_tables.push(PivotEntry { opts });
2421 self
2422 }
2423
2424 pub fn name(&self) -> &str {
2426 &self.name
2427 }
2428}
2429
2430#[derive(Debug, Clone)]
2448struct NamedCellStyle {
2449 name: String,
2450 format_id: u32,
2451}
2452
2453#[derive(Debug)]
2475pub struct WorkbookBuilder {
2476 sheets: Vec<SheetBuilder>,
2477 shared_strings: Vec<String>,
2478 string_index: HashMap<String, usize>,
2479 defined_names: Vec<DefinedNameBuilder>,
2480 fonts: Vec<FontStyle>,
2482 font_index: HashMap<FontStyleKey, usize>,
2483 fills: Vec<FillStyle>,
2484 fill_index: HashMap<FillStyleKey, usize>,
2485 borders: Vec<BorderStyle>,
2486 border_index: HashMap<BorderStyleKey, usize>,
2487 number_formats: Vec<String>,
2488 number_format_index: HashMap<String, u32>,
2489 cell_formats: Vec<CellFormatRecord>,
2490 cell_format_index: HashMap<CellFormatKey, usize>,
2491 extra_cell_styles: Vec<NamedCellStyle>,
2493 #[cfg(feature = "sml-protection")]
2495 workbook_protection: Option<types::WorkbookProtection>,
2496}
2497
2498#[derive(Debug, Clone, Hash, PartialEq, Eq)]
2500struct FontStyleKey {
2501 name: Option<String>,
2502 size_bits: Option<u64>, bold: bool,
2504 italic: bool,
2505 underline: Option<String>,
2506 strikethrough: bool,
2507 color: Option<String>,
2508}
2509
2510impl From<&FontStyle> for FontStyleKey {
2511 fn from(f: &FontStyle) -> Self {
2512 Self {
2513 name: f.name.clone(),
2514 size_bits: f.size.map(|s| s.to_bits()),
2515 bold: f.bold,
2516 italic: f.italic,
2517 underline: f.underline.map(|u| u.to_xml_value().to_string()),
2518 strikethrough: f.strikethrough,
2519 color: f.color.clone(),
2520 }
2521 }
2522}
2523
2524#[derive(Debug, Clone, Hash, PartialEq, Eq)]
2525struct FillStyleKey {
2526 pattern: String,
2527 fg_color: Option<String>,
2528 bg_color: Option<String>,
2529}
2530
2531impl From<&FillStyle> for FillStyleKey {
2532 fn from(f: &FillStyle) -> Self {
2533 Self {
2534 pattern: f.pattern.to_xml_value().to_string(),
2535 fg_color: f.fg_color.clone(),
2536 bg_color: f.bg_color.clone(),
2537 }
2538 }
2539}
2540
2541#[derive(Debug, Clone, Hash, PartialEq, Eq)]
2542struct BorderStyleKey {
2543 left: Option<(String, Option<String>)>,
2544 right: Option<(String, Option<String>)>,
2545 top: Option<(String, Option<String>)>,
2546 bottom: Option<(String, Option<String>)>,
2547}
2548
2549impl From<&BorderStyle> for BorderStyleKey {
2550 fn from(b: &BorderStyle) -> Self {
2551 Self {
2552 left: b
2553 .left
2554 .as_ref()
2555 .map(|s| (s.style.to_xml_value().to_string(), s.color.clone())),
2556 right: b
2557 .right
2558 .as_ref()
2559 .map(|s| (s.style.to_xml_value().to_string(), s.color.clone())),
2560 top: b
2561 .top
2562 .as_ref()
2563 .map(|s| (s.style.to_xml_value().to_string(), s.color.clone())),
2564 bottom: b
2565 .bottom
2566 .as_ref()
2567 .map(|s| (s.style.to_xml_value().to_string(), s.color.clone())),
2568 }
2569 }
2570}
2571
2572#[derive(Debug, Clone, Hash, PartialEq, Eq)]
2573struct CellFormatKey {
2574 font_id: usize,
2575 fill_id: usize,
2576 border_id: usize,
2577 num_fmt_id: u32,
2578 horizontal: Option<String>,
2579 vertical: Option<String>,
2580 wrap_text: bool,
2581}
2582
2583#[derive(Debug, Clone)]
2584#[allow(dead_code)] struct CellFormatRecord {
2586 font_id: usize,
2587 fill_id: usize,
2588 border_id: usize,
2589 num_fmt_id: u32,
2590 horizontal: Option<HorizontalAlignment>,
2591 vertical: Option<VerticalAlignment>,
2592 wrap_text: bool,
2593}
2594
2595impl Default for WorkbookBuilder {
2596 fn default() -> Self {
2597 Self::new()
2598 }
2599}
2600
2601impl WorkbookBuilder {
2602 pub fn new() -> Self {
2604 Self {
2605 sheets: Vec::new(),
2606 shared_strings: Vec::new(),
2607 string_index: HashMap::new(),
2608 defined_names: Vec::new(),
2609 fonts: Vec::new(),
2610 font_index: HashMap::new(),
2611 fills: Vec::new(),
2612 fill_index: HashMap::new(),
2613 borders: Vec::new(),
2614 border_index: HashMap::new(),
2615 number_formats: Vec::new(),
2616 number_format_index: HashMap::new(),
2617 cell_formats: Vec::new(),
2618 cell_format_index: HashMap::new(),
2619 extra_cell_styles: Vec::new(),
2620 #[cfg(feature = "sml-protection")]
2621 workbook_protection: None,
2622 }
2623 }
2624
2625 pub fn add_sheet(&mut self, name: impl Into<String>) -> &mut SheetBuilder {
2627 self.sheets.push(SheetBuilder::new(name));
2628 self.sheets.last_mut().unwrap()
2629 }
2630
2631 pub fn sheet_mut(&mut self, index: usize) -> Option<&mut SheetBuilder> {
2633 self.sheets.get_mut(index)
2634 }
2635
2636 pub fn sheet_count(&self) -> usize {
2638 self.sheets.len()
2639 }
2640
2641 pub fn add_defined_name(&mut self, name: impl Into<String>, reference: impl Into<String>) {
2651 self.defined_names
2652 .push(DefinedNameBuilder::new(name, reference));
2653 }
2654
2655 pub fn add_defined_name_with_scope(
2668 &mut self,
2669 name: impl Into<String>,
2670 reference: impl Into<String>,
2671 sheet_index: u32,
2672 ) {
2673 self.defined_names
2674 .push(DefinedNameBuilder::with_sheet_scope(
2675 name,
2676 reference,
2677 sheet_index,
2678 ));
2679 }
2680
2681 pub fn add_defined_name_builder(&mut self, builder: DefinedNameBuilder) {
2695 self.defined_names.push(builder);
2696 }
2697
2698 pub fn set_print_area(&mut self, sheet_index: u32, reference: impl Into<String>) {
2708 self.defined_names
2709 .push(DefinedNameBuilder::print_area(sheet_index, reference));
2710 }
2711
2712 pub fn set_print_titles(&mut self, sheet_index: u32, reference: impl Into<String>) {
2723 self.defined_names
2724 .push(DefinedNameBuilder::print_titles(sheet_index, reference));
2725 }
2726
2727 pub fn set_workbook_protection(
2746 &mut self,
2747 lock_structure: bool,
2748 lock_windows: bool,
2749 password: Option<&str>,
2750 ) {
2751 #[cfg(feature = "sml-protection")]
2752 {
2753 let workbook_password = password.filter(|p| !p.is_empty()).map(ooxml_xor_hash);
2754
2755 self.workbook_protection = Some(types::WorkbookProtection {
2756 workbook_password,
2757 workbook_password_character_set: None,
2758 revisions_password: None,
2759 revisions_password_character_set: None,
2760 lock_structure: if lock_structure { Some(true) } else { None },
2761 lock_windows: if lock_windows { Some(true) } else { None },
2762 lock_revision: None,
2763 revisions_algorithm_name: None,
2764 revisions_hash_value: None,
2765 revisions_salt_value: None,
2766 revisions_spin_count: None,
2767 workbook_algorithm_name: None,
2768 workbook_hash_value: None,
2769 workbook_salt_value: None,
2770 workbook_spin_count: None,
2771 #[cfg(feature = "extra-attrs")]
2772 extra_attrs: Default::default(),
2773 });
2774 }
2775 #[cfg(not(feature = "sml-protection"))]
2776 let _ = (lock_structure, lock_windows, password);
2777 }
2778
2779 pub fn add_cell_style(&mut self, name: &str, format_id: u32) -> u32 {
2798 let idx = self.extra_cell_styles.len() as u32;
2799 self.extra_cell_styles.push(NamedCellStyle {
2800 name: name.to_string(),
2801 format_id,
2802 });
2803 idx + 1
2805 }
2806
2807 pub fn save<P: AsRef<Path>>(self, path: P) -> Result<()> {
2809 let file = File::create(path)?;
2810 let writer = BufWriter::new(file);
2811 self.write(writer)
2812 }
2813
2814 pub fn write<W: Write + Seek>(mut self, writer: W) -> Result<()> {
2816 self.collect_shared_strings();
2818 self.collect_styles();
2819
2820 let has_styles = !self.cell_formats.is_empty() || !self.extra_cell_styles.is_empty();
2821
2822 let mut pkg = PackageWriter::new(writer);
2823
2824 pkg.add_default_content_type("rels", CT_RELATIONSHIPS);
2826 pkg.add_default_content_type("xml", CT_XML);
2827
2828 let root_rels = format!(
2830 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2831<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
2832 <Relationship Id="rId1" Type="{}" Target="xl/workbook.xml"/>
2833</Relationships>"#,
2834 REL_OFFICE_DOCUMENT
2835 );
2836
2837 let mut wb_rels = String::new();
2839 wb_rels.push_str(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#);
2840 wb_rels.push('\n');
2841 wb_rels.push_str(
2842 r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">"#,
2843 );
2844 wb_rels.push('\n');
2845
2846 let mut next_rel_id = 1;
2847 for (i, _sheet) in self.sheets.iter().enumerate() {
2848 wb_rels.push_str(&format!(
2849 r#" <Relationship Id="rId{}" Type="{}" Target="worksheets/sheet{}.xml"/>"#,
2850 next_rel_id,
2851 REL_WORKSHEET,
2852 i + 1
2853 ));
2854 wb_rels.push('\n');
2855 next_rel_id += 1;
2856 }
2857
2858 if has_styles {
2860 wb_rels.push_str(&format!(
2861 r#" <Relationship Id="rId{}" Type="{}" Target="styles.xml"/>"#,
2862 next_rel_id, REL_STYLES
2863 ));
2864 wb_rels.push('\n');
2865 next_rel_id += 1;
2866 }
2867
2868 if !self.shared_strings.is_empty() {
2870 wb_rels.push_str(&format!(
2871 r#" <Relationship Id="rId{}" Type="{}" Target="sharedStrings.xml"/>"#,
2872 next_rel_id, REL_SHARED_STRINGS
2873 ));
2874 wb_rels.push('\n');
2875 }
2876
2877 wb_rels.push_str("</Relationships>");
2878
2879 let workbook = self.build_workbook();
2881 let workbook_xml = serialize_workbook(&workbook)?;
2882
2883 pkg.add_part("_rels/.rels", CT_RELATIONSHIPS, root_rels.as_bytes())?;
2885 pkg.add_part(
2886 "xl/_rels/workbook.xml.rels",
2887 CT_RELATIONSHIPS,
2888 wb_rels.as_bytes(),
2889 )?;
2890 pkg.add_part("xl/workbook.xml", CT_WORKBOOK, &workbook_xml)?;
2891
2892 if has_styles {
2894 let styles_xml = self.serialize_styles()?;
2895 pkg.add_part("xl/styles.xml", CT_STYLES, &styles_xml)?;
2896 }
2897
2898 #[cfg(feature = "sml-charts")]
2902 let mut next_drawing_num = 1usize;
2903 #[cfg(feature = "sml-charts")]
2904 let mut next_chart_num = 1usize;
2905 #[cfg(feature = "sml-pivot")]
2906 let mut next_pivot_num = 1usize;
2907
2908 for (i, sheet) in self.sheets.iter().enumerate() {
2910 let sheet_num = i + 1;
2911 let has_comments = !sheet.comments.is_empty();
2912
2913 let ext_hyperlinks: Vec<&str> = sheet
2915 .hyperlinks
2916 .iter()
2917 .filter_map(|h| {
2918 if let HyperlinkDest::External(url) = &h.dest {
2919 Some(url.as_str())
2920 } else {
2921 None
2922 }
2923 })
2924 .collect();
2925
2926 #[cfg(feature = "sml-charts")]
2927 let has_charts = !sheet.charts.is_empty();
2928 #[cfg(not(feature = "sml-charts"))]
2929 let has_charts = false;
2930
2931 #[cfg(feature = "sml-pivot")]
2932 let has_pivots = !sheet.pivot_tables.is_empty();
2933 #[cfg(not(feature = "sml-pivot"))]
2934 let has_pivots = false;
2935
2936 let needs_rels = has_comments || !ext_hyperlinks.is_empty() || has_charts || has_pivots;
2937
2938 let mut sheet_rel_id = 1usize;
2940 let comments_rel_id = if has_comments { sheet_rel_id } else { 0 };
2941 if has_comments {
2942 sheet_rel_id += 1;
2943 }
2944
2945 #[cfg(feature = "sml-charts")]
2947 let drawing_rel_id = if has_charts {
2948 let id = sheet_rel_id;
2949 sheet_rel_id += 1;
2950 id
2951 } else {
2952 0
2953 };
2954
2955 #[cfg(feature = "sml-pivot")]
2957 let pivot_rel_id_start = sheet_rel_id;
2958
2959 #[cfg(feature = "sml-hyperlinks")]
2961 let hyperlink_rel_id_start = {
2962 let mut start = 1usize;
2963 if has_comments {
2964 start += 1;
2965 }
2966 if has_charts {
2967 start += 1;
2968 }
2969 #[cfg(feature = "sml-pivot")]
2970 {
2971 start += sheet.pivot_tables.len();
2972 }
2973 start
2974 };
2975 #[cfg(not(feature = "sml-hyperlinks"))]
2976 let hyperlink_rel_id_start = 1usize;
2977
2978 if needs_rels {
2979 let mut sheet_rels = String::new();
2980 sheet_rels.push_str(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#);
2981 sheet_rels.push('\n');
2982 sheet_rels.push_str(
2983 r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">"#,
2984 );
2985 sheet_rels.push('\n');
2986
2987 if has_comments {
2988 sheet_rels.push_str(&format!(
2989 r#" <Relationship Id="rId{}" Type="{}" Target="../comments{}.xml"/>"#,
2990 comments_rel_id, REL_COMMENTS, sheet_num
2991 ));
2992 sheet_rels.push('\n');
2993 }
2994
2995 #[cfg(feature = "sml-charts")]
2996 if has_charts {
2997 sheet_rels.push_str(&format!(
2998 r#" <Relationship Id="rId{}" Type="{}" Target="../drawings/drawing{}.xml"/>"#,
2999 drawing_rel_id, REL_DRAWING, next_drawing_num
3000 ));
3001 sheet_rels.push('\n');
3002 }
3003
3004 #[cfg(feature = "sml-pivot")]
3006 {
3007 let mut prel = pivot_rel_id_start;
3008 for (pi, _pt) in sheet.pivot_tables.iter().enumerate() {
3009 let global_pivot = next_pivot_num + pi;
3010 sheet_rels.push_str(&format!(
3011 r#" <Relationship Id="rId{}" Type="{}" Target="../pivotTables/pivotTable{}.xml"/>"#,
3012 prel, REL_PIVOT_TABLE, global_pivot
3013 ));
3014 sheet_rels.push('\n');
3015 prel += 1;
3016 }
3017 }
3018
3019 for (hi, url) in ext_hyperlinks.iter().enumerate() {
3020 sheet_rels.push_str(&format!(
3021 r#" <Relationship Id="rId{}" Type="{}" Target="{}" TargetMode="External"/>"#,
3022 hyperlink_rel_id_start + hi,
3023 REL_HYPERLINK,
3024 escape_xml(url)
3025 ));
3026 sheet_rels.push('\n');
3027 }
3028
3029 sheet_rels.push_str("</Relationships>");
3030 let rels_part = format!("xl/worksheets/_rels/sheet{}.xml.rels", sheet_num);
3031 pkg.add_part(&rels_part, CT_RELATIONSHIPS, sheet_rels.as_bytes())?;
3032 }
3033
3034 let sheet_xml = self.serialize_sheet_with_drawing(
3036 sheet,
3037 hyperlink_rel_id_start,
3038 #[cfg(feature = "sml-charts")]
3039 if has_charts {
3040 Some(drawing_rel_id)
3041 } else {
3042 None
3043 },
3044 #[cfg(not(feature = "sml-charts"))]
3045 None,
3046 )?;
3047 let part_name = format!("xl/worksheets/sheet{}.xml", sheet_num);
3048 pkg.add_part(&part_name, CT_WORKSHEET, &sheet_xml)?;
3049
3050 if has_comments {
3051 let comments_xml = self.serialize_comments(sheet)?;
3052 let comments_part = format!("xl/comments{}.xml", sheet_num);
3053 pkg.add_part(&comments_part, CT_COMMENTS, &comments_xml)?;
3054 }
3055
3056 #[cfg(feature = "sml-charts")]
3058 if has_charts {
3059 let drawing_num = next_drawing_num;
3060 next_drawing_num += 1;
3061
3062 let drawing_xml = build_drawing_xml(&sheet.charts, next_chart_num);
3064 let drawing_part = format!("xl/drawings/drawing{}.xml", drawing_num);
3065 pkg.add_part(&drawing_part, CT_DRAWING, drawing_xml.as_bytes())?;
3066
3067 let mut drawing_rels = String::new();
3069 drawing_rels.push_str(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#);
3070 drawing_rels.push('\n');
3071 drawing_rels.push_str(
3072 r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">"#,
3073 );
3074 drawing_rels.push('\n');
3075 for (ci, _chart) in sheet.charts.iter().enumerate() {
3076 let chart_num = next_chart_num + ci;
3077 drawing_rels.push_str(&format!(
3078 r#" <Relationship Id="rId{}" Type="{}" Target="../charts/chart{}.xml"/>"#,
3079 ci + 1,
3080 REL_CHART,
3081 chart_num,
3082 ));
3083 drawing_rels.push('\n');
3084 }
3085 drawing_rels.push_str("</Relationships>");
3086 let drawing_rels_part =
3087 format!("xl/drawings/_rels/drawing{}.xml.rels", drawing_num);
3088 pkg.add_part(
3089 &drawing_rels_part,
3090 CT_RELATIONSHIPS,
3091 drawing_rels.as_bytes(),
3092 )?;
3093
3094 for chart in &sheet.charts {
3096 let chart_part = format!("xl/charts/chart{}.xml", next_chart_num);
3097 pkg.add_part(&chart_part, CT_CHART, &chart.chart_xml)?;
3098 next_chart_num += 1;
3099 }
3100 }
3101
3102 #[cfg(feature = "sml-pivot")]
3104 {
3105 for (pi, pt) in sheet.pivot_tables.iter().enumerate() {
3106 let pn = next_pivot_num + pi;
3107
3108 let cache_def_xml = build_pivot_cache_definition_xml(&pt.opts, pn);
3110 let cache_def_part = format!("xl/pivotCache/pivotCacheDefinition{}.xml", pn);
3111 pkg.add_part(
3112 &cache_def_part,
3113 CT_PIVOT_CACHE_DEF,
3114 cache_def_xml.as_bytes(),
3115 )?;
3116
3117 let cache_def_rels = format!(
3119 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
3120<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
3121 <Relationship Id="rId1" Type="{}" Target="pivotCacheRecords{}.xml"/>
3122</Relationships>"#,
3123 REL_PIVOT_CACHE_REC, pn
3124 );
3125 let cache_def_rels_part =
3126 format!("xl/pivotCache/_rels/pivotCacheDefinition{}.xml.rels", pn);
3127 pkg.add_part(
3128 &cache_def_rels_part,
3129 CT_RELATIONSHIPS,
3130 cache_def_rels.as_bytes(),
3131 )?;
3132
3133 let cache_rec_xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
3135<pivotCacheRecords xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="0"/>"#;
3136 let cache_rec_part = format!("xl/pivotCache/pivotCacheRecords{}.xml", pn);
3137 pkg.add_part(
3138 &cache_rec_part,
3139 CT_PIVOT_CACHE_REC,
3140 cache_rec_xml.as_bytes(),
3141 )?;
3142
3143 let pivot_xml = build_pivot_table_xml(&pt.opts, pn, &sheet.name);
3145 let pivot_part = format!("xl/pivotTables/pivotTable{}.xml", pn);
3146 pkg.add_part(&pivot_part, CT_PIVOT_TABLE, pivot_xml.as_bytes())?;
3147
3148 let pivot_rels = format!(
3150 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
3151<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
3152 <Relationship Id="rId1" Type="{}" Target="../pivotCache/pivotCacheDefinition{}.xml"/>
3153</Relationships>"#,
3154 REL_PIVOT_CACHE_DEF, pn
3155 );
3156 let pivot_rels_part = format!("xl/pivotTables/_rels/pivotTable{}.xml.rels", pn);
3157 pkg.add_part(&pivot_rels_part, CT_RELATIONSHIPS, pivot_rels.as_bytes())?;
3158 }
3159 next_pivot_num += sheet.pivot_tables.len();
3161 }
3162 }
3163
3164 if !self.shared_strings.is_empty() {
3166 let ss_xml = self.serialize_shared_strings()?;
3167 pkg.add_part("xl/sharedStrings.xml", CT_SHARED_STRINGS, &ss_xml)?;
3168 }
3169
3170 pkg.finish()?;
3171 Ok(())
3172 }
3173
3174 fn collect_shared_strings(&mut self) {
3176 for sheet in &self.sheets {
3177 for cell in sheet.cells.values() {
3178 if let WriteCellValue::String(s) = &cell.value
3179 && !self.string_index.contains_key(s)
3180 {
3181 let idx = self.shared_strings.len();
3182 self.shared_strings.push(s.clone());
3183 self.string_index.insert(s.clone(), idx);
3184 }
3185 }
3186 }
3187 }
3188
3189 fn collect_styles(&mut self) {
3191 let default_font = FontStyle::new().with_name("Calibri").with_size(11.0);
3193 self.get_or_add_font(&default_font);
3194
3195 let none_fill = FillStyle::new();
3197 let gray_fill = FillStyle::new().with_pattern(FillPattern::Gray125);
3198 self.get_or_add_fill(&none_fill);
3199 self.get_or_add_fill(&gray_fill);
3200
3201 let default_border = BorderStyle::new();
3203 self.get_or_add_border(&default_border);
3204
3205 let styles: Vec<CellStyle> = self
3207 .sheets
3208 .iter()
3209 .flat_map(|sheet| sheet.cells.values())
3210 .filter_map(|cell| cell.style.clone())
3211 .collect();
3212
3213 for style in &styles {
3215 self.get_or_add_cell_format(style);
3216 }
3217 }
3218
3219 fn get_or_add_font(&mut self, font: &FontStyle) -> usize {
3221 let key = FontStyleKey::from(font);
3222 if let Some(&idx) = self.font_index.get(&key) {
3223 return idx;
3224 }
3225 let idx = self.fonts.len();
3226 self.fonts.push(font.clone());
3227 self.font_index.insert(key, idx);
3228 idx
3229 }
3230
3231 fn get_or_add_fill(&mut self, fill: &FillStyle) -> usize {
3233 let key = FillStyleKey::from(fill);
3234 if let Some(&idx) = self.fill_index.get(&key) {
3235 return idx;
3236 }
3237 let idx = self.fills.len();
3238 self.fills.push(fill.clone());
3239 self.fill_index.insert(key, idx);
3240 idx
3241 }
3242
3243 fn get_or_add_border(&mut self, border: &BorderStyle) -> usize {
3245 let key = BorderStyleKey::from(border);
3246 if let Some(&idx) = self.border_index.get(&key) {
3247 return idx;
3248 }
3249 let idx = self.borders.len();
3250 self.borders.push(border.clone());
3251 self.border_index.insert(key, idx);
3252 idx
3253 }
3254
3255 fn get_or_add_number_format(&mut self, format: &str) -> u32 {
3257 if let Some(&id) = self.number_format_index.get(format) {
3258 return id;
3259 }
3260 let id = 164 + self.number_formats.len() as u32;
3262 self.number_formats.push(format.to_string());
3263 self.number_format_index.insert(format.to_string(), id);
3264 id
3265 }
3266
3267 fn get_or_add_cell_format(&mut self, style: &CellStyle) -> usize {
3269 let font_id = style.font.as_ref().map_or(0, |f| self.get_or_add_font(f));
3270 let fill_id = style.fill.as_ref().map_or(0, |f| self.get_or_add_fill(f));
3271 let border_id = style
3272 .border
3273 .as_ref()
3274 .map_or(0, |b| self.get_or_add_border(b));
3275 let num_fmt_id = style
3276 .number_format
3277 .as_ref()
3278 .map_or(0, |f| self.get_or_add_number_format(f));
3279
3280 let key = CellFormatKey {
3281 font_id,
3282 fill_id,
3283 border_id,
3284 num_fmt_id,
3285 horizontal: style
3286 .horizontal_alignment
3287 .map(|a| a.to_xml_value().to_string()),
3288 vertical: style
3289 .vertical_alignment
3290 .map(|a| a.to_xml_value().to_string()),
3291 wrap_text: style.wrap_text,
3292 };
3293
3294 if let Some(&idx) = self.cell_format_index.get(&key) {
3295 return idx;
3296 }
3297
3298 let record = CellFormatRecord {
3299 font_id,
3300 fill_id,
3301 border_id,
3302 num_fmt_id,
3303 horizontal: style.horizontal_alignment,
3304 vertical: style.vertical_alignment,
3305 wrap_text: style.wrap_text,
3306 };
3307
3308 let idx = self.cell_formats.len();
3309 self.cell_formats.push(record);
3310 self.cell_format_index.insert(key, idx);
3311 idx
3312 }
3313
3314 fn get_cell_style_index(&self, style: &Option<CellStyle>) -> Option<usize> {
3316 style.as_ref().map(|s| {
3317 let font_id = s.font.as_ref().map_or(0, |f| {
3318 let key = FontStyleKey::from(f);
3319 *self.font_index.get(&key).unwrap_or(&0)
3320 });
3321 let fill_id = s.fill.as_ref().map_or(0, |f| {
3322 let key = FillStyleKey::from(f);
3323 *self.fill_index.get(&key).unwrap_or(&0)
3324 });
3325 let border_id = s.border.as_ref().map_or(0, |b| {
3326 let key = BorderStyleKey::from(b);
3327 *self.border_index.get(&key).unwrap_or(&0)
3328 });
3329 let num_fmt_id = s
3330 .number_format
3331 .as_ref()
3332 .map_or(0, |f| *self.number_format_index.get(f).unwrap_or(&0));
3333
3334 let key = CellFormatKey {
3335 font_id,
3336 fill_id,
3337 border_id,
3338 num_fmt_id,
3339 horizontal: s.horizontal_alignment.map(|a| a.to_xml_value().to_string()),
3340 vertical: s.vertical_alignment.map(|a| a.to_xml_value().to_string()),
3341 wrap_text: s.wrap_text,
3342 };
3343
3344 self.cell_format_index.get(&key).map_or(0, |&idx| idx + 1)
3346 })
3347 }
3348
3349 fn serialize_comments(&self, sheet: &SheetBuilder) -> Result<Vec<u8>> {
3353 let mut authors: Vec<String> = Vec::new();
3355 let mut author_index: HashMap<String, u32> = HashMap::new();
3356
3357 for comment in &sheet.comments {
3358 let author = comment.author.clone().unwrap_or_default();
3359 if !author_index.contains_key(&author) {
3360 author_index.insert(author.clone(), authors.len() as u32);
3361 authors.push(author);
3362 }
3363 }
3364
3365 let comment_list: Vec<types::Comment> = sheet
3367 .comments
3368 .iter()
3369 .map(|_c| {
3370 #[cfg(feature = "sml-comments")]
3371 let author_id = {
3372 let author = _c.author.clone().unwrap_or_default();
3373 *author_index.get(&author).unwrap_or(&0)
3374 };
3375
3376 #[cfg(feature = "sml-comments")]
3378 let rich_text = if _c.runs.is_empty() {
3379 types::RichString {
3381 cell_type: None,
3382 reference: vec![types::RichTextElement {
3383 r_pr: None,
3384 cell_type: _c.text.clone(),
3385 #[cfg(feature = "extra-children")]
3386 extra_children: Vec::new(),
3387 }],
3388 r_ph: Vec::new(),
3389 phonetic_pr: None,
3390 #[cfg(feature = "extra-children")]
3391 extra_children: Vec::new(),
3392 }
3393 } else {
3394 let runs: Vec<types::RichTextElement> = _c
3396 .runs
3397 .iter()
3398 .map(|run| {
3399 let has_props = run.bold
3400 || run.italic
3401 || run.color.is_some()
3402 || run.font_size.is_some();
3403 let r_pr = if has_props {
3404 Some(Box::new(types::RichTextRunProperties {
3405 r_font: None,
3406 charset: None,
3407 family: None,
3408 b: if run.bold {
3409 Some(Box::new(types::BooleanProperty {
3410 value: None,
3411 #[cfg(feature = "extra-attrs")]
3412 extra_attrs: Default::default(),
3413 }))
3414 } else {
3415 None
3416 },
3417 i: if run.italic {
3418 Some(Box::new(types::BooleanProperty {
3419 value: None,
3420 #[cfg(feature = "extra-attrs")]
3421 extra_attrs: Default::default(),
3422 }))
3423 } else {
3424 None
3425 },
3426 strike: None,
3427 outline: None,
3428 shadow: None,
3429 condense: None,
3430 extend: None,
3431 #[cfg(feature = "sml-styling")]
3432 color: run.color.as_ref().map(|c| {
3433 Box::new(types::Color {
3434 auto: None,
3435 indexed: None,
3436 rgb: Some(hex_color_to_bytes(c)),
3437 theme: None,
3438 tint: None,
3439 #[cfg(feature = "extra-attrs")]
3440 extra_attrs: Default::default(),
3441 })
3442 }),
3443 #[cfg(not(feature = "sml-styling"))]
3444 color: None,
3445 sz: run.font_size.map(|s| {
3446 Box::new(types::FontSize {
3447 value: s,
3448 #[cfg(feature = "extra-attrs")]
3449 extra_attrs: Default::default(),
3450 })
3451 }),
3452 u: None,
3453 vert_align: None,
3454 scheme: None,
3455 #[cfg(feature = "extra-children")]
3456 extra_children: Vec::new(),
3457 }))
3458 } else {
3459 None
3460 };
3461 types::RichTextElement {
3462 r_pr,
3463 cell_type: run.text.clone(),
3464 #[cfg(feature = "extra-children")]
3465 extra_children: Vec::new(),
3466 }
3467 })
3468 .collect();
3469 types::RichString {
3470 cell_type: None,
3471 reference: runs,
3472 r_ph: Vec::new(),
3473 phonetic_pr: None,
3474 #[cfg(feature = "extra-children")]
3475 extra_children: Vec::new(),
3476 }
3477 };
3478
3479 types::Comment {
3480 #[cfg(feature = "sml-comments")]
3481 reference: _c.reference.clone(),
3482 #[cfg(feature = "sml-comments")]
3483 author_id,
3484 #[cfg(feature = "sml-comments")]
3485 guid: None,
3486 #[cfg(feature = "sml-comments")]
3487 shape_id: None,
3488 #[cfg(feature = "sml-comments")]
3489 text: Box::new(rich_text),
3490 comment_pr: None,
3491 #[cfg(feature = "extra-attrs")]
3492 extra_attrs: Default::default(),
3493 #[cfg(feature = "extra-children")]
3494 extra_children: Vec::new(),
3495 }
3496 })
3497 .collect();
3498
3499 let comments = types::Comments {
3500 authors: Box::new(types::Authors {
3501 author: authors,
3502 #[cfg(feature = "extra-children")]
3503 extra_children: Vec::new(),
3504 }),
3505 comment_list: Box::new(types::CommentList {
3506 comment: comment_list,
3507 #[cfg(feature = "extra-children")]
3508 extra_children: Vec::new(),
3509 }),
3510 extension_list: None,
3511 #[cfg(feature = "extra-children")]
3512 extra_children: Vec::new(),
3513 };
3514
3515 serialize_with_namespaces(&comments, "comments")
3516 }
3517
3518 #[cfg(feature = "sml-styling")]
3520 fn serialize_styles(&self) -> Result<Vec<u8>> {
3521 let stylesheet = self.build_stylesheet();
3522 serialize_with_namespaces(&stylesheet, "styleSheet")
3523 }
3524
3525 #[cfg(not(feature = "sml-styling"))]
3527 fn serialize_styles(&self) -> Result<Vec<u8>> {
3528 let stylesheet = types::Stylesheet::default();
3530 serialize_with_namespaces(&stylesheet, "styleSheet")
3531 }
3532
3533 #[cfg(feature = "sml-styling")]
3535 fn build_stylesheet(&self) -> types::Stylesheet {
3536 let num_fmts: Option<Box<types::NumberFormats>> = if self.number_formats.is_empty() {
3538 None
3539 } else {
3540 Some(Box::new(types::NumberFormats {
3541 count: Some(self.number_formats.len() as u32),
3542 num_fmt: self
3543 .number_formats
3544 .iter()
3545 .enumerate()
3546 .map(|(i, fmt)| types::NumberFormat {
3547 number_format_id: (164 + i) as u32,
3548 format_code: fmt.clone(),
3549 #[cfg(feature = "extra-attrs")]
3550 extra_attrs: Default::default(),
3551 })
3552 .collect(),
3553 #[cfg(feature = "extra-attrs")]
3554 extra_attrs: Default::default(),
3555 #[cfg(feature = "extra-children")]
3556 extra_children: Vec::new(),
3557 }))
3558 };
3559
3560 let fonts = Box::new(types::Fonts {
3562 count: Some(self.fonts.len() as u32),
3563 font: self.fonts.iter().map(build_font).collect(),
3564 #[cfg(feature = "extra-attrs")]
3565 extra_attrs: Default::default(),
3566 #[cfg(feature = "extra-children")]
3567 extra_children: Vec::new(),
3568 });
3569
3570 let fills = Box::new(types::Fills {
3572 count: Some(self.fills.len() as u32),
3573 fill: self.fills.iter().map(build_fill).collect(),
3574 #[cfg(feature = "extra-attrs")]
3575 extra_attrs: Default::default(),
3576 #[cfg(feature = "extra-children")]
3577 extra_children: Vec::new(),
3578 });
3579
3580 let borders = Box::new(types::Borders {
3582 count: Some(self.borders.len() as u32),
3583 border: self.borders.iter().map(build_border).collect(),
3584 #[cfg(feature = "extra-attrs")]
3585 extra_attrs: Default::default(),
3586 #[cfg(feature = "extra-children")]
3587 extra_children: Vec::new(),
3588 });
3589
3590 let cell_style_xfs = Box::new(types::CellStyleFormats {
3592 count: Some(1),
3593 xf: vec![types::Format {
3594 #[cfg(feature = "sml-styling")]
3595 number_format_id: Some(0),
3596 #[cfg(feature = "sml-styling")]
3597 font_id: Some(0),
3598 #[cfg(feature = "sml-styling")]
3599 fill_id: Some(0),
3600 #[cfg(feature = "sml-styling")]
3601 border_id: Some(0),
3602 #[cfg(feature = "sml-styling")]
3603 format_id: None,
3604 #[cfg(feature = "sml-styling")]
3605 quote_prefix: None,
3606 #[cfg(feature = "sml-pivot")]
3607 pivot_button: None,
3608 #[cfg(feature = "sml-styling")]
3609 apply_number_format: None,
3610 #[cfg(feature = "sml-styling")]
3611 apply_font: None,
3612 #[cfg(feature = "sml-styling")]
3613 apply_fill: None,
3614 #[cfg(feature = "sml-styling")]
3615 apply_border: None,
3616 #[cfg(feature = "sml-styling")]
3617 apply_alignment: None,
3618 #[cfg(feature = "sml-styling")]
3619 apply_protection: None,
3620 #[cfg(feature = "sml-styling")]
3621 alignment: None,
3622 #[cfg(feature = "sml-protection")]
3623 protection: None,
3624 #[cfg(feature = "sml-extensions")]
3625 extension_list: None,
3626 #[cfg(feature = "extra-attrs")]
3627 extra_attrs: Default::default(),
3628 #[cfg(feature = "extra-children")]
3629 extra_children: Vec::new(),
3630 }],
3631 #[cfg(feature = "extra-attrs")]
3632 extra_attrs: Default::default(),
3633 #[cfg(feature = "extra-children")]
3634 extra_children: Vec::new(),
3635 });
3636
3637 let mut xf_list: Vec<types::Format> = vec![types::Format {
3639 #[cfg(feature = "sml-styling")]
3640 number_format_id: Some(0),
3641 #[cfg(feature = "sml-styling")]
3642 font_id: Some(0),
3643 #[cfg(feature = "sml-styling")]
3644 fill_id: Some(0),
3645 #[cfg(feature = "sml-styling")]
3646 border_id: Some(0),
3647 #[cfg(feature = "sml-styling")]
3648 format_id: Some(0),
3649 #[cfg(feature = "sml-styling")]
3650 quote_prefix: None,
3651 #[cfg(feature = "sml-pivot")]
3652 pivot_button: None,
3653 #[cfg(feature = "sml-styling")]
3654 apply_number_format: None,
3655 #[cfg(feature = "sml-styling")]
3656 apply_font: None,
3657 #[cfg(feature = "sml-styling")]
3658 apply_fill: None,
3659 #[cfg(feature = "sml-styling")]
3660 apply_border: None,
3661 #[cfg(feature = "sml-styling")]
3662 apply_alignment: None,
3663 #[cfg(feature = "sml-styling")]
3664 apply_protection: None,
3665 #[cfg(feature = "sml-styling")]
3666 alignment: None,
3667 #[cfg(feature = "sml-protection")]
3668 protection: None,
3669 #[cfg(feature = "sml-extensions")]
3670 extension_list: None,
3671 #[cfg(feature = "extra-attrs")]
3672 extra_attrs: Default::default(),
3673 #[cfg(feature = "extra-children")]
3674 extra_children: Vec::new(),
3675 }];
3676
3677 for xf in &self.cell_formats {
3678 xf_list.push(build_cell_format(xf));
3679 }
3680
3681 let cell_xfs = Box::new(types::CellFormats {
3682 count: Some(xf_list.len() as u32),
3683 xf: xf_list,
3684 #[cfg(feature = "extra-attrs")]
3685 extra_attrs: Default::default(),
3686 #[cfg(feature = "extra-children")]
3687 extra_children: Vec::new(),
3688 });
3689
3690 let mut cell_style_list = vec![types::CellStyle {
3692 name: Some("Normal".to_string()),
3693 format_id: 0,
3694 builtin_id: Some(0),
3695 i_level: None,
3696 hidden: None,
3697 custom_builtin: None,
3698 extension_list: None,
3699 #[cfg(feature = "extra-attrs")]
3700 extra_attrs: Default::default(),
3701 #[cfg(feature = "extra-children")]
3702 extra_children: Vec::new(),
3703 }];
3704 for cs in &self.extra_cell_styles {
3705 cell_style_list.push(types::CellStyle {
3706 name: Some(cs.name.clone()),
3707 format_id: cs.format_id,
3708 builtin_id: None,
3709 i_level: None,
3710 hidden: None,
3711 custom_builtin: Some(true),
3712 extension_list: None,
3713 #[cfg(feature = "extra-attrs")]
3714 extra_attrs: Default::default(),
3715 #[cfg(feature = "extra-children")]
3716 extra_children: Vec::new(),
3717 });
3718 }
3719 let count = cell_style_list.len() as u32;
3720 let cell_styles = Box::new(types::CellStyles {
3721 count: Some(count),
3722 cell_style: cell_style_list,
3723 #[cfg(feature = "extra-attrs")]
3724 extra_attrs: Default::default(),
3725 #[cfg(feature = "extra-children")]
3726 extra_children: Vec::new(),
3727 });
3728
3729 types::Stylesheet {
3730 #[cfg(feature = "sml-styling")]
3731 num_fmts,
3732 #[cfg(feature = "sml-styling")]
3733 fonts: Some(fonts),
3734 #[cfg(feature = "sml-styling")]
3735 fills: Some(fills),
3736 #[cfg(feature = "sml-styling")]
3737 borders: Some(borders),
3738 #[cfg(feature = "sml-styling")]
3739 cell_style_xfs: Some(cell_style_xfs),
3740 #[cfg(feature = "sml-styling")]
3741 cell_xfs: Some(cell_xfs),
3742 #[cfg(feature = "sml-styling")]
3743 cell_styles: Some(cell_styles),
3744 #[cfg(feature = "sml-styling")]
3745 dxfs: None,
3746 #[cfg(feature = "sml-styling")]
3747 table_styles: None,
3748 #[cfg(feature = "sml-styling")]
3749 colors: None,
3750 #[cfg(feature = "sml-extensions")]
3751 extension_list: None,
3752 #[cfg(feature = "extra-children")]
3753 extra_children: Vec::new(),
3754 }
3755 }
3756
3757 fn serialize_sheet_with_drawing(
3766 &self,
3767 sheet: &SheetBuilder,
3768 hyperlink_rel_id_start: usize,
3769 drawing_rel_id: Option<usize>,
3770 ) -> Result<Vec<u8>> {
3771 #[cfg(feature = "sml-styling")]
3773 let row_heights = &sheet.row_heights;
3774
3775 let mut rows_map: HashMap<u32, Vec<(u32, &BuilderCell)>> = HashMap::new();
3777 for ((row, col), cell) in &sheet.cells {
3778 rows_map.entry(*row).or_default().push((*col, cell));
3779 }
3780
3781 #[cfg(feature = "sml-structure")]
3783 for &row_num in sheet
3784 .row_outline_levels
3785 .keys()
3786 .chain(sheet.row_collapsed.keys())
3787 {
3788 rows_map.entry(row_num).or_default();
3789 }
3790
3791 let mut row_nums: Vec<_> = rows_map.keys().copied().collect();
3793 row_nums.sort();
3794
3795 let rows: Vec<types::Row> = row_nums
3796 .iter()
3797 .map(|&row_num| {
3798 let cells_data = rows_map.get(&row_num).unwrap();
3799 let mut sorted_cells: Vec<_> = cells_data.clone();
3800 sorted_cells.sort_by_key(|(col, _)| *col);
3801
3802 let cells: Vec<types::Cell> = sorted_cells
3803 .iter()
3804 .map(|(col, cell)| {
3805 let ref_str = column_to_letter(*col) + &row_num.to_string();
3806 self.build_cell(&ref_str, cell)
3807 })
3808 .collect();
3809
3810 types::Row {
3811 reference: Some(row_num),
3812 cell_spans: None,
3813 style_index: None,
3814 #[cfg(feature = "sml-styling")]
3815 custom_format: None,
3816 #[cfg(feature = "sml-styling")]
3817 height: row_heights.get(&row_num).copied(),
3818 #[cfg(feature = "sml-structure")]
3819 hidden: None,
3820 #[cfg(feature = "sml-styling")]
3821 custom_height: row_heights.get(&row_num).map(|_| true),
3822 #[cfg(feature = "sml-structure")]
3823 outline_level: sheet.row_outline_levels.get(&row_num).copied(),
3824 #[cfg(feature = "sml-structure")]
3825 collapsed: sheet.row_collapsed.get(&row_num).copied(),
3826 #[cfg(feature = "sml-styling")]
3827 thick_top: None,
3828 #[cfg(feature = "sml-styling")]
3829 thick_bot: None,
3830 #[cfg(feature = "sml-i18n")]
3831 placeholder: None,
3832 cells,
3833 #[cfg(feature = "sml-extensions")]
3834 extension_list: None,
3835 #[cfg(feature = "extra-attrs")]
3836 extra_attrs: Default::default(),
3837 #[cfg(feature = "extra-children")]
3838 extra_children: Vec::new(),
3839 }
3840 })
3841 .collect();
3842
3843 let mut worksheet = sheet.worksheet.clone();
3845 worksheet.sheet_data = Box::new(types::SheetData {
3846 row: rows,
3847 #[cfg(feature = "extra-children")]
3848 extra_children: Vec::new(),
3849 });
3850
3851 #[cfg(all(feature = "sml-styling", feature = "sml-structure"))]
3855 if !sheet.col_outline_levels.is_empty() || !sheet.col_collapsed.is_empty() {
3856 let mut col_nums: std::collections::HashSet<u32> = std::collections::HashSet::new();
3858 col_nums.extend(sheet.col_outline_levels.keys().copied());
3859 col_nums.extend(sheet.col_collapsed.keys().copied());
3860
3861 for col_num in col_nums {
3862 let level = sheet.col_outline_levels.get(&col_num).copied();
3863 let collapsed = sheet.col_collapsed.get(&col_num).copied();
3864
3865 let mut found = false;
3867 for cols_group in &mut worksheet.cols {
3868 for col_entry in &mut cols_group.col {
3869 if col_entry.start_column <= col_num && col_num <= col_entry.end_column {
3870 if level.is_some() {
3871 col_entry.outline_level = level;
3872 }
3873 if collapsed.is_some() {
3874 col_entry.collapsed = collapsed;
3875 }
3876 found = true;
3877 break;
3878 }
3879 }
3880 if found {
3881 break;
3882 }
3883 }
3884
3885 if !found {
3886 let col_entry = types::Column {
3889 #[cfg(feature = "sml-styling")]
3890 start_column: col_num,
3891 #[cfg(feature = "sml-styling")]
3892 end_column: col_num,
3893 #[cfg(feature = "sml-styling")]
3894 width: None,
3895 #[cfg(feature = "sml-styling")]
3896 style: None,
3897 #[cfg(feature = "sml-structure")]
3898 hidden: None,
3899 #[cfg(feature = "sml-styling")]
3900 best_fit: None,
3901 #[cfg(feature = "sml-styling")]
3902 custom_width: None,
3903 #[cfg(feature = "sml-i18n")]
3904 phonetic: None,
3905 #[cfg(feature = "sml-structure")]
3906 outline_level: level,
3907 #[cfg(feature = "sml-structure")]
3908 collapsed,
3909 #[cfg(feature = "extra-attrs")]
3910 extra_attrs: Default::default(),
3911 };
3912 if let Some(cols_group) = worksheet.cols.first_mut() {
3913 cols_group.col.push(col_entry);
3914 } else {
3915 worksheet.cols.push(types::Columns {
3916 col: vec![col_entry],
3917 #[cfg(feature = "extra-children")]
3918 extra_children: Vec::new(),
3919 });
3920 }
3921 }
3922 }
3923 }
3924
3925 #[cfg(feature = "sml-styling")]
3927 if sheet.show_gridlines.is_some() || sheet.show_row_col_headers.is_some() {
3928 if let Some(views) = worksheet.sheet_views.as_mut() {
3929 if let Some(sv) = views.sheet_view.first_mut() {
3931 if let Some(v) = sheet.show_gridlines {
3932 sv.show_grid_lines = Some(v);
3933 }
3934 if let Some(v) = sheet.show_row_col_headers {
3935 sv.show_row_col_headers = Some(v);
3936 }
3937 }
3938 } else {
3939 let sv = types::SheetView {
3941 #[cfg(feature = "sml-protection")]
3942 window_protection: None,
3943 #[cfg(feature = "sml-formulas")]
3944 show_formulas: None,
3945 #[cfg(feature = "sml-styling")]
3946 show_grid_lines: sheet.show_gridlines,
3947 #[cfg(feature = "sml-styling")]
3948 show_row_col_headers: sheet.show_row_col_headers,
3949 #[cfg(feature = "sml-styling")]
3950 show_zeros: None,
3951 #[cfg(feature = "sml-i18n")]
3952 right_to_left: None,
3953 tab_selected: None,
3954 #[cfg(feature = "sml-layout")]
3955 show_ruler: None,
3956 #[cfg(feature = "sml-structure")]
3957 show_outline_symbols: None,
3958 #[cfg(feature = "sml-styling")]
3959 default_grid_color: None,
3960 #[cfg(feature = "sml-layout")]
3961 show_white_space: None,
3962 view: None,
3963 top_left_cell: None,
3964 #[cfg(feature = "sml-styling")]
3965 color_id: None,
3966 zoom_scale: None,
3967 zoom_scale_normal: None,
3968 #[cfg(feature = "sml-layout")]
3969 zoom_scale_sheet_layout_view: None,
3970 #[cfg(feature = "sml-layout")]
3971 zoom_scale_page_layout_view: None,
3972 workbook_view_id: 0,
3973 #[cfg(feature = "sml-structure")]
3974 pane: None,
3975 selection: Vec::new(),
3976 #[cfg(feature = "sml-pivot")]
3977 pivot_selection: Vec::new(),
3978 #[cfg(feature = "sml-extensions")]
3979 extension_list: None,
3980 #[cfg(feature = "extra-attrs")]
3981 extra_attrs: Default::default(),
3982 #[cfg(feature = "extra-children")]
3983 extra_children: Vec::new(),
3984 };
3985 worksheet.sheet_views = Some(Box::new(types::SheetViews {
3986 sheet_view: vec![sv],
3987 extension_list: None,
3988 #[cfg(feature = "extra-children")]
3989 extra_children: Vec::new(),
3990 }));
3991 }
3992 }
3993
3994 #[cfg(feature = "sml-hyperlinks")]
3996 if !sheet.hyperlinks.is_empty() {
3997 let mut rel_id = hyperlink_rel_id_start;
3998 let hyperlink_list: Vec<types::Hyperlink> = sheet
3999 .hyperlinks
4000 .iter()
4001 .map(|h| {
4002 let (id, location) = match &h.dest {
4003 HyperlinkDest::External(_) => {
4004 let r = Some(format!("rId{}", rel_id));
4005 rel_id += 1;
4006 (r, None)
4007 }
4008 HyperlinkDest::Internal(loc) => (None, Some(loc.clone())),
4009 };
4010 types::Hyperlink {
4011 reference: h.reference.clone(),
4012 id,
4013 #[cfg(feature = "sml-hyperlinks")]
4014 location,
4015 #[cfg(feature = "sml-hyperlinks")]
4016 tooltip: h.tooltip.clone(),
4017 #[cfg(feature = "sml-hyperlinks")]
4018 display: h.display.clone(),
4019 #[cfg(feature = "extra-attrs")]
4020 extra_attrs: Default::default(),
4021 }
4022 })
4023 .collect();
4024 worksheet.hyperlinks = Some(Box::new(types::Hyperlinks {
4025 hyperlink: hyperlink_list,
4026 #[cfg(feature = "extra-children")]
4027 extra_children: Vec::new(),
4028 }));
4029 }
4030 #[cfg(not(feature = "sml-hyperlinks"))]
4031 let _ = hyperlink_rel_id_start;
4032
4033 #[cfg(feature = "sml-drawings")]
4036 if let Some(rel_id) = drawing_rel_id {
4037 worksheet.drawing = Some(Box::new(types::Drawing {
4038 id: format!("rId{}", rel_id),
4039 #[cfg(feature = "extra-attrs")]
4040 extra_attrs: Default::default(),
4041 }));
4042 }
4043 #[cfg(not(feature = "sml-drawings"))]
4044 let _ = drawing_rel_id;
4045
4046 serialize_with_namespaces(&worksheet, "worksheet")
4047 }
4048
4049 fn build_cell(&self, reference: &str, cell: &BuilderCell) -> types::Cell {
4051 let style_index = self
4052 .get_cell_style_index(&cell.style)
4053 .filter(|&s| s > 0)
4054 .map(|s| s as u32);
4055
4056 let (cell_type, value, formula) = match &cell.value {
4057 WriteCellValue::String(s) => {
4058 let idx = self.string_index.get(s).unwrap_or(&0);
4059 (
4060 Some(types::CellType::SharedString),
4061 Some(idx.to_string()),
4062 None,
4063 )
4064 }
4065 WriteCellValue::Number(n) => (None, Some(n.to_string()), None),
4066 WriteCellValue::Boolean(b) => {
4067 let val = if *b { "1" } else { "0" };
4068 (Some(types::CellType::Boolean), Some(val.to_string()), None)
4069 }
4070 WriteCellValue::Formula(f) => (
4071 None,
4072 None,
4073 Some(Box::new(types::CellFormula {
4074 text: Some(f.clone()),
4075 cell_type: None,
4076 #[cfg(feature = "sml-formulas-advanced")]
4077 aca: None,
4078 reference: None,
4079 #[cfg(feature = "sml-formulas-advanced")]
4080 dt2_d: None,
4081 #[cfg(feature = "sml-formulas-advanced")]
4082 dtr: None,
4083 #[cfg(feature = "sml-formulas-advanced")]
4084 del1: None,
4085 #[cfg(feature = "sml-formulas-advanced")]
4086 del2: None,
4087 #[cfg(feature = "sml-formulas-advanced")]
4088 r1: None,
4089 #[cfg(feature = "sml-formulas-advanced")]
4090 r2: None,
4091 #[cfg(feature = "sml-formulas-advanced")]
4092 ca: None,
4093 si: None,
4094 #[cfg(feature = "sml-formulas-advanced")]
4095 bx: None,
4096 #[cfg(feature = "extra-attrs")]
4097 extra_attrs: Default::default(),
4098 #[cfg(feature = "extra-children")]
4099 extra_children: Vec::new(),
4100 })),
4101 ),
4102 WriteCellValue::Empty => (None, None, None),
4103 };
4104
4105 types::Cell {
4106 reference: Some(reference.to_string()),
4107 style_index,
4108 cell_type,
4109 #[cfg(feature = "sml-metadata")]
4110 cm: None,
4111 #[cfg(feature = "sml-metadata")]
4112 vm: None,
4113 #[cfg(feature = "sml-i18n")]
4114 placeholder: None,
4115 formula,
4116 value,
4117 is: None,
4118 #[cfg(feature = "sml-extensions")]
4119 extension_list: None,
4120 #[cfg(feature = "extra-attrs")]
4121 extra_attrs: Default::default(),
4122 #[cfg(feature = "extra-children")]
4123 extra_children: Vec::new(),
4124 }
4125 }
4126
4127 fn build_workbook(&self) -> types::Workbook {
4129 let sheets: Vec<types::Sheet> = self
4131 .sheets
4132 .iter()
4133 .enumerate()
4134 .map(|(i, sheet)| types::Sheet {
4135 name: sheet.name.clone(),
4136 sheet_id: (i + 1) as u32,
4137 #[cfg(feature = "sml-structure")]
4138 state: None,
4139 id: format!("rId{}", i + 1),
4140 #[cfg(feature = "extra-attrs")]
4141 extra_attrs: Default::default(),
4142 })
4143 .collect();
4144
4145 let defined_names: Option<Box<types::DefinedNames>> = if self.defined_names.is_empty() {
4147 None
4148 } else {
4149 Some(Box::new(types::DefinedNames {
4150 defined_name: self
4151 .defined_names
4152 .iter()
4153 .map(|dn| types::DefinedName {
4154 text: Some(dn.reference.clone()),
4155 name: dn.name.clone(),
4156 comment: dn.comment.clone(),
4157 #[cfg(feature = "sml-formulas-advanced")]
4158 custom_menu: None,
4159 description: None,
4160 #[cfg(feature = "sml-formulas-advanced")]
4161 help: None,
4162 #[cfg(feature = "sml-formulas-advanced")]
4163 status_bar: None,
4164 local_sheet_id: dn.local_sheet_id,
4165 #[cfg(feature = "sml-structure")]
4166 hidden: if dn.hidden { Some(true) } else { None },
4167 #[cfg(feature = "sml-formulas-advanced")]
4168 function: None,
4169 #[cfg(feature = "sml-formulas-advanced")]
4170 vb_procedure: None,
4171 #[cfg(feature = "sml-formulas-advanced")]
4172 xlm: None,
4173 #[cfg(feature = "sml-formulas-advanced")]
4174 function_group_id: None,
4175 #[cfg(feature = "sml-formulas-advanced")]
4176 shortcut_key: None,
4177 #[cfg(feature = "sml-formulas-advanced")]
4178 publish_to_server: None,
4179 #[cfg(feature = "sml-formulas-advanced")]
4180 workbook_parameter: None,
4181 #[cfg(feature = "extra-attrs")]
4182 extra_attrs: Default::default(),
4183 #[cfg(feature = "extra-children")]
4184 extra_children: Vec::new(),
4185 })
4186 .collect(),
4187 #[cfg(feature = "extra-children")]
4188 extra_children: Vec::new(),
4189 }))
4190 };
4191
4192 types::Workbook {
4193 conformance: None,
4194 file_version: None,
4195 #[cfg(feature = "sml-protection")]
4196 file_sharing: None,
4197 workbook_pr: None,
4198 #[cfg(feature = "sml-protection")]
4199 workbook_protection: self.workbook_protection.clone().map(Box::new),
4200 book_views: None,
4201 sheets: Box::new(types::Sheets {
4202 sheet: sheets,
4203 #[cfg(feature = "extra-children")]
4204 extra_children: Vec::new(),
4205 }),
4206 #[cfg(feature = "sml-formulas-advanced")]
4207 function_groups: None,
4208 #[cfg(feature = "sml-external")]
4209 external_references: None,
4210 defined_names,
4211 #[cfg(feature = "sml-formulas")]
4212 calc_pr: None,
4213 #[cfg(feature = "sml-external")]
4214 ole_size: None,
4215 #[cfg(feature = "sml-structure")]
4216 custom_workbook_views: None,
4217 #[cfg(feature = "sml-pivot")]
4218 pivot_caches: None,
4219 #[cfg(feature = "sml-metadata")]
4220 smart_tag_pr: None,
4221 #[cfg(feature = "sml-metadata")]
4222 smart_tag_types: None,
4223 #[cfg(feature = "sml-external")]
4224 web_publishing: None,
4225 file_recovery_pr: Vec::new(),
4226 #[cfg(feature = "sml-external")]
4227 web_publish_objects: None,
4228 #[cfg(feature = "sml-extensions")]
4229 extension_list: None,
4230 #[cfg(feature = "extra-attrs")]
4231 extra_attrs: Default::default(),
4232 #[cfg(feature = "extra-children")]
4233 extra_children: Vec::new(),
4234 }
4235 }
4236
4237 fn serialize_shared_strings(&self) -> Result<Vec<u8>> {
4239 let count = self.shared_strings.len() as u32;
4240 let sst = types::SharedStrings {
4241 count: Some(count),
4242 unique_count: Some(count),
4243 si: self
4244 .shared_strings
4245 .iter()
4246 .map(|s| types::RichString {
4247 cell_type: Some(s.clone()),
4248 reference: Vec::new(),
4249 r_ph: Vec::new(),
4250 phonetic_pr: None,
4251 #[cfg(feature = "extra-children")]
4252 extra_children: Vec::new(),
4253 })
4254 .collect(),
4255 extension_list: None,
4256 #[cfg(feature = "extra-attrs")]
4257 extra_attrs: Default::default(),
4258 #[cfg(feature = "extra-children")]
4259 extra_children: Vec::new(),
4260 };
4261 serialize_with_namespaces(&sst, "sst")
4262 }
4263}
4264
4265#[cfg(feature = "sml-charts")]
4276fn build_drawing_xml(charts: &[ChartEntry], _first_chart_num: usize) -> String {
4277 const NS_XDR: &str = "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing";
4278 const NS_A: &str = "http://schemas.openxmlformats.org/drawingml/2006/main";
4279 const NS_C: &str = "http://schemas.openxmlformats.org/drawingml/2006/chart";
4280 const NS_R: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
4281
4282 let mut xml = format!(
4283 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
4284<xdr:wsDr xmlns:xdr="{NS_XDR}" xmlns:a="{NS_A}" xmlns:c="{NS_C}" xmlns:r="{NS_R}">"#
4285 );
4286
4287 for (idx, chart) in charts.iter().enumerate() {
4288 let rel_id = idx + 1; let to_col = chart.x + chart.width;
4290 let to_row = chart.y + chart.height;
4291 xml.push_str(&format!(
4292 r#"
4293<xdr:twoCellAnchor>
4294 <xdr:from><xdr:col>{}</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>{}</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:from>
4295 <xdr:to><xdr:col>{}</xdr:col><xdr:colOff>0</xdr:colOff><xdr:row>{}</xdr:row><xdr:rowOff>0</xdr:rowOff></xdr:to>
4296 <xdr:graphicFrame macro="">
4297 <xdr:nvGraphicFramePr>
4298 <xdr:cNvPr id="{}" name="Chart {}"/>
4299 <xdr:cNvGraphicFramePr/>
4300 </xdr:nvGraphicFramePr>
4301 <xdr:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/></xdr:xfrm>
4302 <a:graphic>
4303 <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/chart">
4304 <c:chart r:id="rId{}"/>
4305 </a:graphicData>
4306 </a:graphic>
4307 </xdr:graphicFrame>
4308 <xdr:clientData/>
4309</xdr:twoCellAnchor>"#,
4310 chart.x, chart.y, to_col, to_row,
4311 idx + 2, idx + 1,
4313 rel_id,
4314 ));
4315 }
4316
4317 xml.push_str("\n</xdr:wsDr>");
4318 xml
4319}
4320
4321#[cfg(feature = "sml-pivot")]
4329fn build_pivot_cache_definition_xml(opts: &PivotTableOptions, _pn: usize) -> String {
4330 let (sheet_name, ref_range) = parse_source_ref(&opts.source_ref);
4332
4333 let all_fields = collect_all_fields(opts);
4335 let field_count = all_fields.len();
4336
4337 let mut xml = format!(
4338 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
4339<pivotCacheDefinition xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
4340 xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
4341 r:id="rId1" refreshedBy="ooxml-sml" refreshedDate="0" createdVersion="3"
4342 refreshedVersion="3" minRefreshableVersion="3" recordCount="0">
4343 <cacheSource type="worksheet">
4344 <worksheetSource ref="{ref_range}" sheet="{sheet_name}"/>
4345 </cacheSource>
4346 <cacheFields count="{field_count}">"#
4347 );
4348
4349 for name in &all_fields {
4350 xml.push_str(&format!(
4351 r#"
4352 <cacheField name="{}" numFmtId="0"><sharedItems/></cacheField>"#,
4353 escape_xml(name)
4354 ));
4355 }
4356
4357 xml.push_str(
4358 r#"
4359 </cacheFields>
4360</pivotCacheDefinition>"#,
4361 );
4362 xml
4363}
4364
4365#[cfg(feature = "sml-pivot")]
4369fn build_pivot_table_xml(opts: &PivotTableOptions, cache_id: usize, _sheet_name: &str) -> String {
4370 let all_fields = collect_all_fields(opts);
4371 let total_fields = all_fields.len();
4372
4373 let field_index: HashMap<&str, usize> = all_fields
4375 .iter()
4376 .enumerate()
4377 .map(|(i, n)| (n.as_str(), i))
4378 .collect();
4379
4380 let row_field_indices: Vec<usize> = opts
4382 .row_fields
4383 .iter()
4384 .filter_map(|n| field_index.get(n.as_str()).copied())
4385 .collect();
4386
4387 let col_field_indices: Vec<usize> = opts
4389 .col_fields
4390 .iter()
4391 .filter_map(|n| field_index.get(n.as_str()).copied())
4392 .collect();
4393
4394 let data_field_indices: Vec<usize> = opts
4396 .data_fields
4397 .iter()
4398 .filter_map(|n| field_index.get(n.as_str()).copied())
4399 .collect();
4400
4401 let dest = &opts.dest_ref;
4403 let header_rows = 1u32;
4404 let data_rows = opts.row_fields.len().max(1) as u32;
4405 let data_cols = opts.col_fields.len().max(1) as u32;
4406 let (dest_col, dest_row) = parse_cell_ref_for_pivot(dest);
4408 let end_col = dest_col + data_cols;
4409 let end_row = dest_row + header_rows + data_rows;
4410 let location_ref = format!("{}:{}", dest, format_cell_ref(end_col, end_row));
4411
4412 let mut xml = format!(
4413 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
4414<pivotTableDefinition xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
4415 name="{name}" cacheId="{cache_id}" dataOnRows="0" dataPosition="0"
4416 dataCaption="Values" createdVersion="3" updatedVersion="3" minRefreshableVersion="3"
4417 useAutoFormatting="1" itemPrintTitles="1" indent="0" outline="0" outlineData="0">
4418 <location ref="{location_ref}" firstHeaderRow="1" firstDataRow="2" firstDataCol="1"/>
4419 <pivotFields count="{total_fields}">"#,
4420 name = escape_xml(&opts.name),
4421 cache_id = cache_id,
4422 location_ref = location_ref,
4423 total_fields = total_fields,
4424 );
4425
4426 for _f in &all_fields {
4428 xml.push_str(
4429 r#"
4430 <pivotField showAll="0"/>"#,
4431 );
4432 }
4433
4434 xml.push_str(
4435 r#"
4436 </pivotFields>"#,
4437 );
4438
4439 if !row_field_indices.is_empty() {
4441 xml.push_str(&format!(
4442 r#"
4443 <rowFields count="{}">"#,
4444 row_field_indices.len()
4445 ));
4446 for idx in &row_field_indices {
4447 xml.push_str(&format!(
4448 r#"
4449 <field x="{}"/>"#,
4450 idx
4451 ));
4452 }
4453 xml.push_str(
4454 r#"
4455 </rowFields>"#,
4456 );
4457 }
4458
4459 if !col_field_indices.is_empty() {
4462 xml.push_str(&format!(
4463 r#"
4464 <colFields count="{}">"#,
4465 col_field_indices.len()
4466 ));
4467 for idx in &col_field_indices {
4468 xml.push_str(&format!(
4469 r#"
4470 <field x="{}"/>"#,
4471 idx
4472 ));
4473 }
4474 xml.push_str(
4475 r#"
4476 </colFields>"#,
4477 );
4478 } else if !data_field_indices.is_empty() {
4479 xml.push_str(
4481 r#"
4482 <colFields count="1">
4483 <field x="-2"/>
4484 </colFields>"#,
4485 );
4486 }
4487
4488 if !data_field_indices.is_empty() {
4490 xml.push_str(&format!(
4491 r#"
4492 <dataFields count="{}">"#,
4493 data_field_indices.len()
4494 ));
4495 for (di, &fld_idx) in data_field_indices.iter().enumerate() {
4496 let field_name = &opts.data_fields[di];
4497 xml.push_str(&format!(
4498 r#"
4499 <dataField name="Sum of {}" fld="{}" subtotal="sum"/>"#,
4500 escape_xml(field_name),
4501 fld_idx
4502 ));
4503 }
4504 xml.push_str(
4505 r#"
4506 </dataFields>"#,
4507 );
4508 }
4509
4510 xml.push_str(
4511 r#"
4512</pivotTableDefinition>"#,
4513 );
4514 xml
4515}
4516
4517#[cfg(feature = "sml-pivot")]
4521fn collect_all_fields(opts: &PivotTableOptions) -> Vec<String> {
4522 let mut seen = std::collections::HashSet::new();
4523 let mut fields = Vec::new();
4524 for name in opts
4525 .row_fields
4526 .iter()
4527 .chain(opts.col_fields.iter())
4528 .chain(opts.data_fields.iter())
4529 {
4530 if seen.insert(name.as_str()) {
4531 fields.push(name.clone());
4532 }
4533 }
4534 fields
4535}
4536
4537#[cfg(feature = "sml-pivot")]
4540fn parse_source_ref(source_ref: &str) -> (&str, &str) {
4541 if let Some(bang) = source_ref.find('!') {
4542 (&source_ref[..bang], &source_ref[bang + 1..])
4543 } else {
4544 ("Sheet1", source_ref)
4545 }
4546}
4547
4548#[cfg(feature = "sml-pivot")]
4550fn parse_cell_ref_for_pivot(cell_ref: &str) -> (u32, u32) {
4551 let clean: String = cell_ref.chars().filter(|c| *c != '$').collect();
4553 if let Some((row, col)) = parse_cell_reference(&clean) {
4555 (col, row)
4556 } else {
4557 (1, 1) }
4559}
4560
4561#[cfg(feature = "sml-pivot")]
4563fn format_cell_ref(col: u32, row: u32) -> String {
4564 format!("{}{}", column_to_letter(col), row)
4565}
4566
4567#[cfg(feature = "sml-styling")]
4569fn build_one_conditional_format(cf: &ConditionalFormat) -> types::ConditionalFormatting {
4570 types::ConditionalFormatting {
4571 #[cfg(feature = "sml-pivot")]
4572 pivot: None,
4573 square_reference: Some(cf.range.clone()),
4574 cf_rule: cf
4575 .rules
4576 .iter()
4577 .map(|rule| types::ConditionalRule {
4578 r#type: Some(map_conditional_rule_type(&rule.rule_type)),
4579 dxf_id: rule.dxf_id,
4580 priority: rule.priority as i32,
4581 stop_if_true: None,
4582 above_average: None,
4583 percent: None,
4584 bottom: None,
4585 operator: rule
4586 .operator
4587 .as_ref()
4588 .and_then(|op| parse_conditional_operator(op)),
4589 text: rule.text.clone(),
4590 time_period: None,
4591 rank: None,
4592 std_dev: None,
4593 equal_average: None,
4594 formula: rule.formulas.clone(),
4595 #[cfg(feature = "sml-styling")]
4596 color_scale: None,
4597 #[cfg(feature = "sml-styling")]
4598 data_bar: None,
4599 #[cfg(feature = "sml-styling")]
4600 icon_set: None,
4601 #[cfg(feature = "sml-extensions")]
4602 extension_list: None,
4603 #[cfg(feature = "extra-attrs")]
4604 extra_attrs: Default::default(),
4605 #[cfg(feature = "extra-children")]
4606 extra_children: Vec::new(),
4607 })
4608 .collect(),
4609 #[cfg(feature = "sml-extensions")]
4610 extension_list: None,
4611 #[cfg(feature = "extra-attrs")]
4612 extra_attrs: Default::default(),
4613 #[cfg(feature = "extra-children")]
4614 extra_children: Vec::new(),
4615 }
4616}
4617
4618#[cfg(feature = "sml-styling")]
4620fn map_conditional_rule_type(rule_type: &crate::ConditionalRuleType) -> types::ConditionalType {
4621 match rule_type {
4622 crate::ConditionalRuleType::Expression => types::ConditionalType::Expression,
4623 crate::ConditionalRuleType::CellIs => types::ConditionalType::CellIs,
4624 crate::ConditionalRuleType::ColorScale => types::ConditionalType::ColorScale,
4625 crate::ConditionalRuleType::DataBar => types::ConditionalType::DataBar,
4626 crate::ConditionalRuleType::IconSet => types::ConditionalType::IconSet,
4627 crate::ConditionalRuleType::Top10 => types::ConditionalType::Top10,
4628 crate::ConditionalRuleType::UniqueValues => types::ConditionalType::UniqueValues,
4629 crate::ConditionalRuleType::DuplicateValues => types::ConditionalType::DuplicateValues,
4630 crate::ConditionalRuleType::ContainsText => types::ConditionalType::ContainsText,
4631 crate::ConditionalRuleType::NotContainsText => types::ConditionalType::NotContainsText,
4632 crate::ConditionalRuleType::BeginsWith => types::ConditionalType::BeginsWith,
4633 crate::ConditionalRuleType::EndsWith => types::ConditionalType::EndsWith,
4634 crate::ConditionalRuleType::ContainsBlanks => types::ConditionalType::ContainsBlanks,
4635 crate::ConditionalRuleType::NotContainsBlanks => types::ConditionalType::NotContainsBlanks,
4636 crate::ConditionalRuleType::ContainsErrors => types::ConditionalType::ContainsErrors,
4637 crate::ConditionalRuleType::NotContainsErrors => types::ConditionalType::NotContainsErrors,
4638 crate::ConditionalRuleType::TimePeriod => types::ConditionalType::TimePeriod,
4639 crate::ConditionalRuleType::AboveAverage => types::ConditionalType::AboveAverage,
4640 }
4641}
4642
4643#[cfg(feature = "sml-styling")]
4645fn parse_conditional_operator(op: &str) -> Option<types::ConditionalOperator> {
4646 match op {
4647 "lessThan" => Some(types::ConditionalOperator::LessThan),
4648 "lessThanOrEqual" => Some(types::ConditionalOperator::LessThanOrEqual),
4649 "equal" => Some(types::ConditionalOperator::Equal),
4650 "notEqual" => Some(types::ConditionalOperator::NotEqual),
4651 "greaterThanOrEqual" => Some(types::ConditionalOperator::GreaterThanOrEqual),
4652 "greaterThan" => Some(types::ConditionalOperator::GreaterThan),
4653 "between" => Some(types::ConditionalOperator::Between),
4654 "notBetween" => Some(types::ConditionalOperator::NotBetween),
4655 "containsText" => Some(types::ConditionalOperator::ContainsText),
4656 "notContains" => Some(types::ConditionalOperator::NotContains),
4657 "beginsWith" => Some(types::ConditionalOperator::BeginsWith),
4658 "endsWith" => Some(types::ConditionalOperator::EndsWith),
4659 _ => None,
4660 }
4661}
4662
4663#[cfg(feature = "sml-validation")]
4665fn build_one_data_validation(dv: &DataValidationBuilder) -> types::DataValidation {
4666 types::DataValidation {
4667 r#type: map_validation_type(&dv.validation_type),
4668 error_style: map_validation_error_style(&dv.error_style),
4669 ime_mode: None,
4670 operator: map_validation_operator(&dv.operator),
4671 allow_blank: if dv.allow_blank { Some(true) } else { None },
4672 show_drop_down: None,
4673 show_input_message: if dv.show_input_message {
4674 Some(true)
4675 } else {
4676 None
4677 },
4678 show_error_message: if dv.show_error_message {
4679 Some(true)
4680 } else {
4681 None
4682 },
4683 error_title: dv.error_title.clone(),
4684 error: dv.error_message.clone(),
4685 prompt_title: dv.prompt_title.clone(),
4686 prompt: dv.prompt_message.clone(),
4687 square_reference: dv.range.clone(),
4688 formula1: dv.formula1.clone(),
4689 formula2: dv.formula2.clone(),
4690 #[cfg(feature = "extra-attrs")]
4691 extra_attrs: Default::default(),
4692 #[cfg(feature = "extra-children")]
4693 extra_children: Vec::new(),
4694 }
4695}
4696
4697#[cfg(feature = "sml-validation")]
4699fn map_validation_type(vt: &crate::DataValidationType) -> Option<types::ValidationType> {
4700 match vt {
4701 crate::DataValidationType::None => None, crate::DataValidationType::Whole => Some(types::ValidationType::Whole),
4703 crate::DataValidationType::Decimal => Some(types::ValidationType::Decimal),
4704 crate::DataValidationType::List => Some(types::ValidationType::List),
4705 crate::DataValidationType::Date => Some(types::ValidationType::Date),
4706 crate::DataValidationType::Time => Some(types::ValidationType::Time),
4707 crate::DataValidationType::TextLength => Some(types::ValidationType::TextLength),
4708 crate::DataValidationType::Custom => Some(types::ValidationType::Custom),
4709 }
4710}
4711
4712#[cfg(feature = "sml-validation")]
4714fn map_validation_operator(
4715 op: &crate::DataValidationOperator,
4716) -> Option<types::ValidationOperator> {
4717 match op {
4718 crate::DataValidationOperator::Between => Some(types::ValidationOperator::Between),
4719 crate::DataValidationOperator::NotBetween => Some(types::ValidationOperator::NotBetween),
4720 crate::DataValidationOperator::Equal => Some(types::ValidationOperator::Equal),
4721 crate::DataValidationOperator::NotEqual => Some(types::ValidationOperator::NotEqual),
4722 crate::DataValidationOperator::LessThan => Some(types::ValidationOperator::LessThan),
4723 crate::DataValidationOperator::LessThanOrEqual => {
4724 Some(types::ValidationOperator::LessThanOrEqual)
4725 }
4726 crate::DataValidationOperator::GreaterThan => Some(types::ValidationOperator::GreaterThan),
4727 crate::DataValidationOperator::GreaterThanOrEqual => {
4728 Some(types::ValidationOperator::GreaterThanOrEqual)
4729 }
4730 }
4731}
4732
4733#[cfg(feature = "sml-validation")]
4735fn map_validation_error_style(
4736 style: &crate::DataValidationErrorStyle,
4737) -> Option<types::ValidationErrorStyle> {
4738 match style {
4739 crate::DataValidationErrorStyle::Stop => None, crate::DataValidationErrorStyle::Warning => Some(types::ValidationErrorStyle::Warning),
4741 crate::DataValidationErrorStyle::Information => {
4742 Some(types::ValidationErrorStyle::Information)
4743 }
4744 }
4745}
4746
4747fn parse_cell_reference(reference: &str) -> Option<(u32, u32)> {
4749 let mut col_part = String::new();
4750 let mut row_part = String::new();
4751
4752 for c in reference.chars() {
4753 if c.is_ascii_alphabetic() {
4754 col_part.push(c.to_ascii_uppercase());
4755 } else if c.is_ascii_digit() {
4756 row_part.push(c);
4757 }
4758 }
4759
4760 let col = column_letter_to_number(&col_part)?;
4761 let row: u32 = row_part.parse().ok()?;
4762
4763 Some((row, col))
4764}
4765
4766fn escape_xml(s: &str) -> String {
4768 s.replace('&', "&")
4769 .replace('<', "<")
4770 .replace('>', ">")
4771 .replace('"', """)
4772}
4773
4774fn column_letter_to_number(letters: &str) -> Option<u32> {
4776 if letters.is_empty() {
4777 return None;
4778 }
4779
4780 let mut result: u32 = 0;
4781 for c in letters.chars() {
4782 if !c.is_ascii_alphabetic() {
4783 return None;
4784 }
4785 result = result * 26 + (c.to_ascii_uppercase() as u32 - 'A' as u32 + 1);
4786 }
4787 Some(result)
4788}
4789
4790fn column_to_letter(mut col: u32) -> String {
4792 let mut result = String::new();
4793 while col > 0 {
4794 col -= 1;
4795 result.insert(0, (b'A' + (col % 26) as u8) as char);
4796 col /= 26;
4797 }
4798 result
4799}
4800
4801const NS_DECLS: &[(&str, &str)] = &[("xmlns", NS_SPREADSHEET)];
4807
4808const NS_WORKBOOK_DECLS: &[(&str, &str)] =
4810 &[("xmlns", NS_SPREADSHEET), ("xmlns:r", NS_RELATIONSHIPS)];
4811
4812fn serialize_with_namespaces(value: &impl ToXml, tag: &str) -> Result<Vec<u8>> {
4814 serialize_with_ns_decls(value, tag, NS_DECLS)
4815}
4816
4817fn serialize_workbook(value: &impl ToXml) -> Result<Vec<u8>> {
4819 serialize_with_ns_decls(value, "workbook", NS_WORKBOOK_DECLS)
4820}
4821
4822fn serialize_with_ns_decls(
4824 value: &impl ToXml,
4825 tag: &str,
4826 ns_decls: &[(&str, &str)],
4827) -> Result<Vec<u8>> {
4828 use quick_xml::Writer;
4829 use quick_xml::events::{BytesEnd, BytesStart, Event};
4830
4831 let inner = Vec::new();
4832 let mut writer = Writer::new(inner);
4833
4834 let start = BytesStart::new(tag);
4836 let start = value.write_attrs(start);
4837 let mut start = start;
4838 for &(key, val) in ns_decls {
4839 start.push_attribute((key, val));
4840 }
4841
4842 if value.is_empty_element() {
4843 writer.write_event(Event::Empty(start))?;
4844 } else {
4845 writer.write_event(Event::Start(start))?;
4846 value.write_children(&mut writer)?;
4847 writer.write_event(Event::End(BytesEnd::new(tag)))?;
4848 }
4849
4850 let inner = writer.into_inner();
4851 let mut buf = Vec::with_capacity(
4852 b"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n".len() + inner.len(),
4853 );
4854 buf.extend_from_slice(b"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n");
4855 buf.extend_from_slice(&inner);
4856 Ok(buf)
4857}
4858
4859#[cfg(feature = "sml-styling")]
4861fn build_font(font: &FontStyle) -> types::Font {
4862 types::Font {
4863 #[cfg(feature = "sml-styling")]
4864 name: font.name.as_ref().map(|n| {
4865 Box::new(types::FontName {
4866 value: n.clone(),
4867 #[cfg(feature = "extra-attrs")]
4868 extra_attrs: Default::default(),
4869 })
4870 }),
4871 #[cfg(feature = "sml-styling")]
4872 charset: None,
4873 #[cfg(feature = "sml-styling")]
4874 family: None,
4875 #[cfg(feature = "sml-styling")]
4876 b: if font.bold {
4877 Some(Box::new(types::BooleanProperty {
4878 value: None, #[cfg(feature = "extra-attrs")]
4880 extra_attrs: Default::default(),
4881 }))
4882 } else {
4883 None
4884 },
4885 #[cfg(feature = "sml-styling")]
4886 i: if font.italic {
4887 Some(Box::new(types::BooleanProperty {
4888 value: None,
4889 #[cfg(feature = "extra-attrs")]
4890 extra_attrs: Default::default(),
4891 }))
4892 } else {
4893 None
4894 },
4895 #[cfg(feature = "sml-styling")]
4896 strike: if font.strikethrough {
4897 Some(Box::new(types::BooleanProperty {
4898 value: None,
4899 #[cfg(feature = "extra-attrs")]
4900 extra_attrs: Default::default(),
4901 }))
4902 } else {
4903 None
4904 },
4905 #[cfg(feature = "sml-styling")]
4906 outline: None,
4907 #[cfg(feature = "sml-styling")]
4908 shadow: None,
4909 #[cfg(feature = "sml-styling")]
4910 condense: None,
4911 #[cfg(feature = "sml-styling")]
4912 extend: None,
4913 #[cfg(feature = "sml-styling")]
4914 color: font.color.as_ref().map(|c| {
4915 Box::new(types::Color {
4916 auto: None,
4917 indexed: None,
4918 rgb: Some(hex_color_to_bytes(c)),
4919 theme: None,
4920 tint: None,
4921 #[cfg(feature = "extra-attrs")]
4922 extra_attrs: Default::default(),
4923 })
4924 }),
4925 #[cfg(feature = "sml-styling")]
4926 sz: font.size.map(|s| {
4927 Box::new(types::FontSize {
4928 value: s,
4929 #[cfg(feature = "extra-attrs")]
4930 extra_attrs: Default::default(),
4931 })
4932 }),
4933 #[cfg(feature = "sml-styling")]
4934 u: font.underline.map(|u| {
4935 Box::new(types::UnderlineProperty {
4936 value: Some(convert_underline_style(u)),
4937 #[cfg(feature = "extra-attrs")]
4938 extra_attrs: Default::default(),
4939 })
4940 }),
4941 #[cfg(feature = "sml-styling")]
4942 vert_align: None,
4943 #[cfg(feature = "sml-styling")]
4944 scheme: None,
4945 #[cfg(feature = "extra-children")]
4946 extra_children: Vec::new(),
4947 }
4948}
4949
4950#[cfg(feature = "sml-styling")]
4952fn build_fill(fill: &FillStyle) -> types::Fill {
4953 types::Fill {
4954 #[cfg(feature = "sml-styling")]
4955 pattern_fill: Some(Box::new(types::PatternFill {
4956 pattern_type: Some(convert_pattern_type(fill.pattern)),
4957 fg_color: fill.fg_color.as_ref().map(|c| {
4958 Box::new(types::Color {
4959 auto: None,
4960 indexed: None,
4961 rgb: Some(hex_color_to_bytes(c)),
4962 theme: None,
4963 tint: None,
4964 #[cfg(feature = "extra-attrs")]
4965 extra_attrs: Default::default(),
4966 })
4967 }),
4968 bg_color: fill.bg_color.as_ref().map(|c| {
4969 Box::new(types::Color {
4970 auto: None,
4971 indexed: None,
4972 rgb: Some(hex_color_to_bytes(c)),
4973 theme: None,
4974 tint: None,
4975 #[cfg(feature = "extra-attrs")]
4976 extra_attrs: Default::default(),
4977 })
4978 }),
4979 #[cfg(feature = "extra-attrs")]
4980 extra_attrs: Default::default(),
4981 #[cfg(feature = "extra-children")]
4982 extra_children: Vec::new(),
4983 })),
4984 #[cfg(feature = "sml-styling")]
4985 gradient_fill: None,
4986 #[cfg(feature = "extra-children")]
4987 extra_children: Vec::new(),
4988 }
4989}
4990
4991#[cfg(feature = "sml-styling")]
4993fn build_border(border: &BorderStyle) -> types::Border {
4994 types::Border {
4995 #[cfg(feature = "sml-styling")]
4996 diagonal_up: if border.diagonal_up { Some(true) } else { None },
4997 #[cfg(feature = "sml-styling")]
4998 diagonal_down: if border.diagonal_down {
4999 Some(true)
5000 } else {
5001 None
5002 },
5003 #[cfg(feature = "sml-styling")]
5004 outline: None,
5005 start: None,
5006 end: None,
5007 #[cfg(feature = "sml-styling")]
5008 left: build_border_properties(&border.left),
5009 #[cfg(feature = "sml-styling")]
5010 right: build_border_properties(&border.right),
5011 #[cfg(feature = "sml-styling")]
5012 top: build_border_properties(&border.top),
5013 #[cfg(feature = "sml-styling")]
5014 bottom: build_border_properties(&border.bottom),
5015 #[cfg(feature = "sml-styling")]
5016 diagonal: build_border_properties(&border.diagonal),
5017 #[cfg(feature = "sml-styling")]
5018 vertical: None,
5019 #[cfg(feature = "sml-styling")]
5020 horizontal: None,
5021 #[cfg(feature = "extra-attrs")]
5022 extra_attrs: Default::default(),
5023 #[cfg(feature = "extra-children")]
5024 extra_children: Vec::new(),
5025 }
5026}
5027
5028#[cfg(feature = "sml-styling")]
5030fn build_border_properties(side: &Option<BorderSideStyle>) -> Option<Box<types::BorderProperties>> {
5031 let (style, color) = if let Some(s) = side {
5033 if s.style != BorderLineStyle::None {
5034 (
5035 Some(convert_border_style(s.style)),
5036 s.color.as_ref().map(|c| {
5037 Box::new(types::Color {
5038 auto: None,
5039 indexed: None,
5040 rgb: Some(hex_color_to_bytes(c)),
5041 theme: None,
5042 tint: None,
5043 #[cfg(feature = "extra-attrs")]
5044 extra_attrs: Default::default(),
5045 })
5046 }),
5047 )
5048 } else {
5049 (None, None)
5050 }
5051 } else {
5052 (None, None)
5053 };
5054
5055 Some(Box::new(types::BorderProperties {
5056 style,
5057 color,
5058 #[cfg(feature = "extra-attrs")]
5059 extra_attrs: Default::default(),
5060 #[cfg(feature = "extra-children")]
5061 extra_children: Vec::new(),
5062 }))
5063}
5064
5065#[cfg(feature = "sml-styling")]
5067fn build_cell_format(xf: &CellFormatRecord) -> types::Format {
5068 let has_alignment = xf.horizontal.is_some() || xf.vertical.is_some() || xf.wrap_text;
5069
5070 let alignment = if has_alignment {
5071 Some(Box::new(types::CellAlignment {
5072 horizontal: xf.horizontal.map(convert_horizontal_alignment),
5073 vertical: xf.vertical.map(convert_vertical_alignment),
5074 text_rotation: None,
5075 wrap_text: if xf.wrap_text { Some(true) } else { None },
5076 indent: None,
5077 relative_indent: None,
5078 justify_last_line: None,
5079 shrink_to_fit: None,
5080 reading_order: None,
5081 #[cfg(feature = "extra-attrs")]
5082 extra_attrs: Default::default(),
5083 }))
5084 } else {
5085 None
5086 };
5087
5088 types::Format {
5089 #[cfg(feature = "sml-styling")]
5090 number_format_id: Some(xf.num_fmt_id),
5091 #[cfg(feature = "sml-styling")]
5092 font_id: Some(xf.font_id as u32),
5093 #[cfg(feature = "sml-styling")]
5094 fill_id: Some(xf.fill_id as u32),
5095 #[cfg(feature = "sml-styling")]
5096 border_id: Some(xf.border_id as u32),
5097 #[cfg(feature = "sml-styling")]
5098 format_id: Some(0),
5099 #[cfg(feature = "sml-styling")]
5100 quote_prefix: None,
5101 #[cfg(feature = "sml-pivot")]
5102 pivot_button: None,
5103 #[cfg(feature = "sml-styling")]
5104 apply_number_format: if xf.num_fmt_id > 0 { Some(true) } else { None },
5105 #[cfg(feature = "sml-styling")]
5106 apply_font: if xf.font_id > 0 { Some(true) } else { None },
5107 #[cfg(feature = "sml-styling")]
5108 apply_fill: if xf.fill_id > 0 { Some(true) } else { None },
5109 #[cfg(feature = "sml-styling")]
5110 apply_border: if xf.border_id > 0 { Some(true) } else { None },
5111 #[cfg(feature = "sml-styling")]
5112 apply_alignment: if has_alignment { Some(true) } else { None },
5113 #[cfg(feature = "sml-styling")]
5114 apply_protection: None,
5115 #[cfg(feature = "sml-styling")]
5116 alignment,
5117 #[cfg(feature = "sml-protection")]
5118 protection: None,
5119 #[cfg(feature = "sml-extensions")]
5120 extension_list: None,
5121 #[cfg(feature = "extra-attrs")]
5122 extra_attrs: Default::default(),
5123 #[cfg(feature = "extra-children")]
5124 extra_children: Vec::new(),
5125 }
5126}
5127
5128#[cfg(feature = "sml-styling")]
5130fn convert_underline_style(style: UnderlineStyle) -> types::UnderlineStyle {
5131 match style {
5132 UnderlineStyle::Single => types::UnderlineStyle::Single,
5133 UnderlineStyle::Double => types::UnderlineStyle::Double,
5134 UnderlineStyle::SingleAccounting => types::UnderlineStyle::SingleAccounting,
5135 UnderlineStyle::DoubleAccounting => types::UnderlineStyle::DoubleAccounting,
5136 }
5137}
5138
5139#[cfg(feature = "sml-styling")]
5141fn convert_pattern_type(pattern: FillPattern) -> types::PatternType {
5142 match pattern {
5143 FillPattern::None => types::PatternType::None,
5144 FillPattern::Solid => types::PatternType::Solid,
5145 FillPattern::MediumGray => types::PatternType::MediumGray,
5146 FillPattern::DarkGray => types::PatternType::DarkGray,
5147 FillPattern::LightGray => types::PatternType::LightGray,
5148 FillPattern::DarkHorizontal => types::PatternType::DarkHorizontal,
5149 FillPattern::DarkVertical => types::PatternType::DarkVertical,
5150 FillPattern::DarkDown => types::PatternType::DarkDown,
5151 FillPattern::DarkUp => types::PatternType::DarkUp,
5152 FillPattern::DarkGrid => types::PatternType::DarkGrid,
5153 FillPattern::DarkTrellis => types::PatternType::DarkTrellis,
5154 FillPattern::LightHorizontal => types::PatternType::LightHorizontal,
5155 FillPattern::LightVertical => types::PatternType::LightVertical,
5156 FillPattern::LightDown => types::PatternType::LightDown,
5157 FillPattern::LightUp => types::PatternType::LightUp,
5158 FillPattern::LightGrid => types::PatternType::LightGrid,
5159 FillPattern::LightTrellis => types::PatternType::LightTrellis,
5160 FillPattern::Gray125 => types::PatternType::Gray125,
5161 FillPattern::Gray0625 => types::PatternType::Gray0625,
5162 }
5163}
5164
5165#[cfg(feature = "sml-styling")]
5167fn convert_border_style(style: BorderLineStyle) -> types::BorderStyle {
5168 match style {
5169 BorderLineStyle::None => types::BorderStyle::None,
5170 BorderLineStyle::Thin => types::BorderStyle::Thin,
5171 BorderLineStyle::Medium => types::BorderStyle::Medium,
5172 BorderLineStyle::Dashed => types::BorderStyle::Dashed,
5173 BorderLineStyle::Dotted => types::BorderStyle::Dotted,
5174 BorderLineStyle::Thick => types::BorderStyle::Thick,
5175 BorderLineStyle::Double => types::BorderStyle::Double,
5176 BorderLineStyle::Hair => types::BorderStyle::Hair,
5177 BorderLineStyle::MediumDashed => types::BorderStyle::MediumDashed,
5178 BorderLineStyle::DashDot => types::BorderStyle::DashDot,
5179 BorderLineStyle::MediumDashDot => types::BorderStyle::MediumDashDot,
5180 BorderLineStyle::DashDotDot => types::BorderStyle::DashDotDot,
5181 BorderLineStyle::MediumDashDotDot => types::BorderStyle::MediumDashDotDot,
5182 BorderLineStyle::SlantDashDot => types::BorderStyle::SlantDashDot,
5183 }
5184}
5185
5186#[cfg(feature = "sml-styling")]
5188fn convert_horizontal_alignment(align: HorizontalAlignment) -> types::HorizontalAlignment {
5189 match align {
5190 HorizontalAlignment::General => types::HorizontalAlignment::General,
5191 HorizontalAlignment::Left => types::HorizontalAlignment::Left,
5192 HorizontalAlignment::Center => types::HorizontalAlignment::Center,
5193 HorizontalAlignment::Right => types::HorizontalAlignment::Right,
5194 HorizontalAlignment::Fill => types::HorizontalAlignment::Fill,
5195 HorizontalAlignment::Justify => types::HorizontalAlignment::Justify,
5196 HorizontalAlignment::CenterContinuous => types::HorizontalAlignment::CenterContinuous,
5197 HorizontalAlignment::Distributed => types::HorizontalAlignment::Distributed,
5198 }
5199}
5200
5201#[cfg(feature = "sml-styling")]
5203fn convert_vertical_alignment(align: VerticalAlignment) -> types::VerticalAlignment {
5204 match align {
5205 VerticalAlignment::Top => types::VerticalAlignment::Top,
5206 VerticalAlignment::Center => types::VerticalAlignment::Center,
5207 VerticalAlignment::Bottom => types::VerticalAlignment::Bottom,
5208 VerticalAlignment::Justify => types::VerticalAlignment::Justify,
5209 VerticalAlignment::Distributed => types::VerticalAlignment::Distributed,
5210 }
5211}
5212
5213#[cfg(feature = "sml-styling")]
5215fn hex_color_to_bytes(color: &str) -> Vec<u8> {
5216 let rgb = u32::from_str_radix(color, 16).unwrap_or(0);
5218 vec![0xFF, (rgb >> 16) as u8, (rgb >> 8) as u8, rgb as u8]
5219}
5220
5221#[cfg(test)]
5222mod tests {
5223 use super::*;
5224
5225 #[test]
5226 fn test_column_letter_to_number() {
5227 assert_eq!(column_letter_to_number("A"), Some(1));
5228 assert_eq!(column_letter_to_number("B"), Some(2));
5229 assert_eq!(column_letter_to_number("Z"), Some(26));
5230 assert_eq!(column_letter_to_number("AA"), Some(27));
5231 assert_eq!(column_letter_to_number("AB"), Some(28));
5232 assert_eq!(column_letter_to_number("AZ"), Some(52));
5233 }
5234
5235 #[test]
5236 fn test_column_to_letter() {
5237 assert_eq!(column_to_letter(1), "A");
5238 assert_eq!(column_to_letter(2), "B");
5239 assert_eq!(column_to_letter(26), "Z");
5240 assert_eq!(column_to_letter(27), "AA");
5241 assert_eq!(column_to_letter(28), "AB");
5242 assert_eq!(column_to_letter(52), "AZ");
5243 }
5244
5245 #[test]
5246 fn test_parse_cell_reference() {
5247 assert_eq!(parse_cell_reference("A1"), Some((1, 1)));
5248 assert_eq!(parse_cell_reference("B2"), Some((2, 2)));
5249 assert_eq!(parse_cell_reference("Z10"), Some((10, 26)));
5250 assert_eq!(parse_cell_reference("AA1"), Some((1, 27)));
5251 }
5252
5253 #[test]
5254 fn test_workbook_builder() {
5255 let mut wb = WorkbookBuilder::new();
5256 let sheet = wb.add_sheet("Test");
5257 sheet.set_cell("A1", "Hello");
5258 sheet.set_cell("B1", 42.0);
5259 sheet.set_cell("C1", true);
5260 sheet.set_formula("D1", "SUM(A1:C1)");
5261
5262 assert_eq!(wb.sheet_count(), 1);
5263 }
5264
5265 #[test]
5266 fn test_roundtrip_simple() {
5267 use std::io::Cursor;
5268
5269 let mut wb = WorkbookBuilder::new();
5270 let sheet = wb.add_sheet("Sheet1");
5271 sheet.set_cell("A1", "Test Value");
5272 sheet.set_cell("B1", 123.45);
5273
5274 let mut buffer = Cursor::new(Vec::new());
5276 wb.write(&mut buffer).unwrap();
5277
5278 buffer.set_position(0);
5280 let mut workbook = crate::Workbook::from_reader(buffer).unwrap();
5281 let read_sheet = workbook.resolved_sheet(0).unwrap();
5282
5283 assert_eq!(read_sheet.name(), "Sheet1");
5284 assert_eq!(read_sheet.value_at("A1"), Some("Test Value".to_string()));
5285 assert_eq!(read_sheet.number_at("B1"), Some(123.45));
5286 }
5287
5288 #[test]
5289 fn test_roundtrip_merged_cells() {
5290 use std::io::Cursor;
5291
5292 let mut wb = WorkbookBuilder::new();
5293 let sheet = wb.add_sheet("Sheet1");
5294 sheet.set_cell("A1", "Merged Header");
5295 sheet.merge_cells("A1:C1");
5296 sheet.set_cell("A2", "Data 1");
5297 sheet.set_cell("B2", "Data 2");
5298 sheet.merge_cells("A3:B4");
5299 sheet.set_cell("A3", "Block");
5300
5301 let mut buffer = Cursor::new(Vec::new());
5303 wb.write(&mut buffer).unwrap();
5304
5305 buffer.set_position(0);
5307 let mut workbook = crate::Workbook::from_reader(buffer).unwrap();
5308 let read_sheet = workbook.resolved_sheet(0).unwrap();
5309
5310 let merged = read_sheet.merged_cells().expect("Should have merged cells");
5312 assert_eq!(merged.merge_cell.len(), 2);
5313 assert_eq!(merged.merge_cell[0].reference.as_str(), "A1:C1");
5314 assert_eq!(merged.merge_cell[1].reference.as_str(), "A3:B4");
5315
5316 assert_eq!(read_sheet.value_at("A1"), Some("Merged Header".to_string()));
5318 }
5319
5320 #[test]
5321 #[cfg(feature = "full")]
5322 fn test_roundtrip_dimensions() {
5323 use std::io::Cursor;
5324
5325 let mut wb = WorkbookBuilder::new();
5326 let sheet = wb.add_sheet("Sheet1");
5327 sheet.set_cell("A1", "Header 1");
5328 sheet.set_cell("B1", "Header 2");
5329 sheet.set_cell("C1", "Header 3");
5330 sheet.set_cell("A2", "Data");
5331
5332 sheet.set_column_width("A", 20.0);
5334 sheet.set_column_width_range("B", "C", 15.5);
5335
5336 sheet.set_row_height(1, 25.0);
5338 sheet.set_row_height(2, 18.0);
5339
5340 let mut buffer = Cursor::new(Vec::new());
5342 wb.write(&mut buffer).unwrap();
5343
5344 buffer.set_position(0);
5346 let mut workbook = crate::Workbook::from_reader(buffer).unwrap();
5347 let read_sheet = workbook.resolved_sheet(0).unwrap();
5348
5349 let columns = read_sheet.columns();
5352 assert!(!columns.is_empty());
5353
5354 let all_cols: Vec<_> = columns.iter().flat_map(|c| &c.col).collect();
5356 assert_eq!(all_cols.len(), 2);
5357
5358 assert_eq!(all_cols[0].start_column, 1);
5360 assert_eq!(all_cols[0].end_column, 1);
5361 assert_eq!(all_cols[0].width, Some(20.0));
5362
5363 assert_eq!(all_cols[1].start_column, 2);
5365 assert_eq!(all_cols[1].end_column, 3);
5366 assert_eq!(all_cols[1].width, Some(15.5));
5367
5368 let row1 = read_sheet.row(1).unwrap();
5370 assert_eq!(row1.height, Some(25.0));
5371
5372 let row2 = read_sheet.row(2).unwrap();
5373 assert_eq!(row2.height, Some(18.0));
5374 }
5375
5376 #[test]
5377 #[cfg(feature = "full")]
5378 fn test_roundtrip_freeze_rows() {
5379 use std::io::Cursor;
5380
5381 let mut wb = WorkbookBuilder::new();
5382 let sheet = wb.add_sheet("Sheet1");
5383 sheet.set_cell("A1", "Header");
5384 sheet.freeze_rows(1);
5385
5386 let mut buffer = Cursor::new(Vec::new());
5387 wb.write(&mut buffer).unwrap();
5388
5389 buffer.set_position(0);
5390 let mut workbook = crate::Workbook::from_reader(buffer).unwrap();
5391 let read_sheet = workbook.resolved_sheet(0).unwrap();
5392
5393 assert!(read_sheet.has_freeze_panes(), "Should have freeze panes");
5394 let pane = read_sheet.freeze_pane().expect("Should have pane");
5395 assert_eq!(pane.y_split, Some(1.0), "Should freeze 1 row");
5396 assert_eq!(pane.x_split, None, "Should not freeze any columns");
5397 assert_eq!(
5398 pane.state,
5399 Some(crate::types::PaneState::Frozen),
5400 "State should be Frozen"
5401 );
5402 }
5403
5404 #[test]
5405 #[cfg(feature = "full")]
5406 fn test_roundtrip_freeze_cols() {
5407 use std::io::Cursor;
5408
5409 let mut wb = WorkbookBuilder::new();
5410 let sheet = wb.add_sheet("Sheet1");
5411 sheet.set_cell("A1", "Row label");
5412 sheet.freeze_cols(1);
5413
5414 let mut buffer = Cursor::new(Vec::new());
5415 wb.write(&mut buffer).unwrap();
5416
5417 buffer.set_position(0);
5418 let mut workbook = crate::Workbook::from_reader(buffer).unwrap();
5419 let read_sheet = workbook.resolved_sheet(0).unwrap();
5420
5421 assert!(read_sheet.has_freeze_panes());
5422 let pane = read_sheet.freeze_pane().expect("Should have pane");
5423 assert_eq!(pane.x_split, Some(1.0), "Should freeze 1 column");
5424 assert_eq!(pane.y_split, None, "Should not freeze any rows");
5425 }
5426
5427 #[test]
5428 #[cfg(feature = "full")]
5429 fn test_roundtrip_freeze_both() {
5430 use std::io::Cursor;
5431
5432 let mut wb = WorkbookBuilder::new();
5433 let sheet = wb.add_sheet("Sheet1");
5434 sheet.set_cell("A1", "Header");
5435 sheet.set_freeze_pane(2, 1);
5436
5437 let mut buffer = Cursor::new(Vec::new());
5438 wb.write(&mut buffer).unwrap();
5439
5440 buffer.set_position(0);
5441 let mut workbook = crate::Workbook::from_reader(buffer).unwrap();
5442 let read_sheet = workbook.resolved_sheet(0).unwrap();
5443
5444 assert!(read_sheet.has_freeze_panes());
5445 let pane = read_sheet.freeze_pane().expect("Should have pane");
5446 assert_eq!(pane.y_split, Some(2.0), "Should freeze 2 rows");
5447 assert_eq!(pane.x_split, Some(1.0), "Should freeze 1 column");
5448 assert_eq!(pane.active_pane, Some(crate::types::PaneType::BottomRight));
5449 }
5450
5451 #[test]
5452 #[cfg(feature = "full")]
5453 fn test_roundtrip_conditional_formatting() {
5454 use std::io::Cursor;
5455
5456 let mut wb = WorkbookBuilder::new();
5457 let sheet = wb.add_sheet("Sheet1");
5458 sheet.set_cell("A1", 10.0);
5459 sheet.set_cell("A2", 20.0);
5460 sheet.set_cell("A3", 30.0);
5461
5462 let cf = ConditionalFormat::new("A1:A3")
5464 .add_cell_is_rule("greaterThan", "15", 1, None)
5465 .add_expression_rule("$A1>$A2", 2, None);
5466 sheet.add_conditional_format(cf);
5467
5468 let cf2 = ConditionalFormat::new("B1:B10").add_duplicate_values_rule(1, None);
5470 sheet.add_conditional_format(cf2);
5471
5472 let mut buffer = Cursor::new(Vec::new());
5474 wb.write(&mut buffer).unwrap();
5475
5476 buffer.set_position(0);
5478 let mut workbook = crate::Workbook::from_reader(buffer).unwrap();
5479 let read_sheet = workbook.resolved_sheet(0).unwrap();
5480
5481 let cfs = read_sheet.conditional_formatting();
5483 assert_eq!(cfs.len(), 2);
5484
5485 assert_eq!(cfs[0].square_reference.as_deref(), Some("A1:A3"));
5487 assert_eq!(cfs[0].cf_rule.len(), 2);
5488
5489 assert_eq!(cfs[1].square_reference.as_deref(), Some("B1:B10"));
5491 assert_eq!(cfs[1].cf_rule.len(), 1);
5492 }
5493
5494 #[test]
5495 #[cfg(feature = "full")]
5496 fn test_roundtrip_data_validation() {
5497 use std::io::Cursor;
5498
5499 let mut wb = WorkbookBuilder::new();
5500 let sheet = wb.add_sheet("Sheet1");
5501 sheet.set_cell("A1", 10.0);
5502
5503 let dv = DataValidationBuilder::list("A1:A10", "\"Yes,No,Maybe\"")
5505 .with_error("Invalid Input", "Please select from the list")
5506 .with_prompt("Select", "Choose a value");
5507 sheet.add_data_validation(dv);
5508
5509 let dv2 = DataValidationBuilder::whole_number(
5511 "B1:B10",
5512 crate::DataValidationOperator::GreaterThan,
5513 "0",
5514 )
5515 .with_error("Invalid Number", "Please enter a positive number");
5516 sheet.add_data_validation(dv2);
5517
5518 let mut buffer = Cursor::new(Vec::new());
5520 wb.write(&mut buffer).unwrap();
5521
5522 buffer.set_position(0);
5524 let mut workbook = crate::Workbook::from_reader(buffer).unwrap();
5525 let read_sheet = workbook.resolved_sheet(0).unwrap();
5526
5527 let dvs = read_sheet
5529 .data_validations()
5530 .expect("Should have data validations");
5531 assert_eq!(dvs.data_validation.len(), 2);
5532
5533 let dv0 = &dvs.data_validation[0];
5535 assert_eq!(dv0.square_reference.as_str(), "A1:A10");
5536 assert_eq!(dv0.error_title.as_deref(), Some("Invalid Input"));
5537 assert_eq!(dv0.error.as_deref(), Some("Please select from the list"));
5538 assert_eq!(dv0.prompt_title.as_deref(), Some("Select"));
5539 assert_eq!(dv0.prompt.as_deref(), Some("Choose a value"));
5540
5541 let dv1 = &dvs.data_validation[1];
5543 assert_eq!(dv1.square_reference.as_str(), "B1:B10");
5544 assert_eq!(dv1.error_title.as_deref(), Some("Invalid Number"));
5545 }
5546
5547 #[test]
5548 fn test_roundtrip_defined_names() {
5549 use std::io::Cursor;
5550
5551 let mut wb = WorkbookBuilder::new();
5552 wb.add_sheet("Sheet1");
5553 wb.add_sheet("Sheet2");
5554
5555 wb.add_defined_name("GlobalRange", "Sheet1!$A$1:$B$10");
5557
5558 wb.add_defined_name_with_scope("LocalRange", "Sheet1!$C$1:$D$5", 0);
5560
5561 let dn = DefinedNameBuilder::new("DataRange", "Sheet2!$A$1:$Z$100")
5563 .with_comment("Main data table");
5564 wb.add_defined_name_builder(dn);
5565
5566 wb.set_print_area(0, "Sheet1!$A$1:$G$20");
5568
5569 let mut buffer = Cursor::new(Vec::new());
5571 wb.write(&mut buffer).unwrap();
5572
5573 buffer.set_position(0);
5575 let workbook = crate::Workbook::from_reader(buffer).unwrap();
5576
5577 let names = workbook.defined_names();
5579 assert_eq!(names.len(), 4);
5580
5581 use crate::DefinedNameExt;
5582
5583 let global = workbook.defined_name("GlobalRange").unwrap();
5585 assert_eq!(global.name, "GlobalRange");
5586 assert_eq!(global.text.as_deref(), Some("Sheet1!$A$1:$B$10"));
5587 assert!(global.local_sheet_id.is_none());
5588
5589 let local = workbook.defined_name_in_sheet("LocalRange", 0).unwrap();
5591 assert_eq!(local.name, "LocalRange");
5592 assert_eq!(local.text.as_deref(), Some("Sheet1!$C$1:$D$5"));
5593 assert_eq!(local.local_sheet_id, Some(0));
5594
5595 let data = workbook.defined_name("DataRange").unwrap();
5597 assert_eq!(data.name, "DataRange");
5598 assert_eq!(data.text.as_deref(), Some("Sheet2!$A$1:$Z$100"));
5599 assert_eq!(data.comment.as_deref(), Some("Main data table"));
5600
5601 let print_area = workbook
5603 .defined_name_in_sheet("_xlnm.Print_Area", 0)
5604 .unwrap();
5605 assert_eq!(print_area.text.as_deref(), Some("Sheet1!$A$1:$G$20"));
5606 assert!(print_area.is_builtin());
5607 }
5608
5609 #[cfg(feature = "sml-comments")]
5610 #[test]
5611 fn test_roundtrip_comments() {
5612 use std::io::Cursor;
5613
5614 let mut wb = WorkbookBuilder::new();
5615 let sheet = wb.add_sheet("Sheet1");
5616 sheet.set_cell("A1", "Hello");
5617 sheet.set_cell("B1", 42.0);
5618
5619 sheet.add_comment("A1", "This is a simple comment");
5621 sheet.add_comment_with_author("B1", "Review this value", "John Doe");
5622
5623 let comment = CommentBuilder::new("C1", "Builder comment").author("Jane Smith");
5625 sheet.add_comment_builder(comment);
5626
5627 let mut buffer = Cursor::new(Vec::new());
5629 wb.write(&mut buffer).unwrap();
5630
5631 buffer.set_position(0);
5633 let mut workbook = crate::Workbook::from_reader(buffer).unwrap();
5634 let read_sheet = workbook.resolved_sheet(0).unwrap();
5635
5636 let comments = read_sheet.comments();
5638 assert_eq!(comments.len(), 3);
5639
5640 let c1 = read_sheet.comment("A1").unwrap();
5642 assert_eq!(c1.reference, "A1");
5643 assert_eq!(c1.text, "This is a simple comment");
5644 assert!(c1.author.is_none() || c1.author.as_ref().is_some_and(|a| a.is_empty()));
5645
5646 let c2 = read_sheet.comment("B1").unwrap();
5648 assert_eq!(c2.reference, "B1");
5649 assert_eq!(c2.text, "Review this value");
5650 assert_eq!(c2.author.as_deref(), Some("John Doe"));
5651
5652 let c3 = read_sheet.comment("C1").unwrap();
5654 assert_eq!(c3.reference, "C1");
5655 assert_eq!(c3.text, "Builder comment");
5656 assert_eq!(c3.author.as_deref(), Some("Jane Smith"));
5657
5658 assert!(read_sheet.has_comment("A1"));
5660 assert!(read_sheet.has_comment("B1"));
5661 assert!(!read_sheet.has_comment("D1"));
5662 }
5663
5664 #[cfg(feature = "sml-filtering")]
5665 #[test]
5666 fn test_roundtrip_auto_filter() {
5667 use std::io::Cursor;
5668
5669 let mut wb = WorkbookBuilder::new();
5670 let sheet = wb.add_sheet("Data");
5671 sheet.set_cell("A1", "Name");
5672 sheet.set_cell("B1", "Score");
5673 sheet.set_cell("A2", "Alice");
5674 sheet.set_cell("B2", 95.0);
5675 sheet.set_auto_filter("A1:B1");
5676
5677 let mut buffer = Cursor::new(Vec::new());
5678 wb.write(&mut buffer).unwrap();
5679
5680 buffer.set_position(0);
5681 let mut workbook = crate::Workbook::from_reader(buffer).unwrap();
5682 let read_sheet = workbook.resolved_sheet(0).unwrap();
5683
5684 assert!(read_sheet.has_auto_filter());
5685 let af = read_sheet.auto_filter().unwrap();
5686 assert_eq!(af.reference.as_deref(), Some("A1:B1"));
5687 }
5688
5689 #[cfg(feature = "sml-hyperlinks")]
5690 #[test]
5691 fn test_roundtrip_hyperlinks() {
5692 use std::io::Cursor;
5693
5694 let mut wb = WorkbookBuilder::new();
5695 let sheet = wb.add_sheet("Links");
5696 sheet.set_cell("A1", "External");
5697 sheet.add_hyperlink("A1", "https://example.com");
5698 sheet.set_cell("B1", "Internal");
5699 sheet.add_internal_hyperlink("B1", "Sheet2!A1");
5700
5701 let mut buffer = Cursor::new(Vec::new());
5702 wb.write(&mut buffer).unwrap();
5703
5704 buffer.set_position(0);
5705 let mut workbook = crate::Workbook::from_reader(buffer).unwrap();
5706 let read_sheet = workbook.resolved_sheet(0).unwrap();
5707
5708 let ws = read_sheet.worksheet();
5710 let hyperlinks = ws.hyperlinks.as_deref().unwrap();
5711 assert_eq!(hyperlinks.hyperlink.len(), 2);
5712
5713 let ext = hyperlinks
5714 .hyperlink
5715 .iter()
5716 .find(|h| h.reference == "A1")
5717 .unwrap();
5718 assert!(ext.id.is_some());
5720 assert!(ext.location.as_deref().unwrap_or("").is_empty());
5721
5722 let int = hyperlinks
5723 .hyperlink
5724 .iter()
5725 .find(|h| h.reference == "B1")
5726 .unwrap();
5727 assert!(int.id.is_none());
5729 assert_eq!(int.location.as_deref(), Some("Sheet2!A1"));
5730 }
5731
5732 #[cfg(all(feature = "sml-hyperlinks", feature = "sml-comments"))]
5733 #[test]
5734 fn test_roundtrip_hyperlinks_with_comments() {
5735 use std::io::Cursor;
5736
5737 let mut wb = WorkbookBuilder::new();
5740 let sheet = wb.add_sheet("Sheet1");
5741 sheet.set_cell("A1", "Click me");
5742 sheet.add_hyperlink("A1", "https://example.com");
5743 sheet.add_comment("A1", "Visit the site");
5744
5745 let mut buffer = Cursor::new(Vec::new());
5746 wb.write(&mut buffer).unwrap();
5747
5748 buffer.set_position(0);
5749 let mut workbook = crate::Workbook::from_reader(buffer).unwrap();
5750 let read_sheet = workbook.resolved_sheet(0).unwrap();
5751
5752 let c = read_sheet.comment("A1").unwrap();
5754 assert_eq!(c.text, "Visit the site");
5755
5756 let ws = read_sheet.worksheet();
5758 let hyperlinks = ws.hyperlinks.as_deref().unwrap();
5759 assert_eq!(hyperlinks.hyperlink.len(), 1);
5760 assert_eq!(hyperlinks.hyperlink[0].reference, "A1");
5761 assert!(hyperlinks.hyperlink[0].id.is_some());
5762 }
5763
5764 #[cfg(feature = "sml-layout")]
5769 #[test]
5770 fn test_roundtrip_page_setup() {
5771 use std::io::Cursor;
5772
5773 let mut wb = WorkbookBuilder::new();
5774 let sheet = wb.add_sheet("Sheet1");
5775 sheet.set_cell("A1", "Print me");
5776 sheet.set_page_setup(
5777 PageSetupOptions::new()
5778 .with_orientation(PageOrientation::Landscape)
5779 .with_paper_size(9) .with_scale(80),
5781 );
5782
5783 let mut buffer = Cursor::new(Vec::new());
5784 wb.write(&mut buffer).unwrap();
5785
5786 buffer.set_position(0);
5787 let mut workbook = crate::Workbook::from_reader(buffer).unwrap();
5788 let read_sheet = workbook.resolved_sheet(0).unwrap();
5789 let ws = read_sheet.worksheet();
5790
5791 let ps = ws.page_setup.as_deref().expect("page_setup should be set");
5792 assert_eq!(ps.paper_size, Some(9));
5793 assert_eq!(ps.scale, Some(80));
5794 assert_eq!(ps.orientation, Some(crate::types::STOrientation::Landscape));
5795 }
5796
5797 #[cfg(feature = "sml-layout")]
5798 #[test]
5799 fn test_roundtrip_page_margins() {
5800 use std::io::Cursor;
5801
5802 let mut wb = WorkbookBuilder::new();
5803 let sheet = wb.add_sheet("Sheet1");
5804 sheet.set_cell("A1", "Data");
5805 sheet.set_page_margins(0.7, 0.7, 0.75, 0.75, 0.3, 0.3);
5806
5807 let mut buffer = Cursor::new(Vec::new());
5808 wb.write(&mut buffer).unwrap();
5809
5810 buffer.set_position(0);
5811 let mut workbook = crate::Workbook::from_reader(buffer).unwrap();
5812 let read_sheet = workbook.resolved_sheet(0).unwrap();
5813 let ws = read_sheet.worksheet();
5814
5815 let pm = ws
5816 .page_margins
5817 .as_deref()
5818 .expect("page_margins should be set");
5819 assert!((pm.left - 0.7).abs() < f64::EPSILON);
5820 assert!((pm.right - 0.7).abs() < f64::EPSILON);
5821 assert!((pm.top - 0.75).abs() < f64::EPSILON);
5822 assert!((pm.bottom - 0.75).abs() < f64::EPSILON);
5823 assert!((pm.header - 0.3).abs() < f64::EPSILON);
5824 assert!((pm.footer - 0.3).abs() < f64::EPSILON);
5825 }
5826
5827 #[cfg(all(feature = "sml-structure", feature = "sml-styling"))]
5832 #[test]
5833 fn test_roundtrip_row_outline() {
5834 use std::io::Cursor;
5835
5836 let mut wb = WorkbookBuilder::new();
5837 let sheet = wb.add_sheet("Sheet1");
5838 sheet.set_cell("A1", "Header");
5839 sheet.set_cell("A2", "Detail 1");
5840 sheet.set_cell("A3", "Detail 2");
5841 sheet.set_row_outline_level(2, 1);
5843 sheet.set_row_outline_level(3, 1);
5844 sheet.set_row_collapsed(2, true);
5846
5847 let mut buffer = Cursor::new(Vec::new());
5848 wb.write(&mut buffer).unwrap();
5849
5850 buffer.set_position(0);
5851 let mut workbook = crate::Workbook::from_reader(buffer).unwrap();
5852 let read_sheet = workbook.resolved_sheet(0).unwrap();
5853
5854 let row2 = read_sheet.row(2).expect("row 2 should exist");
5855 assert_eq!(row2.outline_level, Some(1), "row 2 outline level");
5856 assert_eq!(row2.collapsed, Some(true), "row 2 collapsed");
5857
5858 let row3 = read_sheet.row(3).expect("row 3 should exist");
5859 assert_eq!(row3.outline_level, Some(1), "row 3 outline level");
5860 assert_eq!(row3.collapsed, None, "row 3 not collapsed");
5861 }
5862
5863 #[cfg(all(feature = "sml-structure", feature = "sml-styling"))]
5864 #[test]
5865 fn test_roundtrip_col_outline() {
5866 use std::io::Cursor;
5867
5868 let mut wb = WorkbookBuilder::new();
5869 let sheet = wb.add_sheet("Sheet1");
5870 sheet.set_cell("A1", "Label");
5871 sheet.set_cell("B1", "Detail");
5872 sheet.set_column_outline_level("B", 1);
5873 sheet.set_column_collapsed("B", true);
5874
5875 let mut buffer = Cursor::new(Vec::new());
5876 wb.write(&mut buffer).unwrap();
5877
5878 buffer.set_position(0);
5879 let mut workbook = crate::Workbook::from_reader(buffer).unwrap();
5880 let read_sheet = workbook.resolved_sheet(0).unwrap();
5881
5882 let ws = read_sheet.worksheet();
5883 let col_b = ws
5884 .cols
5885 .iter()
5886 .flat_map(|c| &c.col)
5887 .find(|c| c.start_column <= 2 && 2 <= c.end_column)
5888 .expect("column B should have a definition");
5889 assert_eq!(col_b.outline_level, Some(1), "col B outline level");
5890 assert_eq!(col_b.collapsed, Some(true), "col B collapsed");
5891 }
5892
5893 #[cfg(feature = "sml-validation")]
5898 #[test]
5899 fn test_roundtrip_ignored_errors() {
5900 use std::io::Cursor;
5901
5902 let mut wb = WorkbookBuilder::new();
5903 let sheet = wb.add_sheet("Sheet1");
5904 sheet.set_cell("A1", "123"); sheet.add_ignored_error("A1:A10", IgnoredErrorType::NumberStoredAsText);
5906 sheet.add_ignored_error("B1:B5", IgnoredErrorType::TwoDigitTextYear);
5907
5908 let mut buffer = Cursor::new(Vec::new());
5909 wb.write(&mut buffer).unwrap();
5910
5911 buffer.set_position(0);
5912 let mut workbook = crate::Workbook::from_reader(buffer).unwrap();
5913 let read_sheet = workbook.resolved_sheet(0).unwrap();
5914 let ws = read_sheet.worksheet();
5915
5916 let ie = ws
5917 .ignored_errors
5918 .as_deref()
5919 .expect("ignored_errors should be set");
5920 assert_eq!(ie.ignored_error.len(), 2);
5921
5922 let first = &ie.ignored_error[0];
5923 assert_eq!(first.square_reference.as_str(), "A1:A10");
5924 assert_eq!(first.number_stored_as_text, Some(true));
5925
5926 let second = &ie.ignored_error[1];
5927 assert_eq!(second.square_reference.as_str(), "B1:B5");
5928 assert_eq!(second.two_digit_text_year, Some(true));
5929 }
5930
5931 #[cfg(feature = "sml-styling")]
5936 #[test]
5937 fn test_roundtrip_tab_color() {
5938 use std::io::Cursor;
5939
5940 let mut wb = WorkbookBuilder::new();
5941 let sheet = wb.add_sheet("Red Sheet");
5942 sheet.set_cell("A1", "Hello");
5943 sheet.set_tab_color("FF0000"); let mut buffer = Cursor::new(Vec::new());
5946 wb.write(&mut buffer).unwrap();
5947
5948 buffer.set_position(0);
5949 let mut workbook = crate::Workbook::from_reader(buffer).unwrap();
5950 let read_sheet = workbook.resolved_sheet(0).unwrap();
5951 let ws = read_sheet.worksheet();
5952
5953 let props = ws
5954 .sheet_properties
5955 .as_deref()
5956 .expect("sheet_properties should be set");
5957 let color = props.tab_color.as_deref().expect("tab_color should be set");
5958 assert_eq!(
5960 color.rgb.as_deref(),
5961 Some(&[0xFF_u8, 0xFF, 0x00, 0x00] as &[u8])
5962 );
5963 }
5964
5965 #[cfg(feature = "sml-styling")]
5970 #[test]
5971 fn test_roundtrip_show_gridlines() {
5972 use std::io::Cursor;
5973
5974 let mut wb = WorkbookBuilder::new();
5975 let sheet = wb.add_sheet("Sheet1");
5976 sheet.set_cell("A1", "Hello");
5977 sheet.set_show_gridlines(false);
5978
5979 let mut buffer = Cursor::new(Vec::new());
5980 wb.write(&mut buffer).unwrap();
5981
5982 buffer.set_position(0);
5983 let mut workbook = crate::Workbook::from_reader(buffer).unwrap();
5984 let read_sheet = workbook.resolved_sheet(0).unwrap();
5985 let ws = read_sheet.worksheet();
5986
5987 let sv = ws
5988 .sheet_views
5989 .as_deref()
5990 .and_then(|v| v.sheet_view.first())
5991 .expect("sheet_views should be set");
5992 assert_eq!(sv.show_grid_lines, Some(false));
5993 }
5994
5995 #[cfg(feature = "sml-styling")]
5996 #[test]
5997 fn test_roundtrip_show_row_col_headers() {
5998 use std::io::Cursor;
5999
6000 let mut wb = WorkbookBuilder::new();
6001 let sheet = wb.add_sheet("Sheet1");
6002 sheet.set_cell("A1", "Hello");
6003 sheet.set_show_row_col_headers(false);
6004
6005 let mut buffer = Cursor::new(Vec::new());
6006 wb.write(&mut buffer).unwrap();
6007
6008 buffer.set_position(0);
6009 let mut workbook = crate::Workbook::from_reader(buffer).unwrap();
6010 let read_sheet = workbook.resolved_sheet(0).unwrap();
6011 let ws = read_sheet.worksheet();
6012
6013 let sv = ws
6014 .sheet_views
6015 .as_deref()
6016 .and_then(|v| v.sheet_view.first())
6017 .expect("sheet_views should be set");
6018 assert_eq!(sv.show_row_col_headers, Some(false));
6019 }
6020
6021 #[cfg(all(feature = "sml-styling", feature = "sml-structure"))]
6022 #[test]
6023 fn test_show_gridlines_with_freeze_pane() {
6024 use std::io::Cursor;
6025
6026 let mut wb = WorkbookBuilder::new();
6029 let sheet = wb.add_sheet("Sheet1");
6030 sheet.set_cell("A1", "Header");
6031 sheet.freeze_rows(1);
6032 sheet.set_show_gridlines(false);
6033
6034 let mut buffer = Cursor::new(Vec::new());
6035 wb.write(&mut buffer).unwrap();
6036
6037 buffer.set_position(0);
6038 let mut workbook = crate::Workbook::from_reader(buffer).unwrap();
6039 let read_sheet = workbook.resolved_sheet(0).unwrap();
6040 let ws = read_sheet.worksheet();
6041
6042 let sv = ws
6043 .sheet_views
6044 .as_deref()
6045 .and_then(|v| v.sheet_view.first())
6046 .expect("sheet_views should be set");
6047 assert_eq!(sv.show_grid_lines, Some(false), "gridlines hidden");
6048 let pane = sv.pane.as_deref().expect("freeze pane should be intact");
6050 assert_eq!(pane.y_split, Some(1.0));
6051 }
6052
6053 #[cfg(feature = "sml-protection")]
6058 #[test]
6059 fn test_ooxml_xor_hash_empty() {
6060 let hash = ooxml_xor_hash("");
6062 assert_eq!(hash, vec![0x00, 0x00]);
6063 }
6064
6065 #[cfg(feature = "sml-protection")]
6066 #[test]
6067 fn test_ooxml_xor_hash_known() {
6068 let h1 = ooxml_xor_hash("password");
6073 let h2 = ooxml_xor_hash("password");
6074 assert_eq!(h1, h2, "hash must be deterministic");
6075 assert_ne!(
6076 h1,
6077 vec![0x00, 0x00],
6078 "hash must be non-zero for non-empty password"
6079 );
6080 }
6081
6082 #[cfg(feature = "sml-protection")]
6083 #[test]
6084 fn test_roundtrip_sheet_protection_no_password() {
6085 use std::io::Cursor;
6086
6087 let mut wb = WorkbookBuilder::new();
6088 let sheet = wb.add_sheet("Sheet1");
6089 sheet.set_cell("A1", "Protected");
6090 sheet.set_sheet_protection(SheetProtectionOptions {
6091 sheet: true,
6092 format_cells: true,
6093 insert_rows: true,
6094 ..Default::default()
6095 });
6096
6097 let mut buffer = Cursor::new(Vec::new());
6098 wb.write(&mut buffer).unwrap();
6099
6100 buffer.set_position(0);
6101 let mut workbook = crate::Workbook::from_reader(buffer).unwrap();
6102 let read_sheet = workbook.resolved_sheet(0).unwrap();
6103 let ws = read_sheet.worksheet();
6104
6105 let sp = ws
6106 .sheet_protection
6107 .as_deref()
6108 .expect("sheet_protection should be set");
6109 assert_eq!(sp.sheet, Some(true), "sheet locked");
6110 assert_eq!(sp.format_cells, Some(true), "format_cells locked");
6111 assert_eq!(sp.insert_rows, Some(true), "insert_rows locked");
6112 assert_eq!(sp.password, None, "no password");
6113 }
6114
6115 #[cfg(feature = "sml-protection")]
6116 #[test]
6117 fn test_roundtrip_sheet_protection_with_password() {
6118 use std::io::Cursor;
6119
6120 let mut wb = WorkbookBuilder::new();
6121 let sheet = wb.add_sheet("Sheet1");
6122 sheet.set_cell("A1", "Data");
6123 sheet.set_sheet_protection(SheetProtectionOptions {
6124 sheet: true,
6125 password: Some("secret".to_string()),
6126 ..Default::default()
6127 });
6128
6129 let mut buffer = Cursor::new(Vec::new());
6130 wb.write(&mut buffer).unwrap();
6131
6132 buffer.set_position(0);
6133 let mut workbook = crate::Workbook::from_reader(buffer).unwrap();
6134 let read_sheet = workbook.resolved_sheet(0).unwrap();
6135 let ws = read_sheet.worksheet();
6136
6137 let sp = ws
6138 .sheet_protection
6139 .as_deref()
6140 .expect("sheet_protection should be set");
6141 assert_eq!(sp.sheet, Some(true));
6142 let pw = sp.password.as_ref().expect("password should be set");
6144 assert_eq!(pw.len(), 2);
6145 let expected = ooxml_xor_hash("secret");
6147 assert_eq!(pw, &expected);
6148 }
6149
6150 #[cfg(feature = "sml-comments")]
6155 #[test]
6156 fn test_roundtrip_rich_text_comment() {
6157 use std::io::Cursor;
6158
6159 let mut wb = WorkbookBuilder::new();
6160 let sheet = wb.add_sheet("Sheet1");
6161 sheet.set_cell("A1", "Value");
6162
6163 let mut cb = CommentBuilder::new_rich("A1");
6165 cb.add_run("Bold prefix: ").set_bold(true);
6166 cb.add_run("normal suffix");
6167 sheet.add_comment_builder(cb);
6168
6169 let mut buffer = Cursor::new(Vec::new());
6170 wb.write(&mut buffer).unwrap();
6171
6172 buffer.set_position(0);
6174 let mut workbook = crate::Workbook::from_reader(buffer).unwrap();
6175 let read_sheet = workbook.resolved_sheet(0).unwrap();
6176
6177 let comment = read_sheet.comment("A1").expect("comment should exist");
6178 assert!(
6180 comment.text.contains("Bold prefix:"),
6181 "bold run text present: {:?}",
6182 comment.text
6183 );
6184 assert!(
6185 comment.text.contains("normal suffix"),
6186 "normal run text present: {:?}",
6187 comment.text
6188 );
6189 }
6190
6191 #[cfg(feature = "sml-protection")]
6196 #[test]
6197 fn test_roundtrip_workbook_protection() {
6198 use std::io::Cursor;
6199
6200 let mut wb = WorkbookBuilder::new();
6201 wb.add_sheet("Sheet1");
6202 wb.set_workbook_protection(true, false, Some("wb_pass"));
6203
6204 let mut buffer = Cursor::new(Vec::new());
6205 wb.write(&mut buffer).unwrap();
6206
6207 buffer.set_position(0);
6208 let workbook = crate::Workbook::from_reader(buffer).unwrap();
6209
6210 let wp = workbook
6211 .workbook_protection()
6212 .expect("workbook_protection should be set");
6213 assert_eq!(wp.lock_structure, Some(true), "lock_structure");
6214 assert_eq!(wp.lock_windows, None, "lock_windows not set");
6215 let pw = wp
6216 .workbook_password
6217 .as_ref()
6218 .expect("password should be set");
6219 assert_eq!(pw.len(), 2);
6220 let expected = ooxml_xor_hash("wb_pass");
6221 assert_eq!(pw, &expected);
6222 }
6223
6224 #[cfg(feature = "sml-protection")]
6225 #[test]
6226 fn test_roundtrip_workbook_protection_no_password() {
6227 use std::io::Cursor;
6228
6229 let mut wb = WorkbookBuilder::new();
6230 wb.add_sheet("Sheet1");
6231 wb.set_workbook_protection(true, true, None);
6232
6233 let mut buffer = Cursor::new(Vec::new());
6234 wb.write(&mut buffer).unwrap();
6235
6236 buffer.set_position(0);
6237 let workbook = crate::Workbook::from_reader(buffer).unwrap();
6238
6239 let wp = workbook
6240 .workbook_protection()
6241 .expect("workbook_protection should be set");
6242 assert_eq!(wp.lock_structure, Some(true));
6243 assert_eq!(wp.lock_windows, Some(true));
6244 assert_eq!(wp.workbook_password, None, "no password");
6245 }
6246
6247 #[cfg(feature = "sml-styling")]
6252 #[test]
6253 fn test_add_cell_style_returns_index() {
6254 let mut wb = WorkbookBuilder::new();
6255 wb.add_sheet("Sheet1");
6256
6257 let idx1 = wb.add_cell_style("Good", 0);
6259 assert_eq!(idx1, 1);
6260
6261 let idx2 = wb.add_cell_style("Bad", 0);
6263 assert_eq!(idx2, 2);
6264 }
6265
6266 #[cfg(feature = "sml-styling")]
6267 #[test]
6268 fn test_roundtrip_cell_styles() {
6269 use std::io::Cursor;
6270
6271 let mut wb = WorkbookBuilder::new();
6272 wb.add_sheet("Sheet1");
6273 wb.add_cell_style("Good", 0);
6274 wb.add_cell_style("Neutral", 0);
6275
6276 let mut buffer = Cursor::new(Vec::new());
6277 wb.write(&mut buffer).unwrap();
6278
6279 buffer.set_position(0);
6280 let workbook = crate::Workbook::from_reader(buffer).unwrap();
6281
6282 let stylesheet = workbook.styles();
6284 let cell_styles = stylesheet
6285 .cell_styles
6286 .as_deref()
6287 .expect("cell_styles should be set");
6288 assert_eq!(cell_styles.count, Some(3));
6289 assert_eq!(cell_styles.cell_style[0].name.as_deref(), Some("Normal"));
6290 assert_eq!(cell_styles.cell_style[1].name.as_deref(), Some("Good"));
6291 assert_eq!(cell_styles.cell_style[2].name.as_deref(), Some("Neutral"));
6292 assert_eq!(cell_styles.cell_style[1].custom_builtin, Some(true));
6294 }
6295
6296 #[cfg(feature = "sml-charts")]
6302 #[test]
6303 fn test_embed_chart_creates_drawing_and_chart_parts() {
6304 use std::collections::HashSet;
6305 use std::io::Cursor;
6306
6307 let chart_xml = br#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
6308<c:chartSpace xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart">
6309 <c:chart/>
6310</c:chartSpace>"#;
6311
6312 let mut wb = WorkbookBuilder::new();
6313 let sheet = wb.add_sheet("Sheet1");
6314 sheet.set_cell("A1", "data");
6315 sheet.embed_chart(chart_xml, 0, 5, 8, 15);
6316
6317 let mut buffer = Cursor::new(Vec::new());
6318 wb.write(&mut buffer).unwrap();
6319
6320 buffer.set_position(0);
6322 let mut zip = zip::ZipArchive::new(buffer).unwrap();
6323 let names: HashSet<String> = (0..zip.len())
6324 .map(|i| zip.by_index(i).unwrap().name().to_string())
6325 .collect();
6326
6327 assert!(
6329 names.contains("xl/drawings/drawing1.xml"),
6330 "drawing part missing; parts: {names:?}"
6331 );
6332 assert!(
6333 names.contains("xl/charts/chart1.xml"),
6334 "chart part missing; parts: {names:?}"
6335 );
6336 assert!(
6338 names.contains("xl/drawings/_rels/drawing1.xml.rels"),
6339 "drawing rels missing; parts: {names:?}"
6340 );
6341 assert!(
6343 names.contains("xl/worksheets/_rels/sheet1.xml.rels"),
6344 "sheet rels missing; parts: {names:?}"
6345 );
6346
6347 let drawing_bytes = {
6349 let mut f = zip.by_name("xl/drawings/drawing1.xml").unwrap();
6350 let mut v = Vec::new();
6351 std::io::Read::read_to_end(&mut f, &mut v).unwrap();
6352 v
6353 };
6354 let drawing_str = String::from_utf8_lossy(&drawing_bytes);
6355 assert!(
6356 drawing_str.contains("twoCellAnchor"),
6357 "drawing XML should contain twoCellAnchor"
6358 );
6359 assert!(
6360 drawing_str.contains("rId1"),
6361 "drawing XML should reference rId1 for the chart"
6362 );
6363
6364 let chart_bytes = {
6366 let mut f = zip.by_name("xl/charts/chart1.xml").unwrap();
6367 let mut v = Vec::new();
6368 std::io::Read::read_to_end(&mut f, &mut v).unwrap();
6369 v
6370 };
6371 assert_eq!(chart_bytes, chart_xml);
6372 }
6373
6374 #[cfg(feature = "sml-charts")]
6376 #[test]
6377 fn test_embed_two_charts_same_sheet() {
6378 use std::collections::HashSet;
6379 use std::io::Cursor;
6380
6381 let chart_xml = b"<c:chartSpace/>";
6382
6383 let mut wb = WorkbookBuilder::new();
6384 let sheet = wb.add_sheet("Sheet1");
6385 sheet.embed_chart(chart_xml, 0, 0, 4, 10);
6386 sheet.embed_chart(chart_xml, 5, 0, 4, 10);
6387
6388 let mut buffer = Cursor::new(Vec::new());
6389 wb.write(&mut buffer).unwrap();
6390
6391 buffer.set_position(0);
6392 let mut zip = zip::ZipArchive::new(buffer).unwrap();
6393 let names: HashSet<String> = (0..zip.len())
6394 .map(|i| zip.by_index(i).unwrap().name().to_string())
6395 .collect();
6396
6397 assert!(names.contains("xl/drawings/drawing1.xml"));
6399 assert!(names.contains("xl/charts/chart1.xml"));
6400 assert!(names.contains("xl/charts/chart2.xml"));
6401 assert!(!names.contains("xl/drawings/drawing2.xml"));
6403 }
6404
6405 #[cfg(feature = "sml-charts")]
6407 #[test]
6408 fn test_embed_charts_multiple_sheets() {
6409 use std::collections::HashSet;
6410 use std::io::Cursor;
6411
6412 let chart_xml = b"<c:chartSpace/>";
6413
6414 let mut wb = WorkbookBuilder::new();
6415 let s1 = wb.add_sheet("Sheet1");
6416 s1.embed_chart(chart_xml, 0, 0, 4, 10);
6417 let s2 = wb.add_sheet("Sheet2");
6418 s2.embed_chart(chart_xml, 0, 0, 4, 10);
6419
6420 let mut buffer = Cursor::new(Vec::new());
6421 wb.write(&mut buffer).unwrap();
6422
6423 buffer.set_position(0);
6424 let mut zip = zip::ZipArchive::new(buffer).unwrap();
6425 let names: HashSet<String> = (0..zip.len())
6426 .map(|i| zip.by_index(i).unwrap().name().to_string())
6427 .collect();
6428
6429 assert!(names.contains("xl/drawings/drawing1.xml"));
6430 assert!(names.contains("xl/drawings/drawing2.xml"));
6431 assert!(names.contains("xl/charts/chart1.xml"));
6432 assert!(names.contains("xl/charts/chart2.xml"));
6433 }
6434
6435 #[cfg(feature = "sml-pivot")]
6441 #[test]
6442 fn test_pivot_table_creates_expected_parts() {
6443 use std::collections::HashSet;
6444 use std::io::Cursor;
6445
6446 let mut wb = WorkbookBuilder::new();
6447 let sheet = wb.add_sheet("Sheet1");
6448 sheet.set_cell("A1", "Region");
6449 sheet.set_cell("B1", "Sales");
6450 sheet.add_pivot_table(PivotTableOptions {
6451 name: "PivotTable1".to_string(),
6452 source_ref: "Sheet1!$A$1:$B$5".to_string(),
6453 dest_ref: "D1".to_string(),
6454 row_fields: vec!["Region".to_string()],
6455 col_fields: vec![],
6456 data_fields: vec!["Sales".to_string()],
6457 });
6458
6459 let mut buffer = Cursor::new(Vec::new());
6460 wb.write(&mut buffer).unwrap();
6461
6462 buffer.set_position(0);
6463 let mut zip = zip::ZipArchive::new(buffer).unwrap();
6464 let names: HashSet<String> = (0..zip.len())
6465 .map(|i| zip.by_index(i).unwrap().name().to_string())
6466 .collect();
6467
6468 assert!(
6469 names.contains("xl/pivotCache/pivotCacheDefinition1.xml"),
6470 "pivot cache definition missing; parts: {names:?}"
6471 );
6472 assert!(
6473 names.contains("xl/pivotCache/pivotCacheRecords1.xml"),
6474 "pivot cache records missing; parts: {names:?}"
6475 );
6476 assert!(
6477 names.contains("xl/pivotTables/pivotTable1.xml"),
6478 "pivot table missing; parts: {names:?}"
6479 );
6480 assert!(
6481 names.contains("xl/pivotTables/_rels/pivotTable1.xml.rels"),
6482 "pivot table rels missing; parts: {names:?}"
6483 );
6484 assert!(
6485 names.contains("xl/pivotCache/_rels/pivotCacheDefinition1.xml.rels"),
6486 "pivot cache definition rels missing; parts: {names:?}"
6487 );
6488 assert!(
6489 names.contains("xl/worksheets/_rels/sheet1.xml.rels"),
6490 "sheet rels missing; parts: {names:?}"
6491 );
6492 }
6493
6494 #[cfg(feature = "sml-pivot")]
6496 #[test]
6497 fn test_pivot_table_xml_structure() {
6498 use std::io::Cursor;
6499
6500 let mut wb = WorkbookBuilder::new();
6501 let sheet = wb.add_sheet("Sheet1");
6502 sheet.add_pivot_table(PivotTableOptions {
6503 name: "MySales".to_string(),
6504 source_ref: "Sheet1!$A$1:$C$10".to_string(),
6505 dest_ref: "E1".to_string(),
6506 row_fields: vec!["Region".to_string()],
6507 col_fields: vec!["Quarter".to_string()],
6508 data_fields: vec!["Revenue".to_string()],
6509 });
6510
6511 let mut buffer = Cursor::new(Vec::new());
6512 wb.write(&mut buffer).unwrap();
6513
6514 buffer.set_position(0);
6515 let mut zip = zip::ZipArchive::new(buffer).unwrap();
6516
6517 let pt_bytes = {
6519 let mut f = zip.by_name("xl/pivotTables/pivotTable1.xml").unwrap();
6520 let mut v = Vec::new();
6521 std::io::Read::read_to_end(&mut f, &mut v).unwrap();
6522 v
6523 };
6524 let pt_str = String::from_utf8_lossy(&pt_bytes);
6525 assert!(
6526 pt_str.contains("MySales"),
6527 "should contain pivot table name"
6528 );
6529 assert!(
6530 pt_str.contains("pivotTableDefinition"),
6531 "should have root element"
6532 );
6533 assert!(
6534 pt_str.contains("rowFields"),
6535 "should have rowFields element"
6536 );
6537 assert!(
6538 pt_str.contains("dataFields"),
6539 "should have dataFields element"
6540 );
6541 assert!(
6542 pt_str.contains("Sum of Revenue"),
6543 "should have data field name"
6544 );
6545
6546 let cd_bytes = {
6548 let mut f = zip
6549 .by_name("xl/pivotCache/pivotCacheDefinition1.xml")
6550 .unwrap();
6551 let mut v = Vec::new();
6552 std::io::Read::read_to_end(&mut f, &mut v).unwrap();
6553 v
6554 };
6555 let cd_str = String::from_utf8_lossy(&cd_bytes);
6556 assert!(
6557 cd_str.contains("worksheetSource"),
6558 "should have worksheetSource"
6559 );
6560 assert!(cd_str.contains("Sheet1"), "should reference source sheet");
6561 assert!(cd_str.contains("cacheFields"), "should have cacheFields");
6562 assert!(cd_str.contains("Region"), "should list Region field");
6563 assert!(cd_str.contains("Revenue"), "should list Revenue field");
6564 }
6565
6566 #[cfg(feature = "sml-pivot")]
6568 #[test]
6569 fn test_two_pivot_tables_same_sheet() {
6570 use std::collections::HashSet;
6571 use std::io::Cursor;
6572
6573 let mut wb = WorkbookBuilder::new();
6574 let sheet = wb.add_sheet("Sheet1");
6575 sheet.add_pivot_table(PivotTableOptions {
6576 name: "Pivot1".to_string(),
6577 source_ref: "Sheet1!$A$1:$B$5".to_string(),
6578 dest_ref: "D1".to_string(),
6579 row_fields: vec!["A".to_string()],
6580 col_fields: vec![],
6581 data_fields: vec!["B".to_string()],
6582 });
6583 sheet.add_pivot_table(PivotTableOptions {
6584 name: "Pivot2".to_string(),
6585 source_ref: "Sheet1!$A$1:$B$5".to_string(),
6586 dest_ref: "G1".to_string(),
6587 row_fields: vec!["A".to_string()],
6588 col_fields: vec![],
6589 data_fields: vec!["B".to_string()],
6590 });
6591
6592 let mut buffer = Cursor::new(Vec::new());
6593 wb.write(&mut buffer).unwrap();
6594
6595 buffer.set_position(0);
6596 let mut zip = zip::ZipArchive::new(buffer).unwrap();
6597 let names: HashSet<String> = (0..zip.len())
6598 .map(|i| zip.by_index(i).unwrap().name().to_string())
6599 .collect();
6600
6601 assert!(names.contains("xl/pivotTables/pivotTable1.xml"));
6602 assert!(names.contains("xl/pivotTables/pivotTable2.xml"));
6603 assert!(names.contains("xl/pivotCache/pivotCacheDefinition1.xml"));
6604 assert!(names.contains("xl/pivotCache/pivotCacheDefinition2.xml"));
6605 }
6606}