Skip to main content

ironcalc/export/
mod.rs

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    // A list of all files in the zip
36    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
63/// Exports a model to an xlsx file
64pub 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    // root folder
87    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    // Package-relationship item
98    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
151/// Exports a model to an icalc file
152pub 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}