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
//! テストで使う入力を組み立てるところ
//!
//! folio は読むだけなので、試すファイルはテストの中で組み立てる。
//! XML を手で書いてここで ZIP に包むので、何を試しているかはテストを読めば分かる。
#![allow(dead_code)]
pub mod deflate;
pub mod zip;
use std::path::Path;
/// XML の宣言
pub const HEAD: &str = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>";
/// 関係の型の前置き
pub const RELATION: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
/// 表計算の名前空間
pub const SPREADSHEET: &str = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
/// 文書の名前空間
pub const WORD: &str = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
/// スライドの名前空間
pub const PRESENTATION: &str = "http://schemas.openxmlformats.org/presentationml/2006/main";
/// 図形の名前空間
pub const DRAWING: &str = "http://schemas.openxmlformats.org/drawingml/2006/main";
/// 関係の一覧の名前空間
const RELATIONSHIPS: &str = "http://schemas.openxmlformats.org/package/2006/relationships";
/// パートの型の一覧。folio は読まないが、形をそろえるために置く
const CONTENT_TYPES: &str = "<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">\
<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\
<Default Extension=\"xml\" ContentType=\"application/xml\"/></Types>";
/// パートを並べて ZIP にする。どのパートも deflate で縮める
pub fn package(path: &Path, part: &[(String, String)]) {
let mut writer = zip::Writer::create(path).unwrap();
for (name, data) in part {
writer.add(name, data.as_bytes()).unwrap();
}
writer.finish().unwrap();
}
/// 関係の一覧。(Id, 型の末尾, 指し先) を並べる
fn relationship(link: &[(String, &str, String)]) -> String {
let mut result = format!("{HEAD}<Relationships xmlns=\"{RELATIONSHIPS}\">");
for (id, kind, target) in link {
result.push_str(&format!("<Relationship Id=\"{id}\" Type=\"{RELATION}/{kind}\" Target=\"{target}\"/>"));
}
result.push_str("</Relationships>");
result
}
/// 本体のパートを指すだけの、パッケージ自身の関係
fn root(main: &str) -> (String, String) {
let link = relationship(&[("rId1".to_string(), "officeDocument", main.to_string())]);
("_rels/.rels".to_string(), link)
}
/// シートの中身を worksheet で包む
pub fn worksheet(body: &str) -> String {
format!("{HEAD}<worksheet xmlns=\"{SPREADSHEET}\" xmlns:r=\"{RELATION}\">{body}</worksheet>")
}
/// styles.xml を組み立てる
///
/// format は自前の表示形式 (番号, 文字列)、xf は cellXfs に並べる表示形式の番号である。
/// cellStyleXfs にも xf を 1 つ置く。セルが指すのは cellXfs のほうだけであることを確かめるためである。
pub fn styles(format: &[(u32, &str)], xf: &[u32]) -> String {
let mut result = format!("{HEAD}<styleSheet xmlns=\"{SPREADSHEET}\">");
if !format.is_empty() {
result.push_str(&format!("<numFmts count=\"{}\">", format.len()));
for (id, code) in format {
result.push_str(&format!("<numFmt numFmtId=\"{id}\" formatCode=\"{code}\"/>"));
}
result.push_str("</numFmts>");
}
result.push_str("<cellStyleXfs count=\"1\"><xf numFmtId=\"14\"/></cellStyleXfs>");
result.push_str(&format!("<cellXfs count=\"{}\">", xf.len()));
for id in xf {
result.push_str(&format!("<xf numFmtId=\"{id}\" xfId=\"0\"/>"));
}
result.push_str("</cellXfs></styleSheet>");
result
}
/// 組み立てる xlsx
#[derive(Default)]
pub struct Workbook {
/// シートの名前と worksheet の XML。この順に並ぶ
sheet: Vec<(String, String)>,
/// 隠すシートの名前
hidden: Vec<String>,
/// 共有文字列の `<si>` の中身。空なら sharedStrings.xml を置かない
shared: Vec<String>,
/// sharedStrings.xml を丸ごと渡したとき。shared より優先する
shared_xml: String,
/// styles.xml。空なら置かない
style: String,
/// 日付を 1904 年方式で数えるか
epoch1904: bool,
}
impl Workbook {
pub fn new() -> Workbook {
Workbook::default()
}
/// シートを足す。body は worksheet の中身 (`<sheetData>` など)
pub fn sheet(self, name: &str, body: &str) -> Workbook {
self.worksheet(name, &worksheet(body))
}
/// worksheet の XML を丸ごと渡して、シートを足す
pub fn worksheet(mut self, name: &str, xml: &str) -> Workbook {
self.sheet.push((name.to_string(), xml.to_string()));
self
}
/// そのシートを隠す
pub fn hide(mut self, name: &str) -> Workbook {
self.hidden.push(name.to_string());
self
}
/// 共有文字列を置く。1 つが `<si>` の中身である
pub fn shared(mut self, item: &[&str]) -> Workbook {
self.shared = item.iter().map(|one| one.to_string()).collect();
self
}
/// sharedStrings.xml を丸ごと渡して置く
pub fn shared_xml(mut self, xml: &str) -> Workbook {
self.shared_xml = xml.to_string();
self
}
/// styles.xml を置く
pub fn style(mut self, xml: &str) -> Workbook {
self.style = xml.to_string();
self
}
/// 日付を 1904 年方式で数える
pub fn epoch1904(mut self) -> Workbook {
self.epoch1904 = true;
self
}
/// ファイルに書く
pub fn write(&self, path: &Path) {
let mut part = vec![("[Content_Types].xml".to_string(), format!("{HEAD}{CONTENT_TYPES}"))];
part.push(root("xl/workbook.xml"));
let mut workbook = format!("{HEAD}<workbook xmlns=\"{SPREADSHEET}\" xmlns:r=\"{RELATION}\">");
if self.epoch1904 {
workbook.push_str("<workbookPr date1904=\"1\"/>");
}
workbook.push_str("<sheets>");
let mut link = Vec::new();
for (index, (name, xml)) in self.sheet.iter().enumerate() {
let number = index + 1;
let state = if self.hidden.contains(name) {" state=\"hidden\""} else {""};
workbook.push_str(&format!("<sheet name=\"{name}\" sheetId=\"{number}\"{state} r:id=\"rId{number}\"/>"));
link.push((format!("rId{number}"), "worksheet", format!("worksheets/sheet{number}.xml")));
part.push((format!("xl/worksheets/sheet{number}.xml"), xml.clone()));
}
workbook.push_str("</sheets></workbook>");
part.push(("xl/workbook.xml".to_string(), workbook));
if !self.shared_xml.is_empty() {
link.push(("rIdShared".to_string(), "sharedStrings", "sharedStrings.xml".to_string()));
part.push(("xl/sharedStrings.xml".to_string(), self.shared_xml.clone()));
} else if !self.shared.is_empty() {
let mut xml = format!("{HEAD}<sst xmlns=\"{SPREADSHEET}\" count=\"{}\">", self.shared.len());
for one in &self.shared {
xml.push_str(&format!("<si>{one}</si>"));
}
xml.push_str("</sst>");
link.push(("rIdShared".to_string(), "sharedStrings", "sharedStrings.xml".to_string()));
part.push(("xl/sharedStrings.xml".to_string(), xml));
}
if !self.style.is_empty() {
link.push(("rIdStyle".to_string(), "styles", "styles.xml".to_string()));
part.push(("xl/styles.xml".to_string(), self.style.clone()));
}
part.push(("xl/_rels/workbook.xml.rels".to_string(), relationship(&link)));
package(path, &part);
}
}
/// docx を組み立てる。body は `<w:body>` の中身
pub fn document(path: &Path, body: &str) {
let xml = format!(
"{HEAD}<w:document xmlns:w=\"{WORD}\" xmlns:r=\"{RELATION}\"><w:body>{body}</w:body></w:document>"
);
package(path, &[
("[Content_Types].xml".to_string(), format!("{HEAD}{CONTENT_TYPES}")),
root("word/document.xml"),
("word/document.xml".to_string(), xml),
]);
}
/// pptx を組み立てる。slide は各スライドの `<p:spTree>` の中身
///
/// スライドのパートには、並びと逆の番号の名前を付ける。
/// 並び順をパートの名前ではなく presentation.xml から決めていることも、これで確かめられる。
pub fn presentation(path: &Path, slide: &[String]) {
let count = slide.len();
let mut part = vec![("[Content_Types].xml".to_string(), format!("{HEAD}{CONTENT_TYPES}"))];
part.push(root("ppt/presentation.xml"));
let mut main = format!(
"{HEAD}<p:presentation xmlns:a=\"{DRAWING}\" xmlns:r=\"{RELATION}\" xmlns:p=\"{PRESENTATION}\"><p:sldIdLst>"
);
let mut link = Vec::new();
for (index, body) in slide.iter().enumerate() {
let number = count - index;
main.push_str(&format!("<p:sldId id=\"{}\" r:id=\"rId{}\"/>", 256 + index, index + 1));
link.push((format!("rId{}", index + 1), "slide", format!("slides/slide{number}.xml")));
let xml = format!(
"{HEAD}<p:sld xmlns:a=\"{DRAWING}\" xmlns:r=\"{RELATION}\" xmlns:p=\"{PRESENTATION}\">\
<p:cSld><p:spTree>{body}</p:spTree></p:cSld></p:sld>"
);
part.push((format!("ppt/slides/slide{number}.xml"), xml));
}
main.push_str("</p:sldIdLst></p:presentation>");
part.push(("ppt/presentation.xml".to_string(), main));
part.push(("ppt/_rels/presentation.xml.rels".to_string(), relationship(&link)));
package(path, &part);
}
/// 文字を持つ図形を 1 つ組み立てる
///
/// place は差し込み口の種類で、空なら付けない。位置と大きさは EMU。paragraph は `<a:p>` を並べたもの。
pub fn shape(name: &str, place: &str, x: i64, y: i64, width: i64, height: i64, paragraph: &str) -> String {
let placeholder = if place.is_empty() {String::new()} else {format!("<p:ph type=\"{place}\"/>")};
format!(
"<p:sp><p:nvSpPr><p:cNvPr id=\"2\" name=\"{name}\"/><p:cNvSpPr/><p:nvPr>{placeholder}</p:nvPr></p:nvSpPr>\
<p:spPr><a:xfrm><a:off x=\"{x}\" y=\"{y}\"/><a:ext cx=\"{width}\" cy=\"{height}\"/></a:xfrm></p:spPr>\
<p:txBody><a:bodyPr/>{paragraph}</p:txBody></p:sp>"
)
}