diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 0a714041..6975a2ec 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -6,6 +6,7 @@ jobs: rust: runs-on: ubuntu-24.04 strategy: + fail-fast: false matrix: toolchain: - "1.83" # MSRV. @@ -45,3 +46,6 @@ jobs: if: ${{ matrix.toolchain == 'stable' }} run: | cargo clippy --lib --examples --tests -- -Dwarnings + - name: Package check + if: ${{ matrix.toolchain == 'stable' }} + run: cargo package --allow-dirty diff --git a/Cargo.toml b/Cargo.toml index 44392d4e..110e217b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,17 +1,27 @@ [package] -name = "calamine" -version = "0.33.0" -authors = ["Johann Tuffe "] -repository = "https://github.com/tafia/calamine" -documentation = "https://docs.rs/calamine" -description = "An Excel/OpenDocument Spreadsheet reader and deserializer in pure Rust" +name = "calamine-styles" +version = "0.1.0" +authors = [ + "Johann Tuffe ", + "Wolfgang Schoenberger", +] +repository = "https://github.com/SynthGL/calamine" +documentation = "https://docs.rs/calamine-styles" +description = "Fork of calamine with Font, Fill, Border, Alignment, and NumberFormat style parsing for xlsx" license = "MIT" readme = "README.md" -keywords = ["excel", "ods", "xls", "xlsx", "xlsb"] +keywords = ["excel", "xlsx", "styles", "calamine", "formatting"] categories = ["encoding", "parsing", "text-processing"] exclude = ["tests/**/*"] edition = "2021" rust-version = "1.83" # For quick-xml + encoding_rs. +autobenches = false + +[lib] +# Preserve calamine's public crate path even though the published package has +# a distinct name. This keeps upstream tests, examples, and downstream source +# compatible while Cargo can install both packages explicitly. +name = "calamine" [dependencies] log = "0.4" @@ -33,6 +43,18 @@ sha2 = "0.10" env_logger = "0.11" serde_derive = "1.0" rstest = { version = "0.26", default-features = false } +rust_xlsxwriter = "0.93" +# Criterion 0.8 requires Rust 1.86; keep development targets honest at the +# crate's declared Rust 1.83 floor. +criterion = "0.5" + +[[example]] +name = "generate_styles_1M" +path = "benches/generate_styles_1M.rs" + +[[bench]] +name = "style" +harness = false [features] default = [] @@ -48,4 +70,4 @@ dates = ["chrono"] [package.metadata.docs.rs] features = ["chrono", "picture"] -rustdoc-args = ["--cfg", "docsrs"] \ No newline at end of file +rustdoc-args = ["--cfg", "docsrs"] diff --git a/README.md b/README.md index 896dbf9a..3d07bed7 100644 --- a/README.md +++ b/README.md @@ -1,414 +1,61 @@ -# calamine +# calamine-styles -An Excel/OpenDocument Spreadsheets file reader/deserializer, in pure Rust. +A maintained fork of [calamine](https://github.com/tafia/calamine) that adds +cell-style parsing for XLSX files: fonts, fills, borders, alignment, and number +formats. -[![GitHub CI Rust tests](https://github.com/tafia/calamine/workflows/Rust/badge.svg)](https://github.com/tafia/calamine/actions) -[![Build status](https://ci.appveyor.com/api/projects/status/njpnhq54h5hxsgel/branch/master?svg=true)](https://ci.appveyor.com/project/tafia/calamine/branch/master) +## Relationship to calamine -[Documentation](https://docs.rs/calamine/) +The package is published as `calamine-styles`, while its Rust library keeps the +`calamine` crate name so existing source imports remain compatible. The current +fork baseline is calamine 0.33; promotion is gated on rebasing the style delta +onto the current upstream release and passing the full test matrix. -## Description +The main addition is [`Reader::worksheet_style()`], which returns an +RLE-compressed [`StyleRange`] alongside the existing value-only worksheet +range API: -**calamine** is a pure Rust library to read and deserialize any spreadsheet file: +- Font: bold, italic, underline, strikethrough, size, color, and name +- Fill: pattern type and foreground/background color +- Border: side/diagonal style and color +- Alignment: horizontal, vertical, wrapping, indent, and rotation +- Number format: format-code string such as `#,##0.00` or `yyyy-mm-dd` -- excel like (`xls`, `xlsx`, `xlsm`, `xlsb`, `xla`, `xlam`) -- opendocument spreadsheets (`ods`) +## Usage -As long as your files are *simple enough*, this library should just work. - -## Examples - -### Serde deserialization - -It is as simple as: - -```rust -use calamine::{open_workbook, Error, Xlsx, Reader, RangeDeserializerBuilder}; - -fn example() -> Result<(), Error> { - let path = format!("{}/tests/temperature.xlsx", env!("CARGO_MANIFEST_DIR")); - let mut workbook: Xlsx<_> = open_workbook(path)?; - let range = workbook.worksheet_range("Sheet1")?; - - - let mut iter = RangeDeserializerBuilder::new().from_range(&range)?; - - if let Some(result) = iter.next() { - let (label, value): (String, f64) = result?; - assert_eq!(label, "celsius"); - assert_eq!(value, 22.2222); - Ok(()) - } else { - Err(From::from("expected at least one record but got none")) - } -} -``` - -Calamine provides helper functions to deal with invalid type values. For -instance, to deserialize a column which should contain floats but may also -contain invalid values (i.e. strings), you can use the -[`deserialize_as_f64_or_none`](https://docs.rs/calamine/latest/calamine/fn.deserialize_as_f64_or_none.html) -helper function with Serde's -[`deserialize_with`](https://serde.rs/field-attrs.html) field attribute: - -```rust -use calamine::{deserialize_as_f64_or_none, open_workbook, RangeDeserializerBuilder, Reader, Xlsx}; -use serde::Deserialize; - -#[derive(Deserialize)] -struct Record { - metric: String, - #[serde(deserialize_with = "deserialize_as_f64_or_none")] - value: Option, -} - -fn main() -> Result<(), Box> { - let path = format!("{}/tests/excel.xlsx", env!("CARGO_MANIFEST_DIR")); - let mut excel: Xlsx<_> = open_workbook(path)?; - - let range = excel - .worksheet_range("Sheet1") - .map_err(|_| calamine::Error::Msg("Cannot find Sheet1"))?; - - let iter_records = - RangeDeserializerBuilder::with_headers(&["metric", "value"]).from_range(&range)?; - - for result in iter_records { - let record: Record = result?; - println!("metric={:?}, value={:?}", record.metric, record.value); - } - - Ok(()) -} -``` - -The -[`deserialize_as_f64_or_none`](https://docs.rs/calamine/latest/calamine/fn.deserialize_as_f64_or_none.html) -function discards all invalid values. If instead you would like to return them -as `String`s, you can use the similar -[`deserialize_as_f64_or_string`](https://docs.rs/calamine/latest/calamine/fn.deserialize_as_f64_or_string.html) -function. - -### Reader: Simple - -```rust -use calamine::{Reader, Xlsx, open_workbook}; - -let mut excel: Xlsx<_> = open_workbook("file.xlsx").unwrap(); -if let Ok(r) = excel.worksheet_range("Sheet1") { - for row in r.rows() { - println!("row={:?}, row[0]={:?}", row, row[0]); - } -} -``` - -### Reader: With header row - -```rs -use calamine::{HeaderRow, Reader, Xlsx, open_workbook}; - -let mut excel: Xlsx<_> = open_workbook("file.xlsx").unwrap(); - -let sheet1 = excel - .with_header_row(HeaderRow::Row(3)) - .worksheet_range("Sheet1") - .unwrap(); +```toml +[dependencies] +calamine = { package = "calamine-styles", version = "0.1", features = ["dates"] } ``` -Note that `xlsx` and `xlsb` files support lazy loading, so specifying a -header row takes effect immediately when reading a sheet range. -In contrast, for `xls` and `ods` files, all sheets are loaded at once when -opening the workbook with default settings. -As a result, setting the header row only applies afterward and does not -provide any performance benefits. - -### Reader: More complex - -Let's assume - -- the file type (xls, xlsx ...) cannot be known at static time -- we need to get all data from the workbook -- we need to parse the vba -- we need to see the defined names -- and the formula! - -```rust -use calamine::{Reader, open_workbook_auto, Xlsx, DataType}; - -// opens a new workbook -let path = ...; // we do not know the file type -let mut workbook = open_workbook_auto(path).expect("Cannot open file"); - -// Read whole worksheet data and provide some statistics -if let Some(Ok(range)) = workbook.worksheet_range("Sheet1") { - let total_cells = range.get_size().0 * range.get_size().1; - let non_empty_cells: usize = range.used_cells().count(); - println!("Found {} cells in 'Sheet1', including {} non empty cells", - total_cells, non_empty_cells); - // alternatively, we can manually filter rows - assert_eq!(non_empty_cells, range.rows() - .flat_map(|r| r.iter().filter(|&c| c != &DataType::Empty)).count()); -} - -// Check if the workbook has a vba project -if let Ok(Some(vba)) = workbook.vba_project() { - let module1 = vba.get_module("Module 1").unwrap(); - println!("Module 1 code:"); - println!("{}", module1); - for r in vba.get_references() { - if r.is_missing() { - println!("Reference {} is broken or not accessible", r.name); - } - } -} - -// You can also get defined names definition (string representation only) -for name in workbook.defined_names() { - println!("name: {}, formula: {}", name.0, name.1); -} - -// Now get all formula! -let sheets = workbook.sheet_names().to_owned(); -for s in sheets { - println!("found {} formula in '{}'", - workbook - .worksheet_formula(&s) - .expect("sheet not found") - .expect("error while getting formula") - .rows().flat_map(|r| r.iter().filter(|f| !f.is_empty())) - .count(), - s); -} -``` - - -## Crate Features - -The following is a list of the optional features supported by the `calamine` -crate. They are all off by default. - -- `chrono`: Adds support for Chrono date/time types to the API. -- `dates`: A deprecated backwards compatible synonym for the `chrono` feature. -- `picture`: Adds support for reading raw data for pictures in spreadsheets. - -A `calamine` feature can be enabled in your `Cargo.toml` file as follows: - -```bash -cargo add calamine -F chrono -``` - - -### Others - -Browse the [examples](https://github.com/tafia/calamine/tree/master/examples) directory. - -## Performance - -As `calamine` is readonly, the comparisons will only involve reading an excel `xlsx` file and then iterating over the rows. Along with `calamine`, three other libraries were chosen, from three different languages: - -- [`excelize`](https://github.com/qax-os/excelize) written in `go` -- [`ClosedXML`](https://github.com/ClosedXML/ClosedXML) written in `C#` -- [`openpyxl`](https://foss.heptapod.net/openpyxl/openpyxl) written in `python` - -The benchmarks were done using this [dataset](https://raw.githubusercontent.com/wiki/jqnatividad/qsv/files/NYC_311_SR_2010-2020-sample-1M.7z), a `186MB` `xlsx` file when the `csv` is converted. The plotting data was gotten from the [`sysinfo`](https://github.com/GuillaumeGomez/sysinfo) crate, at a sample interval of `200ms`. The program samples the reported values for the running process and records it. - -The programs are all structured to follow the same constructs: - -`calamine`: - ```rust use calamine::{open_workbook, Reader, Xlsx}; -fn main() { - // Open workbook - let mut excel: Xlsx<_> = - open_workbook("NYC_311_SR_2010-2020-sample-1M.xlsx").expect("failed to find file"); - - // Get worksheet - let sheet = excel - .worksheet_range("NYC_311_SR_2010-2020-sample-1M") - .unwrap() - .unwrap(); - - // iterate over rows - for _row in sheet.rows() {} -} -``` - -`excelize`: - -```go -package main - -import ( - "fmt" - "github.com/xuri/excelize/v2" -) - -func main() { - // Open workbook - file, err := excelize.OpenFile(`NYC_311_SR_2010-2020-sample-1M.xlsx`) - - if err != nil { - fmt.Println(err) - return - } - - defer func() { - // Close the spreadsheet. - if err := file.Close(); err != nil { - fmt.Println(err) - } - }() - - // Select worksheet - rows, err := file.Rows("NYC_311_SR_2010-2020-sample-1M") - if err != nil { - fmt.Println(err) - return - } - - // Iterate over rows - for rows.Next() { - } -} -``` - -`ClosedXML`: - -```csharp -using ClosedXML.Excel; - -internal class Program -{ - private static void Main(string[] args) - { - // Open workbook - using var workbook = new XLWorkbook("NYC_311_SR_2010-2020-sample-1M.xlsx"); - - // Get Worksheet - // "NYC_311_SR_2010-2020-sample-1M" - var worksheet = workbook.Worksheet(1); - - // Iterate over rows - foreach (var row in worksheet.Rows()) - { +let mut excel: Xlsx<_> = open_workbook("file.xlsx").unwrap(); +let values = excel.worksheet_range("Sheet1").unwrap(); +let styles = excel.worksheet_style("Sheet1").unwrap(); - } - } +println!("{} value rows", values.height()); +for (row, column, style) in styles.cells() { + println!("style at relative ({row}, {column}): {style:?}"); } ``` -`openpyxl`: - -```python -from openpyxl import load_workbook - -# Open workbook -wb = load_workbook( - filename=r'NYC_311_SR_2010-2020-sample-1M.xlsx', read_only=True) - -# Get worksheet -ws = wb['NYC_311_SR_2010-2020-sample-1M'] - -# Iterate over rows -for row in ws.rows: - _ = row - -# Close the workbook after reading -wb.close() -``` - -### Benchmarks - -The benchmarking was done using [`hyperfine`](https://github.com/sharkdp/hyperfine) with `--warmup 3` on an `AMD RYZEN 9 5900X @ 4.0GHz` running `Windows 11`. Both `calamine` and `ClosedXML` were built in release mode. - -```bash -0.22.1 calamine.exe - Time (mean ± σ): 25.278 s ± 0.424 s [User: 24.852 s, System: 0.470 s] - Range (min … max): 24.980 s … 26.369 s 10 runs - -v2.8.0 excelize.exe - Time (mean ± σ): 44.254 s ± 0.574 s [User: 46.071 s, System: 7.754 s] - Range (min … max): 42.947 s … 44.911 s 10 runs - -0.102.1 closedxml.exe - Time (mean ± σ): 178.343 s ± 3.673 s [User: 177.442 s, System: 2.612 s] - Range (min … max): 173.232 s … 185.086 s 10 runs - -3.0.10 openpyxl.py - Time (mean ± σ): 238.554 s ± 1.062 s [User: 238.016 s, System: 0.661 s] - Range (min … max): 236.798 s … 240.167 s 10 runs -``` - -`calamine` is 1.75x faster than `excelize`, 7.05x faster than `ClosedXML`, and 9.43x faster than `openpyxl`. - -The spreadsheet has a range of 1,000,001 rows and 41 columns, for a total of 41,000,041 cells in the range. Of those, 28,056,975 cells had values. - -Going off of that number: - -- `calamine` => 1,122,279 cells per second -- `excelize` => 633,998 cells per second -- `ClosedXML` => 157,320 cells per second -- `openpyxl` => 117,612 cells per second - -### Plots - -#### Disk Read - -![bytes_from_disk](https://github.com/RoloEdits/calamine/assets/12489689/fcca1147-d73f-4d1c-b273-e7e4c183ab29) - -As stated, the filesize on disk is `186MB`: - -- `calamine` => `186MB` -- `ClosedXML` => `208MB`. -- `openpyxl` => `192MB`. -- `excelize` => `1.5GB`. - -When asking one of the maintainers of `excelize`, I got this [response](https://github.com/qax-os/excelize/issues/1695#issuecomment-1772239230): -> To avoid high memory usage for reading large files, this library allows user-specific UnzipXMLSizeLimit options when opening the workbook, to set the memory limit on the unzipping worksheet and shared string table in bytes, worksheet XML will be extracted to the system temporary directory when the file size is over this value, so you can see that data written in reading mode, and you can change the default for that to avoid this behavior. -> -> \- xuri - -#### Disk Write - -![bytes_to_disk](https://github.com/RoloEdits/calamine/assets/12489689/befa9893-7658-41a7-8cbd-b0ce5a7d9341) - -As seen in the previous section, `excelize` is writing to disk to save memory. The others don't employ that kind of mechanism. - -#### Memory - -![mem_usage](https://github.com/RoloEdits/calamine/assets/12489689/c83fdf6b-1442-4e22-8eca-84cbc1db4a26) - -![virt_mem_usage](https://github.com/RoloEdits/calamine/assets/12489689/840a96ed-33d7-44f7-8276-80bb7a02557f) -> [!NOTE] -> `ClosedXML` was reporting a constant `2.5TB` of virtual memory usage, so it was excluded from the chart. - -The stepping and falling for `calamine` is from the grows of `Vec`s and the freeing of memory right after, with the memory usage dropping down again. The sudden jump at the end is when the sheet is being read into memory. The others, being garbage collected, have a more linear climb all the way through. - -#### CPU - -![cpu_usage](https://github.com/RoloEdits/calamine/assets/12489689/c3aa55a8-b008-48ee-ba04-c08bd91c1f6f) - -Very noisy chart, but `excelize`'s spikes must be from the GC? - -## Unsupported - -Many (most) parts of the specifications are not implemented, the focus has been put on reading cell **values** and **vba** code. - -The main unsupported items are: +`StyleRange::get()` uses positions relative to the style range's start, just +like `Range::get()`. For bulk inspection, `StyleRange::cells()` iterates the +compressed range without cloning each `Style`. -- no support for writing excel files, this is a read-only library -- no support for reading extra content, such as formatting, excel parameter, encrypted components etc ... -- no support for reading VB for opendocuments +## Features -## Credits +- `chrono` / `dates`: chrono date and time types +- `picture`: raw picture data -Thanks to [xlsx-js](https://github.com/SheetJS/js-xlsx) developers! -This library is by far the simplest open source implementation I could find and helps making sense out of the official documentation. +## Maintenance contract -Thanks also to all the contributors! +This fork does not claim current-upstream parity until the rebase and +conformance work is complete. Releases require the declared MSRV, stable, +beta, and nightly test lanes plus formatting, Clippy, and package validation. ## License -MIT +MIT, matching upstream calamine. diff --git a/STYLE_FEATURE.md b/STYLE_FEATURE.md new file mode 100644 index 00000000..1b14c56f --- /dev/null +++ b/STYLE_FEATURE.md @@ -0,0 +1,179 @@ +# Style reading in Calamine + +Calamine exposes the cell formatting stored in an XLSX workbook through +`Reader::worksheet_style()`. The method returns a `StyleRange`: a compact, +run-length-encoded view of the explicit cell styles on one worksheet. + +Value ranges and style ranges are separate. `worksheet_range()` returns cell +values; its cells do not contain styles. Use `worksheet_style()` when you need +formatting, and `worksheet_range()` separately when you also need values. + +## Reading worksheet styles + +```rust,no_run +use calamine::{open_workbook, Xlsx}; + +fn main() -> Result<(), Box> { + let mut workbook: Xlsx<_> = open_workbook("file.xlsx")?; + let styles = workbook.worksheet_style("Sheet1")?; + + println!("distinct worksheet styles: {}", styles.unique_style_count()); + println!("RLE runs: {}", styles.run_count()); + + if let Some((start_row, start_column)) = styles.start() { + for (row_offset, column_offset, style) in styles.cells() { + if !style.has_visible_properties() { + continue; + } + + // StyleRange iterator coordinates are relative to its start. + let row = start_row + row_offset as u32; + let column = start_column + column_offset as u32; + println!("styled cell at ({row}, {column}): {style:?}"); + } + } + + Ok(()) +} +``` + +`StyleRange::start()` and `StyleRange::end()` are absolute, zero-based worksheet +coordinates. `StyleRange::get()` and the coordinates returned by +`StyleRange::cells()` are relative to `start()`. Sparse gaps inside the bounding +rectangle return the default empty `Style`; positions outside it return `None`. + +The palette is compacted per worksheet. `unique_style_count()` therefore counts +the styles referenced by that sheet, excluding the synthesized empty style used +for sparse gaps. + +## Inspecting a style + +```rust,no_run +use calamine::{open_workbook, HorizontalAlignment, Xlsx}; + +fn main() -> Result<(), Box> { + let mut workbook: Xlsx<_> = open_workbook("file.xlsx")?; + let styles = workbook.worksheet_style("Sheet1")?; + + for (row, column, style) in styles.cells() { + if let Some(font) = style.get_font() { + println!("({row}, {column}) font name: {:?}", font.name); + println!("font size: {:?}", font.size); + println!("bold: {}", font.is_bold()); + println!("italic: {}", font.is_italic()); + println!("underlined: {}", font.has_underline()); + println!("struck through: {}", font.has_strikethrough()); + + if let Some(color) = font.color { + println!( + "ARGB({}, {}, {}, {})", + color.alpha, color.red, color.green, color.blue + ); + } + } + + if let Some(fill) = style.get_fill() { + if fill.is_visible() { + println!("fill pattern: {:?}", fill.pattern); + println!("fill color: {:?}", fill.get_color()); + } + } + + if let Some(borders) = style.get_borders() { + if borders.has_visible_borders() { + println!("left border: {:?}", borders.left.style); + println!("right border: {:?}", borders.right.style); + println!("top border: {:?}", borders.top.style); + println!("bottom border: {:?}", borders.bottom.style); + } + } + + if let Some(alignment) = style.get_alignment() { + if alignment.horizontal == HorizontalAlignment::Center { + println!("center aligned"); + } + if alignment.wrap_text { + println!("text wrapping enabled"); + } + println!("vertical alignment: {:?}", alignment.vertical); + println!("text rotation: {:?}", alignment.text_rotation); + } + + if let Some(number_format) = style.get_number_format() { + println!("number format ID: {:?}", number_format.format_id); + println!("number format code: {:?}", number_format.format_code); + } + } + + Ok(()) +} +``` + +Alignment fields are concrete values, not `Option`s: an omitted OOXML property +is represented by the enum or boolean default. Font name, size, color, and +family remain optional because the source record may omit them. + +For locale-dependent or unknown built-in number formats, `format_code` is empty +and `format_id` preserves the workbook's numeric identifier. An empty code must +not be interpreted as `General`. + +## Random access + +`StyleRange::get((row, column))` takes coordinates relative to the range start: + +```rust,no_run +use calamine::{open_workbook, Xlsx}; + +fn main() -> Result<(), Box> { + let mut workbook: Xlsx<_> = open_workbook("file.xlsx")?; + let styles = workbook.worksheet_style("Sheet1")?; + + if let Some(style) = styles.get((0, 0)) { + println!("style at the range's top-left cell: {style:?}"); + } + + Ok(()) +} +``` + +Call `start()` first when translating an absolute worksheet coordinate into a +relative `get()` coordinate. + +## Related APIs + +- `worksheet_range()` reads values as `Range`. +- `worksheet_style()` reads explicit cell formatting as `StyleRange`. +- `worksheet_layout()` reads column widths, row heights, defaults, and layout + flags as `WorksheetLayout`. +- `worksheet_cells_reader()` provides XLSX streaming access. Its styled path + exposes cell style information; its value-only path deliberately avoids + cloning styles. +- Rich shared and inline strings may be returned as `Data::RichText`. Call + `RichText::plain_text()` when formatting runs are not needed. + +## Supported style properties + +The XLSX reader extracts: + +- font name, size, family, weight, style, underline, strikethrough, and color; +- fill pattern plus foreground and background colors; +- left, right, top, bottom, and diagonal borders; +- horizontal and vertical alignment, text rotation, wrapping, indentation, and + shrink-to-fit; +- number format ID and code; and +- cell protection flags. + +Theme colors, indexed colors, and OOXML tint values are resolved using the +workbook's theme and indexed palette when those parts are present. + +The `Reader` trait also exposes `worksheet_style()` for XLS, XLSB, and ODS so +generic code can compile across workbook types. Those readers currently return +an empty `StyleRange` after validating the worksheet name; populated style +extraction is currently implemented for XLSX. + +## Boundaries + +- Conditional-formatting rules are not evaluated into cell styles. +- Drawing and chart formatting are outside the worksheet cell-style API. +- A `StyleRange` covers explicit cell style records, not every default inherited + from application rendering behavior. diff --git a/benches/generate_large_styled_xlsx.rs b/benches/generate_large_styled_xlsx.rs new file mode 100644 index 00000000..1b8268ff --- /dev/null +++ b/benches/generate_large_styled_xlsx.rs @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: MIT +// +// Copyright 2016-2025, Johann Tuffe. + +//! Generator for large styled xlsx files for benchmarking. +//! +//! Run with: cargo run --bin generate_large_styled_xlsx +//! +//! This creates `tests/large_styled.xlsx` with 1000 copies of style patterns. + +use rust_xlsxwriter::{ + Color, Format, FormatAlign, FormatBorder, FormatUnderline, Workbook, XlsxError, +}; + +fn main() -> Result<(), XlsxError> { + let output_path = format!("{}/tests/styles_1M.xlsx", env!("CARGO_MANIFEST_DIR")); + println!( + "Generating styles_1M.xlsx (1M styled cells) at: {}", + output_path + ); + + let mut workbook = Workbook::new(); + let worksheet = workbook.add_worksheet(); + worksheet.set_name("Sheet 1")?; + + // Define formats matching styles.xlsx patterns + let bold = Format::new().set_bold(); + let italic = Format::new().set_italic(); + let underline = Format::new().set_underline(FormatUnderline::Single); + let strikethrough = Format::new().set_font_strikethrough(); + let red_font = Format::new().set_font_color(Color::Red); + let fill_yellow = Format::new().set_background_color(Color::Yellow); + let align_center = Format::new().set_align(FormatAlign::Center); + let align_right = Format::new().set_align(FormatAlign::Right); + let number_format = Format::new().set_num_format("0.00%"); + let currency_format = Format::new().set_num_format("$#,##0.00"); + let date_format = Format::new().set_num_format("yyyy-mm-dd"); + + // Border formats + let thin_border = Format::new() + .set_border(FormatBorder::Thin) + .set_border_color(Color::Black); + let thick_border = Format::new() + .set_border(FormatBorder::Thick) + .set_border_color(Color::Blue); + let dashed_border = Format::new() + .set_border(FormatBorder::Dashed) + .set_border_color(Color::Green); + + // Combined formats + let bold_italic = Format::new().set_bold().set_italic(); + let bold_red = Format::new().set_bold().set_font_color(Color::Red); + let italic_underline = Format::new() + .set_italic() + .set_underline(FormatUnderline::Single); + let center_yellow = Format::new() + .set_align(FormatAlign::Center) + .set_background_color(Color::Yellow); + let bold_border = Format::new().set_bold().set_border(FormatBorder::Thin); + + // Font size variations + let size_8 = Format::new().set_font_size(8.0); + let size_12 = Format::new().set_font_size(12.0); + let size_16 = Format::new().set_font_size(16.0); + let size_24 = Format::new().set_font_size(24.0); + + // Font name variations + let arial = Format::new().set_font_name("Arial"); + let times = Format::new().set_font_name("Times New Roman"); + let courier = Format::new().set_font_name("Courier New"); + + // Color variations + let blue_font = Format::new().set_font_color(Color::Blue); + let green_font = Format::new().set_font_color(Color::Green); + let purple_font = Format::new().set_font_color(Color::Purple); + let fill_cyan = Format::new().set_background_color(Color::Cyan); + let fill_magenta = Format::new().set_background_color(Color::Magenta); + let fill_orange = Format::new().set_background_color(Color::Orange); + + // The pattern of styles to repeat (20 columns x 50 rows = 1000 cells per block) + // 1000 repetitions = 1M cells, ~3.2MB file + let block_rows = 50; + let block_cols = 20; + let repetitions = 1000; + + println!( + "Creating {} blocks of {}x{} = {} total cells", + repetitions, + block_rows, + block_cols, + repetitions * block_rows * block_cols + ); + + for rep in 0..repetitions { + let row_offset = (rep * block_rows) as u32; + + for row in 0..block_rows as u32 { + let actual_row = row_offset + row; + + // Column 0: Bold text + worksheet.write_string_with_format(actual_row, 0, "Bold", &bold)?; + + // Column 1: Italic text + worksheet.write_string_with_format(actual_row, 1, "Italic", &italic)?; + + // Column 2: Underline text + worksheet.write_string_with_format(actual_row, 2, "Underline", &underline)?; + + // Column 3: Strikethrough + worksheet.write_string_with_format(actual_row, 3, "Strike", &strikethrough)?; + + // Column 4: Red font + worksheet.write_string_with_format(actual_row, 4, "Red", &red_font)?; + + // Column 5: Yellow fill + worksheet.write_string_with_format(actual_row, 5, "Yellow", &fill_yellow)?; + + // Column 6: Center aligned + worksheet.write_string_with_format(actual_row, 6, "Center", &align_center)?; + + // Column 7: Right aligned + worksheet.write_string_with_format(actual_row, 7, "Right", &align_right)?; + + // Column 8: Number with percentage format + worksheet.write_number_with_format( + actual_row, + 8, + 0.1234 + (row as f64 * 0.001), + &number_format, + )?; + + // Column 9: Currency format + worksheet.write_number_with_format( + actual_row, + 9, + 1234.56 + (row as f64), + ¤cy_format, + )?; + + // Column 10: Date format + worksheet.write_number_with_format( + actual_row, + 10, + 45000.0 + (row as f64), + &date_format, + )?; + + // Column 11: Thin border + worksheet.write_string_with_format(actual_row, 11, "Thin", &thin_border)?; + + // Column 12: Thick border + worksheet.write_string_with_format(actual_row, 12, "Thick", &thick_border)?; + + // Column 13: Dashed border + worksheet.write_string_with_format(actual_row, 13, "Dashed", &dashed_border)?; + + // Column 14: Bold + Italic + worksheet.write_string_with_format(actual_row, 14, "Bold+Ital", &bold_italic)?; + + // Column 15: Bold + Red + worksheet.write_string_with_format(actual_row, 15, "Bold+Red", &bold_red)?; + + // Column 16: Italic + Underline + worksheet.write_string_with_format(actual_row, 16, "Ital+Uline", &italic_underline)?; + + // Column 17: Center + Yellow + worksheet.write_string_with_format(actual_row, 17, "Ctr+Yellow", ¢er_yellow)?; + + // Column 18: Bold + Border + worksheet.write_string_with_format(actual_row, 18, "Bold+Bdr", &bold_border)?; + + // Column 19: Mixed - rotate through variations + match row % 10 { + 0 => worksheet.write_string_with_format(actual_row, 19, "Size8", &size_8)?, + 1 => worksheet.write_string_with_format(actual_row, 19, "Size12", &size_12)?, + 2 => worksheet.write_string_with_format(actual_row, 19, "Size16", &size_16)?, + 3 => worksheet.write_string_with_format(actual_row, 19, "Size24", &size_24)?, + 4 => worksheet.write_string_with_format(actual_row, 19, "Arial", &arial)?, + 5 => worksheet.write_string_with_format(actual_row, 19, "Times", ×)?, + 6 => worksheet.write_string_with_format(actual_row, 19, "Courier", &courier)?, + 7 => worksheet.write_string_with_format(actual_row, 19, "Blue", &blue_font)?, + 8 => worksheet.write_string_with_format(actual_row, 19, "Green", &green_font)?, + _ => worksheet.write_string_with_format(actual_row, 19, "Purple", &purple_font)?, + }; + } + + if rep % 100 == 0 { + println!("Progress: {}/{} blocks", rep, repetitions); + } + } + + // Set some column widths + for col in 0..block_cols as u16 { + worksheet.set_column_width(col, 12.0)?; + } + + workbook.save(&output_path)?; + + println!("Done! File saved to: {}", output_path); + println!( + "Total cells with styles: {}", + repetitions * block_rows * block_cols + ); + + Ok(()) +} diff --git a/benches/generate_styles_1M.rs b/benches/generate_styles_1M.rs new file mode 100644 index 00000000..3e3acbfc --- /dev/null +++ b/benches/generate_styles_1M.rs @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: MIT +// +// Copyright 2016-2025, Johann Tuffe. + +//! Generator for large styled xlsx files for benchmarking. +//! +//! Run with: cargo run --example generate_styles_1M +//! +//! This creates `tests/styles_1M.xlsx` with 1000 copies of style patterns. + +use rust_xlsxwriter::{ + Color, Format, FormatAlign, FormatBorder, FormatUnderline, Workbook, XlsxError, +}; + +fn main() -> Result<(), XlsxError> { + let output_path = format!("{}/tests/styles_1M.xlsx", env!("CARGO_MANIFEST_DIR")); + println!( + "Generating styles_1M.xlsx (1M styled cells) at: {}", + output_path + ); + + let mut workbook = Workbook::new(); + let worksheet = workbook.add_worksheet(); + worksheet.set_name("Sheet 1")?; + + // Define formats matching styles.xlsx patterns + let bold = Format::new().set_bold(); + let italic = Format::new().set_italic(); + let underline = Format::new().set_underline(FormatUnderline::Single); + let strikethrough = Format::new().set_font_strikethrough(); + let red_font = Format::new().set_font_color(Color::Red); + let fill_yellow = Format::new().set_background_color(Color::Yellow); + let align_center = Format::new().set_align(FormatAlign::Center); + let align_right = Format::new().set_align(FormatAlign::Right); + let number_format = Format::new().set_num_format("0.00%"); + let currency_format = Format::new().set_num_format("$#,##0.00"); + let date_format = Format::new().set_num_format("yyyy-mm-dd"); + + // Border formats + let thin_border = Format::new() + .set_border(FormatBorder::Thin) + .set_border_color(Color::Black); + let thick_border = Format::new() + .set_border(FormatBorder::Thick) + .set_border_color(Color::Blue); + let dashed_border = Format::new() + .set_border(FormatBorder::Dashed) + .set_border_color(Color::Green); + + // Combined formats + let bold_italic = Format::new().set_bold().set_italic(); + let bold_red = Format::new().set_bold().set_font_color(Color::Red); + let italic_underline = Format::new() + .set_italic() + .set_underline(FormatUnderline::Single); + let center_yellow = Format::new() + .set_align(FormatAlign::Center) + .set_background_color(Color::Yellow); + let bold_border = Format::new().set_bold().set_border(FormatBorder::Thin); + + // Font size variations + let size_8 = Format::new().set_font_size(8.0); + let size_12 = Format::new().set_font_size(12.0); + let size_16 = Format::new().set_font_size(16.0); + let size_24 = Format::new().set_font_size(24.0); + + // Font name variations + let arial = Format::new().set_font_name("Arial"); + let times = Format::new().set_font_name("Times New Roman"); + let courier = Format::new().set_font_name("Courier New"); + + // Color variations + let blue_font = Format::new().set_font_color(Color::Blue); + let green_font = Format::new().set_font_color(Color::Green); + let purple_font = Format::new().set_font_color(Color::Purple); + + // The pattern of styles to repeat (20 columns x 50 rows = 1000 cells per block) + // 1000 repetitions = 1M cells, ~3.2MB file + let block_rows = 50; + let block_cols = 20; + let repetitions = 1000; + + println!( + "Creating {} blocks of {}x{} = {} total cells", + repetitions, + block_rows, + block_cols, + repetitions * block_rows * block_cols + ); + + for rep in 0..repetitions { + let row_offset = (rep * block_rows) as u32; + + for row in 0..block_rows as u32 { + let actual_row = row_offset + row; + + // Column 0: Bold text + worksheet.write_string_with_format(actual_row, 0, "Bold", &bold)?; + + // Column 1: Italic text + worksheet.write_string_with_format(actual_row, 1, "Italic", &italic)?; + + // Column 2: Underline text + worksheet.write_string_with_format(actual_row, 2, "Underline", &underline)?; + + // Column 3: Strikethrough + worksheet.write_string_with_format(actual_row, 3, "Strike", &strikethrough)?; + + // Column 4: Red font + worksheet.write_string_with_format(actual_row, 4, "Red", &red_font)?; + + // Column 5: Yellow fill + worksheet.write_string_with_format(actual_row, 5, "Yellow", &fill_yellow)?; + + // Column 6: Center aligned + worksheet.write_string_with_format(actual_row, 6, "Center", &align_center)?; + + // Column 7: Right aligned + worksheet.write_string_with_format(actual_row, 7, "Right", &align_right)?; + + // Column 8: Number with percentage format + worksheet.write_number_with_format( + actual_row, + 8, + 0.1234 + (row as f64 * 0.001), + &number_format, + )?; + + // Column 9: Currency format + worksheet.write_number_with_format( + actual_row, + 9, + 1234.56 + (row as f64), + ¤cy_format, + )?; + + // Column 10: Date format + worksheet.write_number_with_format( + actual_row, + 10, + 45000.0 + (row as f64), + &date_format, + )?; + + // Column 11: Thin border + worksheet.write_string_with_format(actual_row, 11, "Thin", &thin_border)?; + + // Column 12: Thick border + worksheet.write_string_with_format(actual_row, 12, "Thick", &thick_border)?; + + // Column 13: Dashed border + worksheet.write_string_with_format(actual_row, 13, "Dashed", &dashed_border)?; + + // Column 14: Bold + Italic + worksheet.write_string_with_format(actual_row, 14, "Bold+Ital", &bold_italic)?; + + // Column 15: Bold + Red + worksheet.write_string_with_format(actual_row, 15, "Bold+Red", &bold_red)?; + + // Column 16: Italic + Underline + worksheet.write_string_with_format(actual_row, 16, "Ital+Uline", &italic_underline)?; + + // Column 17: Center + Yellow + worksheet.write_string_with_format(actual_row, 17, "Ctr+Yellow", ¢er_yellow)?; + + // Column 18: Bold + Border + worksheet.write_string_with_format(actual_row, 18, "Bold+Bdr", &bold_border)?; + + // Column 19: Mixed - rotate through variations + match row % 10 { + 0 => worksheet.write_string_with_format(actual_row, 19, "Size8", &size_8)?, + 1 => worksheet.write_string_with_format(actual_row, 19, "Size12", &size_12)?, + 2 => worksheet.write_string_with_format(actual_row, 19, "Size16", &size_16)?, + 3 => worksheet.write_string_with_format(actual_row, 19, "Size24", &size_24)?, + 4 => worksheet.write_string_with_format(actual_row, 19, "Arial", &arial)?, + 5 => worksheet.write_string_with_format(actual_row, 19, "Times", ×)?, + 6 => worksheet.write_string_with_format(actual_row, 19, "Courier", &courier)?, + 7 => worksheet.write_string_with_format(actual_row, 19, "Blue", &blue_font)?, + 8 => worksheet.write_string_with_format(actual_row, 19, "Green", &green_font)?, + _ => worksheet.write_string_with_format(actual_row, 19, "Purple", &purple_font)?, + }; + } + + if rep % 100 == 0 { + println!("Progress: {}/{} blocks", rep, repetitions); + } + } + + // Set some column widths + for col in 0..block_cols as u16 { + worksheet.set_column_width(col, 12.0)?; + } + + workbook.save(&output_path)?; + + println!("Done! File saved to: {}", output_path); + println!( + "Total cells with styles: {}", + repetitions * block_rows * block_cols + ); + + Ok(()) +} diff --git a/benches/style.rs b/benches/style.rs new file mode 100644 index 00000000..1308cbbc --- /dev/null +++ b/benches/style.rs @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: MIT +// +// Copyright 2016-2025, Johann Tuffe. + +//! Benchmarks for style parsing and extraction features. +//! +//! Uses styles_1M.xlsx (1M styled cells) for realistic performance measurement. +//! +//! ## Setup +//! +//! Generate the test file first: +//! ```bash +//! cargo run --example generate_styles_1M +//! ``` +//! +//! ## Run benchmarks +//! +//! ```bash +//! cargo bench --bench style +//! ``` +//! +//! ## Profiling (identify bottlenecks) +//! +//! Install samply (cross-platform, works on macOS and Linux): +//! ```bash +//! cargo install samply +//! ``` +//! +//! Profile a specific benchmark: +//! ```bash +//! samply record cargo bench --bench style -- "style/worksheet_style" --profile-time 5 +//! ``` +//! +//! This opens Firefox Profiler with an interactive flamegraph showing where time is spent. + +use calamine::{open_workbook, Reader, Xlsx}; +use criterion::{criterion_group, criterion_main, Criterion, SamplingMode}; +use std::fs::File; +use std::hint::black_box; +use std::io::BufReader; +use std::time::Duration; + +const LARGE_FILE: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/styles_1M.xlsx"); + +fn configure(c: &mut Criterion) -> criterion::BenchmarkGroup<'_, criterion::measurement::WallTime> { + let mut group = c.benchmark_group("style"); + group.sample_size(10); + group.warm_up_time(Duration::from_millis(100)); + group.measurement_time(Duration::from_secs(15)); // Accommodate slowest benchmark (~1.2s × 10) + group.sampling_mode(SamplingMode::Flat); // 1 iteration per sample for slow benchmarks + group +} + +fn bench_style_parsing(c: &mut Criterion) { + if !std::path::Path::new(LARGE_FILE).exists() { + eprintln!( + "ERROR: styles_1M.xlsx not found.\n\ + Generate with: cargo run --example generate_styles_1M" + ); + return; + } + + let mut group = configure(c); + + // Core style parsing + group.bench_function("worksheet_style", |b| { + b.iter(|| { + let mut excel: Xlsx> = + open_workbook(LARGE_FILE).expect("cannot open file"); + black_box(excel.worksheet_style("Sheet 1").unwrap()) + }) + }); + + // Layout parsing (column widths, row heights) + group.bench_function("worksheet_layout", |b| { + b.iter(|| { + let mut excel: Xlsx> = + open_workbook(LARGE_FILE).expect("cannot open file"); + black_box(excel.worksheet_layout("Sheet 1").unwrap()) + }) + }); + + // Range parsing (cell values only, no styles) + group.bench_function("worksheet_range", |b| { + b.iter(|| { + let mut excel: Xlsx> = + open_workbook(LARGE_FILE).expect("cannot open file"); + black_box(excel.worksheet_range("Sheet 1").unwrap()) + }) + }); + + // Combined range + style (common real-world usage) + group.bench_function("range_and_style", |b| { + b.iter(|| { + let mut excel: Xlsx> = + open_workbook(LARGE_FILE).expect("cannot open file"); + let range = excel.worksheet_range("Sheet 1").unwrap(); + let style = excel.worksheet_style("Sheet 1").unwrap(); + black_box((range.cells().count(), style.cells().count())) + }) + }); + + // Cell-by-cell iteration via cells_reader + group.bench_function("cells_reader", |b| { + b.iter(|| { + let mut excel: Xlsx> = + open_workbook(LARGE_FILE).expect("cannot open file"); + let mut reader = excel.worksheet_cells_reader("Sheet 1").unwrap(); + let mut count = 0usize; + while let Ok(Some(_)) = reader.next_cell() { + count += 1; + } + black_box(count) + }) + }); + + // Iterate and access ALL style properties + group.bench_function("iterate_all_properties", |b| { + b.iter(|| { + let mut excel: Xlsx> = + open_workbook(LARGE_FILE).expect("cannot open file"); + let styles = excel.worksheet_style("Sheet 1").unwrap(); + let mut count = 0usize; + for (_, _, style) in styles.cells() { + if style.get_font().is_some() { + count += 1; + } + if style.get_fill().is_some() { + count += 1; + } + if style.borders.is_some() { + count += 1; + } + if style.get_alignment().is_some() { + count += 1; + } + if style.get_number_format().is_some() { + count += 1; + } + } + black_box(count) + }) + }); + + group.finish(); +} + +criterion_group!(benches, bench_style_parsing); +criterion_main!(benches); diff --git a/examples/excel_to_csv.rs b/examples/excel_to_csv.rs index 825e32f4..9ced14e4 100644 --- a/examples/excel_to_csv.rs +++ b/examples/excel_to_csv.rs @@ -65,6 +65,7 @@ fn write_to_csv(output_file: &mut W, range: &Range) -> std::io:: Data::Error(e) => write!(output_file, "{e:?}"), Data::Float(f) => write!(output_file, "{f}"), Data::DateTime(d) => write!(output_file, "{}", d.as_f64()), + Data::RichText(value) => write!(output_file, "{}", value.plain_text()), Data::String(s) | Data::DateTimeIso(s) | Data::DurationIso(s) => { write!(output_file, "{s}") } diff --git a/examples/layout.rs b/examples/layout.rs new file mode 100644 index 00000000..a2edf07d --- /dev/null +++ b/examples/layout.rs @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +// +// Copyright 2016-2025, Johann Tuffe. + +use calamine::{open_workbook, Reader, Xlsx}; + +/// Example demonstrating how to capture column widths and row heights from Excel files +fn main() -> Result<(), Box> { + // Open an Excel file + let path = format!("{}/tests/styles.xlsx", env!("CARGO_MANIFEST_DIR")); + let mut workbook: Xlsx<_> = open_workbook(path)?; + + // Get the first sheet name + let sheet_names = workbook.sheet_names(); + if let Some(sheet_name) = sheet_names.first() { + println!("Getting layout information for sheet: {}", sheet_name); + + // Get the worksheet layout information (column widths and row heights) + let layout = workbook.worksheet_layout(sheet_name)?; + + // Display default dimensions + if let Some(default_col_width) = layout.default_column_width { + println!("Default column width: {} characters", default_col_width); + } + if let Some(default_row_height) = layout.default_row_height { + println!("Default row height: {} points", default_row_height); + } + + // Display custom column widths + if !layout.column_widths.is_empty() { + println!("\nCustom column widths:"); + for col_width in layout.column_widths.values() { + println!( + " Column {}: {} characters (custom: {}, hidden: {}, best_fit: {})", + col_width.column, + col_width.width, + col_width.custom_width, + col_width.hidden, + col_width.best_fit + ); + } + } + + // Display custom row heights + if !layout.row_heights.is_empty() { + println!("\nCustom row heights:"); + for row_height in layout.row_heights.values() { + println!( + " Row {}: {} points (custom: {}, hidden: {})", + row_height.row, row_height.height, row_height.custom_height, row_height.hidden + ); + } + } + + // Example of using the helper methods + println!("\nExample queries:"); + let effective_width_0 = layout.get_effective_column_width(0); + let effective_height_0 = layout.get_effective_row_height(0); + println!( + "Effective width of column 0: {} characters", + effective_width_0 + ); + println!("Effective height of row 0: {} points", effective_height_0); + + // Check if a specific column has custom width + if let Some(col_width) = layout.get_column_width(0) { + println!("Column 0 has custom width: {}", col_width.width); + } else { + println!("Column 0 uses default width"); + } + + // Check if layout has any custom dimensions + if layout.has_custom_dimensions() { + println!("This worksheet has custom column widths or row heights"); + } else { + println!("This worksheet uses all default dimensions"); + } + } + + Ok(()) +} diff --git a/examples/style.rs b/examples/style.rs new file mode 100644 index 00000000..06dc42e6 --- /dev/null +++ b/examples/style.rs @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT +// +// Copyright 2016-2025, Johann Tuffe. + +use calamine::{Cell, Color, Data, Font, FontWeight, Style}; + +fn main() -> Result<(), Box> { + // Example of creating a cell with style + let style = Style::new().with_font( + Font::new() + .with_name("Arial".to_string()) + .with_size(12.0) + .with_weight(FontWeight::Bold) + .with_color(Color::rgb(255, 0, 0)), + ); + + let cell = Cell::with_style((0, 0), Data::String("Hello World".to_string()), style); + + println!("Created cell with style:"); + if let Some(cell_style) = cell.get_style() { + if let Some(font) = cell_style.get_font() { + println!( + " Font: {} (size: {})", + font.name.as_deref().unwrap_or("Unknown"), + font.size.unwrap_or(0.0) + ); + println!(" Bold: {}", font.is_bold()); + if let Some(color) = font.color { + println!(" Color: {}", color); + } + } + } + + // Example of creating CellData with style + use calamine::CellData; + + let cell_data = CellData::with_style( + Data::Int(42), + Style::new().with_font(Font::new().with_weight(FontWeight::Bold)), + ); + + println!("\nCreated CellData with style:"); + if cell_data.has_style() { + if let Some(style) = cell_data.get_style() { + if let Some(font) = style.get_font() { + println!(" Bold: {}", font.is_bold()); + } + } + } + + // Example of creating a more complex style + let complex_style = Style::new() + .with_font( + Font::new() + .with_name("Times New Roman".to_string()) + .with_size(14.0) + .with_weight(FontWeight::Bold) + .with_color(Color::rgb(0, 0, 255)), + ) + .with_fill(calamine::Fill::solid(Color::rgb(255, 255, 0))) + .with_borders(calamine::Borders::new()); + + let styled_cell = Cell::with_style((1, 1), Data::Float(std::f64::consts::PI), complex_style); + + println!("\nCreated cell with complex style:"); + if let Some(style) = styled_cell.get_style() { + if let Some(font) = style.get_font() { + println!( + " Font: {} (size: {})", + font.name.as_deref().unwrap_or("Unknown"), + font.size.unwrap_or(0.0) + ); + println!(" Bold: {}", font.is_bold()); + if let Some(color) = font.color { + println!(" Font color: {}", color); + } + } + + if let Some(fill) = style.get_fill() { + if fill.is_visible() { + println!(" Has fill"); + if let Some(color) = fill.get_color() { + println!(" Fill color: {}", color); + } + } + } + } + + println!("\nStyle system is working correctly!"); + + Ok(()) +} diff --git a/profile.json.gz b/profile.json.gz new file mode 100644 index 00000000..88d63ef9 Binary files /dev/null and b/profile.json.gz differ diff --git a/src/auto.rs b/src/auto.rs index 68d51ae3..3bb37e21 100644 --- a/src/auto.rs +++ b/src/auto.rs @@ -8,7 +8,7 @@ use crate::errors::Error; use crate::vba::VbaProject; use crate::{ open_workbook, open_workbook_from_rs, Data, DataRef, HeaderRow, Metadata, Ods, Range, Reader, - ReaderRef, Xls, Xlsb, Xlsx, + ReaderRef, StyleRange, WorksheetLayout, Xls, Xlsb, Xlsx, }; use std::fs::File; use std::io::BufReader; @@ -144,6 +144,24 @@ where } } + fn worksheet_style(&mut self, name: &str) -> Result { + match self { + Sheets::Xls(ref mut e) => e.worksheet_style(name).map_err(Error::Xls), + Sheets::Xlsx(ref mut e) => e.worksheet_style(name).map_err(Error::Xlsx), + Sheets::Xlsb(ref mut e) => e.worksheet_style(name).map_err(Error::Xlsb), + Sheets::Ods(ref mut e) => e.worksheet_style(name).map_err(Error::Ods), + } + } + + fn worksheet_layout(&mut self, name: &str) -> Result { + match self { + Sheets::Xls(ref mut e) => e.worksheet_layout(name).map_err(Error::Xls), + Sheets::Xlsx(ref mut e) => e.worksheet_layout(name).map_err(Error::Xlsx), + Sheets::Xlsb(ref mut e) => e.worksheet_layout(name).map_err(Error::Xlsb), + Sheets::Ods(ref mut e) => e.worksheet_layout(name).map_err(Error::Ods), + } + } + fn worksheets(&mut self) -> Vec<(String, Range)> { match self { Sheets::Xls(e) => e.worksheets(), diff --git a/src/datatype.rs b/src/datatype.rs index bfb256fc..3c425b5c 100644 --- a/src/datatype.rs +++ b/src/datatype.rs @@ -10,6 +10,8 @@ use serde::de::Visitor; use serde::Deserialize; use super::CellErrorType; +use super::RichText; +use super::Style; // Constants used in Excel date calculations. const DAY_SECONDS: f64 = 24.0 * 60.0 * 60.; @@ -30,6 +32,57 @@ const EXCEL_1900_1904_DIFF: f64 = 1462.; #[cfg(feature = "chrono")] const MS_MULTIPLIER: f64 = 24f64 * 60f64 * 60f64 * 1e+3f64; +/// A struct that combines cell value and style information +#[derive(Debug, Clone, PartialEq, Default)] +pub struct CellData { + /// The cell value + pub value: Data, + /// The cell style + pub style: Option