use crate::common::{ ZipEntry, ZipReader, ZipWriter };
use crate::error::{ DocumentError, Result, XlsxError };
use crate::xlsx::{
Cell,
CellReference,
CellValue,
Workbook,
Worksheet,
reader::XlsxReader,
writer::XlsxWriter,
};
use std::collections::HashMap;
use std::io::{ Cursor, Read, Write };
use std::path::Path;
pub struct XlsxModifier {
original_entries: HashMap<String, Vec<u8>>,
workbook: Workbook,
modified_entries: HashMap<String, bool>,
}
#[derive(Debug, Clone)]
pub enum ModifyOperation {
SetCellValue {
sheet_name: String,
cell_ref: CellReference,
value: CellValue,
},
InsertRow {
sheet_name: String,
row_index: u32,
count: u32,
},
DeleteRow {
sheet_name: String,
row_index: u32,
count: u32,
},
InsertColumn {
sheet_name: String,
column_index: u32,
count: u32,
},
DeleteColumn {
sheet_name: String,
column_index: u32,
count: u32,
},
AddWorksheet {
name: String,
},
DeleteWorksheet {
name: String,
},
RenameWorksheet {
old_name: String,
new_name: String,
},
}
#[derive(Debug, Clone)]
pub struct ModifyOptions {
pub preserve_formatting: bool,
pub preserve_formulas: bool,
pub preserve_charts: bool,
pub preserve_macros: bool,
pub preserve_data_validation: bool,
}
impl Default for ModifyOptions {
fn default() -> Self {
Self {
preserve_formatting: true,
preserve_formulas: true,
preserve_charts: true,
preserve_macros: true,
preserve_data_validation: true,
}
}
}
impl XlsxModifier {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let mut zip_reader = ZipReader::new(path)?;
let mut original_entries = HashMap::new();
for entry_name in zip_reader.entry_names()? {
let mut content = Vec::new();
zip_reader.read_entry(&entry_name, &mut content)?;
original_entries.insert(entry_name, content);
}
let workbook = {
let reader = XlsxReader::new();
reader.read_from_zip_entries(&original_entries)?
};
Ok(Self {
original_entries,
workbook,
modified_entries: HashMap::new(),
})
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
let cursor = Cursor::new(data);
let mut zip_reader = ZipReader::from_reader(cursor)?;
let mut original_entries = HashMap::new();
for entry_name in zip_reader.entry_names()? {
let mut content = Vec::new();
zip_reader.read_entry(&entry_name, &mut content)?;
original_entries.insert(entry_name, content);
}
let workbook = {
let reader = XlsxReader::new();
reader.read_from_zip_entries(&original_entries)?
};
Ok(Self {
original_entries,
workbook,
modified_entries: HashMap::new(),
})
}
pub fn workbook(&self) -> &Workbook {
&self.workbook
}
pub fn workbook_mut(&mut self) -> &mut Workbook {
&mut self.workbook
}
pub fn apply_operation(&mut self, operation: ModifyOperation) -> Result<()> {
match operation {
ModifyOperation::SetCellValue { sheet_name, cell_ref, value } =>
self.set_cell_value(&sheet_name, cell_ref, value)?,
ModifyOperation::InsertRow { sheet_name, row_index, count } =>
self.insert_rows(&sheet_name, row_index, count)?,
ModifyOperation::DeleteRow { sheet_name, row_index, count } =>
self.delete_rows(&sheet_name, row_index, count)?,
ModifyOperation::InsertColumn { sheet_name, column_index, count } =>
self.insert_columns(&sheet_name, column_index, count)?,
ModifyOperation::DeleteColumn { sheet_name, column_index, count } =>
self.delete_columns(&sheet_name, column_index, count)?,
ModifyOperation::AddWorksheet { name } => self.add_worksheet(name)?,
ModifyOperation::DeleteWorksheet { name } => self.delete_worksheet(&name)?,
ModifyOperation::RenameWorksheet { old_name, new_name } => {
self.rename_worksheet(&old_name, &new_name)?;
}
}
Ok(())
}
pub fn apply_operations(&mut self, operations: Vec<ModifyOperation>) -> Result<()> {
for operation in operations {
self.apply_operation(operation)?;
}
Ok(())
}
pub fn set_cell_value(
&mut self,
sheet_name: &str,
cell_ref: CellReference,
value: CellValue
) -> Result<()> {
let sheet = self.workbook.get_worksheet_mut(sheet_name).ok_or_else(|| {
DocumentError::Xlsx(XlsxError::WorksheetNotFound {
name: sheet_name.to_string(),
})
})?;
sheet.set_cell_value(cell_ref.row, cell_ref.column, value);
self.mark_sheet_modified(sheet_name);
Ok(())
}
pub fn insert_rows(&mut self, sheet_name: &str, row_index: u32, count: u32) -> Result<()> {
let sheet = self.workbook.get_worksheet_mut(sheet_name).ok_or_else(|| {
DocumentError::Xlsx(XlsxError::WorksheetNotFound {
name: sheet_name.to_string(),
})
})?;
sheet.insert_rows(row_index, count)?;
self.mark_sheet_modified(sheet_name);
Ok(())
}
pub fn delete_rows(&mut self, sheet_name: &str, row_index: u32, count: u32) -> Result<()> {
let sheet = self.workbook.get_worksheet_mut(sheet_name).ok_or_else(|| {
DocumentError::Xlsx(XlsxError::WorksheetNotFound {
name: sheet_name.to_string(),
})
})?;
sheet.delete_rows(row_index, count)?;
self.mark_sheet_modified(sheet_name);
Ok(())
}
pub fn insert_columns(
&mut self,
sheet_name: &str,
column_index: u32,
count: u32
) -> Result<()> {
let sheet = self.workbook.get_worksheet_mut(sheet_name).ok_or_else(|| {
DocumentError::Xlsx(XlsxError::WorksheetNotFound {
name: sheet_name.to_string(),
})
})?;
sheet.insert_columns(column_index, count)?;
self.mark_sheet_modified(sheet_name);
Ok(())
}
pub fn delete_columns(
&mut self,
sheet_name: &str,
column_index: u32,
count: u32
) -> Result<()> {
let sheet = self.workbook.get_worksheet_mut(sheet_name).ok_or_else(|| {
DocumentError::Xlsx(XlsxError::WorksheetNotFound {
name: sheet_name.to_string(),
})
})?;
sheet.delete_columns(column_index, count)?;
self.mark_sheet_modified(sheet_name);
Ok(())
}
pub fn add_worksheet(&mut self, name: String) -> Result<()> {
self.workbook.add_worksheet(name.clone())?;
self.mark_sheet_modified(&name);
self.mark_workbook_modified();
Ok(())
}
pub fn delete_worksheet(&mut self, name: &str) -> Result<()> {
self.workbook.remove_worksheet(name)?;
self.mark_workbook_modified();
Ok(())
}
pub fn rename_worksheet(&mut self, old_name: &str, new_name: &str) -> Result<()> {
self.workbook.rename_worksheet(old_name, new_name)?;
self.mark_sheet_modified(old_name);
self.mark_sheet_modified(new_name);
self.mark_workbook_modified();
Ok(())
}
pub fn save_to_file<P: AsRef<Path>>(&self, path: P, options: &ModifyOptions) -> Result<()> {
let mut zip_writer = ZipWriter::new(path)?;
for (entry_name, content) in &self.original_entries {
if self.is_entry_modified(entry_name) {
let new_content = self.generate_modified_entry(entry_name, options)?;
zip_writer.add_entry(entry_name, &new_content)?;
} else {
zip_writer.add_entry(entry_name, content)?;
}
}
self.add_new_entries(&mut zip_writer, options)?;
zip_writer.finish()?;
Ok(())
}
pub fn save_to_bytes(&self, options: &ModifyOptions) -> Result<Vec<u8>> {
let mut buffer = Vec::new();
{
let cursor = Cursor::new(&mut buffer);
let mut zip_writer = ZipWriter::from_writer(cursor)?;
for (entry_name, content) in &self.original_entries {
if self.is_entry_modified(entry_name) {
let new_content = self.generate_modified_entry(entry_name, options)?;
zip_writer.add_entry(entry_name, &new_content)?;
} else {
zip_writer.add_entry(entry_name, content)?;
}
}
self.add_new_entries(&mut zip_writer, options)?;
zip_writer.finish()?;
}
Ok(buffer)
}
fn mark_sheet_modified(&mut self, sheet_name: &str) {
let sheet_path = format!(
"xl/worksheets/sheet{}.xml",
self.workbook.get_worksheet_index(sheet_name).unwrap_or(1)
);
self.modified_entries.insert(sheet_path, true);
}
fn mark_workbook_modified(&mut self) {
self.modified_entries.insert("xl/workbook.xml".to_string(), true);
self.modified_entries.insert("[Content_Types].xml".to_string(), true);
self.modified_entries.insert("xl/_rels/workbook.xml.rels".to_string(), true);
}
fn is_entry_modified(&self, entry_name: &str) -> bool {
self.modified_entries.get(entry_name).copied().unwrap_or(false)
}
fn generate_modified_entry(
&self,
entry_name: &str,
options: &ModifyOptions
) -> Result<Vec<u8>> {
if entry_name.starts_with("xl/worksheets/") {
self.generate_worksheet_xml(entry_name, options)
} else if entry_name == "xl/workbook.xml" {
self.generate_workbook_xml(options)
} else if entry_name == "[Content_Types].xml" {
self.generate_content_types_xml(options)
} else if entry_name == "xl/_rels/workbook.xml.rels" {
self.generate_workbook_rels_xml(options)
} else {
Ok(self.original_entries.get(entry_name).cloned().unwrap_or_default())
}
}
fn generate_worksheet_xml(
&self,
entry_name: &str,
_options: &ModifyOptions
) -> Result<Vec<u8>> {
let sheet_index = self.extract_sheet_index_from_path(entry_name)?;
let sheet_name = self.workbook
.worksheet_names()
.get(sheet_index)
.ok_or_else(|| {
DocumentError::Xlsx(XlsxError::WorksheetNotFound {
name: format!("Sheet at index {}", sheet_index),
})
})?;
let sheet = self.workbook.get_worksheet(sheet_name).ok_or_else(|| {
DocumentError::Xlsx(XlsxError::WorksheetNotFound {
name: sheet_name.clone(),
})
})?;
let writer = XlsxWriter::new();
writer.generate_worksheet_xml(sheet)
}
fn generate_workbook_xml(&self, _options: &ModifyOptions) -> Result<Vec<u8>> {
let writer = XlsxWriter::new();
writer.generate_workbook_xml(&self.workbook)
}
fn generate_content_types_xml(&self, _options: &ModifyOptions) -> Result<Vec<u8>> {
let writer = XlsxWriter::new();
writer.generate_content_types_xml(&self.workbook)
}
fn generate_workbook_rels_xml(&self, _options: &ModifyOptions) -> Result<Vec<u8>> {
let writer = XlsxWriter::new();
writer.generate_workbook_rels_xml(&self.workbook)
}
fn add_new_entries(&self, zip_writer: &mut ZipWriter, _options: &ModifyOptions) -> Result<()> {
for (index, sheet_name) in self.workbook.worksheet_names().iter().enumerate() {
let sheet_path = format!("xl/worksheets/sheet{}.xml", index + 1);
if !self.original_entries.contains_key(&sheet_path) {
let sheet = self.workbook.get_worksheet(sheet_name).unwrap();
let writer = XlsxWriter::new();
let content = writer.generate_worksheet_xml(sheet)?;
zip_writer.add_entry(&sheet_path, &content)?;
}
}
Ok(())
}
fn extract_sheet_index_from_path(&self, path: &str) -> Result<usize> {
let filename = path
.split('/')
.last()
.ok_or_else(|| DocumentError::Format("Invalid worksheet path".to_string()))?;
let number_part = filename
.strip_prefix("sheet")
.and_then(|s| s.strip_suffix(".xml"))
.ok_or_else(|| DocumentError::Format("Invalid worksheet filename".to_string()))?;
let index = number_part
.parse::<usize>()
.map_err(|_| DocumentError::Format("Invalid sheet index".to_string()))?;
Ok(index.saturating_sub(1)) }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::xlsx::CellValue;
#[test]
fn test_modify_operations() {
}
#[test]
fn test_preserve_content() {
}
}