Skip to main content

ironcalc/import/
mod.rs

1mod conditional_formatting;
2mod metadata;
3pub(crate) mod shared_strings;
4mod styles;
5mod tables;
6mod theme;
7mod util;
8mod workbook;
9mod worksheets;
10
11use std::{
12    collections::HashMap,
13    fs,
14    io::{BufReader, Cursor, Read},
15};
16
17use roxmltree::Node;
18
19use ironcalc_base::{
20    expressions::{
21        parser::{new_parser_english, stringify::to_english_string},
22        types::CellReferenceRC,
23    },
24    types::{Metadata, Workbook, WorkbookSettings, WorkbookView},
25    Model,
26};
27
28use crate::error::XlsxError;
29
30use shared_strings::read_shared_strings;
31
32use metadata::load_metadata;
33use styles::load_styles;
34use util::get_attribute;
35use workbook::load_workbook;
36use worksheets::{load_sheets, Relationship};
37
38fn load_relationships<R: Read + std::io::Seek>(
39    archive: &mut zip::ZipArchive<R>,
40) -> Result<HashMap<String, Relationship>, XlsxError> {
41    let mut file = archive.by_name("xl/_rels/workbook.xml.rels")?;
42    let mut text = String::new();
43    file.read_to_string(&mut text)?;
44    let doc = roxmltree::Document::parse(&text)?;
45    let nodes: Vec<Node> = doc
46        .descendants()
47        .filter(|n| n.has_tag_name("Relationship"))
48        .collect();
49    let mut rels = HashMap::new();
50    for node in nodes {
51        rels.insert(
52            get_attribute(&node, "Id")?.to_string(),
53            Relationship {
54                rel_type: get_attribute(&node, "Type")?.to_string(),
55                target: get_attribute(&node, "Target")?.to_string(),
56            },
57        );
58    }
59    Ok(rels)
60}
61
62fn resolve_theme_path(rels: &HashMap<String, Relationship>) -> Option<String> {
63    let target = rels
64        .values()
65        .find(|r| r.rel_type.ends_with("/theme"))?
66        .target
67        .clone();
68    Some(if let Some(absolute) = target.strip_prefix('/') {
69        absolute.to_string()
70    } else {
71        format!("xl/{target}")
72    })
73}
74
75// FIXME: This is a bit of a HACK. We basically re-parse all the defined names assuming the context is `A1`
76// and there is no tables or defined names.
77fn reparse_formula_hack(formula: &str, worksheets: &[String]) -> Result<String, XlsxError> {
78    let defined_names = Vec::new();
79    let tables = HashMap::new();
80    let mut parser = new_parser_english(worksheets.to_owned(), defined_names, tables);
81    let cell_reference = CellReferenceRC {
82        sheet: worksheets[0].clone(),
83        column: 1,
84        row: 1,
85    };
86    let t = parser.parse(formula, &cell_reference);
87
88    Ok(to_english_string(&t, &cell_reference))
89}
90
91fn load_xlsx_from_reader<R: Read + std::io::Seek>(
92    name: String,
93    reader: R,
94    locale: &str,
95    tz: &str,
96) -> Result<Workbook, XlsxError> {
97    let mut archive = zip::ZipArchive::new(reader)?;
98
99    let mut shared_strings = read_shared_strings(&mut archive)?;
100    let mut workbook = load_workbook(&mut archive)?;
101    let rels = load_relationships(&mut archive)?;
102    let theme_path = resolve_theme_path(&rels);
103    let theme = theme::load(&mut archive, theme_path.as_deref());
104    let mut tables = HashMap::new();
105    // Styles must be loaded before the worksheets: conditional-formatting rules
106    // stored in x14 `extLst` extensions carry inline `<x14:dxf>` formats that we
107    // append to `styles.dxfs`, referencing them back by index from the rule.
108    let mut styles = load_styles(&mut archive, &theme)?;
109    let (worksheets, selected_sheet) = load_sheets(
110        &mut archive,
111        &rels,
112        &workbook,
113        &mut tables,
114        &mut shared_strings,
115        &theme,
116        &mut styles.dxfs,
117    )?;
118    // reparse formulas in defined names, since they may refer to sheets and tables that have been loaded
119    let worksheet_names = worksheets
120        .iter()
121        .map(|s| s.name.clone())
122        .collect::<Vec<_>>();
123    for dn in &mut workbook.defined_names {
124        dn.formula = reparse_formula_hack(&dn.formula, &worksheet_names)?;
125    }
126    let metadata = match load_metadata(&mut archive) {
127        Ok(metadata) => metadata,
128        Err(_) => {
129            // In case there is no metadata, add some
130            Metadata {
131                application: "Unknown application".to_string(),
132                app_version: "".to_string(),
133                creator: "".to_string(),
134                last_modified_by: "".to_string(),
135                created: "".to_string(),
136                last_modified: "".to_string(),
137            }
138        }
139    };
140    let mut views = HashMap::new();
141    views.insert(
142        0,
143        WorkbookView {
144            sheet: selected_sheet,
145            window_width: 800,
146            window_height: 600,
147        },
148    );
149    Ok(Workbook {
150        shared_strings,
151        defined_names: workbook.defined_names,
152        worksheets,
153        styles,
154        name,
155        settings: WorkbookSettings {
156            tz: tz.to_string(),
157            locale: locale.to_string(),
158        },
159        metadata,
160        tables,
161        views,
162        theme,
163    })
164}
165
166// Imports a file from disk into an internal representation
167fn load_from_excel(file_name: &str, locale: &str, tz: &str) -> Result<Workbook, XlsxError> {
168    let file_path = std::path::Path::new(file_name);
169    let file = fs::File::open(file_path)?;
170    let reader = BufReader::new(file);
171    let name = file_path
172        .file_stem()
173        .ok_or_else(|| XlsxError::IO("Could not extract workbook name".to_string()))?
174        .to_string_lossy()
175        .to_string();
176    load_xlsx_from_reader(name, reader, locale, tz)
177}
178
179/// Loads a [Workbook] from the bytes of an xlsx file.
180/// This is useful, for instance, when bytes are transferred over the network
181pub fn load_from_xlsx_bytes(
182    bytes: &[u8],
183    name: &str,
184    locale: &str,
185    tz: &str,
186) -> Result<Workbook, XlsxError> {
187    let cursor = Cursor::new(bytes);
188    let reader = BufReader::new(cursor);
189    load_xlsx_from_reader(name.to_string(), reader, locale, tz)
190}
191
192/// Loads a [Model] from an xlsx file
193pub fn load_from_xlsx<'a>(
194    file_name: &str,
195    locale: &str,
196    tz: &str,
197    language: &'a str,
198) -> Result<Model<'a>, XlsxError> {
199    let workbook = load_from_excel(file_name, locale, tz)?;
200    Model::from_workbook(workbook, language).map_err(XlsxError::Workbook)
201}
202
203/// Loads a [Model] from an `ic` file (a file in the IronCalc internal representation)
204pub fn load_from_icalc<'a>(file_name: &str, language_id: &'a str) -> Result<Model<'a>, XlsxError> {
205    let contents = fs::read(file_name)
206        .map_err(|e| XlsxError::IO(format!("Could not extract workbook name: {e}")))?;
207    let workbook: Workbook = bitcode::decode(&contents)
208        .map_err(|e| XlsxError::IO(format!("Failed to decode file: {e}")))?;
209    Model::from_workbook(workbook, language_id).map_err(XlsxError::Workbook)
210}