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, properties: DocProperties, custom_properties: IndexMap, workbook_sheet_properties: WorkbookSheetProperties, } impl Workbook { /// Create a new workbook. /// There is no need to give the file name at this place, because /// nothing gets written to disk yet, this will all be done when the /// `write_file` or `write` method gets called. pub fn new() -> Self { Default::default() } pub fn write_file>(self, path: P) -> Result<()> { let file = File::create(path).context(error::Io)?; self.write(file) } pub fn write(mut self, w: W) -> Result<()> { // Add a default worksheet if none have been added. if self.sheets.is_empty() { self.add_worksheet(None)?; } // TODO: Set workbook and worksheet VBA codenames if a macro has // been added. // TODO: Set the defined names for the worksheets such as Print // Titles. // TODO: Prepare the drawings, charts and images. // TODO: Add cached data to charts. // TODO: Create a packager object to assemble sub-elements into a // zip file. // TODO: If the packager fails it is generally due to a zip // permission error. 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)?; // TODO: _write_chart_files // TODO: _write_drawing_files 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)?; // TODO: _write_worksheet_rels_file // TODO: _write_chartsheet_rels_file // TODO: _write_drawing_rels_file // TODO: _write_image_files // TODO: _add_vba_project 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>( &mut self, sheetname: S, ) -> Result { unimplemented!(); } pub fn add_format(&mut self) -> Result { unimplemented!(); } pub fn add_chart(&mut self, chart_type: ChartType) -> Result { 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, // TODO: check if we really want this type or something else. value: DateTime, ) -> Result<()> { Self::check_custom_property_name(name)?; self.custom_properties .insert(name.to_string(), CustomProperty::D(value)); Ok(()) } pub fn get_worksheet_by_name>( &mut self, name: N, ) -> Option<&mut Worksheet> { unimplemented!(); } pub fn get_chartsheet_by_name>( &mut self, name: N, ) -> Option<&mut Chartsheet> { unimplemented!(); } pub fn validate_sheet_name>( &self, name: N, ) -> Result<()> { unimplemented!(); } pub fn add_vba_project>( &mut self, filename: P, ) -> Result<()> { unimplemented!(); } pub fn set_vba_name>(&mut self, name: N) -> Result<()> { unimplemented!(); } fn build_content_types(&self) -> ContentTypes { let mut content_types = ContentTypes::default(); // TODO: fill all the other infos into the content_types. // see libxlsxwriter src/packager.c _write_content_types_file() // TODO: if vba, then use different content type 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(), ); } } } } // TODO: fill chart infos into the content_types. // see libxlsxwriter src/packager.c _write_content_types_file() // TODO: fill drawing infos into the content_types. // see libxlsxwriter src/packager.c _write_content_types_file() 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", ); } // TODO: add vba project ms package relationship if we have // the vba project set in the workbook. 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(&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)?; // TODO: write_defined_names self.write_calc_pr(w)?; w.end_tag(tag)?; Ok(()) } } impl Workbook { fn write_file_version(&self, w: &mut W) -> Result<()> { let attrs = indexmap! { "appName"=> "xl", "lastEdited"=> "4", "lowestEdited"=> "4", "rupBuild"=> "4505", }; // TODO: if self.vba_project push attribute // ("codeName", "{37E998C4-C9E5-D4B9-71C8-EB1FF731991C}"). w.empty_tag_with_attrs("fileVersion", attrs) } fn write_workbook_pr(&self, w: &mut W) -> Result<()> { let attrs = indexmap! { "defaultThemeVersion"=> "124226", }; // TODO: if self.vba_project push attribute // ("codeName", self.vba_codename). w.empty_tag_with_attrs("workbookPr", attrs) } fn write_book_views(&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(&self, w: &mut W) -> Result<()> { let attrs = indexmap! { "xWindow" => "240", "yWindow" => "15", "windowWidth" => "16095", "windowHeight" => "9660", }; // TODO: // if self.first_sheet { // attrs.insert("firstSheet", self.first_sheet); // } // TODO: // if self.active_tab { // attrs.inesrt("activeTab", self.active_sheet); // } w.empty_tag_with_attrs("workbookView", attrs) } fn write_sheets(&self, w: &mut W) -> Result<()> { let tag = "sheets"; w.start_tag(tag)?; for (i, name) in self.sheets.keys().enumerate() { // TODO: set the hidden flag. let hidden = false; self.write_sheet(w, name, i, hidden)?; } w.end_tag(tag)?; Ok(()) } fn write_sheet( &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(&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>, } 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), } impl XmlWritable for IndexMap { fn write_xml(&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( &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: &mut W, v: &str) -> Result<()> { w.tag_with_text("vt:lpwstr", v) } fn write_vt_i4(w: &mut W, v: i32) -> Result<()> { w.tag_with_text("vt:i4", &format!("{}", v)) } fn write_vt_r8(w: &mut W, v: f64) -> Result<()> { w.tag_with_text("vt:r8", &format!("{}", v)) } fn write_vt_bool(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: &mut W, v: DateTime, ) -> 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)) } }