use crate::app::App;
use crate::core::Core;
use crate::{
error, Chart, ChartType, Chartsheet, ContentTypes, Format, Formats,
Relationships, Result, SharedStrings, WorkbookSheetProperties,
Worksheet, WriteZip, XmlWritable, XmlWriter, SCHEMA_OFFICEDOC,
};
use chrono::{DateTime, Datelike, Timelike, Utc};
use indexmap::{indexmap, IndexMap};
use snafu::{ensure, ResultExt};
use std::fs::File;
use std::io::{Seek, Write};
use std::path::Path;
use zip::ZipWriter;
const CUSTOM_PROPERTY_MIN_LEN: usize = 1;
const CUSTOM_PROPERTY_MAX_LEN: usize = 255;
#[derive(Default)]
pub struct Workbook {
shared_strings: SharedStrings,
formats: Formats,
sheets: IndexMap<String, Sheet>,
properties: DocProperties,
custom_properties: IndexMap<String, CustomProperty>,
workbook_sheet_properties: WorkbookSheetProperties,
}
impl Workbook {
pub fn new() -> Self {
Default::default()
}
pub fn write_file<P: AsRef<Path>>(self, path: P) -> Result<()> {
let file = File::create(path).context(error::Io)?;
self.write(file)
}
pub fn write<W: Write + Seek>(mut self, w: W) -> Result<()> {
if self.sheets.is_empty() {
self.add_worksheet(None)?;
}
let mut zip = ZipWriter::new(w);
zip.write_xml_file(
"[Content_Types].xml",
&self.build_content_types(),
)?;
zip.write_xml_file(
"_rels/.rels",
&self.build_root_relationships(),
)?;
zip.write_xml_file(
"xl/_rels/workbook.xml.rels",
&self.build_workbook_relationships(),
)?;
for (i, worksheet) in self
.sheets
.values()
.filter_map(Sheet::as_worksheet)
.enumerate()
{
zip.write_xml_file(
format!("xl/worksheets/sheet{}.xml", i + 1),
worksheet,
)?;
}
for (i, chartsheet) in self
.sheets
.values()
.filter_map(Sheet::as_chartsheet)
.enumerate()
{
zip.write_xml_file(
format!("xl/chartsheets/sheet{}.xml", i + 1),
chartsheet,
)?;
}
zip.write_xml_file("xl/workbook.xml", &self)?;
zip.write_xml_file("xl/sharedStrings.xml", &self.shared_strings)?;
if !self.custom_properties.is_empty() {
zip.write_xml_file(
"docProps/custom.xml",
&self.custom_properties,
)?;
}
zip.write_bytes_file(
"xl/theme/theme1.xml",
include_bytes!("../res/theme1.xml"),
)?;
zip.write_xml_file("xl/styles.xml", &self.formats)?;
zip.write_xml_file("docProps/core.xml", &self.build_core())?;
zip.write_xml_file("docProps/app.xml", &self.build_app())?;
Ok(())
}
pub fn add_worksheet(
&mut self,
sheetname: Option<&str>,
) -> Result<&mut Worksheet> {
let sheet_count = self.sheets.len();
let name = sheetname
.map(str::to_string)
.unwrap_or_else(|| format!("Sheet{}", sheet_count + 1));
let entry = self.sheets.entry(name.to_string());
use indexmap::map::Entry;
match entry {
Entry::Occupied(_) => {
error::SheetNameAlreadyInUse { name }.fail()?
}
Entry::Vacant(e) => Ok(e
.insert(Sheet::Worksheet(Worksheet::new(
sheet_count,
self.shared_strings.clone(),
self.formats.clone(),
self.workbook_sheet_properties.clone(),
)))
.as_mut_worksheet()
.unwrap()),
}
}
pub fn add_chartsheet<S: AsRef<str>>(
&mut self,
sheetname: S,
) -> Result<Chartsheet> {
unimplemented!();
}
pub fn add_format(&mut self) -> Result<Format> {
unimplemented!();
}
pub fn add_chart(&mut self, chart_type: ChartType) -> Result<Chart> {
unimplemented!();
}
pub fn properties(&mut self) -> &mut DocProperties {
&mut self.properties
}
fn check_custom_property_name(name: &str) -> Result<()> {
let name_chars = name.chars().count();
ensure!(
name_chars >= CUSTOM_PROPERTY_MIN_LEN
&& name_chars <= CUSTOM_PROPERTY_MAX_LEN,
error::CustomPropertyNameLengthOutOfRange {
name: name.to_string(),
size: name_chars,
min: CUSTOM_PROPERTY_MIN_LEN,
max: CUSTOM_PROPERTY_MAX_LEN
}
);
Ok(())
}
pub fn set_custom_property_str(
&mut self,
name: &str,
value: &str,
) -> Result<()> {
Self::check_custom_property_name(name)?;
let value_chars = value.chars().count();
ensure!(
value_chars >= CUSTOM_PROPERTY_MIN_LEN
&& value_chars <= CUSTOM_PROPERTY_MAX_LEN,
error::CustomPropertyStringValueLengthOutOfRange {
value: value.to_string(),
size: value_chars,
min: CUSTOM_PROPERTY_MIN_LEN,
max: CUSTOM_PROPERTY_MAX_LEN
}
);
self.custom_properties.insert(
name.to_string(),
CustomProperty::S(value.to_string()),
);
Ok(())
}
pub fn set_custom_property_integer(
&mut self,
name: &str,
value: i32,
) -> Result<()> {
Self::check_custom_property_name(name)?;
self.custom_properties
.insert(name.to_string(), CustomProperty::I(value));
Ok(())
}
pub fn set_custom_property_number(
&mut self,
name: &str,
value: f64,
) -> Result<()> {
Self::check_custom_property_name(name)?;
self.custom_properties
.insert(name.to_string(), CustomProperty::N(value));
Ok(())
}
pub fn set_custom_property_boolean(
&mut self,
name: &str,
value: bool,
) -> Result<()> {
Self::check_custom_property_name(name)?;
self.custom_properties
.insert(name.to_string(), CustomProperty::B(value));
Ok(())
}
pub fn set_custom_property_datetime(
&mut self,
name: &str,
value: DateTime<Utc>,
) -> Result<()> {
Self::check_custom_property_name(name)?;
self.custom_properties
.insert(name.to_string(), CustomProperty::D(value));
Ok(())
}
pub fn get_worksheet_by_name<N: AsRef<str>>(
&mut self,
name: N,
) -> Option<&mut Worksheet> {
unimplemented!();
}
pub fn get_chartsheet_by_name<N: AsRef<str>>(
&mut self,
name: N,
) -> Option<&mut Chartsheet> {
unimplemented!();
}
pub fn validate_sheet_name<N: AsRef<str>>(
&self,
name: N,
) -> Result<()> {
unimplemented!();
}
pub fn add_vba_project<P: AsRef<Path>>(
&mut self,
filename: P,
) -> Result<()> {
unimplemented!();
}
pub fn set_vba_name<N: AsRef<str>>(&mut self, name: N) -> Result<()> {
unimplemented!();
}
fn build_content_types(&self) -> ContentTypes {
let mut content_types = ContentTypes::default();
content_types.add_override(
"/xl/workbook.xml",
format!(
"{}spreadsheetml.sheet.main+xml",
crate::content_types::APP_DOCUMENT
),
);
{
let mut worksheet_index = 1;
let mut chartsheet_index = 1;
let worksheet_entry = format!(
"{}spreadsheetml.worksheet+xml",
crate::content_types::APP_DOCUMENT
);
let chartsheet_entry = format!(
"{}spreadsheetml.chartsheet+xml",
crate::content_types::APP_DOCUMENT
);
for sheet in self.sheets.values() {
match sheet {
Sheet::Worksheet(_) => {
let filename = format!(
"/xl/worksheets/sheet{}.xml",
worksheet_index
);
worksheet_index += 1;
content_types
.overrides
.insert(filename, worksheet_entry.to_string());
}
Sheet::Chartsheet(_) => {
let filename = format!(
"/xl/chartsheets/sheet{}.xml",
chartsheet_index
);
chartsheet_index += 1;
content_types.overrides.insert(
filename,
chartsheet_entry.to_string(),
);
}
}
}
}
if !self.shared_strings.is_empty() {
content_types.add_shared_strings();
}
if !self.custom_properties.is_empty() {
content_types.add_custom_properties();
}
content_types
}
fn build_root_relationships(&self) -> Relationships {
let mut rels = Relationships::default();
rels.add_document_relationship(
"/officeDocument",
"xl/workbook.xml",
);
rels.add_package_relationship(
"/metadata/core-properties",
"docProps/core.xml",
);
rels.add_document_relationship(
"/extended-properties",
"docProps/app.xml",
);
if !self.custom_properties.is_empty() {
rels.add_document_relationship(
"/custom-properties",
"docProps/custom.xml",
);
}
rels
}
fn build_workbook_relationships(&self) -> Relationships {
let mut rels = Relationships::default();
{
let mut worksheet_index = 1;
let mut chartsheet_index = 1;
for sheet in self.sheets.values() {
match sheet {
Sheet::Worksheet(_) => {
let sheetname = format!(
"worksheets/sheet{}.xml",
worksheet_index
);
worksheet_index += 1;
rels.add_document_relationship(
"/worksheet",
sheetname,
);
}
Sheet::Chartsheet(_) => {
let sheetname = format!(
"chartsheets/sheet{}.xml",
chartsheet_index
);
chartsheet_index += 1;
rels.add_document_relationship(
"/chartsheet",
sheetname,
);
}
}
}
}
rels.add_document_relationship("/theme", "theme/theme1.xml");
rels.add_document_relationship("/styles", "styles.xml");
if !self.shared_strings.is_empty() {
rels.add_document_relationship(
"/sharedStrings",
"sharedStrings.xml",
);
}
rels
}
fn build_core(&self) -> Core {
Core {
properties: &self.properties,
}
}
fn build_app(&self) -> App {
let worksheet_count =
self.sheets.values().filter_map(Sheet::as_worksheet).count();
let chartsheet_count = self
.sheets
.values()
.filter_map(Sheet::as_chartsheet)
.count();
let mut heading_pairs = IndexMap::new();
if worksheet_count > 0 {
heading_pairs.insert(
"Worksheets".to_string(),
format!("{}", worksheet_count),
);
}
if chartsheet_count > 0 {
heading_pairs.insert(
"Charts".to_string(),
format!("{}", chartsheet_count),
);
}
let part_names = self.sheets.keys().cloned().collect();
App {
heading_pairs,
part_names,
properties: &self.properties,
}
}
}
impl XmlWritable for Workbook {
fn write_xml<W: XmlWriter>(&self, w: &mut W) -> Result<()> {
let tag = "workbook";
let attrs = indexmap! {
"xmlns" =>
"http://schemas.openxmlformats.org/spreadsheetml/2006/main",
"xmlns:r" =>
"http://schemas.openxmlformats.org/officeDocument/2006/relationships",
};
w.start_tag_with_attrs(tag, attrs)?;
self.write_file_version(w)?;
self.write_workbook_pr(w)?;
self.write_book_views(w)?;
self.write_sheets(w)?;
self.write_calc_pr(w)?;
w.end_tag(tag)?;
Ok(())
}
}
impl Workbook {
fn write_file_version<W: XmlWriter>(&self, w: &mut W) -> Result<()> {
let attrs = indexmap! {
"appName"=> "xl",
"lastEdited"=> "4",
"lowestEdited"=> "4",
"rupBuild"=> "4505",
};
w.empty_tag_with_attrs("fileVersion", attrs)
}
fn write_workbook_pr<W: XmlWriter>(&self, w: &mut W) -> Result<()> {
let attrs = indexmap! {
"defaultThemeVersion"=> "124226",
};
w.empty_tag_with_attrs("workbookPr", attrs)
}
fn write_book_views<W: XmlWriter>(&self, w: &mut W) -> Result<()> {
let tag = "bookViews";
w.start_tag(tag)?;
self.write_workbook_view(w)?;
w.end_tag(tag)?;
Ok(())
}
fn write_workbook_view<W: XmlWriter>(&self, w: &mut W) -> Result<()> {
let attrs = indexmap! {
"xWindow" => "240",
"yWindow" => "15",
"windowWidth" => "16095",
"windowHeight" => "9660",
};
w.empty_tag_with_attrs("workbookView", attrs)
}
fn write_sheets<W: XmlWriter>(&self, w: &mut W) -> Result<()> {
let tag = "sheets";
w.start_tag(tag)?;
for (i, name) in self.sheets.keys().enumerate() {
let hidden = false;
self.write_sheet(w, name, i, hidden)?;
}
w.end_tag(tag)?;
Ok(())
}
fn write_sheet<W: XmlWriter>(
&self,
w: &mut W,
sheet_name: &str,
index: usize,
hidden: bool,
) -> Result<()> {
let mut attrs = indexmap! {
"name" => sheet_name.to_string(),
"sheetId" => format!("{}", index + 1),
"r:id" => format!("rId{}", index + 1),
};
if hidden {
attrs.insert("state", "hidden".to_string());
}
w.empty_tag_with_attrs("sheet", attrs)
}
fn write_calc_pr<W: XmlWriter>(&self, w: &mut W) -> Result<()> {
let mut attrs = indexmap! {
"calcId" => "124519",
"fullCalcOnLoad" => "1",
};
w.empty_tag_with_attrs("calcPr", attrs)
}
}
pub struct DocProperties {
pub title: String,
pub subject: String,
pub author: String,
pub manager: String,
pub company: String,
pub category: String,
pub keywords: String,
pub comments: String,
pub status: String,
pub hyperlink_base: String,
pub created: Option<DateTime<Utc>>,
}
impl Default for DocProperties {
fn default() -> Self {
DocProperties {
title: Default::default(),
subject: Default::default(),
author: Default::default(),
manager: Default::default(),
company: Default::default(),
category: Default::default(),
keywords: Default::default(),
comments: Default::default(),
status: Default::default(),
hyperlink_base: Default::default(),
created: Default::default(),
}
}
}
enum Sheet {
Worksheet(Worksheet),
Chartsheet(Chartsheet),
}
impl Sheet {
fn as_worksheet(&self) -> Option<&Worksheet> {
match self {
Sheet::Worksheet(s) => Some(s),
Sheet::Chartsheet(_) => None,
}
}
fn as_mut_worksheet(&mut self) -> Option<&mut Worksheet> {
match self {
Sheet::Worksheet(s) => Some(s),
Sheet::Chartsheet(_) => None,
}
}
fn as_chartsheet(&self) -> Option<&Chartsheet> {
match self {
Sheet::Worksheet(_) => None,
Sheet::Chartsheet(s) => Some(s),
}
}
fn as_mut_chartsheet(&mut self) -> Option<&mut Chartsheet> {
match self {
Sheet::Worksheet(_) => None,
Sheet::Chartsheet(s) => Some(s),
}
}
}
enum CustomProperty {
S(String),
B(bool),
I(i32),
N(f64),
D(DateTime<Utc>),
}
impl XmlWritable for IndexMap<String, CustomProperty> {
fn write_xml<W: XmlWriter>(&self, w: &mut W) -> Result<()> {
let attrs = indexmap! {
"xmlns" => format!("{}/custom-properties", SCHEMA_OFFICEDOC),
"xmlns:vt" => format!("{}/docPropsVTypes", SCHEMA_OFFICEDOC),
};
let tag = "Properties";
w.start_tag_with_attrs(tag, attrs)?;
for (i, (name, value)) in self.iter().enumerate() {
value.write_xml(w, name, i + 2)?;
}
w.end_tag(tag)?;
Ok(())
}
}
impl CustomProperty {
fn write_xml<W: XmlWriter>(
&self,
w: &mut W,
name: &str,
pid: usize,
) -> Result<()> {
let fmtid = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}".to_string();
let attrs = indexmap! {
"fmtid" => fmtid,
"pid" => format!("{}", pid),
"name" => name.to_string()
};
let tag = "property";
w.start_tag_with_attrs(tag, attrs)?;
match self {
CustomProperty::S(v) => Self::write_lpwstr(w, v),
CustomProperty::I(v) => Self::write_vt_i4(w, *v),
CustomProperty::N(v) => Self::write_vt_r8(w, *v),
CustomProperty::B(v) => Self::write_vt_bool(w, *v),
CustomProperty::D(v) => Self::write_vt_filetime(w, *v),
}?;
w.end_tag(tag)?;
Ok(())
}
fn write_lpwstr<W: XmlWriter>(w: &mut W, v: &str) -> Result<()> {
w.tag_with_text("vt:lpwstr", v)
}
fn write_vt_i4<W: XmlWriter>(w: &mut W, v: i32) -> Result<()> {
w.tag_with_text("vt:i4", &format!("{}", v))
}
fn write_vt_r8<W: XmlWriter>(w: &mut W, v: f64) -> Result<()> {
w.tag_with_text("vt:r8", &format!("{}", v))
}
fn write_vt_bool<W: XmlWriter>(w: &mut W, v: bool) -> Result<()> {
let v = if v { "true" } else { "false" };
w.tag_with_text("vt:bool", &format!("{}", v))
}
fn write_vt_filetime<W: XmlWriter>(
w: &mut W,
v: DateTime<Utc>,
) -> Result<()> {
let v = format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
v.year(),
v.month(),
v.day(),
v.hour(),
v.minute(),
v.second()
);
w.tag_with_text("vt:filetime", &format!("{}", v))
}
}