Registry / RepositoryTypeScriptRustPython
Rollpie
ReadmeFiles
Versions
Info
Download
0.1.199.9 KB2026-09-230.1.098.9 KB2026-09-14
Version
0.1.1
Copyright
Rollpie, Irohabook
Publisher
math
Published
2026-09-23
Size
99.9 KB
Downloads
14
Checksum
e691f02f331393c7b5c3406746caa7ed2636c7af24b1cef422bc957ed16ecd74
Dependencies
None

[email protected]

iroha get rust/[email protected]

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.

Huge sheets

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.

Many files

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.

Usage

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);
	}
}

What a cell holds

Value has five variants.

  • Empty - nothing. A cell that has only a format is also Empty
  • Number(f64) - a number. Dates are stored here too
  • Text(String) - text
  • Boolean(bool) - true or false
  • Error(String) - an error such as #N/A

An 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);
			}
		}
	}
}

JSON output

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.

docx

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

pptx

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());
	}
}

Older formats

.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.

Internals

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 sheets
  • sheet - reads a sheet row by row
  • cell, shared, style, date - cell values, shared strings, number formats, date numbers
  • json - writes rows and the sheet list as JSON
  • document - documents: open, and read the body
  • paragraph, table - paragraphs and runs, docx tables
  • presentation - presentations: open, and read slides
  • slide - one slide: its shapes and the paragraphs inside them
  • package - OPC: parts and relationships
  • xml - a reader that returns one event at a time
  • scan - tools that scan sheets and shared strings as bytes
  • zip - reading ZIP, with ZIP64
  • inflate, crc - deflate decompression and the checksum
  • cfb - the container of the older formats (compound file)
  • xls, doc, ppt - the contents of the older formats

A 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.

What it does not do

  • Nothing is written. folio only reads
  • Files encrypted with a password are not read
  • Formulas are kept as text and never calculated
  • Fonts and borders are not read
  • Only the body of a docx is read. Headers, footers, comments and footnotes are not
  • docx table borders can be read only when the table itself specifies them. Borders that come from a style do not appear
  • The position and format a pptx shape inherits from its placeholder are not read. Only what the shape itself specifies is returned
  • Error messages are in Japanese only. The examples also print their labels in Japanese

Trying it

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

License

Dual licensed under MIT and Apache-2.0. Take whichever you prefer.