1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
# folio
docx、xlsx、pptx を読み書きするライブラリです。依存はありません。
3 形式とも動きます。ZIP と XML とパッケージの土台は共通で、その上に xlsx、docx、pptx の層が載っています。
旧形式 (.xls、.doc、.ppt) も読めます。`Book::open` と `Document::open` と `Presentation::open` が
先頭のバイトを見て振り分けるので、読む側は形式を気にしなくてかまいません。
```
1| A=名前 B=数 C=日付
2| A=あ B=3.5 C=2026-03-31 00:00:00
3| A=い B=-1 C=2026-09-08 13:45:30
```
英語の説明は [README.md](./README.md) にあります。
## 巨大なシート
100 万行かける 5 列の xlsx を、同じ機械で測りました。folio は書きも読みも、メモリの山が 17MB で止まります。
シート全体をメモリに置かず、行を順に流すためです。
```
書き 読み メモリの山
folio 4.4 秒 1.0 秒 17MB
openpyxl 26.5 秒 27.3 秒 114MB (read_only)
openpyxl 32.7 秒 1790MB (ふつうの読み込み)
```
読みは 27 倍、書きは 6 倍です。ふつうの読み込みで 1.8GB 要るところが、17MB で済みます。
出来上がるファイルの大きさは folio が 24MB、openpyxl が 26MB でした。
## 使い方
読むときは、シートを開いて行を順に受け取ります。
```
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!("{} 列 {:?}", cell.column, cell.value);
}
}
```
作るときは、シートの名前を先に全部渡します。`[Content_Types].xml` をファイルの先頭に置くために、
どんなシートが入るかを書き始める前に決めておく必要があるためです。
```
use folio::book::Writer;
let mut writer = Writer::create(Path::new("out.xlsx"), &["データ"])?;
writer.start("データ")?;
writer.row(&["名前".into(), 3.5.into(), true.into()])?;
writer.finish()?;
```
行番号や列を飛ばすときは `place` を使います。書式もここで指定します。
```
use folio::cell::Cell;
use folio::style::Format;
writer.place(10, &[
Cell::new(1, "とびとび".into()),
Cell::styled(3, serial.into(), Format::Date.index()),
])?;
```
## 直す
`Writer::edit` は、書き直すシートを除いた全部を圧縮したまま写します。展開も圧縮もしないので速く、
folio が中身を知らないパート (グラフ、ピボット、画像、印刷設定) もそのまま残ります。
```
let source = Book::open(Path::new("data.xlsx"))?;
let mut writer = Writer::edit(source, Path::new("out.xlsx"), &["集計"])?;
writer.start("集計")?;
writer.row(&["書き直した".into(), 999.into()])?;
writer.finish()?;
```
書き直すシートも、`<sheetData>` の外は残します。列幅、結合セル、条件付き書式、図の指し先は消えません。
そのために元のシートを一度読み流すので、大きなシートではその手間がかかります。
## セルに入るもの
`Value` は 5 通りです。
- `Empty` - 何も入っていない。書式だけ付いたセルもこれになる
- `Number(f64)` - 数。日付もここに入る
- `Text(String)` - 文字
- `Boolean(bool)` - 真偽
- `Error(String)` - `#N/A` などのエラー
xlsx のセルに日付という型はありません。日付は数で入っていて、表示形式が日付なら日付として読みます。
```
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 で出す
ほかの言語から読むときは、`examples/json.rs` で行を 1 つずつ JSON にして出せます (JSON Lines)。
シート名を渡すとそのシートの行を、渡さないとシートの一覧を出します。
```
cargo run --release --example json -- data.xlsx
cargo run --release --example json -- data.xlsx Sheet1
```
1 行は次の形です。何も入っていないセルは出しません。
```
{"number":2,"cell":[{"column":1,"letter":"A","value":"あ"},{"column":2,"letter":"B","value":3.5}]}
```
値は JSON の型で `value` に入ります。文字は文字列、数は数、真偽は真偽です。
表示形式が日付の数は、数のまま `value` に入り、`date` に `2026-03-31T00:00:00` の形の日時が付きます。
エラーは `value` の代わりに `error` を持ちます。数式のあるセルには `formula` が付きます。
Rust からは `folio::json` の `row` と `sheet` で、同じ形の文字列を作れます。
## docx
本文を段落と表に分けて、順に受け取ります。文字だけが要るときは `text` で一度に取れます。
```
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!("表 {} 行", one.row.len()),
}
}
```
段落は「続きの文字」(`Run`) の並びです。太字のところだけ書式が違うので、1 つの文が複数の続きに分かれます。
`Run` が持つのは太字、斜体、下線、取り消し線、大きさ、色、書体です。改行は `\n`、タブは `\t` として文字に入ります。
作るときは段落と表を順に書きます。段落の書式は Normal と Title、Heading1 から Heading3 まで用意してあります。
```
use folio::document::Writer;
use folio::paragraph::{Paragraph, Run};
use folio::table::{Row, Table};
let mut writer = Writer::create(Path::new("out.docx"))?;
writer.paragraph(&Paragraph::styled("見出し", "Heading1"))?;
writer.paragraph(&Paragraph::new("本文です。"))?;
writer.table(&Table::new(vec![Row::new(&["あ", "い"])]))?;
writer.finish()?;
```
`Writer::edit` は本文だけを書き直し、ほかのパートは圧縮したまま写します。本文の後ろにある用紙の大きさ、
余白、ヘッダとフッタの指し先も残ります。
```
let source = Document::open(Path::new("file.docx"))?;
let mut writer = Writer::edit(source, Path::new("out.docx"))?;
writer.paragraph(&Paragraph::new("書き直しました。"))?;
writer.finish()?;
```
2 万段落の docx で測ると、次のようになります。
```
書き 読み
folio 0.04 秒 0.01 秒
python-docx 2.5 秒 0.39 秒
```
## pptx
スライドは図形の並びです。文字は図形の中にあり、図形の外に文字は置けません。
位置と大きさは EMU で、`INCH` が 1 インチにあたります。
```
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());
}
}
```
作るときは枚数を先に渡します。
```
use folio::presentation::Writer;
use folio::slide::{INCH, Shape, Slide};
let mut writer = Writer::create(Path::new("out.pptx"), 1)?;
let title = Shape::text("題", INCH, INCH, INCH * 11, INCH, &["folio"]);
writer.slide(0, &Slide::new(vec![title]))?;
writer.finish()?;
```
`Writer::edit` は、書き直す枚だけを入れ替えます。図形の外にある背景、切り替え、差し込み口の書式は残り、
書き直さない枚とほかのパートは圧縮したまま写します。
```
let source = Presentation::open(Path::new("deck.pptx"))?;
let mut writer = Writer::edit(source, Path::new("out.pptx"), &[1])?;
writer.slide(0, &Slide::new(vec![shape]))?;
writer.finish()?;
```
## 旧形式
.xls、.doc、.ppt は、ZIP ではなく複合ファイルという別の容れ物に、それぞれのバイナリを入れたものです。
中身も XML ではありません。読み取りだけを持ちます。書き出しは新しい形式で作り直してください。
```
use folio::book::Book;
// 拡張子ではなく、先頭のバイトで決まる
let mut book = Book::open(Path::new("old.xls"))?;
let mut sheet = book.read("Sheet1")?;
while let Some(row) = sheet.read()? {
// xlsx のときと同じ書き方
}
```
読めるのは Excel 97 以降 (BIFF8) と Word 97 以降です。それより前の形式は、文字がその土地の符号で
入っていて戻せないので、はっきり断ります。パスワードの掛かったファイルも断ります。
旧形式で返らないものがあります。.xls は数式の文字列 (計算しておいた答えは返ります)。
.doc は太字などの飾りと、表の行や列のまとまり (セルは 1 つずつ段落として出てきます)。
.ppt は図形の位置と大きさです。
## 中の作り
土台は 3 形式で共通です。上から順に、パッケージ、XML、ZIP、圧縮と重なっています。
- `book` - ワークブック。開く、作る、直す
- `sheet` - シートを行ごとに読む。行を書く
- `cell`, `shared`, `style`, `date` - セルの値、共有文字列、表示形式、日付の数
- `json` - 行とシートの一覧を JSON に書く
- `document` - 文書。開く、作る、直す
- `paragraph`, `table` - 段落と続きの文字、docx の表
- `presentation` - プレゼンテーション。開く、作る、直す
- `slide` - スライド 1 枚。図形と、その中の段落
- `package` - OPC。パートと関係、触っていないパートの持ち回り
- `xml` - 事象を 1 つずつ返す読み取りと、書くときの逃がし方
- `zip` - ZIP の読み書き。ZIP64 と、圧縮したままの複写
- `inflate`, `deflate`, `crc` - deflate の展開と圧縮
- `cfb` - 旧形式の容れ物 (複合ファイル)
- `xls`, `doc`, `ppt` - 旧形式の中身
展開は別のスレッドが進めます。読む側と同時に動くので、待ち時間が減ります。
縮めたバイトもまとめては読まず、ファイルから少しずつ渡します。
## 何が入っていないか
- 旧形式は読むだけで、書けない
- パスワードで暗号化されたファイルは読まない
- 数式は文字列として持つだけで、計算はしない
- フォントや罫線は読まない。直さないファイルではパートごと写すので、失われることはない
- 書き直したシートに新しい図や結合セルを足すことはできない。元からあるものは残る
- docx の画像、コメント、脚注、目次は組み立てられない。直さないファイルではパートごと写すので残る
- docx の表の枠線は、表そのものに指定があるときだけ読める。書式から来る枠線は出ない
- pptx の図形は四角だけ。図や表、グラフは組み立てられない。直さない枚ではパートごと写すので残る
- pptx の差し込み口から受け継ぐ位置と書式は読まない。図形自身に書いてあるぶんだけを返す
## 試す
```
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 -- 1000000
```
## ライセンス
MIT と Apache-2.0 の二択です。好きなほうを選んでください。
- [LICENSE-MIT](./LICENSE-MIT)
- [LICENSE-APACHE](./LICENSE-APACHE)