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
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
//! 旧形式のプレゼンテーション (MS-PPT)
//!
//! .ppt は複合ファイルの中の PowerPoint Document ストリームである。
//! レコードは 8 バイトの見出し (版と種類と長さ) を持ち、版が 0xF のものは入れ子になる。
//!
//! # 古い版が同じストリームに残っている
//!
//! PowerPoint は保存のたびにストリームの末尾へ書き足すので、消したはずのスライドも残っている。
//! いま生きているのがどれかは、Current User ストリームが指す UserEditAtom から始まる鎖と、
//! そこにぶら下がる持続ディレクトリで決まる。番号から位置を引く表を作ってから、
//! スライドの並びをたどる。素直に先頭から探すと、消したスライドまで拾ってしまう。
//!
//! # 何を返すか
//!
//! 図形ごとの段落を返す。位置と大きさは返さない。旧形式の位置は、この容れ物の中で
//! 単位も並び順もはっきりしないので、当て推量にしないことにした。
use crate::error::Error;
use crate::paragraph::Run;
use crate::slide::{Paragraph, Shape, Slide};
use std::collections::HashMap;
/// いま生きている編集の位置を持つレコード
const CURRENT_USER: u16 = 0x0ff6;
/// 編集 1 回分
const USER_EDIT: u16 = 0x0ff5;
/// 番号から位置を引く表
const PERSIST_DIRECTORY: u16 = 0x1772;
/// スライドの並び
const SLIDE_LIST: u16 = 0x0ff0;
/// 並びの中の 1 枚
const SLIDE_PERSIST: u16 = 0x03f3;
/// スライドそのもの
const SLIDE: u16 = 0x03ee;
/// 図形が、並びの中の何番目の文字を使うか
const TEXT_REF: u16 = 0x0f9e;
/// 図形 1 つ
const SHAPE: u16 = 0xf004;
/// 文字の役目
const TEXT_HEADER: u16 = 0x0f9f;
/// 2 バイト文字
const TEXT_WIDE: u16 = 0x0fa0;
/// 1 バイト文字
const TEXT_NARROW: u16 = 0x0fa8;
/// 入れ子になっているレコードの版
const CONTAINER: u16 = 0xf;
/// SlideListWithText に入っている文字のかたまり
///
/// 実物の PowerPoint は、題や本文の文字を図形の中ではなく、こちらにまとめて置く。
/// 図形のほうには「並びの何番目を使うか」だけが入っている。
struct Block {
place: String,
paragraph: Vec<Paragraph>,
}
/// スライド 1 枚の在り処と、その文字
struct Place {
from: usize,
to: usize,
block: Vec<Block>,
}
/// スライドを並び順に読む
pub fn read(main: &[u8], user: &[u8]) -> Result<Vec<Slide>, Error> {
let Some(order) = arrange(main, user) else {
// 鎖がたどれないファイルは、先頭から順に拾う
return Ok(scan(main));
};
let mut result = Vec::with_capacity(order.len());
for one in order {
result.push(read_slide(main, one.from, one.to, &one.block));
}
Ok(result)
}
/// 生きているスライドの位置と文字を、並び順に求める
fn arrange(main: &[u8], user: &[u8]) -> Option<Vec<Place>> {
let start = current_edit(user)?;
let place = directory(main, start)?;
let document = document_offset(main, start)?;
let one = head(main, *place.get(&document)?)?;
let list = find(main, one.body, one.body + one.length, SLIDE_LIST)?;
let mut result: Vec<Place> = Vec::new();
let mut at = list.body;
let stop = list.body + list.length;
while at < stop {
let entry = head(main, at)?;
match entry.kind {
SLIDE_PERSIST => {
let id = long(main, entry.body)?;
if let Some(offset) = place.get(&id) {
if let Some(slide) = head(main, *offset) {
if slide.kind == SLIDE {
result.push(Place {
from: slide.body,
to: slide.body + slide.length,
block: Vec::new(),
});
}
}
}
}
TEXT_HEADER => {
if let Some(last) = result.last_mut() {
let kind = long(main, entry.body).unwrap_or(1);
last.block.push(Block {
place: place_name(kind).to_string(),
paragraph: Vec::new(),
});
}
}
TEXT_WIDE | TEXT_NARROW => {
if let Some(last) = result.last_mut() {
if last.block.is_empty() {
last.block.push(Block {place: "body".to_string(), paragraph: Vec::new()});
}
let text = decode(main, &entry);
let block = last.block.last_mut().unwrap();
add(&mut block.paragraph, &text);
}
}
_ => {}
}
at = entry.body + entry.length;
}
if result.is_empty() {
return None;
}
Some(result)
}
/// Current User ストリームから、いま生きている編集の位置を読む
fn current_edit(user: &[u8]) -> Option<usize> {
let one = head(user, 0)?;
if one.kind != CURRENT_USER {
return None;
}
Some(long(user, one.body + 8)? as usize)
}
/// 編集の鎖をたどって、番号から位置を引く表を作る
///
/// 新しい編集ほど先に見るので、同じ番号は先に入ったほうを残す。
fn directory(main: &[u8], start: usize) -> Option<HashMap<u32, usize>> {
let mut result = HashMap::new();
let mut at = start;
let mut seen = 0;
loop {
let one = head(main, at)?;
if one.kind != USER_EDIT {
return None;
}
let offset = long(main, one.body + 12)? as usize;
if let Some(entry) = head(main, offset) {
if entry.kind == PERSIST_DIRECTORY {
read_directory(main, entry.body, entry.body + entry.length, &mut result);
}
}
let previous = long(main, one.body + 8)? as usize;
seen += 1;
if previous == 0 || previous >= main.len() || seen > 1024 {
break;
}
at = previous;
}
if result.is_empty() {
return None;
}
Some(result)
}
/// 持続ディレクトリの中身を読む
///
/// 先頭の 4 バイトに、始めの番号 (下位 20 ビット) と個数 (上位 12 ビット) が入っていて、
/// そのあとに位置が個数だけ並ぶ。
fn read_directory(main: &[u8], from: usize, to: usize, place: &mut HashMap<u32, usize>) {
let mut at = from;
while at + 4 <= to {
let Some(value) = long(main, at) else {
return;
};
let first = value & 0x000fffff;
let count = value >> 20;
at += 4;
for index in 0..count {
if at + 4 > to {
return;
}
let Some(offset) = long(main, at) else {
return;
};
place.entry(first + index).or_insert(offset as usize);
at += 4;
}
}
}
/// いま生きている編集が指す、本体の番号
fn document_offset(main: &[u8], start: usize) -> Option<u32> {
let one = head(main, start)?;
long(main, one.body + 16)
}
/// 入れ物の中から、その種類のレコードを探す
fn find(main: &[u8], from: usize, to: usize, kind: u16) -> Option<Head> {
let mut at = from;
while at < to {
let one = head(main, at)?;
if one.kind == kind {
return Some(one);
}
at = one.body + one.length;
}
None
}
/// 鎖をたどれないときに、スライドを先頭から拾う
fn scan(main: &[u8]) -> Vec<Slide> {
let mut result = Vec::new();
let mut at = 0;
while let Some(one) = head(main, at) {
if one.body + one.length > main.len() {
break;
}
if one.kind == SLIDE {
result.push(read_slide(main, one.body, one.body + one.length, &[]));
}
at = one.body + one.length;
}
result
}
/// スライド 1 枚を組み立てる
fn read_slide(main: &[u8], from: usize, to: usize, block: &[Block]) -> Slide {
let mut result = Slide::default();
let mut used = vec![false; block.len()];
collect(main, from, to, &mut result, block, &mut used);
// 図形から指されなかったかたまりも、落とさずに足す
for (index, one) in block.iter().enumerate() {
if used[index] || one.paragraph.is_empty() {
continue;
}
result.shape.push(Shape {
place: one.place.clone(),
paragraph: one.paragraph.clone(),
..Shape::default()
});
}
result
}
/// 入れ子をたどって図形を集める
fn collect(main: &[u8], from: usize, to: usize, slide: &mut Slide, block: &[Block], used: &mut [bool]) {
let mut at = from;
while at < to {
let Some(one) = head(main, at) else {
return;
};
if one.body + one.length > to {
return;
}
if one.kind == SHAPE {
let mut shape = Shape::default();
let reference = fill(main, one.body, one.body + one.length, &mut shape);
// 図形に文字がなければ、並びの中の何番目かを見て持ってくる
if shape.paragraph.is_empty() {
if let Some(index) = reference {
if let Some(found) = block.get(index as usize) {
shape.paragraph = found.paragraph.clone();
if shape.place.is_empty() {
shape.place = found.place.clone();
}
used[index as usize] = true;
}
}
}
if !shape.paragraph.is_empty() {
slide.shape.push(shape);
}
} else if one.version == CONTAINER {
collect(main, one.body, one.body + one.length, slide, block, used);
}
at = one.body + one.length;
}
}
/// 図形の中から文字を集める。並びの中の何番目を使うかも返す
fn fill(main: &[u8], from: usize, to: usize, shape: &mut Shape) -> Option<u32> {
let mut at = from;
let mut reference = None;
while at < to {
let one = head(main, at)?;
if one.body + one.length > to {
return reference;
}
match one.kind {
TEXT_HEADER => {
if let Some(value) = long(main, one.body) {
shape.place = place_name(value).to_string();
}
}
TEXT_REF => reference = long(main, one.body),
TEXT_WIDE | TEXT_NARROW => {
let text = decode(main, &one);
add(&mut shape.paragraph, &text);
}
_ if one.version == CONTAINER => {
if let Some(value) = fill(main, one.body, one.body + one.length, shape) {
reference = Some(value);
}
}
_ => {}
}
at = one.body + one.length;
}
reference
}
/// 文字のレコードを文字列にする
fn decode(main: &[u8], one: &Head) -> String {
if one.kind == TEXT_NARROW {
return main[one.body..one.body + one.length]
.iter()
.map(|value| *value as char)
.collect();
}
let mut unit = Vec::with_capacity(one.length / 2);
for index in (one.body..one.body + one.length).step_by(2) {
if index + 2 > main.len() {
break;
}
unit.push(u16::from_le_bytes([main[index], main[index + 1]]));
}
char::decode_utf16(unit)
.map(|value| value.unwrap_or('\u{fffd}'))
.collect()
}
/// 文字を段落に切って足す
///
/// 0x0D が段落の終わり、0x0B は段落の中の改行である。
fn add(into: &mut Vec<Paragraph>, text: &str) {
let mut line = String::new();
for value in text.chars() {
match value {
'\r' => into.push(take(&mut line)),
'\u{b}' => line.push('\n'),
_ => line.push(value),
}
}
if !line.is_empty() {
into.push(take(&mut line));
}
}
/// 組み立てた 1 行を段落にする
fn take(line: &mut String) -> Paragraph {
Paragraph {
level: 0,
align: crate::paragraph::Align::None,
run: vec![Run::new(&std::mem::take(line))],
}
}
/// 文字の役目を、pptx と同じ言い方に直す
fn place_name(value: u32) -> &'static str {
match value {
0 | 5 => "title",
2 => "notes",
3 => "other",
_ => "body",
}
}
/// レコードの見出し
struct Head {
/// 0xF なら入れ子になっている
version: u16,
kind: u16,
/// 中身の長さ
length: usize,
/// 中身が始まる位置
body: usize,
}
/// レコードの見出しを読む
fn head(data: &[u8], at: usize) -> Option<Head> {
if at + 8 > data.len() {
return None;
}
let first = u16::from_le_bytes([data[at], data[at + 1]]);
let kind = u16::from_le_bytes([data[at + 2], data[at + 3]]);
let length = long(data, at + 4)? as usize;
if at + 8 + length > data.len() {
return None;
}
Some(Head {version: first & 0x000f, kind, length, body: at + 8})
}
/// 4 バイトを読む
fn long(data: &[u8], offset: usize) -> Option<u32> {
if offset + 4 > data.len() {
return None;
}
Some(u32::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]))
}