use crate::cell::Value;
use crate::error::Result;
use crate::shared_strings::SharedStrings;
use crate::style::{NumberFormat, Style};
use crate::workbook::Workbook;
use crate::xml;
use std::io::Write;
use std::path::Path;
use zip::write::SimpleFileOptions;
pub fn write_workbook(wb: &mut Workbook, path: &Path) -> Result<()> {
ensure_date_styles(wb);
let mut strings = SharedStrings::new();
for ws in &wb.sheets {
for (_, _, cell) in ws.iter_cells() {
if let Value::String(s) = &cell.value {
strings.add(s);
}
}
}
let file = std::fs::File::create(path)?;
let mut zip = zip::ZipWriter::new(file);
let options = SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated)
.unix_permissions(0o644);
zip.start_file("[Content_Types].xml", options)?;
write_content_types(&mut zip, wb, !strings.is_empty())?;
zip.start_file("_rels/.rels", options)?;
write_package_rels(&mut zip)?;
zip.start_file("docProps/core.xml", options)?;
write_core_props(&mut zip, wb)?;
zip.start_file("docProps/app.xml", options)?;
write_app_props(&mut zip, wb)?;
zip.start_file("xl/_rels/workbook.xml.rels", options)?;
write_workbook_rels(&mut zip, wb, !strings.is_empty())?;
zip.start_file("xl/workbook.xml", options)?;
crate::xml::workbook::write(&mut zip, wb)?;
zip.start_file("xl/styles.xml", options)?;
crate::xml::styles::write(&mut zip, &wb.style_manager)?;
if !strings.is_empty() {
zip.start_file("xl/sharedStrings.xml", options)?;
crate::xml::shared_strings::write(&mut zip, &strings)?;
}
for (idx, ws) in wb.sheets.iter().enumerate() {
let filename = format!("xl/worksheets/sheet{}.xml", idx + 1);
zip.start_file(&filename, options)?;
crate::xml::worksheet::write(&mut zip, wb, ws, &mut strings)?;
}
zip.finish()?;
Ok(())
}
fn ensure_date_styles(wb: &mut Workbook) {
let mut date_style = None;
let mut needs_date_style = false;
for ws in &wb.sheets {
for (_, _, cell) in ws.iter_cells() {
if matches!(cell.value, Value::Date(_)) && cell.style_id.is_none() {
needs_date_style = true;
}
}
}
if needs_date_style {
date_style = Some(wb.register_style(Style::default().number_format(NumberFormat::new("yyyy-mm-dd"))));
}
if let Some(style_id) = date_style {
for ws in &mut wb.sheets {
for cell in ws.cells.values_mut() {
if matches!(cell.value, Value::Date(_)) && cell.style_id.is_none() {
cell.set_style_id(style_id);
}
}
}
}
}
fn write_content_types<W: Write>(writer: &mut W, wb: &Workbook, has_strings: bool) -> Result<()> {
let mut xml = xml::writer(writer);
let mut root = quick_xml::events::BytesStart::new("Types");
root.push_attribute(("xmlns", xml::NS_CONTENT_TYPES));
xml.write_event(quick_xml::events::Event::Start(root))?;
write_override(&mut xml, "/docProps/core.xml", "application/vnd.openxmlformats-package.core-properties+xml")?;
write_override(&mut xml, "/docProps/app.xml", "application/vnd.openxmlformats-officedocument.extended-properties+xml")?;
write_override(&mut xml, "/xl/workbook.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml")?;
write_override(&mut xml, "/xl/styles.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml")?;
if has_strings {
write_override(&mut xml, "/xl/sharedStrings.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml")?;
}
for i in 0..wb.sheets.len() {
write_override(
&mut xml,
&format!("/xl/worksheets/sheet{}.xml", i + 1),
"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",
)?;
}
xml.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(
"Types",
)))?;
Ok(())
}
fn write_override<W: Write>(
xml: &mut quick_xml::Writer<W>,
part_name: &str,
content_type: &str,
) -> Result<()> {
let mut elem = quick_xml::events::BytesStart::new("Override");
elem.push_attribute(("PartName", part_name));
elem.push_attribute(("ContentType", content_type));
xml.write_event(quick_xml::events::Event::Empty(elem))?;
Ok(())
}
fn write_package_rels<W: Write>(writer: &mut W) -> Result<()> {
let mut xml = xml::writer(writer);
let mut root = quick_xml::events::BytesStart::new("Relationships");
root.push_attribute(("xmlns", xml::NS_RELS));
xml.write_event(quick_xml::events::Event::Start(root))?;
write_rel(&mut xml, "rId1", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument", "xl/workbook.xml")?;
write_rel(&mut xml, "rId2", "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties", "docProps/core.xml")?;
write_rel(&mut xml, "rId3", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties", "docProps/app.xml")?;
xml.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(
"Relationships",
)))?;
Ok(())
}
fn write_workbook_rels<W: Write>(writer: &mut W, wb: &Workbook, has_strings: bool) -> Result<()> {
let mut xml = xml::writer(writer);
let mut root = quick_xml::events::BytesStart::new("Relationships");
root.push_attribute(("xmlns", xml::NS_RELS));
xml.write_event(quick_xml::events::Event::Start(root))?;
for (idx, _ws) in wb.sheets.iter().enumerate() {
write_rel(
&mut xml,
&format!("rId{}", idx + 1),
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet",
&format!("worksheets/sheet{}.xml", idx + 1),
)?;
}
let mut next_rid = wb.sheets.len() + 1;
write_rel(
&mut xml,
&format!("rId{}", next_rid),
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
"styles.xml",
)?;
next_rid += 1;
if has_strings {
write_rel(
&mut xml,
&format!("rId{}", next_rid),
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings",
"sharedStrings.xml",
)?;
}
xml.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(
"Relationships",
)))?;
Ok(())
}
fn write_rel<W: Write>(
xml: &mut quick_xml::Writer<W>,
id: &str,
rel_type: &str,
target: &str,
) -> Result<()> {
let mut elem = quick_xml::events::BytesStart::new("Relationship");
elem.push_attribute(("Id", id));
elem.push_attribute(("Type", rel_type));
elem.push_attribute(("Target", target));
xml.write_event(quick_xml::events::Event::Empty(elem))?;
Ok(())
}
fn write_core_props<W: Write>(writer: &mut W, wb: &Workbook) -> Result<()> {
let mut xml = xml::writer(writer);
let mut root = quick_xml::events::BytesStart::new("cp:coreProperties");
root.push_attribute(("xmlns:cp", xml::NS_CORE_PROPS));
root.push_attribute(("xmlns:dc", xml::NS_DC));
root.push_attribute(("xmlns:dcterms", xml::NS_DCTERMS));
root.push_attribute(("xmlns:xsi", xml::NS_XSI));
xml.write_event(quick_xml::events::Event::Start(root))?;
xml.write_event(quick_xml::events::Event::Start(quick_xml::events::BytesStart::new(
"dc:creator",
)))?;
xml::write_text(&mut xml, wb.creator.as_deref().unwrap_or("openprxl"))?;
xml.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(
"dc:creator",
)))?;
if let Some(title) = &wb.title {
xml.write_event(quick_xml::events::Event::Start(quick_xml::events::BytesStart::new(
"dc:title",
)))?;
xml::write_text(&mut xml, title)?;
xml.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(
"dc:title",
)))?;
}
let now = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
let mut created = quick_xml::events::BytesStart::new("dcterms:created");
created.push_attribute(("xsi:type", "dcterms:W3CDTF"));
xml.write_event(quick_xml::events::Event::Start(created))?;
xml::write_text(&mut xml, &now)?;
xml.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(
"dcterms:created",
)))?;
let mut modified = quick_xml::events::BytesStart::new("dcterms:modified");
modified.push_attribute(("xsi:type", "dcterms:W3CDTF"));
xml.write_event(quick_xml::events::Event::Start(modified))?;
xml::write_text(&mut xml, &now)?;
xml.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(
"dcterms:modified",
)))?;
xml.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(
"cp:coreProperties",
)))?;
Ok(())
}
fn write_app_props<W: Write>(writer: &mut W, wb: &Workbook) -> Result<()> {
let mut xml = xml::writer(writer);
let mut root = quick_xml::events::BytesStart::new("Properties");
root.push_attribute(("xmlns", "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"));
root.push_attribute(("xmlns:vt", "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"));
xml.write_event(quick_xml::events::Event::Start(root))?;
xml.write_event(quick_xml::events::Event::Start(quick_xml::events::BytesStart::new(
"Application",
)))?;
xml::write_text(&mut xml, "openprxl")?;
xml.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(
"Application",
)))?;
xml.write_event(quick_xml::events::Event::Start(quick_xml::events::BytesStart::new(
"DocSecurity",
)))?;
xml::write_text(&mut xml, "0")?;
xml.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(
"DocSecurity",
)))?;
xml.write_event(quick_xml::events::Event::Start(quick_xml::events::BytesStart::new(
"ScaleCrop",
)))?;
xml::write_text(&mut xml, "false")?;
xml.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(
"ScaleCrop",
)))?;
xml.write_event(quick_xml::events::Event::Start(quick_xml::events::BytesStart::new(
"HeadingPairs",
)))?;
let mut vector1 = quick_xml::events::BytesStart::new("vt:vector");
vector1.push_attribute(("size", "2"));
vector1.push_attribute(("baseType", "variant"));
xml.write_event(quick_xml::events::Event::Start(vector1))?;
xml.write_event(quick_xml::events::Event::Start(quick_xml::events::BytesStart::new(
"vt:variant",
)))?;
xml.write_event(quick_xml::events::Event::Start(quick_xml::events::BytesStart::new(
"vt:lpstr",
)))?;
xml::write_text(&mut xml, "Worksheets")?;
xml.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(
"vt:lpstr",
)))?;
xml.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(
"vt:variant",
)))?;
xml.write_event(quick_xml::events::Event::Start(quick_xml::events::BytesStart::new(
"vt:variant",
)))?;
xml.write_event(quick_xml::events::Event::Start(quick_xml::events::BytesStart::new(
"vt:i4",
)))?;
xml::write_text(&mut xml, &wb.sheets.len().to_string())?;
xml.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(
"vt:i4",
)))?;
xml.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(
"vt:variant",
)))?;
xml.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(
"vt:vector",
)))?;
xml.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(
"HeadingPairs",
)))?;
xml.write_event(quick_xml::events::Event::Start(quick_xml::events::BytesStart::new(
"TitlesOfParts",
)))?;
let mut vector2 = quick_xml::events::BytesStart::new("vt:vector");
let sheet_count = wb.sheets.len().to_string();
vector2.push_attribute(("size", sheet_count.as_str()));
vector2.push_attribute(("baseType", "lpstr"));
xml.write_event(quick_xml::events::Event::Start(vector2))?;
for sheet in &wb.sheets {
xml.write_event(quick_xml::events::Event::Start(quick_xml::events::BytesStart::new(
"vt:lpstr",
)))?;
xml::write_text(&mut xml, &sheet.name)?;
xml.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(
"vt:lpstr",
)))?;
}
xml.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(
"vt:vector",
)))?;
xml.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(
"TitlesOfParts",
)))?;
xml.write_event(quick_xml::events::Event::Start(quick_xml::events::BytesStart::new(
"Company",
)))?;
xml.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(
"Company",
)))?;
xml.write_event(quick_xml::events::Event::Start(quick_xml::events::BytesStart::new(
"LinksUpToDate",
)))?;
xml::write_text(&mut xml, "false")?;
xml.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(
"LinksUpToDate",
)))?;
xml.write_event(quick_xml::events::Event::Start(quick_xml::events::BytesStart::new(
"SharedDoc",
)))?;
xml::write_text(&mut xml, "false")?;
xml.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(
"SharedDoc",
)))?;
xml.write_event(quick_xml::events::Event::Start(quick_xml::events::BytesStart::new(
"HyperlinksChanged",
)))?;
xml::write_text(&mut xml, "false")?;
xml.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(
"HyperlinksChanged",
)))?;
xml.write_event(quick_xml::events::Event::Start(quick_xml::events::BytesStart::new(
"AppVersion",
)))?;
xml::write_text(&mut xml, "16.0300")?;
xml.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(
"AppVersion",
)))?;
xml.write_event(quick_xml::events::Event::End(quick_xml::events::BytesEnd::new(
"Properties",
)))?;
Ok(())
}