From 83dd6408ec0e6d50abac9b44d177df26c25179aa Mon Sep 17 00:00:00 2001 From: David DiMaria Date: Wed, 23 Jul 2025 12:44:30 -0600 Subject: [PATCH 01/65] First pass at adding formatting --- STYLE_FEATURE.md | 253 +++++++++++++ examples/style_example.rs | 92 +++++ src/datatype.rs | 145 +++++++ src/lib.rs | 121 +++++- src/style.rs | 768 ++++++++++++++++++++++++++++++++++++++ src/xlsb/mod.rs | 1 + src/xlsx/cells_reader.rs | 143 ++++--- src/xlsx/mod.rs | 52 +-- src/xlsx/style_parser.rs | 469 +++++++++++++++++++++++ 9 files changed, 1961 insertions(+), 83 deletions(-) create mode 100644 STYLE_FEATURE.md create mode 100644 examples/style_example.rs create mode 100644 src/style.rs create mode 100644 src/xlsx/style_parser.rs diff --git a/STYLE_FEATURE.md b/STYLE_FEATURE.md new file mode 100644 index 00000000..3f0e361e --- /dev/null +++ b/STYLE_FEATURE.md @@ -0,0 +1,253 @@ +# Style Support in Calamine + +This document describes the new style extraction functionality added to calamine, inspired by the umya-spreadsheet library. + +## Overview + +Calamine now supports extracting style information from Excel files, including: +- Font properties (name, size, weight, color, etc.) +- Fill properties (background colors, patterns) +- Border properties (style, color, position) +- Alignment properties (horizontal, vertical, text rotation, etc.) +- Protection properties (locked, hidden) + +## Data Structures + +### Style Components + +The style system is built around several key data structures: + +#### Color +```rust +use calamine::Color; + +let red_color = Color::rgb(255, 0, 0); +let custom_color = Color::new(255, 128, 64, 32); // ARGB +``` + +#### Font +```rust +use calamine::{Font, FontWeight, FontStyle, UnderlineStyle}; + +let font = Font::new() + .with_name("Arial".to_string()) + .with_size(12.0) + .with_weight(FontWeight::Bold) + .with_style(FontStyle::Italic) + .with_color(Color::rgb(255, 0, 0)); +``` + +#### Fill +```rust +use calamine::{Fill, FillPattern}; + +let fill = Fill::solid(Color::rgb(255, 255, 0)); +let pattern_fill = Fill::new() + .with_pattern(FillPattern::DarkGray) + .with_foreground_color(Color::rgb(255, 0, 0)) + .with_background_color(Color::rgb(0, 0, 255)); +``` + +#### Borders +```rust +use calamine::{Borders, Border, BorderStyle}; + +let borders = Borders::new(); +let border = Border::with_color(BorderStyle::Thick, Color::rgb(0, 0, 0)); +``` + +#### Alignment +```rust +use calamine::{Alignment, HorizontalAlignment, VerticalAlignment, TextRotation}; + +let alignment = Alignment::new() + .with_horizontal(HorizontalAlignment::Center) + .with_vertical(VerticalAlignment::Middle) + .with_wrap_text(true); +``` + +#### Complete Style +```rust +use calamine::Style; + +let style = Style::new() + .with_font(font) + .with_fill(fill) + .with_borders(borders) + .with_alignment(alignment); +``` + +## Usage Examples + +### Reading Styles from Excel Files + +```rust +use calamine::{open_workbook, Reader, Data}; + +let mut workbook = open_workbook("file.xlsx")?; + +if let Ok(range) = workbook.worksheet_range("Sheet1") { + for (row, col, cell) in range.cells() { + if let Some(cell_data) = cell { + if cell_data.has_style() { + if let Some(style) = cell_data.get_style() { + // Access font properties + if let Some(font) = style.get_font() { + println!("Font: {}", font.name.as_deref().unwrap_or("Unknown")); + println!("Size: {}", font.size.unwrap_or(0.0)); + println!("Bold: {}", font.is_bold()); + if let Some(color) = font.color { + println!("Color: {}", color); + } + } + + // Access fill properties + 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); + } + } + } + + // Access border properties + if let Some(borders) = style.get_borders() { + if borders.has_visible_borders() { + println!("Has borders"); + if borders.left.is_visible() { + println!("Left border"); + } + } + } + } + } + } + } +} +``` + +### Creating Cells with Styles + +```rust +use calamine::{Cell, Data, Style, Font, FontWeight, Color}; + +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".to_string()), style); +``` + +### Working with CellData + +```rust +use calamine::{CellData, Data, Style}; + +let cell_data = CellData::with_style( + Data::Int(42), + Style::new().with_font(Font::new().with_weight(FontWeight::Bold)) +); + +if cell_data.has_style() { + if let Some(style) = cell_data.get_style() { + // Access style properties + } +} +``` + +## Supported Formats + +Currently, style extraction is supported for: +- **XLSX**: Full style support including fonts, fills, borders, and alignment +- **XLSB**: Basic style support (format-based) +- **XLS**: Basic style support (format-based) +- **ODS**: Basic style support (format-based) + +## Style Parsing + +The style parser extracts information from the Excel styles.xml file, including: + +### Font Properties +- Font name +- Font size +- Font weight (bold/normal) +- Font style (italic/normal) +- Underline style +- Strikethrough +- Font color +- Font family + +### Fill Properties +- Fill pattern (solid, patterns, etc.) +- Foreground color +- Background color + +### Border Properties +- Border style (thin, medium, thick, etc.) +- Border color +- Border position (left, right, top, bottom, diagonal) + +### Alignment Properties +- Horizontal alignment (left, center, right, justify, etc.) +- Vertical alignment (top, center, bottom, justify, etc.) +- Text rotation +- Wrap text +- Indent level +- Shrink to fit + +### Protection Properties +- Cell locked +- Cell hidden + +## Limitations + +1. **Theme Colors**: Theme color support is limited and may not fully match Excel's rendering +2. **Indexed Colors**: Indexed color support is basic +3. **Complex Patterns**: Some complex fill patterns may not be fully supported +4. **Conditional Formatting**: Conditional formatting styles are not yet supported + +## Future Enhancements + +Planned improvements include: +- Full theme color support +- Conditional formatting style extraction +- Style writing capabilities +- Enhanced pattern support +- Better color space handling + +## API Reference + +### Core Types + +- `Style`: Complete cell style container +- `Font`: Font properties +- `Fill`: Fill properties +- `Borders`: Border properties +- `Alignment`: Alignment properties +- `Protection`: Protection properties +- `Color`: Color representation +- `CellData`: Cell value with optional style + +### Key Methods + +- `Cell::with_style()`: Create a cell with style +- `Cell::get_style()`: Get cell style +- `Cell::has_style()`: Check if cell has style +- `Style::is_empty()`: Check if style has any properties +- `Style::has_visible_properties()`: Check if style has visible properties + +## Migration Guide + +For existing code, the new style functionality is backward compatible. Existing code will continue to work without changes. To add style support: + +1. Update your cell iteration to check for styles +2. Use `Cell::with_style()` when creating cells with styles +3. Access style properties through the style getter methods + +## Examples + +See the `examples/style_example.rs` file for a complete working example of style extraction and usage. \ No newline at end of file diff --git a/examples/style_example.rs b/examples/style_example.rs new file mode 100644 index 00000000..17202cf1 --- /dev/null +++ b/examples/style_example.rs @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT +// +// Copyright 2016-2025, Johann Tuffe. + +use calamine::{open_workbook, Cell, Color, Data, Font, FontWeight, Reader, 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(3.14), 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/datatype.rs b/src/datatype.rs index cb79df50..6858eb61 100644 --- a/src/datatype.rs +++ b/src/datatype.rs @@ -10,6 +10,7 @@ use serde::de::Visitor; use serde::Deserialize; use super::CellErrorType; +use super::Style; #[cfg(feature = "dates")] static EXCEL_EPOCH: OnceLock = OnceLock::new(); @@ -21,6 +22,67 @@ const EXCEL_1900_1904_DIFF: f64 = 1462.; #[cfg(feature = "dates")] 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