From bfba04f466cdc6d77328413148d792fe0d9047ff Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Tue, 12 May 2026 10:33:28 -0600 Subject: [PATCH] feat(xlsx): implement cell style extraction with rich text and worksheet layout Add style support for Calamine xlsx files. Public API: - `Xlsx::worksheet_style(sheet)` returns a row x col grid of cell styles using run-length encoding for memory efficiency on large workbooks. - `Xlsx::worksheet_layout(sheet)` returns column widths and row heights. Style types (in `src/style.rs`): - `Style` with optional Font / Fill / Borders / Alignment / NumberFormat / Protection. - `Color` with theme + tint resolution and indexed-color fallback. - `RichText` / `TextRun` for cells with mixed inline formatting. - `StyleRange` with RLE storage and a `cells()` iterator. Parser in `src/xlsx/style_parser.rs` handles fonts (bold / italic / underline / strikethrough / sz / color), fills, borders (with color and style per side), number formats (built-in + custom format codes), alignment (horizontal / vertical / wrap / indent / shrink / text rotation incl. stacked), protection (locked default per OOXML), theme colors with tint, and sysClr lastClr fallback. Shared-string reader now decodes rich text runs and preserves their formatting, while also handling plain text that precedes rich runs (consistent with upstream PR #637). Includes benchmarks in `benches/style.rs` and test fixtures (styles.xlsx, borders.xlsx, EMSI_JobChange_UK.xlsx, problematic_formats.xlsx, styles_1M.xlsx) covering the various code paths. Co-authored-by: Cursor --- Cargo.toml | 5 + benches/generate_large_styled_xlsx.rs | 206 ++++ benches/generate_styles_1M.rs | 206 ++++ benches/style.rs | 149 +++ examples/excel_to_csv.rs | 1 + examples/layout.rs | 81 ++ examples/style.rs | 92 ++ src/auto.rs | 20 +- src/datatype.rs | 267 ++++- src/de.rs | 10 + src/lib.rs | 101 +- src/ods.rs | 15 +- src/style.rs | 1482 +++++++++++++++++++++++++ src/xls.rs | 12 +- src/xlsb/mod.rs | 12 + src/xlsx/cells_reader.rs | 233 +++- src/xlsx/mod.rs | 1038 ++++++++++++++++- src/xlsx/style_parser.rs | 871 +++++++++++++++ tests/EMSI_JobChange_UK.xlsx | Bin 0 -> 121833 bytes tests/borders.xlsx | Bin 0 -> 8472 bytes tests/problematic_formats.xlsx | Bin 0 -> 9129 bytes tests/styles.xlsx | Bin 0 -> 8936 bytes tests/styles_1M.xlsx | Bin 0 -> 3377012 bytes tests/test.rs | 703 +++++++++++- 24 files changed, 5367 insertions(+), 137 deletions(-) create mode 100644 benches/generate_large_styled_xlsx.rs create mode 100644 benches/generate_styles_1M.rs create mode 100644 benches/style.rs create mode 100644 examples/layout.rs create mode 100644 examples/style.rs create mode 100644 src/style.rs create mode 100644 src/xlsx/style_parser.rs create mode 100644 tests/EMSI_JobChange_UK.xlsx create mode 100644 tests/borders.xlsx create mode 100644 tests/problematic_formats.xlsx create mode 100644 tests/styles.xlsx create mode 100644 tests/styles_1M.xlsx diff --git a/Cargo.toml b/Cargo.toml index 0bb160fe..94acf651 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,8 +33,13 @@ sha2 = "0.10" env_logger = "0.11" serde_derive = "1.0" rstest = { version = "0.26", default-features = false } +rust_xlsxwriter = "0.93" criterion = { version = "0.7", features = ["html_reports"] } +[[example]] +name = "generate_styles_1M" +path = "benches/generate_styles_1M.rs" + [[bench]] name = "basic" harness = false diff --git a/benches/generate_large_styled_xlsx.rs b/benches/generate_large_styled_xlsx.rs new file mode 100644 index 00000000..a688f436 --- /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..a88ea941 --- /dev/null +++ b/benches/generate_styles_1M.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 --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); + 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/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 b2d41f49..96f4897a 100644 --- a/examples/excel_to_csv.rs +++ b/examples/excel_to_csv.rs @@ -68,6 +68,7 @@ fn write_to_csv(output_file: &mut W, range: &Range) -> std::io:: Data::String(s) | Data::DateTimeIso(s) | Data::DurationIso(s) => { write!(output_file, "{s}") } + Data::RichText(r) => write!(output_file, "{}", r.plain_text()), }?; // Write the field separator except for the last column. 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..3615128c --- /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(42.0), 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/src/auto.rs b/src/auto.rs index dca74aa3..253d32af 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 e720d9a4..582bee46 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,8 +32,59 @@ 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