-
Notifications
You must be signed in to change notification settings - Fork 250
feat(xlsx): implement cell style extraction with rich text and worksheet layout #653
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ddimaria
wants to merge
1
commit into
tafia:master
Choose a base branch
from
ddimaria:feat/styles
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| // | ||
| // Copyright 2016-2025, Johann Tuffe. | ||
|
|
||
| //! Benchmarks for style parsing and extraction features. | ||
| //! | ||
| //! Uses `tests/styles_1M.xlsx` (1M styled cells) for realistic performance | ||
| //! measurement. | ||
| //! | ||
| //! ## 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: tests/styles_1M.xlsx not found."); | ||
| return; | ||
| } | ||
|
|
||
| let mut group = configure(c); | ||
|
|
||
| // Core style parsing | ||
| group.bench_function("worksheet_style", |b| { | ||
| b.iter(|| { | ||
| let mut excel: Xlsx<BufReader<File>> = | ||
| 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<BufReader<File>> = | ||
| 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<BufReader<File>> = | ||
| 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<BufReader<File>> = | ||
| 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<BufReader<File>> = | ||
| 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<BufReader<File>> = | ||
| 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); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<dyn std::error::Error>> { | ||
| // Open an Excel file | ||
| let path = "tests/styles.xlsx"; | ||
| 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(()) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<dyn std::error::Error>> { | ||
| // 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(()) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Give this example a better name like
read_row_and_column_dimensions.rs. Also add it to theexamples/README.mdfile.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed to examples/read_row_and_column_dimensions.rs and added it to examples/README.md.