From 6b25dbc1e418743c3831ab839ca01683114924d6 Mon Sep 17 00:00:00 2001 From: Terry Payne Date: Fri, 5 May 2023 22:15:44 -0500 Subject: [PATCH] Adding in ability to read merge cells from xls and xlsx files. --- src/xls.rs | 60 +++++++++++++++++++++++++++---- src/xlsx/mod.rs | 80 +++++++++++++++++++++++++++++++++++++++++ tests/merge_cells.xls | Bin 0 -> 19456 bytes tests/merge_cells.xlsx | Bin 0 -> 8948 bytes tests/test.rs | 36 +++++++++++++++++-- 5 files changed, 168 insertions(+), 8 deletions(-) create mode 100644 tests/merge_cells.xls create mode 100644 tests/merge_cells.xlsx diff --git a/src/xls.rs b/src/xls.rs index 7b4b7bb6..f837bd79 100644 --- a/src/xls.rs +++ b/src/xls.rs @@ -139,9 +139,15 @@ pub struct XlsOptions { pub force_codepage: Option, } +struct SheetData { + range: Range, + formula: Range, + merge_cells: Vec, +} + /// A struct representing an old xls format file (CFB) pub struct Xls { - sheets: BTreeMap, Range)>, + sheets: BTreeMap, vba: Option, metadata: Metadata, marker: PhantomData, @@ -204,6 +210,19 @@ impl Xls { Ok(xls) } + + /// Gets the worksheet merge cell dimensions + pub fn worksheet_merge_cells(&self, name: &str) -> Option> { + self.sheets.get(name).map(|r| r.merge_cells.clone()) + } + + /// Get the nth worksheet. Shortcut for getting the nth + /// sheet_name, then the corresponding worksheet. + pub fn worksheet_merge_cells_at(&self, n: usize) -> Option> { + let sheet = self.metadata().sheets.get(n)?; + + self.worksheet_merge_cells(&sheet.name) + } } impl Reader for Xls { @@ -225,14 +244,14 @@ impl Reader for Xls { fn worksheet_range(&mut self, name: &str) -> Result, XlsError> { self.sheets .get(name) - .map(|r| r.0.clone()) + .map(|r| r.range.clone()) .ok_or_else(|| XlsError::WorksheetNotFound(name.into())) } fn worksheets(&mut self) -> Vec<(String, Range)> { self.sheets .iter() - .map(|(name, (data, _))| (name.to_owned(), data.clone())) + .map(|(name, sheet)| (name.to_owned(), sheet.range.clone())) .collect() } @@ -240,7 +259,7 @@ impl Reader for Xls { self.sheets .get(name) .ok_or_else(|| XlsError::WorksheetNotFound(name.into())) - .map(|r| r.1.clone()) + .map(|r| r.formula.clone()) } #[cfg(feature = "picture")] @@ -390,6 +409,7 @@ impl Xls { let mut cells = Vec::new(); let mut formulas = Vec::new(); let mut fmla_pos = (0, 0); + let mut merge_cells = Vec::new(); for record in records { let r = record?; match r.typ { @@ -412,7 +432,8 @@ impl Xls { 0x027E => cells.push(parse_rk(r.data, &self.formats, self.is_1904)?), // 638: Rk 0x00FD => cells.extend(parse_label_sst(r.data, &strings)?), // LabelSst 0x00BD => parse_mul_rk(r.data, &mut cells, &self.formats, self.is_1904)?, // 189: MulRk - 0x000A => break, // 10: EOF, + 0x00E5 => parse_merge_cells(r.data, &mut merge_cells)?, // 229: Merge Cells + 0x000A => break, // 10: EOF, 0x0006 => { // 6: Formula if r.data.len() < 20 { @@ -452,7 +473,14 @@ impl Xls { } let range = Range::from_sparse(cells); let formula = Range::from_sparse(formulas); - sheets.insert(name, (range, formula)); + sheets.insert( + name, + SheetData { + range, + formula, + merge_cells, + }, + ); } self.sheets = sheets; @@ -629,6 +657,26 @@ fn parse_rk(r: &[u8], formats: &[CellFormat], is_1904: bool) -> Result) -> Result<(), XlsError> { + let count = read_u16(r); + + for i in 0..count { + let offset: usize = (2 + i * 8).into(); + + let rf = read_u16(&r[offset + 0..]); + let rl = read_u16(&r[offset + 2..]); + let cf = read_u16(&r[offset + 4..]); + let cl = read_u16(&r[offset + 6..]); + + merge_cells.push(Dimensions { + start: (rf.into(), cf.into()), + end: (rl.into(), cl.into()), + }) + } + + Ok(()) +} + fn parse_mul_rk( r: &[u8], cells: &mut Vec>, diff --git a/src/xlsx/mod.rs b/src/xlsx/mod.rs index 75863609..b82d0626 100644 --- a/src/xlsx/mod.rs +++ b/src/xlsx/mod.rs @@ -757,6 +757,55 @@ impl Xlsx { data: tbl_rng, }) } + + /// Gets the worksheet merge cell dimensions + pub fn worksheet_merge_cells( + &mut self, + name: &str, + ) -> Option, XlsxError>> { + let (_, path) = self.sheets.iter().find(|(n, _)| n == name)?; + let xml = xml_reader(&mut self.zip, path); + + xml.map(|xml| { + let mut xml = xml?; + let mut merge_cells = Vec::new(); + let mut buffer = Vec::new(); + + loop { + buffer.clear(); + + match xml.read_event_into(&mut buffer) { + Ok(Event::Start(event)) if event.local_name().as_ref() == b"mergeCells" => { + if let Ok(cells) = read_merge_cells(&mut xml) { + merge_cells = cells; + } + + break; + } + Ok(Event::Eof) => break, + Err(e) => return Err(XlsxError::Xml(e)), + _ => (), + } + } + + Ok(merge_cells) + }) + } + + /// Get the nth worksheet. Shortcut for getting the nth + /// sheet_name, then the corresponding worksheet. + pub fn worksheet_merge_cells_at( + &mut self, + n: usize, + ) -> Option, XlsxError>> { + let name = self + .metadata() + .sheets + .get(n) + .map(|sheet| sheet.name.clone())?; + + self.worksheet_merge_cells(&name) + } } struct InnerTableMetadata { @@ -1117,6 +1166,37 @@ fn check_for_password_protected(reader: &mut RS) -> Result<(), Ok(()) } +fn read_merge_cells(xml: &mut XlReader<'_>) -> Result, XlsxError> { + let mut merge_cells = Vec::new(); + + loop { + let mut buffer = Vec::new(); + + match xml.read_event_into(&mut buffer) { + Ok(Event::Start(event)) if event.local_name().as_ref() == b"mergeCell" => { + for attribute in event.attributes() { + let attribute = attribute.map_err(XlsxError::XmlAttr)?; + + if attribute.key == QName(b"ref") { + let dimensions = get_dimension(&attribute.value)?; + merge_cells.push(dimensions); + + break; + } + } + } + Ok(Event::End(event)) if event.local_name().as_ref() == b"mergeCells" => { + break; + } + Ok(Event::Eof) => return Err(XlsxError::XmlEof("")), + Err(e) => return Err(XlsxError::Xml(e)), + _ => (), + } + } + + Ok(merge_cells) +} + /// check if a char vector is a valid cell name /// column name must be between A and XFD, /// last char must be digit diff --git a/tests/merge_cells.xls b/tests/merge_cells.xls new file mode 100644 index 0000000000000000000000000000000000000000..5af5df7860af718631a0967b72d01c61271f9b29 GIT binary patch literal 19456 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 literal 0 HcmV?d00001 diff --git a/tests/merge_cells.xlsx b/tests/merge_cells.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..5c7840697b814c1a002a8c7862fd5bfc5ea1b6c1 GIT binary patch literal 8948 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 Result<(), calamine::Error> { Ok(()) } +#[test] +fn issue_305_merge_cells() { + let path = format!("{}/tests/merge_cells.xlsx", env!("CARGO_MANIFEST_DIR")); + let mut excel: Xlsx<_> = open_workbook(&path).unwrap(); + let merge_cells = excel.worksheet_merge_cells_at(0).unwrap().unwrap(); + + assert_eq!( + merge_cells, + vec![ + Dimensions::new((0, 0), (0, 1)), + Dimensions::new((1, 0), (3, 0)), + Dimensions::new((1, 1), (3, 3)) + ] + ); +} + +#[test] +fn issue_305_merge_cells_xls() { + let path = format!("{}/tests/merge_cells.xls", env!("CARGO_MANIFEST_DIR")); + let excel: Xls<_> = open_workbook(&path).unwrap(); + let merge_cells = excel.worksheet_merge_cells_at(0).unwrap(); + + assert_eq!( + merge_cells, + vec![ + Dimensions::new((0, 0), (0, 1)), + Dimensions::new((1, 0), (3, 0)), + Dimensions::new((1, 1), (3, 3)) + ] + ); +} + // cargo test --features picture #[test] #[cfg(feature = "picture")]