use crate::common::xml_utils::{ XmlElement, XmlGenerator };
use crate::common::zip_utils::ZipWriter;
use crate::context::ErrorContext;
use crate::error::Result;
use crate::xlsx::{ Cell, CellValue, Workbook, Worksheet };
use std::collections::HashMap;
use std::fs::File;
use std::path::Path;
pub struct XlsxWriter {
zip_writer: ZipWriter<File>,
shared_strings: Vec<String>,
shared_strings_map: HashMap<String, usize>,
}
impl XlsxWriter {
pub fn create<P: AsRef<Path>>(path: P) -> Result<Self> {
let zip_writer = ZipWriter::create_file(&path).map_err(|e| {
e.with_context(ErrorContext {
operation: Some("创建XLSX文件".to_string()),
file_path: Some(path.as_ref().to_string_lossy().to_string()),
..Default::default()
})
})?;
Ok(Self {
zip_writer,
shared_strings: Vec::new(),
shared_strings_map: HashMap::new(),
})
}
pub fn write_workbook(&mut self, workbook: &Workbook) -> Result<()> {
self.collect_shared_strings(workbook)?;
self.write_content_types()?;
self.write_app_properties()?;
self.write_core_properties()?;
self.write_relationships()?;
self.write_workbook_relationships(workbook)?;
if !self.shared_strings.is_empty() {
self.write_shared_strings()?;
}
self.write_workbook_xml(workbook)?;
for (index, worksheet) in workbook.worksheets().enumerate() {
self.write_worksheet(worksheet, index + 1)?;
}
Ok(())
}
fn collect_shared_strings(&mut self, workbook: &Workbook) -> Result<()> {
for worksheet in workbook.worksheets() {
if let Some(dim) = worksheet.dimension() {
for row in 0..=dim.max_row {
for col in 0..=dim.max_column {
if let Some(cell) = worksheet.get_cell(row, col) {
if let CellValue::Text(text) = &cell.value {
if !self.shared_strings_map.contains_key(text) {
let index = self.shared_strings.len();
self.shared_strings.push(text.clone());
self.shared_strings_map.insert(text.clone(), index);
}
}
}
}
}
}
}
Ok(())
}
fn write_content_types(&mut self) -> Result<()> {
let mut root = XmlElement::new("Types");
root.add_attribute("xmlns", "http://schemas.openxmlformats.org/package/2006/content-types");
let mut default_rels = XmlElement::new("Default");
default_rels.add_attribute("Extension", "rels");
default_rels.add_attribute(
"ContentType",
"application/vnd.openxmlformats-package.relationships+xml"
);
root.add_child(default_rels);
let mut default_xml = XmlElement::new("Default");
default_xml.add_attribute("Extension", "xml");
default_xml.add_attribute("ContentType", "application/xml");
root.add_child(default_xml);
let mut override_workbook = XmlElement::new("Override");
override_workbook.add_attribute("PartName", "/xl/workbook.xml");
override_workbook.add_attribute(
"ContentType",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"
);
root.add_child(override_workbook);
if !self.shared_strings.is_empty() {
let mut override_shared_strings = XmlElement::new("Override");
override_shared_strings.add_attribute("PartName", "/xl/sharedStrings.xml");
override_shared_strings.add_attribute(
"ContentType",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"
);
root.add_child(override_shared_strings);
}
let mut override_app = XmlElement::new("Override");
override_app.add_attribute("PartName", "/docProps/app.xml");
override_app.add_attribute(
"ContentType",
"application/vnd.openxmlformats-officedocument.extended-properties+xml"
);
root.add_child(override_app);
let mut override_core = XmlElement::new("Override");
override_core.add_attribute("PartName", "/docProps/core.xml");
override_core.add_attribute(
"ContentType",
"application/vnd.openxmlformats-package.core-properties+xml"
);
root.add_child(override_core);
let generator = XmlGenerator::new();
let xml_content = generator.generate_string(&root)?;
self.zip_writer.add_file_from_string("[Content_Types].xml", &xml_content)?;
Ok(())
}
fn write_app_properties(&mut self) -> Result<()> {
let mut root = XmlElement::new("Properties");
root.add_attribute(
"xmlns",
"http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
);
root.add_attribute(
"xmlns:vt",
"http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"
);
let mut application = XmlElement::new("Application");
application.set_text_content("office-rs");
root.add_child(application);
let mut doc_security = XmlElement::new("DocSecurity");
doc_security.set_text_content("0");
root.add_child(doc_security);
let mut scale_crop = XmlElement::new("ScaleCrop");
scale_crop.set_text_content("false");
root.add_child(scale_crop);
let generator = XmlGenerator::new();
let xml_content = generator.generate_string(&root)?;
self.zip_writer.add_file_from_string("docProps/app.xml", &xml_content)?;
Ok(())
}
fn write_core_properties(&mut self) -> Result<()> {
let mut root = XmlElement::new("cp:coreProperties");
root.add_attribute(
"xmlns:cp",
"http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
);
root.add_attribute("xmlns:dc", "http://purl.org/dc/elements/1.1/");
root.add_attribute("xmlns:dcterms", "http://purl.org/dc/terms/");
root.add_attribute("xmlns:dcmitype", "http://purl.org/dc/dcmitype/");
root.add_attribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
let mut creator = XmlElement::new("dc:creator");
creator.set_text_content("office-rs");
root.add_child(creator);
let mut created = XmlElement::new("dcterms:created");
created.add_attribute("xsi:type", "dcterms:W3CDTF");
created.set_text_content(chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string());
root.add_child(created);
let generator = XmlGenerator::new();
let xml_content = generator.generate_string(&root)?;
self.zip_writer.add_file_from_string("docProps/core.xml", &xml_content)?;
Ok(())
}
fn write_relationships(&mut self) -> Result<()> {
let mut root = XmlElement::new("Relationships");
root.add_attribute("xmlns", "http://schemas.openxmlformats.org/package/2006/relationships");
let mut rel_workbook = XmlElement::new("Relationship");
rel_workbook.add_attribute("Id", "rId1");
rel_workbook.add_attribute(
"Type",
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
);
rel_workbook.add_attribute("Target", "xl/workbook.xml");
root.add_child(rel_workbook);
let mut rel_core = XmlElement::new("Relationship");
rel_core.add_attribute("Id", "rId2");
rel_core.add_attribute(
"Type",
"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties"
);
rel_core.add_attribute("Target", "docProps/core.xml");
root.add_child(rel_core);
let mut rel_app = XmlElement::new("Relationship");
rel_app.add_attribute("Id", "rId3");
rel_app.add_attribute(
"Type",
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties"
);
rel_app.add_attribute("Target", "docProps/app.xml");
root.add_child(rel_app);
let generator = XmlGenerator::new();
let xml_content = generator.generate_string(&root)?;
self.zip_writer.add_file_from_string("_rels/.rels", &xml_content)?;
Ok(())
}
fn write_workbook_relationships(&mut self, workbook: &Workbook) -> Result<()> {
let mut root = XmlElement::new("Relationships");
root.add_attribute("xmlns", "http://schemas.openxmlformats.org/package/2006/relationships");
let mut rel_id = 1;
for (index, _) in workbook.worksheets().enumerate() {
let mut rel_worksheet = XmlElement::new("Relationship");
rel_worksheet.add_attribute("Id", format!("rId{}", rel_id));
rel_worksheet.add_attribute(
"Type",
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"
);
rel_worksheet.add_attribute("Target", format!("worksheets/sheet{}.xml", index + 1));
root.add_child(rel_worksheet);
rel_id += 1;
}
if !self.shared_strings.is_empty() {
let mut rel_shared_strings = XmlElement::new("Relationship");
rel_shared_strings.add_attribute("Id", format!("rId{}", rel_id));
rel_shared_strings.add_attribute(
"Type",
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"
);
rel_shared_strings.add_attribute("Target", "sharedStrings.xml");
root.add_child(rel_shared_strings);
}
let generator = XmlGenerator::new();
let xml_content = generator.generate_string(&root)?;
self.zip_writer.add_file_from_string("xl/_rels/workbook.xml.rels", &xml_content)?;
Ok(())
}
fn write_shared_strings(&mut self) -> Result<()> {
let mut root = XmlElement::new("sst");
root.add_attribute("xmlns", "http://schemas.openxmlformats.org/spreadsheetml/2006/main");
root.add_attribute("count", self.shared_strings.len().to_string());
root.add_attribute("uniqueCount", self.shared_strings.len().to_string());
for string in &self.shared_strings {
let mut si = XmlElement::new("si");
let mut t = XmlElement::new("t");
t.set_text_content(string.clone());
si.add_child(t);
root.add_child(si);
}
let generator = XmlGenerator::new();
let xml_content = generator.generate_string(&root)?;
self.zip_writer.add_file_from_string("xl/sharedStrings.xml", &xml_content)?;
Ok(())
}
fn write_workbook_xml(&mut self, workbook: &Workbook) -> Result<()> {
let mut root = XmlElement::new("workbook");
root.add_attribute("xmlns", "http://schemas.openxmlformats.org/spreadsheetml/2006/main");
root.add_attribute(
"xmlns:r",
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
);
let mut file_version = XmlElement::new("fileVersion");
file_version.add_attribute("appName", "xl");
file_version.add_attribute("lastEdited", "7");
file_version.add_attribute("lowestEdited", "7");
file_version.add_attribute("rupBuild", "24816");
root.add_child(file_version);
let mut workbook_pr = XmlElement::new("workbookPr");
workbook_pr.add_attribute("defaultThemeVersion", "166925");
root.add_child(workbook_pr);
let mut sheets = XmlElement::new("sheets");
for (index, worksheet) in workbook.worksheets().enumerate() {
let mut sheet = XmlElement::new("sheet");
sheet.add_attribute("name", &worksheet.properties.name);
sheet.add_attribute("sheetId", &(index + 1).to_string());
sheet.add_attribute("r:id", &format!("rId{}", index + 1));
sheets.add_child(sheet);
}
root.add_child(sheets);
let generator = XmlGenerator::new();
let xml_content = generator.generate_string(&root)?;
self.zip_writer.add_file_from_string("xl/workbook.xml", &xml_content)?;
Ok(())
}
fn write_worksheet(&mut self, worksheet: &Worksheet, sheet_id: usize) -> Result<()> {
let mut root = XmlElement::new("worksheet");
root.add_attribute("xmlns", "http://schemas.openxmlformats.org/spreadsheetml/2006/main");
root.add_attribute(
"xmlns:r",
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
);
let mut sheet_pr = XmlElement::new("sheetPr");
sheet_pr.add_attribute("tabColor", "auto");
root.add_child(sheet_pr);
if let Some(dim) = worksheet.dimension() {
if dim.max_row > 0 && dim.max_column > 0 {
let mut dimension = XmlElement::new("dimension");
let range = format!(
"{}:{}",
self.cell_reference(1, 1),
self.cell_reference(dim.max_row + 1, dim.max_column + 1)
);
dimension.add_attribute("ref", range);
root.add_child(dimension);
}
}
let mut sheet_views = XmlElement::new("sheetViews");
let mut sheet_view = XmlElement::new("sheetView");
sheet_view.add_attribute("tabSelected", "1");
sheet_view.add_attribute("workbookViewId", "0");
sheet_views.add_child(sheet_view);
root.add_child(sheet_views);
let mut sheet_format_pr = XmlElement::new("sheetFormatPr");
sheet_format_pr.add_attribute("defaultRowHeight", "15");
root.add_child(sheet_format_pr);
let mut sheet_data = XmlElement::new("sheetData");
if let Some(dim) = worksheet.dimension() {
for row_idx in 0..=dim.max_row {
let mut has_data = false;
let mut row = XmlElement::new("row");
let row_num = row_idx + 1; row.add_attribute("r", row_num.to_string());
for col_idx in 0..=dim.max_column {
if let Some(cell) = worksheet.get_cell(row_idx, col_idx) {
if !matches!(cell.value, CellValue::Empty) {
let col_num = col_idx + 1; let cell_element = self.create_cell_element(cell, row_num, col_num)?;
row.add_child(cell_element);
has_data = true;
}
}
}
if has_data {
sheet_data.add_child(row);
}
}
}
root.add_child(sheet_data);
let generator = XmlGenerator::new();
let xml_content = generator.generate_string(&root)?;
let file_path = format!("xl/worksheets/sheet{}.xml", sheet_id);
self.zip_writer.add_file_from_string(&file_path, &xml_content)?;
Ok(())
}
fn create_cell_element(&self, cell: &Cell, row: u32, col: u32) -> Result<XmlElement> {
let mut cell_element = XmlElement::new("c");
cell_element.add_attribute("r", self.cell_reference(row, col));
match &cell.value {
CellValue::Text(text) => {
cell_element.add_attribute("t", "s");
let mut v = XmlElement::new("v");
if let Some(&index) = self.shared_strings_map.get(text) {
v.set_text_content(index.to_string());
}
cell_element.add_child(v);
}
CellValue::Number(num) => {
let mut v = XmlElement::new("v");
v.set_text_content(num.to_string());
cell_element.add_child(v);
}
CellValue::Boolean(b) => {
cell_element.add_attribute("t", "b");
let mut v = XmlElement::new("v");
v.set_text_content((if *b { "1" } else { "0" }).to_string());
cell_element.add_child(v);
}
CellValue::Formula(formula) => {
let mut f = XmlElement::new("f");
f.set_text_content(formula.clone());
cell_element.add_child(f);
}
CellValue::Empty => {
}
CellValue::DateTime(dt) => {
cell_element.add_attribute("t", "n");
let mut v = XmlElement::new("v");
v.set_text_content(dt.to_string());
cell_element.add_child(v);
}
CellValue::Error(err) => {
cell_element.add_attribute("t", "e");
let mut v = XmlElement::new("v");
v.set_text_content(err.clone());
cell_element.add_child(v);
}
}
Ok(cell_element)
}
fn cell_reference(&self, row: u32, col: u32) -> String {
let mut col_str = String::new();
let mut col_num = col;
while col_num > 0 {
col_num -= 1;
col_str.insert(0, (b'A' + ((col_num % 26) as u8)) as char);
col_num /= 26;
}
format!("{}{}", col_str, row)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::xlsx::CellValue;
struct TestXlsxWriter {
shared_strings: Vec<String>,
shared_strings_map: HashMap<String, usize>,
}
impl TestXlsxWriter {
fn new() -> Self {
Self {
shared_strings: Vec::new(),
shared_strings_map: HashMap::new(),
}
}
fn cell_reference(&self, row: u32, col: u32) -> String {
let mut col_str = String::new();
let mut col_num = col;
while col_num > 0 {
col_num -= 1;
col_str.insert(0, (b'A' + ((col_num % 26) as u8)) as char);
col_num /= 26;
}
format!("{}{}", col_str, row)
}
}
#[test]
fn test_cell_reference() {
let writer = TestXlsxWriter::new();
assert_eq!(writer.cell_reference(1, 1), "A1");
assert_eq!(writer.cell_reference(2, 2), "B2");
assert_eq!(writer.cell_reference(26, 26), "Z26");
assert_eq!(writer.cell_reference(27, 27), "AA27");
}
#[test]
fn test_shared_strings_collection() {
let temp_file = std::env::temp_dir().join("test_xlsx_writer.xlsx");
let mut writer = XlsxWriter::create(&temp_file).unwrap();
let mut workbook = Workbook::new();
if let Some(ws) = workbook.get_worksheet_mut("Sheet1") {
ws.set_cell_value(1, 1, CellValue::Text("Hello".to_string()));
ws.set_cell_value(1, 2, CellValue::Text("World".to_string()));
ws.set_cell_value(2, 1, CellValue::Text("Hello".to_string())); }
writer.collect_shared_strings(&workbook).unwrap();
assert_eq!(writer.shared_strings.len(), 2); assert!(writer.shared_strings.contains(&"Hello".to_string()));
assert!(writer.shared_strings.contains(&"World".to_string()));
let _ = std::fs::remove_file(&temp_file);
}
}