iroha get rust/folio
A Rust library that reads docx, xlsx and pptx. It has no dependencies.
All three formats can be read. They share one base of ZIP, XML and the package, and the xlsx, docx and pptx layers sit on top of it.
The older formats (.xls, .doc, .ppt) can be read too.
Book::open, Document::open and Presentation::open look at the first bytes of the file to tell the formats apart.
The caller does not need to care which format a file is in.
1| A=Name B=Number C=Date
2| A=first B=3.5 C=2026-03-31 00:00:00
3| A=second B=-1 C=2026-09-08 13:45:30
The Japanese documentation is in README.ja.md.
Both were measured on the same machine, reading an xlsx of 1 million rows by 5 columns. folio's memory peaks at 6MB. It never holds the whole sheet in memory, and passes the rows through one at a time, in order.
read peak memory
folio 0.17 s 6MB
openpyxl 27.3 s 114MB (read_only)
openpyxl 32.7 s 1790MB (normal load)
Reading is about 160 times faster. A normal load needs 1.8GB, and folio does the same with 6MB.
A Book can be moved to another thread, so files can be split across threads and read at the same time.
Reading 1,000 small xlsx files of 3 sheets by 200 rows took this long (Apple M4, 8 cores).
1 thread 8 threads
folio 0.31 s 0.06 s
examples/scale.rs shows how the files are split.
Open a sheet and take its rows one by one, in order.
use folio::book::Book;
use folio::cell::Value;
use std::path::Path;
let mut book = Book::open(Path::new("data.xlsx"))?;
for one in book.sheet() {
println!("{}", one.name);
}
let mut sheet = book.read("Sheet1")?;
while let Some(row) = sheet.read()? {
for cell in &row.cell {
println!("column {} {:?}", cell.column, cell.value);
}
}
Value has five variants.
Empty - nothing. A cell that has only a format is also EmptyNumber(f64) - a number. Dates are stored here tooText(String) - textBoolean(bool) - true or falseError(String) - an error such as #N/AAn xlsx cell has no date type. A date is stored as a number, and it is read as a date when the cell's number format is a date format.
let style = sheet.style();
let epoch1904 = sheet.epoch1904();
while let Some(row) = sheet.read()? {
for cell in &row.cell {
if let Value::Number(number) = &cell.value {
if style.is_date(cell.style) {
let when = folio::date::moment(*number, epoch1904);
println!("{}-{:02}-{:02}", when.year, when.month, when.day);
}
}
}
}
For reading from other languages, examples/json.rs writes each row as one line of JSON (JSON Lines).
Given a sheet name, it writes that sheet's rows. Without one, it writes the list of sheets.
cargo run --release --example json -- data.xlsx
cargo run --release --example json -- data.xlsx Sheet1
Each line has this shape. Empty cells are left out.
{"number":2,"cell":[{"column":1,"letter":"A","value":"first"},{"column":2,"letter":"B","value":3.5}]}
Values go in value with their JSON types: text is a string, a number is a number, and true or false is a boolean.
A number whose format is a date stays a number in value, and date is added with the date and time in the form 2026-03-31T00:00:00.
An error has error in place of value. A cell with a formula also gets formula.
From Rust, row and sheet in folio::json build strings of the same shape.
The body is split into paragraphs and tables, which come in order.
When only the text is needed, text returns all of it at once.
use folio::document::{Block, Document};
let mut document = Document::open(Path::new("file.docx"))?;
println!("{}", document.text()?);
let mut body = document.read()?;
while let Some(block) = body.read()? {
match block {
Block::Paragraph(one) => println!("{} {}", one.style, one.text()),
Block::Table(one) => println!("table of {} rows", one.row.len()),
}
}
A paragraph is a sequence of runs (Run), each a stretch of text with one format.
When only part of a sentence is bold, that part has a different format, so the sentence is split into several runs.
A Run holds bold, italic, underline, strikethrough, size, color and typeface.
A line break goes into the text as \n, and a tab as \t.
Measured with a docx of 20,000 paragraphs:
read
folio 0.01 s
python-docx 0.39 s
A slide is a sequence of shapes. Text sits inside shapes, and cannot be placed outside one.
Positions and sizes are in EMU, and INCH is one inch.
use folio::presentation::Presentation;
let mut file = Presentation::open(Path::new("deck.pptx"))?;
for index in 0..file.count() {
let one = file.read(index)?;
for shape in &one.shape {
println!("{} {}", shape.name, shape.body());
}
}
.xls, .doc and .ppt are not ZIP files. Each keeps its own binary data in a different container called a compound file, and the data is not XML either. They are read with the same code as the newer formats.
use folio::book::Book;
// decided by the first bytes, not by the extension
let mut book = Book::open(Path::new("old.xls"))?;
let mut sheet = book.read("Sheet1")?;
while let Some(row) = sheet.read()? {
// the same code as for xlsx
}
Excel 97 and later (BIFF8) and Word 97 and later can be read. Earlier formats store text in a locale-specific encoding that cannot be converted back, so they are refused with a clear error. Files protected with a password are refused as well.
Some things are not returned from the older formats. From .xls, the text of formulas is not returned, although the results saved with the file are. From .doc, character formatting such as bold is not returned, nor the grouping of table cells into rows and columns. Each cell comes out as a separate paragraph. From .ppt, the position and size of shapes are not returned.
The base is shared by all three formats. From the top, it is layered as package, XML, ZIP and decompression.
book - workbooks: open, and read sheetssheet - reads a sheet row by rowcell, shared, style, date - cell values, shared strings, number formats, date numbersjson - writes rows and the sheet list as JSONdocument - documents: open, and read the bodyparagraph, table - paragraphs and runs, docx tablespresentation - presentations: open, and read slidesslide - one slide: its shapes and the paragraphs inside thempackage - OPC: parts and relationshipsxml - a reader that returns one event at a timescan - tools that scan sheets and shared strings as byteszip - reading ZIP, with ZIP64inflate, crc - deflate decompression and the checksumcfb - the container of the older formats (compound file)xls, doc, ppt - the contents of the older formatsA large part is decompressed on a separate thread. It works at the same time as the reading side, so there is less waiting. The compressed bytes are not read all at once either. They are passed from the file a little at a time. A small part is decompressed on the spot, without a thread, and a file of 4MB or less is read whole when it is opened.
cargo test
cargo run --release --example show -- data.xlsx
cargo run --release --example show -- data.xlsx Sheet1
cargo run --release --example show -- file.docx
cargo run --release --example show -- deck.pptx
cargo run --release --example show -- old.xls Sheet1
cargo run --release --example json -- data.xlsx Sheet1
cargo run --release --example scale -- data.xlsx
cargo run --release --example scale -- directory 8
Dual licensed under MIT and Apache-2.0. Take whichever you prefer.