diff --git a/Cargo.toml b/Cargo.toml index 1ede2d39..b44f5a59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,10 @@ criterion = { version = "0.7", features = ["html_reports"] } name = "basic" harness = false +[[bench]] +name = "style" +harness = false + # Example that requires the `picture` feature. [[example]] name = "read_picture_data" diff --git a/benches/style.rs b/benches/style.rs new file mode 100644 index 00000000..523c616a --- /dev/null +++ b/benches/style.rs @@ -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> = + 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/README.md b/examples/README.md index 89b109f8..844b292c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -16,6 +16,8 @@ This directory contains some example of Calamine usage. - `read_hyperlinks.rs`: Reads the hyperlinks defined in an XLSX worksheet, either by sheet name or by sheet index. - `read_picture_data.rs`: Reads pictures and their metadata from an XLSX file. +- `read_row_and_column_dimensions.rs`: Reads the column widths and row heights + (the worksheet layout) from an XLSX file. ### Serialization examples 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/read_row_and_column_dimensions.rs b/examples/read_row_and_column_dimensions.rs new file mode 100644 index 00000000..73dce696 --- /dev/null +++ b/examples/read_row_and_column_dimensions.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 = "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(()) +} 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 a380b26f..b8a8594c 100644 --- a/src/auto.rs +++ b/src/auto.rs @@ -10,7 +10,7 @@ use crate::vba::VbaProject; use crate::Picture; 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; @@ -147,6 +147,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 567b951d..f7f24ea9 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