High-performance Rust crate for reading, writing, and editing Excel .xlsx files.
- Unified: Read, write, and edit in one crate — no separate reader/writer dependencies
- Fast: 4 dependencies, ~6.6K lines of Rust, constant-memory streaming mode for large files
- Excel-compatible: Validated against Microsoft Excel with zero repair errors
- Ergonomic: Builder patterns,
impl IntoExcelData, chainable methods
use zavora_xlsx::{Workbook, Format};
let mut wb = Workbook::new();
let ws = wb.worksheet(0)?;
// Write data
ws.write(0, 0, "Hello")?;
ws.write(0, 1, 42.5)?;
ws.write(0, 2, true)?;
// With formatting
let bold = Format::new().bold().font_size(14.0);
ws.write_with_format(1, 0, "Formatted", &bold)?;
wb.save("output.xlsx")?;[dependencies]
zavora-xlsx = "0.1"[dependencies]
zavora-xlsx = { version = "0.1", features = ["serde-support", "async-tokio"] }| Feature | Description |
|---|---|
mmap |
Memory-mapped file I/O via memmap2 |
serde-support |
Serialize/deserialize rows via serde |
async-tokio |
Async file I/O via tokio |
wasm |
WASM target support |
cffi |
C FFI bindings |
Dependencies: quick-xml, zip, atoi_simd, fast-float2 — no heavy frameworks.
| Category | Features |
|---|---|
| Core | Read, write, edit xlsx; multi-sheet; formulas (regular, array, dynamic); defined names; doc properties; open from file or buffer |
| Formatting | Bold, italic, underline, strikethrough, font size/name/color, background color, foreground color, number formats, borders (all sides + diagonal), alignment (9 types), text wrap, shrink to fit, indent, rotation, cell lock/unlock, quote prefix, 18 pattern fills |
| Charts | 9 types (Column, Bar, Line, Pie, Scatter, Area, Doughnut, Radar, Stock), combo charts, secondary axis, data labels, trendlines (6 types), data table, titles, legends, axis names, pixel offset positioning |
| Tables | Headers, 20+ styles, autofilter with column criteria, total row |
| Conditional Formatting | Cell value, 2/3-color scales, data bars, icon sets, formula, top/bottom N, text contains/begins/ends, duplicates/uniques, above/below average, date occurring — all with DXF support |
| Data Validation | Dropdown list, whole number, decimal, date, time, text length, custom formula — with input/error messages |
| Images | PNG and JPEG with auto dimension detection, scale width/height |
| Rich Text | Multiple fonts/colors/styles in one cell, superscript/subscript |
| Page setup, margins, headers/footers, page breaks, repeat rows/columns, print area, scale/fit-to-page, print gridlines/headings, centering, black & white, first page number | |
| View | Freeze panes, zoom, hide gridlines/headings, right-to-left, tab colors, active sheet, selection, top-left cell, default row height |
| Structure | Insert/remove rows & columns (with formula shift), hidden rows/columns, row/column grouping, autofit column widths, merge cells, column/row format, write blank, clear cell, ignore error indicators |
| Protection | Sheet protection, workbook protection, password hashing, cell lock/unlock, unprotect ranges |
| Read | Cell values, formulas, sheet visibility, merge ranges, column widths, row heights, freeze panes, extract images |
| Edit Mode | Open → modify → save with VBA/macro passthrough; preserves drawings, charts, comments, merges, widths, heights on dirty sheets |
use zavora_xlsx::{Workbook, CalcMode, DocProperties};
let mut wb = Workbook::new(); // New workbook
let mut wb = Workbook::open("file.xlsx")?; // Edit existing
let mut wb = Workbook::open_readonly("file.xlsx")?; // Read-only
let mut wb = Workbook::open_from_buffer(&bytes)?; // Edit from memory
let mut wb = Workbook::open_readonly_from_buffer(&bytes)?; // Read from memory
// Sheets
let ws = wb.add_worksheet(); // Add sheet
let ws = wb.add_worksheet_with_name("Sales")?; // Named sheet
let ws = wb.worksheet(0)?; // By index
let ws = wb.worksheet_by_name("Sales")?; // By name
wb.remove_worksheet(1)?; // Remove
wb.rename_worksheet(0, "NewName")?; // Rename
wb.move_worksheet(0, 2)?; // Reorder
let names = wb.sheet_names(); // List names
let n = wb.sheet_count(); // Count
// Settings
wb.define_name("TaxRate", "'Config'!$A$1");
wb.set_properties(DocProperties::new().title("Report").author("Team"));
wb.set_calc_mode(CalcMode::Manual);
wb.set_active_sheet(0); // Which sheet opens first
wb.protect(); // Structure protection
wb.protect_with_password("secret");
// Save
wb.save("output.xlsx")?;
let bytes = wb.save_to_buffer()?;All coordinates are 0-based: (row: u32, col: u16).
let ws = wb.worksheet(0)?;
// Write cells
ws.write(0, 0, "text")?; // String
ws.write(0, 1, 42.5)?; // Number
ws.write(0, 2, true)?; // Boolean
ws.write_formula(0, 3, "SUM(A1:C1)")?; // Formula
ws.write_formula_with_result(0, 4, "A1*2", 42.0)?; // With cached result
ws.write_array_formula(0, 5, 1, 6, "MMULT(A1:B2,C1:D2)")?; // CSE array
ws.write_dynamic_formula(0, 7, "_xlfn.UNIQUE(A:A)")?; // Excel 365 spill
ws.write_blank(0, 8, &fmt)?; // Formatted empty cell
ws.clear_cell(0, 9); // Remove cell value
ws.write_with_format(0, 0, "bold", &fmt)?; // With format
// Batch write
ws.write_row(0, 0, ["Q1", "Q2", "Q3", "Q4"])?; // Row
ws.write_column(0, 0, [100, 200, 300])?; // Column
// Rich text
use zavora_xlsx::{RichText, RichTextRun};
let rt = RichText::new()
.add_run(RichTextRun::new("Bold ").bold())
.add_run(RichTextRun::new("Red").color("#FF0000"));
ws.write_rich_text(0, 0, &rt)?;
// Merge
ws.merge_range(0, 0, 0, 3, "Header", &fmt)?;use zavora_xlsx::CellValue;
let val = ws.read_cell(0, 0); // CellValue enum
match val {
CellValue::String(s) => println!("{s}"),
CellValue::Number(n) => println!("{n}"),
CellValue::Bool(b) => println!("{b}"),
CellValue::Empty => println!("empty"),
_ => {}
}
let range = ws.used_range(); // Option<(r1,c1,r2,c2)>ws.set_column_width(0, 20.0)?; // Column A = 20 chars
ws.set_row_height(0, 30.0)?; // Row 1 = 30pt
ws.set_freeze_panes(1, 0)?; // Freeze top row
ws.autofit()?; // Auto-size columns
ws.set_zoom(85); // 10-400%
ws.hide_gridlines();
ws.hide_headings();
ws.set_right_to_left();
ws.set_tab_color("#4472C4");
ws.set_selection(4, 1); // Cursor position
ws.set_top_left_cell(0, 0); // Scroll position
ws.set_default_row_height(20.0); // All rows 20pt
ws.set_column_format(1, ¤cy_fmt); // Format entire column
ws.set_row_format(0, &header_fmt); // Format entire row
ws.set_hidden(); // Hide sheet
ws.set_very_hidden(); // VBA-only access
ws.ignore_error("numberStoredAsText", "A1:A100"); // No green triangles
ws.set_row_hidden(5, true);
ws.set_column_hidden(3, true);
ws.group_rows(2, 5, 1); // Outline level 1
ws.group_columns(1, 3, 1);ws.insert_rows(5, 3)?; // Insert 3 rows at row 5
ws.remove_rows(5, 2)?; // Remove 2 rows at row 5
ws.insert_columns(2, 1)?; // Insert 1 column at C
ws.remove_columns(2, 1)?; // Remove column C
// Formulas automatically shift referencesBuilder pattern — all methods consume and return Self:
use zavora_xlsx::{Format, Align, BorderStyle, Underline};
let fmt = Format::new()
.bold()
.italic()
.underline(Underline::Single)
.strikethrough()
.font_size(12.0)
.font_name("Calibri")
.font_color("#FFFFFF")
.background_color("#4472C4")
.num_format("#,##0.00")
.align(Align::Center)
.align(Align::VerticalCenter) // Chain both
.border(BorderStyle::Thin)
.border_color("#000000")
.text_wrap()
.shrink_to_fit()
.indent(2)
.rotation(45)
.unlocked() // For protected sheets
.diagonal_border(BorderStyle::Thin, DiagonalType::Up) // Diagonal border
.pattern_fill(Pattern::DarkDown) // Pattern fill
.foreground_color("#FF0000") // Pattern foreground
.quote_prefix(); // Force text display
// Apply to cell or range
ws.set_cell_format(0, 0, &fmt)?;
ws.set_range_format(0, 0, 10, 5, &fmt)?;use zavora_xlsx::{Chart, ChartType, TrendlineType, LegendPosition};
let mut chart = Chart::new(ChartType::Column);
chart.set_title("Revenue by Quarter");
chart.set_x_axis_name("Quarter");
chart.set_y_axis_name("Revenue ($)");
chart.set_legend_position(LegendPosition::Bottom);
chart.set_width(800);
chart.set_height(500);
// Add series
chart.add_series()
.set_values("Sheet1!$B$2:$E$2")
.set_categories("Sheet1!$B$1:$E$1")
.set_name("North");
// Data labels
chart.add_series()
.set_values("Sheet1!$B$3:$E$3")
.set_name("South")
.set_data_labels(true);
// Trendline
chart.add_series()
.set_values("Sheet1!$B$4:$E$4")
.set_name("East")
.set_trendline(TrendlineType::Linear);
ws.insert_chart(7, 0, &chart)?;Combo charts — override chart type per series:
let mut chart = Chart::new(ChartType::Column);
chart.add_series()
.set_values("Sheet1!$B$2:$E$2")
.set_name("Revenue"); // Column (default)
chart.add_series()
.set_values("Sheet1!$B$3:$E$3")
.set_name("Margin %")
.set_chart_type(ChartType::Line) // Override to line
.set_secondary_axis(true); // Right Y-axis
chart.set_y2_axis_name("Margin %");
ws.insert_chart(7, 0, &chart)?;Trendline types: Linear, Exponential, Polynomial, Power, Logarithmic, MovingAverage
Chart types: Column, Bar, Line, Pie, Scatter, Area, Doughnut, Radar
use zavora_xlsx::{Table, TableColumn, TableStyle};
let table = Table::new()
.set_columns(&[
TableColumn::new("Name"),
TableColumn::new("Revenue"),
TableColumn::new("Growth"),
])
.set_style(TableStyle::Medium2)
.set_total_row(true);
ws.add_table(0, 0, 10, 2, &table)?;use zavora_xlsx::*;
// Cell value rule
ws.add_conditional_format(1, 1, 10, 1,
ConditionalFormatCell::new(CfOperator::GreaterThan, 100.0))?;
// Color scales
ws.add_conditional_format(1, 2, 10, 2,
ConditionalFormat2ColorScale::new("#FFFFFF", "#FF0000"))?;
ws.add_conditional_format(1, 3, 10, 3,
ConditionalFormat3ColorScale::new("#F8696B", "#FFEB84", "#63BE7B"))?;
// Data bars
ws.add_conditional_format(1, 4, 10, 4,
ConditionalFormatDataBar::new("#4472C4"))?;
// Icon sets
ws.add_conditional_format(1, 5, 10, 5,
ConditionalFormatIconSet::new(IconSetType::ThreeArrows))?;
// Formula-based
ws.add_conditional_format(1, 0, 10, 5,
ConditionalFormatFormula::new("MOD(ROW(),2)=0"))?;
// Top/Bottom
ws.add_conditional_format(1, 1, 10, 1,
ConditionalFormatTopBottom::new(TopBottomType::Top, 5))?;
// Text rules
ws.add_conditional_format(1, 0, 10, 0,
ConditionalFormatText::new(TextOperator::Contains, "overdue"))?;
// Duplicates / Uniques
ws.add_conditional_format(1, 0, 10, 0, ConditionalFormatDuplicate::new())?;
ws.add_conditional_format(1, 0, 10, 0, ConditionalFormatUnique::new())?;
// Above/Below Average
ws.add_conditional_format(1, 1, 10, 1,
ConditionalFormatAverage::new(AverageType::Above))?;
// Date Occurring
ws.add_conditional_format(1, 2, 10, 2,
ConditionalFormatDate::new(DateOccurring::ThisWeek))?;use zavora_xlsx::{DataValidation, ValidationRule, ErrorStyle};
// Dropdown list
let dv = DataValidation::new()
.list(&["Yes", "No", "Maybe"])
.input_message("Select", "Choose an option")
.error_message(ErrorStyle::Stop, "Invalid", "Pick from the list");
ws.add_data_validation(1, 0, 10, 0, &dv)?;
// Number range
let dv = DataValidation::new()
.rule(ValidationRule::WholeNumber { min: 1, max: 100 });
ws.add_data_validation(1, 1, 10, 1, &dv)?;
// Custom formula
let dv = DataValidation::new()
.custom("AND(A1>0,A1<1000)");
ws.add_data_validation(1, 2, 10, 2, &dv)?;use zavora_xlsx::Image;
let img = Image::from_path("logo.png")?; // Auto-detects dimensions
ws.insert_image(0, 0, &img)?;use zavora_xlsx::{Sparkline, SparklineType};
let sp = Sparkline::new("Sheet1!A1:F1", SparklineType::Line);
ws.add_sparkline(0, 6, &sp)?;ws.add_comment(0, 0, "Review this value");
ws.add_comment_with_author(1, 0, "Looks correct", "James");ws.write_url(0, 0, "https://example.com", "Click here")?;
ws.write_internal_link(1, 0, "'Sheet2'!A1", "Go to Sheet2")?;use zavora_xlsx::{PrintSettings, Orientation};
let ps = PrintSettings::new()
.orientation(Orientation::Landscape)
.paper_size(1) // Letter
.margins(0.75, 0.75, 0.7, 0.7)
.header("&C&\"Arial,Bold\"Monthly Report")
.footer("&CPage &P of &N");
ws.set_print_settings(&ps);
ws.set_page_breaks(&[20, 40], &[]); // Row breaks
ws.set_print_area(0, 0, 50, 10);
ws.set_repeat_rows(0, 0); // Repeat header row
ws.set_repeat_columns(0, 1); // Repeat columns A:B
ws.set_print_scale(85); // 85%// Sheet protection
ws.protect(); // Default (no password)
ws.protect_with_password("secret"); // With password
// Workbook structure protection
wb.protect();
wb.protect_with_password("secret");ws.set_autofilter(0, 0, 100, 5); // Header row through dataFor large files (100K+ rows) with constant memory usage:
use zavora_xlsx::{StreamingWorkbook, Format, DocProperties};
let mut sw = StreamingWorkbook::new();
sw.add_worksheet("Data");
sw.set_current_sheet(0);
sw.set_column_width(0, 20.0);
sw.merge_range(0, 0, 0, 3);
for row in 0..100_000 {
sw.write_number(row, 0, row as f64)?;
sw.write_string(row, 1, "data")?;
}
sw.save("large_file.xlsx")?;Open existing files, modify, and save — preserving VBA macros and unknown parts:
let mut wb = Workbook::open("existing.xlsx")?;
let ws = wb.worksheet(0)?;
ws.write(0, 0, "Updated")?;
wb.save("modified.xlsx")?;Evaluate all formula cells programmatically:
let mut wb = Workbook::new();
let ws = wb.worksheet(0)?;
ws.write(0, 0, 10.0)?;
ws.write(0, 1, 20.0)?;
ws.write_formula(0, 2, "A1+B1")?;
ws.write_formula(0, 3, "SUM(A1:C1)")?;
let count = wb.recalculate()?; // Evaluates in dependency order
println!("Evaluated {} cells", count); // 2Handles dependency graphs, circular reference detection, and volatile functions (RAND, NOW, TODAY).
use zavora_xlsx::Color;
// Named colors
Format::new().font_color(Color::Red);
// Hex string
Format::new().font_color("#4472C4");
// RGB array
Format::new().font_color([0x44, 0x72, 0xC4]);| Crate | Purpose |
|---|---|
quick-xml |
XML parsing and writing |
zip |
ZIP archive read/write (deflate) |
atoi_simd |
SIMD-accelerated integer parsing |
fast-float2 |
Fast float parsing |
Dev dependencies: calamine (read verification), tempfile (test isolation).
- 61 library tests covering all features
- 38 integration tests (demo suite) validating end-to-end file generation
- All output files validated in Microsoft Excel with zero repair errors
cargo test # Run all tests
cargo test --lib # Library tests onlyApache-2.0