1#![allow(clippy::unwrap_used)]
2
3mod _rels;
4mod conditional_formatting;
5mod doc_props;
6mod dxfs_styles;
7mod escape;
8mod shared_strings;
9mod styles;
10mod styles_util;
11mod theme;
12mod workbook;
13mod workbook_xml_rels;
14mod worksheets;
15mod xml_constants;
16
17use std::io::BufWriter;
18use std::{
19 fs,
20 io::{Seek, Write},
21};
22
23use ironcalc_base::expressions::utils::number_to_column;
24use ironcalc_base::types::Workbook;
25use ironcalc_base::{get_milliseconds_since_epoch, Model};
26
27use self::xml_constants::XML_DECLARATION;
28
29use crate::error::XlsxError;
30
31#[cfg(test)]
32mod test;
33
34fn get_content_types_xml(workbook: &Workbook) -> String {
35 let mut content = vec![
37 r#"<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">"#.to_string(),
38 r#"<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>"#.to_string(),
39 r#"<Default Extension="xml" ContentType="application/xml"/>"#.to_string(),
40 r#"<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>"#.to_string(),
41 ];
42 for worksheet in 0..workbook.worksheets.len() {
43 let sheet = format!(
44 r#"<Override PartName="/xl/worksheets/sheet{}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>"#,
45 worksheet + 1
46 );
47 content.push(sheet);
48 }
49 content.push(
50 r#"<Override PartName="/xl/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>"#.to_string(),
51 );
52 content.extend([
53 r#"<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>"#.to_string(),
54 r#"<Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/>"#.to_string(),
55 r#"<Override PartName="/xl/metadata.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml"/>"#.to_string(),
56 r#"<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>"#.to_string(),
57 r#"<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>"#.to_string(),
58 r#"</Types>"#.to_string(),
59 ]);
60 format!("{XML_DECLARATION}\n{}", content.join(""))
61}
62
63pub fn save_to_xlsx(model: &Model, file_name: &str) -> Result<(), XlsxError> {
65 let file_path = std::path::Path::new(&file_name);
66 if file_path.exists() {
67 return Err(XlsxError::IO(format!("file {file_name} already exists")));
68 }
69 let file = fs::File::create(file_path).unwrap();
70 let writer = BufWriter::new(file);
71 save_xlsx_to_writer(model, writer)?;
72
73 Ok(())
74}
75
76pub fn save_xlsx_to_writer<W: Write + Seek>(model: &Model, writer: W) -> Result<W, XlsxError> {
77 let workbook = &model.workbook;
78 let selected_sheet = match workbook.views.get(&0) {
79 Some(view) => view.sheet,
80 _ => 0,
81 };
82 let mut zip = zip::ZipWriter::new(writer);
83
84 let options = zip::write::FileOptions::default();
85
86 zip.start_file("[Content_Types].xml", options)?;
88 zip.write_all(get_content_types_xml(workbook).as_bytes())?;
89
90 zip.add_directory("docProps", options)?;
91 zip.start_file("docProps/app.xml", options)?;
92 zip.write_all(doc_props::get_app_xml(workbook).as_bytes())?;
93 zip.start_file("docProps/core.xml", options)?;
94 let milliseconds = get_milliseconds_since_epoch();
95 zip.write_all(doc_props::get_core_xml(workbook, milliseconds)?.as_bytes())?;
96
97 zip.add_directory("_rels", options)?;
99 zip.start_file("_rels/.rels", options)?;
100 zip.write_all(_rels::get_dot_rels(workbook).as_bytes())?;
101
102 zip.add_directory("xl", options)?;
103 zip.start_file("xl/sharedStrings.xml", options)?;
104 zip.write_all(shared_strings::get_shared_strings_xml(workbook).as_bytes())?;
105 zip.start_file("xl/styles.xml", options)?;
106 zip.write_all(styles::get_styles_xml(workbook).as_bytes())?;
107 zip.start_file("xl/workbook.xml", options)?;
108 zip.write_all(workbook::get_workbook_xml(workbook, selected_sheet).as_bytes())?;
109 zip.start_file("xl/metadata.xml", options)?;
110 let metadata_xml = include_str!("metadata.xml");
111 zip.write_all(metadata_xml.as_bytes())?;
112
113 zip.add_directory("xl/theme", options)?;
114 zip.start_file("xl/theme/theme1.xml", options)?;
115 zip.write_all(theme::get_theme_xml(&workbook.theme).as_bytes())?;
116
117 zip.add_directory("xl/_rels", options)?;
118 zip.start_file("xl/_rels/workbook.xml.rels", options)?;
119 zip.write_all(workbook_xml_rels::get_workbook_xml_rels(workbook).as_bytes())?;
120
121 zip.add_directory("xl/worksheets", options)?;
122 let mut has_worksheet_rels = false;
124 for (sheet_index, worksheet) in workbook.worksheets.iter().enumerate() {
125 if let Some(rels_xml) = worksheets::get_worksheet_xml_rels(worksheet) {
126 if !has_worksheet_rels {
127 zip.add_directory("xl/worksheets/_rels", options)?;
128 has_worksheet_rels = true;
129 }
130 zip.start_file(
131 format!("xl/worksheets/_rels/sheet{}.xml.rels", sheet_index + 1),
132 options,
133 )?;
134 zip.write_all(rels_xml.as_bytes())?;
135 }
136 }
137 for (sheet_index, worksheet) in workbook.worksheets.iter().enumerate() {
138 let id = sheet_index + 1;
139 zip.start_file(format!("xl/worksheets/sheet{id}.xml"), options)?;
140 let dimension = model
141 .workbook
142 .worksheet(sheet_index as u32)
143 .unwrap()
144 .dimension();
145 let column_min_str = number_to_column(dimension.min_column).unwrap();
146 let column_max_str = number_to_column(dimension.max_column).unwrap();
147 let min_row = dimension.min_row;
148 let max_row = dimension.max_row;
149 let sheet_dimension_str = &format!("{column_min_str}{min_row}:{column_max_str}{max_row}");
150 let is_sheet_selected = selected_sheet as usize == sheet_index;
151 zip.write_all(
152 worksheets::get_worksheet_xml(
153 worksheet,
154 &model.parsed_formulas[sheet_index],
155 sheet_dimension_str,
156 is_sheet_selected,
157 )
158 .as_bytes(),
159 )?;
160 }
161
162 let writer = zip.finish()?;
163 Ok(writer)
164}
165
166pub fn save_to_icalc(model: &Model, file_name: &str) -> Result<(), XlsxError> {
168 let file_path = std::path::Path::new(&file_name);
169 if file_path.exists() {
170 return Err(XlsxError::IO(format!("file {file_name} already exists")));
171 }
172 let s = bitcode::encode(&model.workbook);
173 let mut file = fs::File::create(file_path)?;
174 file.write_all(&s)?;
175
176 Ok(())
177}