openprxl 0.1.0

A Rust spreadsheet library inspired by Python's openpyxl
Documentation
//! Write a [`Workbook`] to a `.xlsx` zip package.

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 cells have a date number format if they don't already have a style.
    ensure_date_styles(wb);

    // Collect all shared strings from string cells across all sheets.
    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);

    // [Content_Types].xml
    zip.start_file("[Content_Types].xml", options)?;
    write_content_types(&mut zip, wb, !strings.is_empty())?;

    // _rels/.rels
    zip.start_file("_rels/.rels", options)?;
    write_package_rels(&mut zip)?;

    // docProps/core.xml
    zip.start_file("docProps/core.xml", options)?;
    write_core_props(&mut zip, wb)?;

    // docProps/app.xml
    zip.start_file("docProps/app.xml", options)?;
    write_app_props(&mut zip, wb)?;

    // xl/_rels/workbook.xml.rels
    zip.start_file("xl/_rels/workbook.xml.rels", options)?;
    write_workbook_rels(&mut zip, wb, !strings.is_empty())?;

    // xl/workbook.xml
    zip.start_file("xl/workbook.xml", options)?;
    crate::xml::workbook::write(&mut zip, wb)?;

    // xl/styles.xml
    zip.start_file("xl/styles.xml", options)?;
    crate::xml::styles::write(&mut zip, &wb.style_manager)?;

    // xl/sharedStrings.xml
    if !strings.is_empty() {
        zip.start_file("xl/sharedStrings.xml", options)?;
        crate::xml::shared_strings::write(&mut zip, &strings)?;
    }

    // xl/worksheets/sheetN.xml
    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(())
}