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, 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    #[allow(dead_code)]
63    shared_strings: Sst,
64    sst_runtime: SharedStringTable,
65    /// Per-sheet comments, parallel to the `worksheets` vector.
66    sheet_comments: Vec<Option<Comments>>,
67    /// Chart parts: (zip path like "xl/charts/chart1.xml", ChartSpace data).
68    charts: Vec<(String, ChartSpace)>,
69    /// Chart parts preserved as raw XML when typed parsing is not supported.
70    raw_charts: Vec<(String, Vec<u8>)>,
71    /// Drawing parts: (zip path like "xl/drawings/drawing1.xml", WsDr data).
72    drawings: Vec<(String, WsDr)>,
73    /// Image parts: (zip path like "xl/media/image1.png", raw bytes).
74    images: Vec<(String, Vec<u8>)>,
75    /// Maps sheet index -> drawing index in `drawings`.
76    #[allow(dead_code)]
77    worksheet_drawings: HashMap<usize, usize>,
78    /// Per-sheet worksheet relationship files.
79    worksheet_rels: HashMap<usize, Relationships>,
80    /// Per-drawing relationship files: drawing_index -> Relationships.
81    drawing_rels: HashMap<usize, Relationships>,
82    /// Core document properties (docProps/core.xml).
83    core_properties: Option<sheetkit_xml::doc_props::CoreProperties>,
84    /// Extended/application properties (docProps/app.xml).
85    app_properties: Option<sheetkit_xml::doc_props::ExtendedProperties>,
86    /// Custom properties (docProps/custom.xml).
87    custom_properties: Option<sheetkit_xml::doc_props::CustomProperties>,
88    /// Pivot table parts: (zip path, PivotTableDefinition data).
89    pivot_tables: Vec<(String, sheetkit_xml::pivot_table::PivotTableDefinition)>,
90    /// Pivot cache definition parts: (zip path, PivotCacheDefinition data).
91    pivot_cache_defs: Vec<(String, sheetkit_xml::pivot_cache::PivotCacheDefinition)>,
92    /// Pivot cache records parts: (zip path, PivotCacheRecords data).
93    pivot_cache_records: Vec<(String, sheetkit_xml::pivot_cache::PivotCacheRecords)>,
94    /// Raw theme XML bytes from xl/theme/theme1.xml (preserved for round-trip).
95    theme_xml: Option<Vec<u8>>,
96    /// Parsed theme colors from the theme XML.
97    theme_colors: sheetkit_xml::theme::ThemeColors,
98    /// Per-sheet sparkline configurations, parallel to the `worksheets` vector.
99    sheet_sparklines: Vec<Vec<crate::sparkline::SparklineConfig>>,
100    /// Per-sheet VML drawing bytes (for legacy comment rendering), parallel to `worksheets`.
101    /// `None` means no VML part exists for that sheet.
102    sheet_vml: Vec<Option<Vec<u8>>>,
103}
104
105impl Workbook {
106    /// Get the 0-based index of a sheet by name.
107    pub(crate) fn sheet_index(&self, sheet: &str) -> Result<usize> {
108        self.worksheets
109            .iter()
110            .position(|(name, _)| name == sheet)
111            .ok_or_else(|| Error::SheetNotFound {
112                name: sheet.to_string(),
113            })
114    }
115
116    /// Get a mutable reference to the worksheet XML for the named sheet.
117    pub(crate) fn worksheet_mut(&mut self, sheet: &str) -> Result<&mut WorksheetXml> {
118        self.worksheets
119            .iter_mut()
120            .find(|(name, _)| name == sheet)
121            .map(|(_, ws)| ws)
122            .ok_or_else(|| Error::SheetNotFound {
123                name: sheet.to_string(),
124            })
125    }
126
127    /// Get an immutable reference to the worksheet XML for the named sheet.
128    pub(crate) fn worksheet_ref(&self, sheet: &str) -> Result<&WorksheetXml> {
129        self.worksheets
130            .iter()
131            .find(|(name, _)| name == sheet)
132            .map(|(_, ws)| ws)
133            .ok_or_else(|| Error::SheetNotFound {
134                name: sheet.to_string(),
135            })
136    }
137
138    /// Resolve the part path for a sheet index from workbook relationships.
139    /// Falls back to the default `xl/worksheets/sheet{N}.xml` naming.
140    pub(crate) fn sheet_part_path(&self, sheet_idx: usize) -> String {
141        if let Some(sheet_entry) = self.workbook_xml.sheets.sheets.get(sheet_idx) {
142            if let Some(rel) = self
143                .workbook_rels
144                .relationships
145                .iter()
146                .find(|r| r.id == sheet_entry.r_id && r.rel_type == rel_types::WORKSHEET)
147            {
148                return resolve_relationship_target("xl/workbook.xml", &rel.target);
149            }
150        }
151        format!("xl/worksheets/sheet{}.xml", sheet_idx + 1)
152    }
153}