Skip to main content

sheetkit_core/workbook/
mod.rs

1//! Workbook file I/O: reading and writing `.xlsx` files.
2//!
3//! An `.xlsx` file is a ZIP archive containing XML parts. This module provides
4//! [`Workbook`] which holds the parsed XML structures in memory and can
5//! serialize them back to a valid `.xlsx` file.
6
7use std::collections::{HashMap, HashSet};
8use std::io::{Read as _, Write as _};
9use std::path::Path;
10
11use serde::Serialize;
12use sheetkit_xml::chart::ChartSpace;
13use sheetkit_xml::comments::Comments;
14use sheetkit_xml::content_types::{
15    mime_types, ContentTypeDefault, ContentTypeOverride, ContentTypes,
16};
17use sheetkit_xml::drawing::{MarkerType, WsDr};
18use sheetkit_xml::relationships::{self, rel_types, Relationship, Relationships};
19use sheetkit_xml::shared_strings::Sst;
20use sheetkit_xml::styles::StyleSheet;
21use sheetkit_xml::workbook::{WorkbookProtection, WorkbookXml};
22use sheetkit_xml::worksheet::{Cell, CellFormula, CellTypeTag, DrawingRef, Row, WorksheetXml};
23use zip::write::SimpleFileOptions;
24use zip::CompressionMethod;
25
26use crate::cell::CellValue;
27use crate::cell_ref_shift::shift_cell_references_in_text;
28use crate::chart::ChartConfig;
29use crate::comment::CommentConfig;
30use crate::conditional::ConditionalFormatRule;
31use crate::error::{Error, Result};
32use crate::image::ImageConfig;
33use crate::pivot::{PivotTableConfig, PivotTableInfo};
34use crate::protection::WorkbookProtectionConfig;
35use crate::sst::SharedStringTable;
36use crate::utils::cell_ref::{cell_name_to_coordinates, column_name_to_number};
37use crate::utils::constants::MAX_CELL_CHARS;
38use crate::validation::DataValidationConfig;
39use crate::workbook_paths::{
40    default_relationships, relationship_part_path, relative_relationship_target,
41    resolve_relationship_target,
42};
43
44mod cell_ops;
45mod data;
46mod drawing;
47mod features;
48mod io;
49mod sheet_ops;
50
51/// XML declaration prepended to every XML part in the package.
52const XML_DECLARATION: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#;
53
54/// In-memory representation of an `.xlsx` workbook.
55pub struct Workbook {
56    content_types: ContentTypes,
57    package_rels: Relationships,
58    workbook_xml: WorkbookXml,
59    workbook_rels: Relationships,
60    worksheets: Vec<(String, WorksheetXml)>,
61    stylesheet: StyleSheet,
62    sst_runtime: SharedStringTable,
63    /// Per-sheet comments, parallel to the `worksheets` vector.
64    sheet_comments: Vec<Option<Comments>>,
65    /// Chart parts: (zip path like "xl/charts/chart1.xml", ChartSpace data).
66    charts: Vec<(String, ChartSpace)>,
67    /// Chart parts preserved as raw XML when typed parsing is not supported.
68    raw_charts: Vec<(String, Vec<u8>)>,
69    /// Drawing parts: (zip path like "xl/drawings/drawing1.xml", WsDr data).
70    drawings: Vec<(String, WsDr)>,
71    /// Image parts: (zip path like "xl/media/image1.png", raw bytes).
72    images: Vec<(String, Vec<u8>)>,
73    /// Maps sheet index -> drawing index in `drawings`.
74    #[allow(dead_code)]
75    worksheet_drawings: HashMap<usize, usize>,
76    /// Per-sheet worksheet relationship files.
77    worksheet_rels: HashMap<usize, Relationships>,
78    /// Per-drawing relationship files: drawing_index -> Relationships.
79    drawing_rels: HashMap<usize, Relationships>,
80    /// Core document properties (docProps/core.xml).
81    core_properties: Option<sheetkit_xml::doc_props::CoreProperties>,
82    /// Extended/application properties (docProps/app.xml).
83    app_properties: Option<sheetkit_xml::doc_props::ExtendedProperties>,
84    /// Custom properties (docProps/custom.xml).
85    custom_properties: Option<sheetkit_xml::doc_props::CustomProperties>,
86    /// Pivot table parts: (zip path, PivotTableDefinition data).
87    pivot_tables: Vec<(String, sheetkit_xml::pivot_table::PivotTableDefinition)>,
88    /// Pivot cache definition parts: (zip path, PivotCacheDefinition data).
89    pivot_cache_defs: Vec<(String, sheetkit_xml::pivot_cache::PivotCacheDefinition)>,
90    /// Pivot cache records parts: (zip path, PivotCacheRecords data).
91    pivot_cache_records: Vec<(String, sheetkit_xml::pivot_cache::PivotCacheRecords)>,
92    /// Raw theme XML bytes from xl/theme/theme1.xml (preserved for round-trip).
93    theme_xml: Option<Vec<u8>>,
94    /// Parsed theme colors from the theme XML.
95    theme_colors: sheetkit_xml::theme::ThemeColors,
96    /// Per-sheet sparkline configurations, parallel to the `worksheets` vector.
97    sheet_sparklines: Vec<Vec<crate::sparkline::SparklineConfig>>,
98    /// Per-sheet VML drawing bytes (for legacy comment rendering), parallel to `worksheets`.
99    /// `None` means no VML part exists for that sheet.
100    sheet_vml: Vec<Option<Vec<u8>>>,
101    /// O(1) sheet name -> index lookup cache. Must be kept in sync with
102    /// `worksheets` via [`rebuild_sheet_index`].
103    sheet_name_index: HashMap<String, usize>,
104}
105
106impl Workbook {
107    /// Get the 0-based index of a sheet by name. O(1) via HashMap.
108    pub(crate) fn sheet_index(&self, sheet: &str) -> Result<usize> {
109        self.sheet_name_index
110            .get(sheet)
111            .copied()
112            .ok_or_else(|| Error::SheetNotFound {
113                name: sheet.to_string(),
114            })
115    }
116
117    /// Get a mutable reference to the worksheet XML for the named sheet.
118    pub(crate) fn worksheet_mut(&mut self, sheet: &str) -> Result<&mut WorksheetXml> {
119        let idx = self.sheet_index(sheet)?;
120        Ok(&mut self.worksheets[idx].1)
121    }
122
123    /// Get an immutable reference to the worksheet XML for the named sheet.
124    pub(crate) fn worksheet_ref(&self, sheet: &str) -> Result<&WorksheetXml> {
125        let idx = self.sheet_index(sheet)?;
126        Ok(&self.worksheets[idx].1)
127    }
128
129    /// Public immutable reference to a worksheet's XML by sheet name.
130    pub fn worksheet_xml_ref(&self, sheet: &str) -> Result<&WorksheetXml> {
131        self.worksheet_ref(sheet)
132    }
133
134    /// Public immutable reference to the shared string table.
135    pub fn sst_ref(&self) -> &SharedStringTable {
136        &self.sst_runtime
137    }
138
139    /// Rebuild the sheet name -> index lookup after any structural change
140    /// to the worksheets vector.
141    pub(crate) fn rebuild_sheet_index(&mut self) {
142        self.sheet_name_index.clear();
143        for (i, (name, _)) in self.worksheets.iter().enumerate() {
144            self.sheet_name_index.insert(name.clone(), i);
145        }
146    }
147
148    /// Resolve the part path for a sheet index from workbook relationships.
149    /// Falls back to the default `xl/worksheets/sheet{N}.xml` naming.
150    pub(crate) fn sheet_part_path(&self, sheet_idx: usize) -> String {
151        if let Some(sheet_entry) = self.workbook_xml.sheets.sheets.get(sheet_idx) {
152            if let Some(rel) = self
153                .workbook_rels
154                .relationships
155                .iter()
156                .find(|r| r.id == sheet_entry.r_id && r.rel_type == rel_types::WORKSHEET)
157            {
158                return resolve_relationship_target("xl/workbook.xml", &rel.target);
159            }
160        }
161        format!("xl/worksheets/sheet{}.xml", sheet_idx + 1)
162    }
163}