use serde_json::Value;
use std::{io::{Cursor, Read, Write}, sync::{Arc, Mutex}};
use zip::{ZipArchive, ZipWriter, write::SimpleFileOptions};
use std::collections::HashMap;
use crate::{utils::{to_column_name, merge_handlebars_in_xml, register_basic_helpers, post_process_xml, replace_shared_strings_in_sheet, validate_xlsx_format}, XlsxError};
use crate::imagesize::get_image_dimensions;
use uuid::Uuid;
use handlebars::{Handlebars, RenderErrorReason};
const REMOVE_ROW_KEY: &str = "|e5nBk+z4RMKqlyBo+xQ48A-remove-row|";
const TO_NUMBER_KEY: &str = "|e5nBk+z4RMKqlyBo+xQ48A-num|";
const TO_FORMULA_KEY: &str = "|e5nBk+z4RMKqlyBo+xQ48A-formula|";
#[derive(Debug, Clone)]
struct ImageInfo {
col: u32, row: u32, base64_data: String, width: Option<u32>, height: Option<u32>, rid: String, }
pub fn render_template(
zip_bytes: Vec<u8>,
data: &Value,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
validate_xlsx_format(&zip_bytes)?;
let cursor = Cursor::new(zip_bytes);
let mut archive = ZipArchive::new(cursor)?;
let files: Arc<Mutex<HashMap<String, Vec<u8>>>> = Arc::new(Mutex::new(HashMap::new()));
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let file_name = file.name().to_string();
if file_name.ends_with('/') {
continue;
}
if file_name == "xl/calcChain.xml" {
continue;
}
let mut contents = Vec::new();
file.read_to_end(&mut contents)?;
files.lock().unwrap().insert(file_name, contents);
}
let mut shared_strings = Vec::new();
{
let file_name = "xl/sharedStrings.xml";
let contents = files.lock().unwrap().remove(file_name);
if let Some(contents) = contents {
let xml_content = String::from_utf8(contents.clone())?;
let mut start = 0;
while let Some(si_start) = xml_content[start..].find("<si>") {
let abs_start = start + si_start;
if let Some(si_end) = xml_content[abs_start..].find("</si>") {
let abs_end = abs_start + si_end + "</si>".len();
let si_xml = &xml_content[abs_start..abs_end];
let is_xml = si_xml
.replace("<si>", "<is>")
.replace("</si>", "</is>");
shared_strings.push(is_xml);
start = abs_end;
} else {
break;
}
}
let xml_content = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="0" uniqueCount="0"></sst>"#.to_string();
let contents = xml_content.into_bytes();
files.lock().unwrap().insert(file_name.to_string(), contents);
}
}
let mut handlebars = Handlebars::new();
handlebars.set_strict_mode(false);
register_basic_helpers(&mut handlebars)?;
let data1 = Arc::new(Mutex::new(data.clone()));
let data2 = Arc::clone(&data1);
handlebars.register_helper("set_data", Box::new(move |h: &handlebars::Helper, _: &Handlebars, _: &handlebars::Context, _: &mut handlebars::RenderContext, _out: &mut dyn handlebars::Output| -> handlebars::HelperResult {
if let Some(key) = h.param(0).and_then(|v| v.value().as_str())
&& let Some(value) = h.param(1) {
let mut data2 = data2.lock().unwrap();
data2[key] = value.value().clone();
}
Ok(())
}));
let sheet_name = Arc::new(Mutex::new(String::new()));
let sheet_name2 = Arc::clone(&sheet_name);
let row_offset: Arc<Mutex<u32>> = Arc::new(Mutex::new(0));
let row_offset2 = Arc::clone(&row_offset);
let row_offset3 = Arc::clone(&row_offset);
let row_offset4 = Arc::clone(&row_offset);
let row_offset5 = Arc::clone(&row_offset);
let row_offset6 = Arc::clone(&row_offset);
let row_offset_for_remove = Arc::clone(&row_offset);
handlebars.register_helper("row_offset_plus", Box::new(move |h: &handlebars::Helper, _: &Handlebars, _: &handlebars::Context, _: &mut handlebars::RenderContext, _out: &mut dyn handlebars::Output| -> handlebars::HelperResult {
if let Some(value) = h.param(0).and_then(|v| v.value().as_u64()) {
let mut offset = row_offset2.lock().unwrap();
*offset += u32::try_from(value).unwrap();
}
Ok(())
}));
handlebars.register_helper("row_offset_reset", Box::new(move |_: &handlebars::Helper, _: &Handlebars, _: &handlebars::Context, _: &mut handlebars::RenderContext, _out: &mut dyn handlebars::Output| -> handlebars::HelperResult {
let mut offset = row_offset.lock().unwrap();
*offset = 0;
Ok(())
}));
handlebars.register_helper("get_row_offset", Box::new(move |_: &handlebars::Helper, _: &Handlebars, _: &handlebars::Context, _: &mut handlebars::RenderContext, out: &mut dyn handlebars::Output| -> handlebars::HelperResult {
let offset = row_offset6.lock().unwrap();
out.write(&offset.to_string())?;
Ok(())
}));
let row_inline = Arc::new(Mutex::new(1u32));
let row_inline2 = Arc::clone(&row_inline);
let row_inline3 = Arc::clone(&row_inline);
let row_inline4 = Arc::clone(&row_inline);
let row_inline5 = Arc::clone(&row_inline);
handlebars.register_helper("set_row_inline", Box::new(move |h: &handlebars::Helper, _: &Handlebars, _: &handlebars::Context, _: &mut handlebars::RenderContext, _out: &mut dyn handlebars::Output| -> handlebars::HelperResult {
if let Some(value) = h.param(0).and_then(|v| v.value().as_u64()) {
let mut row_inline = row_inline2.lock().unwrap();
*row_inline = u32::try_from(value).expect("set_row_inline too large for u32");
}
Ok(())
}));
handlebars.register_helper("_r", Box::new(move |_h: &handlebars::Helper, _: &Handlebars, _: &handlebars::Context, _: &mut handlebars::RenderContext, out: &mut dyn handlebars::Output| -> handlebars::HelperResult {
let row_inline = row_inline3.lock().unwrap();
let row_offset = row_offset3.lock().unwrap();
let r = *row_inline + *row_offset;
out.write(&r.to_string())?;
Ok(())
}));
let col_offset: Arc<Mutex<u32>> = Arc::new(Mutex::new(0));
let col_offset2 = Arc::clone(&col_offset);
let col_offset3 = Arc::clone(&col_offset);
let col_offset4 = Arc::clone(&col_offset);
let col_offset5 = Arc::clone(&col_offset);
let col_offset6 = Arc::clone(&col_offset);
handlebars.register_helper("col_offset_plus", Box::new(move |h: &handlebars::Helper, _: &Handlebars, _: &handlebars::Context, _: &mut handlebars::RenderContext, _out: &mut dyn handlebars::Output| -> handlebars::HelperResult {
if let Some(value) = h.param(0).and_then(|v| v.value().as_u64()) {
let mut offset = col_offset2.lock().unwrap();
*offset += u32::try_from(value).unwrap();
}
Ok(())
}));
handlebars.register_helper("col_offset_reset", Box::new(move |_: &handlebars::Helper, _: &Handlebars, _: &handlebars::Context, _: &mut handlebars::RenderContext, out: &mut dyn handlebars::Output| -> handlebars::HelperResult {
let mut offset = col_offset.lock().unwrap();
*offset = 0;
out.write("")?;
Ok(())
}));
handlebars.register_helper("get_col_offset", Box::new(move |_: &handlebars::Helper, _: &Handlebars, _: &handlebars::Context, _: &mut handlebars::RenderContext, out: &mut dyn handlebars::Output| -> handlebars::HelperResult {
let offset = col_offset6.lock().unwrap();
out.write(&offset.to_string())?;
Ok(())
}));
let col_inline = Arc::new(Mutex::new(1u32));
let col_inline2 = Arc::clone(&col_inline);
let col_inline3 = Arc::clone(&col_inline);
let col_inline4 = Arc::clone(&col_inline);
let col_inline5 = Arc::clone(&col_inline);
handlebars.register_helper("set_col_inline", Box::new(move |h: &handlebars::Helper, _: &Handlebars, _: &handlebars::Context, _: &mut handlebars::RenderContext, _out: &mut dyn handlebars::Output| -> handlebars::HelperResult {
if let Some(value) = h.param(0).and_then(|v| v.value().as_u64()) {
let mut col_inline = col_inline2.lock().unwrap();
*col_inline = u32::try_from(value).expect("set_col_inline too large for u32");
}
Ok(())
}));
handlebars.register_helper("_c", Box::new(move |_h: &handlebars::Helper, _: &Handlebars, _: &handlebars::Context, _: &mut handlebars::RenderContext, out: &mut dyn handlebars::Output| -> handlebars::HelperResult {
let col_inline = col_inline3.lock().unwrap();
let col_offset = col_offset3.lock().unwrap();
let c_num = *col_inline + *col_offset;
let c_str = to_column_name("A", c_num - 1); out.write(&c_str)?;
Ok(())
}));
handlebars.register_helper("_cr", Box::new(move |h: &handlebars::Helper, _: &Handlebars, _: &handlebars::Context, _: &mut handlebars::RenderContext, out: &mut dyn handlebars::Output| -> handlebars::HelperResult {
let col_inline = col_inline4.lock().unwrap();
let col_offset = col_offset4.lock().unwrap();
let row_inline = row_inline4.lock().unwrap();
let row_offset = row_offset4.lock().unwrap();
let c_str = if let Some(param) = h.param(0) {
if param.value().is_string() {
let col_name = param.value().as_str().unwrap();
let col_index = crate::utils::to_column_index(col_name);
let final_col_index = col_index + *col_offset;
to_column_name("A", final_col_index.saturating_sub(1)) } else if param.value().is_number() {
let col_num = param.value().as_u64().unwrap_or(1) as u32;
let final_col_num = col_num + *col_offset;
to_column_name("A", final_col_num.saturating_sub(1)) } else {
let c_num = *col_inline + *col_offset;
to_column_name("A", c_num.saturating_sub(1)) }
} else {
let c_num = *col_inline + *col_offset;
to_column_name("A", c_num.saturating_sub(1)) };
let r_num = if let Some(param) = h.param(1).and_then(|v| v.value().as_u64()) {
param as u32 + *row_offset
} else {
*row_inline + *row_offset };
out.write(&format!("{c_str}{r_num}"))?;
Ok(())
}));
handlebars.register_helper("removeRow", Box::new(move |_: &handlebars::Helper, _: &Handlebars, _: &handlebars::Context, _: &mut handlebars::RenderContext, out: &mut dyn handlebars::Output| -> handlebars::HelperResult {
let mut offset = row_offset_for_remove.lock().unwrap();
if *offset > 0 {
*offset -= 1;
}
out.write(REMOVE_ROW_KEY)?;
Ok(())
}));
handlebars.register_helper("num", Box::new(move |h: &handlebars::Helper, _: &Handlebars, _: &handlebars::Context, _: &mut handlebars::RenderContext, out: &mut dyn handlebars::Output| -> handlebars::HelperResult {
out.write(TO_NUMBER_KEY)?; if let Some(param) = h.param(0) {
if param.value().is_number() {
out.write(¶m.value().to_string())?;
} else if param.value().is_string() {
let s = param.value().as_str().unwrap();
if let Ok(n) = s.parse::<f64>() {
out.write(&n.to_string())?;
} else {
out.write("0")?; }
} else {
out.write("0")?; }
} else {
out.write("0")?; }
Ok(())
}));
handlebars.register_helper("formula", Box::new(move |h: &handlebars::Helper, _: &Handlebars, _: &handlebars::Context, _: &mut handlebars::RenderContext, out: &mut dyn handlebars::Output| -> handlebars::HelperResult {
out.write(TO_FORMULA_KEY)?; if let Some(param) = h.param(0) {
if param.value().is_string() {
let formula = param.value().as_str().unwrap();
out.write(formula)?;
} else {
out.write("")?; }
} else {
out.write("")?; }
Ok(())
}));
handlebars.register_helper("concat", Box::new(|h: &handlebars::Helper, _: &Handlebars, _: &handlebars::Context, _: &mut handlebars::RenderContext, out: &mut dyn handlebars::Output| -> handlebars::HelperResult {
let mut result = String::new();
for param in h.params() {
let value = param.value();
if value.is_string() {
result.push_str(value.as_str().unwrap());
} else if value.is_number() || value.is_boolean() {
result.push_str(&value.to_string());
} else if value.is_null() {
} else {
result.push_str(&value.to_string());
}
}
out.write(&result)?;
Ok(())
}));
handlebars.register_helper("toColumnName", Box::new(|h: &handlebars::Helper, _: &Handlebars, _: &handlebars::Context, _: &mut handlebars::RenderContext, out: &mut dyn handlebars::Output| -> handlebars::HelperResult {
if let Some(current_col) = h.param(0) {
let current_str = if current_col.value().is_string() {
current_col.value().as_str().unwrap().to_string()
} else if current_col.value().is_number() {
let col_num = current_col.value().as_u64().unwrap_or(1) as u32;
to_column_name("A", col_num.saturating_sub(1))
} else {
"A".to_string()
};
let increment = if let Some(inc_param) = h.param(1) {
inc_param.value().as_u64().unwrap_or(0) as u32
} else {
0
};
let result = to_column_name(¤t_str, increment);
out.write(&result)?;
} else {
out.write("A")?; }
Ok(())
}));
handlebars.register_helper("toColumnIndex", Box::new(|h: &handlebars::Helper, _: &Handlebars, _: &handlebars::Context, _: &mut handlebars::RenderContext, out: &mut dyn handlebars::Output| -> handlebars::HelperResult {
if let Some(col_name) = h.param(0) {
let col_str = if col_name.value().is_string() {
col_name.value().as_str().unwrap()
} else {
"A" };
let index = crate::utils::to_column_index(col_str);
out.write(&index.to_string())?;
} else {
out.write("1")?; }
Ok(())
}));
let merge_cells: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let merge_cells2 = Arc::clone(&merge_cells);
let hyperlinks_by_sheet: Arc<Mutex<HashMap<String, Vec<crate::utils::HyperlinkInfo>>>> = Arc::new(Mutex::new(HashMap::new()));
let hyperlinks_by_sheet2 = Arc::clone(&hyperlinks_by_sheet);
let sheet_name_for_hyperlink = Arc::clone(&sheet_name);
let images_by_sheet: Arc<Mutex<HashMap<String, Vec<ImageInfo>>>> = Arc::new(Mutex::new(HashMap::new()));
let images_by_sheet2 = Arc::clone(&images_by_sheet);
let sheet_name3 = Arc::clone(&sheet_name);
handlebars.register_helper("mergeCell", Box::new(move |h: &handlebars::Helper, _: &Handlebars, _: &handlebars::Context, _: &mut handlebars::RenderContext, _out: &mut dyn handlebars::Output| -> handlebars::HelperResult {
if let Some(ref_value) = h.param(0)
&& let Some(ref_str) = ref_value.value().as_str() {
if ref_str.contains(':') {
let mut cells = merge_cells2.lock().unwrap();
cells.push(ref_str.to_string());
}
}
Ok(())
}));
handlebars.register_helper("hyperlink", Box::new(move |h: &handlebars::Helper, _: &Handlebars, _: &handlebars::Context, _: &mut handlebars::RenderContext, _out: &mut dyn handlebars::Output| -> handlebars::HelperResult {
let ref_cell = h.param(0).and_then(|v| v.value().as_str());
if ref_cell.is_none() || ref_cell.unwrap().is_empty() {
return Ok(()); }
let ref_cell = ref_cell.unwrap().to_string();
let location = h.param(1).and_then(|v| v.value().as_str());
if location.is_none() || location.unwrap().is_empty() {
return Ok(()); }
let location = location.unwrap();
let display = h.param(2)
.and_then(|v| v.value().as_str())
.unwrap_or("")
.to_string();
let current_sheet = sheet_name_for_hyperlink.lock().unwrap().clone();
if !current_sheet.is_empty() {
hyperlinks_by_sheet2
.lock().unwrap()
.entry(current_sheet)
.or_default()
.push(crate::utils::HyperlinkInfo {
ref_cell,
location: location.to_string(),
display,
});
}
Ok(()) }));
handlebars.register_helper("img", Box::new(move |h: &handlebars::Helper, _: &Handlebars, _: &handlebars::Context, _: &mut handlebars::RenderContext, _out: &mut dyn handlebars::Output| -> handlebars::HelperResult {
let base64_data = h.param(0).and_then(|v| v.value().as_str());
if base64_data.is_none() || base64_data.unwrap().is_empty() {
return Ok(()); }
let base64_data = base64_data.unwrap();
let width = h.param(1).and_then(|v| v.value().as_u64()).map(|w| w as u32);
let height = h.param(2).and_then(|v| v.value().as_u64()).map(|h| h as u32);
let col = *col_inline5.lock().unwrap() + *col_offset5.lock().unwrap();
let row = *row_inline5.lock().unwrap() + *row_offset5.lock().unwrap();
let current_sheet = sheet_name3.lock().unwrap().clone();
if !current_sheet.is_empty() {
let rid = Uuid::new_v4().to_string().replace("-", "");
let rid = format!("rId{}", &rid[..16]);
images_by_sheet2
.lock().unwrap()
.entry(current_sheet)
.or_default()
.push(ImageInfo {
col,
row,
base64_data: base64_data.to_string(),
width,
height,
rid,
});
}
Ok(()) }));
let sheets_to_delete: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let sheets_to_delete2 = Arc::clone(&sheets_to_delete);
let sheet_name4 = Arc::clone(&sheet_name);
handlebars.register_helper("deleteCurrentSheet", Box::new(move |_: &handlebars::Helper, _: &Handlebars, _: &handlebars::Context, _: &mut handlebars::RenderContext, _out: &mut dyn handlebars::Output| -> handlebars::HelperResult {
let current_sheet = sheet_name4.lock().unwrap().clone();
if !current_sheet.is_empty() {
sheets_to_delete2.lock().unwrap().push(current_sheet);
}
Ok(())
}));
let sheets_to_rename: Arc<Mutex<HashMap<String, String>>> = Arc::new(Mutex::new(HashMap::new()));
let sheets_to_rename2 = Arc::clone(&sheets_to_rename);
let sheet_name5 = Arc::clone(&sheet_name);
handlebars.register_helper("setCurrentSheetName", Box::new(move |h: &handlebars::Helper, _: &Handlebars, _: &handlebars::Context, _: &mut handlebars::RenderContext, _out: &mut dyn handlebars::Output| -> handlebars::HelperResult {
if let Some(new_name) = h.param(0).and_then(|v| v.value().as_str()) {
let current_sheet = sheet_name5.lock().unwrap().clone();
if !current_sheet.is_empty() && !new_name.is_empty() {
let clean_name: String = new_name
.chars()
.filter(|c| !matches!(c, '\\' | '/' | '?' | '*' | '[' | ']'))
.take(31)
.collect();
if !clean_name.is_empty() {
sheets_to_rename2.lock().unwrap().insert(current_sheet, clean_name);
}
}
}
Ok(())
}));
let sheets_to_hide: Arc<Mutex<HashMap<String, String>>> = Arc::new(Mutex::new(HashMap::new()));
let sheets_to_hide2 = Arc::clone(&sheets_to_hide);
let sheet_name6 = Arc::clone(&sheet_name);
handlebars.register_helper("hideCurrentSheet", Box::new(move |h: &handlebars::Helper, _: &Handlebars, _: &handlebars::Context, _: &mut handlebars::RenderContext, _out: &mut dyn handlebars::Output| -> handlebars::HelperResult {
let hide_type = h.param(0)
.and_then(|v| v.value().as_str())
.unwrap_or("hidden");
let hide_type = match hide_type {
"veryHidden" => "veryHidden",
_ => "hidden", };
let current_sheet = sheet_name6.lock().unwrap().clone();
if !current_sheet.is_empty() {
sheets_to_hide2.lock().unwrap().insert(current_sheet, hide_type.to_string());
}
Ok(())
}));
{
let mut files = files.lock().unwrap();
let mut sheet_names: Vec<String> = files.keys()
.filter(|name| name.starts_with("xl/worksheets/sheet") && name.ends_with(".xml"))
.cloned()
.collect();
sheet_names.sort();
for sheet_name in sheet_names {
if let Some(contents) = files.get_mut(&sheet_name) {
*sheet_name2.lock().unwrap() = sheet_name.clone();
let xml_content = std::str::from_utf8(contents)?;
let xml_content = "{{row_offset_reset}}".to_string() + xml_content;
let (xml_content, static_merge_refs, static_hyperlinks) = crate::utils::extract_and_remove_merge_cells_and_hyperlinks(&xml_content)?;
let mut shared_strings_modified = shared_strings.clone();
crate::utils::inject_helpers_into_shared_strings(
&xml_content,
&mut shared_strings_modified,
&static_merge_refs,
&static_hyperlinks,
)?;
let xml_content = replace_shared_strings_in_sheet(&xml_content, &shared_strings_modified)?;
let xml_content = merge_handlebars_in_xml(xml_content)?;
let mut xml_content = handlebars.render_template(
&xml_content,
&*data1.lock().map_err(|e| Box::new(std::io::Error::other(format!("Failed to lock data: {e}"))))?,
).map_err(|e| {
let reason: &RenderErrorReason = e.reason();
XlsxError::TemplateRenderError(reason.to_string())
})?;
if xml_content.contains(REMOVE_ROW_KEY) || xml_content.contains(TO_NUMBER_KEY) || xml_content.contains(TO_FORMULA_KEY) {
let remove_key = if xml_content.contains(REMOVE_ROW_KEY) { Some(REMOVE_ROW_KEY) } else { None };
let number_key = if xml_content.contains(TO_NUMBER_KEY) { Some(TO_NUMBER_KEY) } else { None };
let formula_key = if xml_content.contains(TO_FORMULA_KEY) { Some(TO_FORMULA_KEY) } else { None };
let merge_refs = merge_cells.lock().unwrap().clone();
let hyperlinks_map = hyperlinks_by_sheet.lock().unwrap();
let sheet_hyperlinks = hyperlinks_map.get(&sheet_name);
xml_content = post_process_xml(
&xml_content,
remove_key,
number_key,
formula_key,
if merge_refs.is_empty() { None } else { Some(&merge_refs) },
sheet_hyperlinks.map(|v| v.as_slice()),
)?;
}
*contents = xml_content.into_bytes();
}
}
let images_map = images_by_sheet.lock().unwrap();
if !images_map.is_empty() {
process_images(&mut files, &images_map)?;
}
let sheets_to_delete_list = sheets_to_delete.lock().unwrap().clone();
if !sheets_to_delete_list.is_empty() {
delete_sheets(&mut files, &sheets_to_delete_list)?;
}
let sheets_to_rename_map = sheets_to_rename.lock().unwrap().clone();
if !sheets_to_rename_map.is_empty() {
rename_sheets(&mut files, &sheets_to_rename_map)?;
}
let sheets_to_hide_map = sheets_to_hide.lock().unwrap().clone();
if !sheets_to_hide_map.is_empty() {
hide_sheets(&mut files, &sheets_to_hide_map)?;
}
}
let files = Arc::try_unwrap(files).map_err(|_| Box::new(std::io::Error::other("Failed to unwrap Arc")))?.into_inner().map_err(|e| Box::new(std::io::Error::other(format!("Failed to get inner value: {e:?}"))))?;
let mut output = Vec::new();
{
let cursor = Cursor::new(&mut output);
let mut zip_writer = ZipWriter::new(cursor);
for entry in files {
let (file_name, contents): (String, Vec<u8>) = entry;
let options = SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated)
.compression_level(Some(6));
zip_writer.start_file(file_name, options)?;
zip_writer.write_all(&contents)?;
}
zip_writer.finish()?;
}
Ok(output)
}
fn process_images(
files: &mut HashMap<String, Vec<u8>>,
images_map: &HashMap<String, Vec<ImageInfo>>,
) -> Result<(), Box<dyn std::error::Error>> {
use base64::Engine;
let mut image_counter = 1;
for (sheet_path, images) in images_map {
if images.is_empty() {
continue;
}
let sheet_num: u32 = sheet_path
.trim_start_matches("xl/worksheets/sheet")
.trim_end_matches(".xml")
.parse()
.unwrap_or(1);
let drawing_path = format!("xl/drawings/drawing{}.xml", sheet_num);
let drawing_xml = generate_drawing_xml(images, &mut image_counter)?;
files.insert(drawing_path, drawing_xml.into_bytes());
let drawing_rels_path = format!("xl/drawings/_rels/drawing{}.xml.rels", sheet_num);
let drawing_rels = generate_drawing_rels(images);
files.insert(drawing_rels_path, drawing_rels.into_bytes());
let sheet_rels_path = format!("xl/worksheets/_rels/sheet{}.xml.rels", sheet_num);
let sheet_rels = generate_sheet_rels(sheet_num);
files.insert(sheet_rels_path, sheet_rels.into_bytes());
if let Some(sheet_content) = files.get_mut(sheet_path) {
let mut xml = String::from_utf8(sheet_content.clone())?;
if !xml.contains("<drawing") {
xml = xml.replace("</worksheet>", " <drawing r:id=\"rId1\" />\n</worksheet>");
*sheet_content = xml.into_bytes();
}
}
for img_info in images.iter() {
let image_data = base64::engine::general_purpose::STANDARD
.decode(&img_info.base64_data)
.map_err(|e| format!("Failed to decode base64 image: {}", e))?;
let image_path = format!("xl/media/{}.png", img_info.rid);
files.insert(image_path, image_data);
}
}
if let Some(content_types) = files.get_mut("[Content_Types].xml") {
let mut xml = String::from_utf8(content_types.clone())?;
if !xml.contains("Extension=\"png\"") {
xml = xml.replace(
"</Types>",
" <Default Extension=\"png\" ContentType=\"image/png\"/>\n</Types>",
);
}
for (sheet_path, images) in images_map {
if !images.is_empty() {
let sheet_num: u32 = sheet_path
.trim_start_matches("xl/worksheets/sheet")
.trim_end_matches(".xml")
.parse()
.unwrap_or(1);
let drawing_part_name = format!("/xl/drawings/drawing{}.xml", sheet_num);
if !xml.contains(&drawing_part_name) {
xml = xml.replace(
"</Types>",
&format!(
" <Override PartName=\"{}\" ContentType=\"application/vnd.openxmlformats-officedocument.drawing+xml\"/>\n</Types>",
drawing_part_name
),
);
}
}
}
*content_types = xml.into_bytes();
}
Ok(())
}
fn generate_drawing_xml(
images: &[ImageInfo],
image_counter: &mut usize,
) -> Result<String, Box<dyn std::error::Error>> {
let mut xml = String::from(
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xdr:wsDr xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
"#,
);
for img_info in images.iter() {
use base64::Engine;
let image_data = base64::engine::general_purpose::STANDARD
.decode(&img_info.base64_data)
.map_err(|e| format!("Failed to decode base64: {}", e))?;
let (actual_width, actual_height) = get_image_dimensions(&image_data)
.ok_or("Failed to detect image dimensions")?;
let width_px = img_info.width.unwrap_or(actual_width);
let height_px = img_info.height.unwrap_or(actual_height);
let width_emu = width_px as i64 * 9525;
let height_emu = height_px as i64 * 9525;
let from_col = img_info.col - 1; let from_row = img_info.row - 1;
xml.push_str(&format!(
r#" <xdr:oneCellAnchor>
<xdr:from>
<xdr:col>{}</xdr:col>
<xdr:colOff>0</xdr:colOff>
<xdr:row>{}</xdr:row>
<xdr:rowOff>0</xdr:rowOff>
</xdr:from>
<xdr:ext cx="{}" cy="{}"/>
<xdr:pic>
<xdr:nvPicPr>
<xdr:cNvPr id="{}" name="Picture {}"/>
<xdr:cNvPicPr>
<a:picLocks noChangeAspect="1"/>
</xdr:cNvPicPr>
</xdr:nvPicPr>
<xdr:blipFill>
<a:blip xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" r:embed="{}"/>
<a:stretch>
<a:fillRect/>
</a:stretch>
</xdr:blipFill>
<xdr:spPr>
<a:xfrm>
<a:off x="0" y="0"/>
<a:ext cx="{}" cy="{}"/>
</a:xfrm>
<a:prstGeom prst="rect">
<a:avLst/>
</a:prstGeom>
</xdr:spPr>
</xdr:pic>
<xdr:clientData/>
</xdr:oneCellAnchor>
"#,
from_col, from_row, width_emu, height_emu, *image_counter, *image_counter, &img_info.rid, width_emu, height_emu, ));
*image_counter += 1;
}
xml.push_str("</xdr:wsDr>");
Ok(xml)
}
fn generate_drawing_rels(images: &[ImageInfo]) -> String {
let mut xml = String::from(
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
"#,
);
for img_info in images {
xml.push_str(&format!(
r#" <Relationship Id="{}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/{}.png"/>
"#,
img_info.rid, img_info.rid
));
}
xml.push_str("</Relationships>");
xml
}
fn generate_sheet_rels(sheet_num: u32) -> String {
format!(
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing" Target="../drawings/drawing{}.xml"/>
</Relationships>"#,
sheet_num
)
}
fn delete_sheets(
files: &mut HashMap<String, Vec<u8>>,
sheets_to_delete: &[String],
) -> Result<(), Box<dyn std::error::Error>> {
if sheets_to_delete.is_empty() {
return Ok(());
}
let workbook_path = "xl/workbook.xml";
let workbook_content = files.get(workbook_path)
.ok_or("workbook.xml not found")?;
let mut workbook_xml = String::from_utf8(workbook_content.clone())?;
let workbook_rels_path = "xl/_rels/workbook.xml.rels";
let workbook_rels_content = files.get(workbook_rels_path)
.ok_or("workbook.xml.rels not found")?;
let mut workbook_rels_xml = String::from_utf8(workbook_rels_content.clone())?;
let total_sheets = workbook_xml.matches("<sheet ").count();
if sheets_to_delete.len() >= total_sheets {
return Err(Box::new(std::io::Error::other(
"Cannot delete all worksheets. Excel workbook must contain at least one worksheet."
)));
}
for sheet_path in sheets_to_delete {
let sheet_num: u32 = match sheet_path
.trim_start_matches("xl/worksheets/sheet")
.trim_end_matches(".xml")
.parse() {
Ok(num) => num,
Err(_) => continue, };
let rels_target = format!("worksheets/sheet{}.xml", sheet_num);
let mut rid = String::new();
if let Some(rel_start) = workbook_rels_xml.find(&format!("Target=\"{}\"", rels_target)) {
let before = &workbook_rels_xml[..rel_start];
if let Some(id_start) = before.rfind("Id=\"") {
let id_part = &workbook_rels_xml[id_start + 4..];
if let Some(id_end) = id_part.find('"') {
rid = id_part[..id_end].to_string();
}
}
if let Some(node_start) = before.rfind("<Relationship ")
&& let Some(node_end) = workbook_rels_xml[rel_start..].find("/>") {
let full_end = rel_start + node_end + 2;
let mut delete_start = node_start;
let mut delete_end = full_end;
while delete_start > 0 && matches!(workbook_rels_xml.as_bytes()[delete_start - 1], b' ' | b'\t' | b'\r' | b'\n') {
delete_start -= 1;
}
while delete_end < workbook_rels_xml.len() && matches!(workbook_rels_xml.as_bytes()[delete_end], b' ' | b'\t') {
delete_end += 1;
}
if delete_end < workbook_rels_xml.len() && workbook_rels_xml.as_bytes()[delete_end] == b'\n' {
delete_end += 1;
}
workbook_rels_xml.replace_range(delete_start..delete_end, "");
}
}
if !rid.is_empty() {
let sheet_pattern = format!("r:id=\"{}\"", rid);
if let Some(sheet_pos) = workbook_xml.find(&sheet_pattern) {
let before = &workbook_xml[..sheet_pos];
if let Some(tag_start) = before.rfind("<sheet ") {
let after = &workbook_xml[sheet_pos..];
if let Some(tag_end) = after.find("/>") {
let full_end = sheet_pos + tag_end + 2;
let mut delete_start = tag_start;
let mut delete_end = full_end;
while delete_start > 0 && matches!(workbook_xml.as_bytes()[delete_start - 1], b' ' | b'\t' | b'\r' | b'\n') {
delete_start -= 1;
}
while delete_end < workbook_xml.len() && matches!(workbook_xml.as_bytes()[delete_end], b' ' | b'\t') {
delete_end += 1;
}
if delete_end < workbook_xml.len() && workbook_xml.as_bytes()[delete_end] == b'\n' {
delete_end += 1;
}
workbook_xml.replace_range(delete_start..delete_end, "");
}
}
}
}
files.remove(sheet_path);
let sheet_rels = format!("xl/worksheets/_rels/sheet{}.xml.rels", sheet_num);
files.remove(&sheet_rels);
if let Some(content_types) = files.get_mut("[Content_Types].xml") {
let mut ct_xml = String::from_utf8(content_types.clone())?;
let worksheet_override = format!(
" <Override PartName=\"/xl/worksheets/sheet{}.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml\"/>\n",
sheet_num
);
ct_xml = ct_xml.replace(&worksheet_override, "");
*content_types = ct_xml.into_bytes();
}
}
files.insert(workbook_path.to_string(), workbook_xml.into_bytes());
files.insert(workbook_rels_path.to_string(), workbook_rels_xml.into_bytes());
Ok(())
}
fn rename_sheets(
files: &mut HashMap<String, Vec<u8>>,
sheets_to_rename: &HashMap<String, String>,
) -> Result<(), Box<dyn std::error::Error>> {
if sheets_to_rename.is_empty() {
return Ok(());
}
let workbook_path = "xl/workbook.xml";
let workbook_content = files.get(workbook_path)
.ok_or("workbook.xml not found")?;
let mut workbook_xml = String::from_utf8(workbook_content.clone())?;
let mut existing_names: Vec<String> = Vec::new();
let mut start = 0;
while let Some(name_pos) = workbook_xml[start..].find("<sheet ") {
let abs_pos = start + name_pos;
if let Some(name_start) = workbook_xml[abs_pos..].find("name=\"") {
let name_abs_start = abs_pos + name_start + 6; if let Some(name_end) = workbook_xml[name_abs_start..].find('"') {
let name = workbook_xml[name_abs_start..name_abs_start + name_end].to_string();
existing_names.push(name);
start = name_abs_start + name_end;
} else {
break;
}
} else {
break;
}
}
for (sheet_path, new_name) in sheets_to_rename {
let sheet_num: u32 = match sheet_path
.trim_start_matches("xl/worksheets/sheet")
.trim_end_matches(".xml")
.parse() {
Ok(num) => num,
Err(_) => continue,
};
let mut final_name = new_name.clone();
let mut counter = 1;
while existing_names.contains(&final_name) {
let suffix = format!(" ({})", counter);
let max_base_len = 31 - suffix.len();
let base = if new_name.len() > max_base_len {
&new_name[..max_base_len]
} else {
new_name
};
final_name = format!("{}{}", base, suffix);
counter += 1;
if counter > 100 {
final_name = format!("Sheet{}", sheet_num);
break;
}
}
let sheet_id_pattern = format!("sheetId=\"{}\"", sheet_num);
if let Some(sheet_id_pos) = workbook_xml.find(&sheet_id_pattern) {
let before = &workbook_xml[..sheet_id_pos];
if let Some(tag_start) = before.rfind("<sheet ") {
let after = &workbook_xml[sheet_id_pos..];
if let Some(tag_end) = after.find("/>") {
let full_end = sheet_id_pos + tag_end + 2;
let sheet_tag = &workbook_xml[tag_start..full_end];
if let Some(name_start) = sheet_tag.find("name=\"") {
let name_abs_start = tag_start + name_start + 6;
if let Some(name_end) = workbook_xml[name_abs_start..].find('"') {
let name_abs_end = name_abs_start + name_end;
let old_name = workbook_xml[name_abs_start..name_abs_end].to_string();
workbook_xml.replace_range(name_abs_start..name_abs_end, &final_name);
if let Some(pos) = existing_names.iter().position(|n| n == &old_name) {
existing_names[pos] = final_name.clone();
}
}
}
}
}
}
}
files.insert(workbook_path.to_string(), workbook_xml.into_bytes());
Ok(())
}
fn hide_sheets(
files: &mut HashMap<String, Vec<u8>>,
sheets_to_hide: &HashMap<String, String>,
) -> Result<(), Box<dyn std::error::Error>> {
if sheets_to_hide.is_empty() {
return Ok(());
}
let workbook_path = "xl/workbook.xml";
let workbook_content = files.get(workbook_path)
.ok_or("workbook.xml not found")?;
let mut workbook_xml = String::from_utf8(workbook_content.clone())?;
let total_sheets = workbook_xml.matches("<sheet ").count();
if sheets_to_hide.len() >= total_sheets {
return Err(Box::new(std::io::Error::other(
"Cannot hide all worksheets. Excel workbook must have at least one visible worksheet."
)));
}
for (sheet_path, hide_type) in sheets_to_hide {
let sheet_num: u32 = match sheet_path
.trim_start_matches("xl/worksheets/sheet")
.trim_end_matches(".xml")
.parse() {
Ok(num) => num,
Err(_) => continue,
};
let sheet_id_pattern = format!("sheetId=\"{}\"", sheet_num);
if let Some(sheet_id_pos) = workbook_xml.find(&sheet_id_pattern) {
let before = &workbook_xml[..sheet_id_pos];
if let Some(tag_start) = before.rfind("<sheet ") {
let after = &workbook_xml[sheet_id_pos..];
if let Some(tag_end) = after.find("/>") {
let full_end = sheet_id_pos + tag_end + 2;
let sheet_tag = &workbook_xml[tag_start..full_end];
if sheet_tag.contains("state=") {
if let Some(state_start) = sheet_tag.find("state=\"") {
let state_abs_start = tag_start + state_start + 7; if let Some(state_end) = workbook_xml[state_abs_start..].find('"') {
let state_abs_end = state_abs_start + state_end;
workbook_xml.replace_range(state_abs_start..state_abs_end, hide_type);
}
}
} else {
let insert_pos = full_end - 2; workbook_xml.insert_str(insert_pos, &format!(" state=\"{}\"", hide_type));
}
}
}
}
}
files.insert(workbook_path.to_string(), workbook_xml.into_bytes());
Ok(())
}