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 for (sheet_index, worksheet) in workbook.worksheets.iter().enumerate() {
123 let id = sheet_index + 1;
124 zip.start_file(format!("xl/worksheets/sheet{id}.xml"), options)?;
125 let dimension = model
126 .workbook
127 .worksheet(sheet_index as u32)
128 .unwrap()
129 .dimension();
130 let column_min_str = number_to_column(dimension.min_column).unwrap();
131 let column_max_str = number_to_column(dimension.max_column).unwrap();
132 let min_row = dimension.min_row;
133 let max_row = dimension.max_row;
134 let sheet_dimension_str = &format!("{column_min_str}{min_row}:{column_max_str}{max_row}");
135 let is_sheet_selected = selected_sheet as usize == sheet_index;
136 zip.write_all(
137 worksheets::get_worksheet_xml(
138 worksheet,
139 &model.parsed_formulas[sheet_index],
140 sheet_dimension_str,
141 is_sheet_selected,
142 )
143 .as_bytes(),
144 )?;
145 }
146
147 let writer = zip.finish()?;
148 Ok(writer)
149}
150
151pub fn save_to_icalc(model: &Model, file_name: &str) -> Result<(), XlsxError> {
153 let file_path = std::path::Path::new(&file_name);
154 if file_path.exists() {
155 return Err(XlsxError::IO(format!("file {file_name} already exists")));
156 }
157 let s = bitcode::encode(&model.workbook);
158 let mut file = fs::File::create(file_path)?;
159 file.write_all(&s)?;
160
161 Ok(())
162}