Files
craft-libs/crates/xlsx/src/testing.rs
T
Dylan M. Taylorandkiwi4boy 119415f46f Add craft-xlsx: XLSX and XLSB part layer (workbook, strings, styles, streamed rows) (#976)
* Add craft-sheet: the cell positions, values and tables the spreadsheet formats share

craft-xls carried its own CellRef, Range, Value, CellError, Visibility, sheet-name
quoting, built-in number format table and indexed palette. craft-xlsx and craft-ods
need the same ones, so they move to a crate with no dependencies and craft-xls
re-exports them: an application gets one Value and one CellRef whichever file it read.

Added on the way: A1 parsing (CellRef::parse, Range::parse), Range::count and a1,
CellError::parse and the newer error values SpreadsheetML stores as text,
builtin_format_id for writers.

* Add craft-xlsx: the part layer of XLSX and XLSB workbooks

Package::open finds the workbook part through craft-opc and reads the workbook
part, shared strings (runs and phonetic readings), the styles tables as indexed
tables, and a worksheet's rows one at a time with the rest of the part as a tree.
Binary workbooks give the same workbook, strings, rows and merged cells from their
records. Every other part stays reachable through Package::opc.

From GridCraft crates/xlsx (7e0d96d7): read.rs, sheet_read.rs, styles.rs, xml.rs
(string items, xstring escapes), xlsb.rs and its tests, split from GridCraft's
document model.

* craft-xlsx: XLSB opens up to 4 million cells, records counted once over the workbook

Binary workbooks over 1 million cells were refused, a quarter of what .xls allows,
and an allowance of 5 million records per part refused ordinary files: Excel writes
four records per non-empty row besides the cells, and one BrtCellBlank for every
formatted empty cell. Cells are now 4 million over the workbook (also the most
shared strings), text 256 MiB, and records 40 million for every part together, as
in craft-xls.

Ported from storytold/gridcraft#3807.

---------

Co-authored-by: kiwi4boy <kiwi4boy@gmail.com>
2026-11-11 19:32:04 -05:00

51 lines
2.4 KiB
Rust

//! Small workbooks built in code, for tests and examples.
use craft_opc::{PackageOut, RelsOut};
const MAIN_NS: &str = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
const R_NS: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
/// An `.xlsx` with the given sheets, each `(name, the rows inside <sheetData>)`, and shared
/// strings. Names and strings are written as given: escape them first if they need it.
pub fn workbook(sheets: &[(&str, &str)], strings: &[&str]) -> Vec<u8> {
let mut out = PackageOut::default();
let mut root = RelsOut::default();
root.add("officeDocument", "xl/workbook.xml", false);
out.rels("", &root);
let mut rels = RelsOut::default();
let mut list = String::new();
for (i, (name, rows)) in sheets.iter().enumerate() {
let n = i + 1;
let part = format!("xl/worksheets/sheet{n}.xml");
let id = rels.add("worksheet", &format!("worksheets/sheet{n}.xml"), false);
list.push_str(&format!(r#"<sheet name="{name}" sheetId="{n}" r:id="{id}"/>"#));
out.xml_part(
&part,
"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",
format!(r#"<worksheet xmlns="{MAIN_NS}"><sheetData>{rows}</sheetData></worksheet>"#).into_bytes(),
);
}
if !strings.is_empty() {
rels.add("sharedStrings", "sharedStrings.xml", false);
let items: String = strings.iter().map(|s| format!("<si><t>{s}</t></si>")).collect();
out.xml_part(
"xl/sharedStrings.xml",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml",
format!(r#"<sst xmlns="{MAIN_NS}" count="{0}" uniqueCount="{0}">{items}</sst>"#, strings.len()).into_bytes(),
);
}
out.rels("xl/workbook.xml", &rels);
out.xml_part(
"xl/workbook.xml",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml",
format!(r#"<workbook xmlns="{MAIN_NS}" xmlns:r="{R_NS}"><sheets>{list}</sheets></workbook>"#).into_bytes(),
);
out.finish().unwrap_or_default()
}
/// A ZIP archive of the given entries, stored as named (no content types are added).
pub fn zip(entries: &[(&str, &[u8])]) -> Vec<u8> {
let entries: Vec<(String, Vec<u8>)> = entries.iter().map(|(n, b)| (n.to_string(), b.to_vec())).collect();
craft_opc::zip_entries(&entries).unwrap_or_default()
}