1use std::sync::Arc;
8
9use crate::cfb::Compound;
10use crate::error::{Error, Result};
11use crate::hash::FastMap;
12use crate::model::{
13 Bullet, Content, Emu, OleObject, Paragraph, Picture, Placeholder, PlaceholderKind, Run,
14 RunKind, RunProps, Shape, SlideContent, SlideKind, TextBody, Transform,
15};
16
17const RT_DOCUMENT: u16 = 0x03e8;
18const RT_DOCUMENT_ATOM: u16 = 0x03e9;
19const RT_SLIDE: u16 = 0x03ee;
20const RT_NOTES: u16 = 0x03f0;
21const RT_NOTES_ATOM: u16 = 0x03f1;
22const RT_SLIDE_PERSIST_ATOM: u16 = 0x03f3;
23const RT_SLIDE_SHOW_SLIDE_INFO_ATOM: u16 = 0x03f9;
24const RT_DRAWING_GROUP: u16 = 0x040b;
25const RT_DRAWING: u16 = 0x040c;
26const RT_PLACEHOLDER_ATOM: u16 = 0x0bc3;
27const RT_OUTLINE_TEXT_REF_ATOM: u16 = 0x0f9e;
28const RT_TEXT_HEADER_ATOM: u16 = 0x0f9f;
29const RT_TEXT_CHARS_ATOM: u16 = 0x0fa0;
30const RT_STYLE_TEXT_PROP_ATOM: u16 = 0x0fa1;
31const RT_MASTER_TEXT_PROP_ATOM: u16 = 0x0fa2;
32const RT_TEXT_BYTES_ATOM: u16 = 0x0fa8;
33const RT_CSTRING: u16 = 0x0fba;
34const RT_SLIDE_LIST_WITH_TEXT: u16 = 0x0ff0;
35const RT_USER_EDIT_ATOM: u16 = 0x0ff5;
36const RT_CURRENT_USER_ATOM: u16 = 0x0ff6;
37const RT_PERSIST_DIRECTORY_ATOM: u16 = 0x1772;
38const RT_CRYPT_SESSION10_CONTAINER: u16 = 0x2f14;
39
40const OA_DGG_CONTAINER: u16 = 0xf000;
41const OA_BSTORE_CONTAINER: u16 = 0xf001;
42const OA_DG_CONTAINER: u16 = 0xf002;
43const OA_SPGR_CONTAINER: u16 = 0xf003;
44const OA_SP_CONTAINER: u16 = 0xf004;
45const OA_FBSE: u16 = 0xf007;
46const OA_FSP: u16 = 0xf00a;
47const OA_FOPT: u16 = 0xf00b;
48const OA_CLIENT_TEXTBOX: u16 = 0xf00d;
49const OA_CHILD_ANCHOR: u16 = 0xf00f;
50const OA_CLIENT_ANCHOR: u16 = 0xf010;
51const OA_CLIENT_DATA: u16 = 0xf011;
52const OA_SECONDARY_FOPT: u16 = 0xf121;
53const OA_TERTIARY_FOPT: u16 = 0xf122;
54
55const HEADER_TOKEN_ENCRYPTED: u32 = 0xf3d1_c4df;
56const NO_PLACEHOLDER: u32 = 0xffff_ffff;
57const MSOSPT_TEXT_BOX: u16 = 202;
58
59const EMU_PER_MASTER_UNIT_NUM: i64 = 3175;
61const EMU_PER_MASTER_UNIT_DEN: i64 = 2;
62
63#[derive(Clone, Copy, Debug)]
65struct Rec {
66 ver: u8,
67 instance: u16,
68 kind: u16,
69 start: usize,
71 len: usize,
72}
73
74impl Rec {
75 fn end(&self) -> usize {
76 self.start + self.len
77 }
78
79 fn is_container(&self) -> bool {
80 self.ver == 0xf
81 }
82}
83
84fn header_at(data: &[u8], at: usize) -> Option<Rec> {
85 let raw = data.get(at..at + 8)?;
86 let word = u16::from_le_bytes([raw[0], raw[1]]);
87 let len = u32::from_le_bytes([raw[4], raw[5], raw[6], raw[7]]) as usize;
88 Some(Rec {
89 ver: (word & 0xf) as u8,
90 instance: word >> 4,
91 kind: u16::from_le_bytes([raw[2], raw[3]]),
92 start: at + 8,
93 len,
94 })
95}
96
97fn records(data: &[u8], start: usize, end: usize) -> Vec<Rec> {
99 let end = end.min(data.len());
100 let mut out = Vec::new();
101 let mut cursor = start;
102 while cursor + 8 <= end {
103 let Some(rec) = header_at(data, cursor) else {
104 break;
105 };
106 if rec.end() > end {
107 break;
108 }
109 out.push(rec);
110 cursor = rec.end();
111 }
112 out
113}
114
115fn children(data: &[u8], rec: &Rec) -> Vec<Rec> {
116 records(data, rec.start, rec.end())
117}
118
119fn u16_at(data: &[u8], at: usize) -> Option<u16> {
120 let raw = data.get(at..at + 2)?;
121 Some(u16::from_le_bytes([raw[0], raw[1]]))
122}
123
124fn u32_at(data: &[u8], at: usize) -> Option<u32> {
125 let raw = data.get(at..at + 4)?;
126 Some(u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]))
127}
128
129fn i32_at(data: &[u8], at: usize) -> Option<i32> {
130 u32_at(data, at).map(|v| v as i32)
131}
132
133fn master_units_to_emu(value: i64) -> Emu {
134 value * EMU_PER_MASTER_UNIT_NUM / EMU_PER_MASTER_UNIT_DEN
135}
136
137fn ppt_error(msg: &str) -> Error {
138 Error::Other(format!("ppt: {msg}"))
139}
140
141#[derive(Clone, Debug)]
143struct OutlineText {
144 text_type: u32,
145 body: TextBody,
146}
147
148#[derive(Clone, Debug)]
149struct SlideEntry {
150 persist: u32,
151 slide_id: u32,
152 outline: Vec<OutlineText>,
153}
154
155#[derive(Clone, Debug)]
157enum BlipSource {
158 Delay(usize),
160 Embedded(usize),
162 Missing,
163}
164
165pub struct LegacyDeck {
168 document: Vec<u8>,
169 pictures: Vec<u8>,
170 directory: FastMap<u32, usize>,
171 slides: Vec<SlideEntry>,
172 notes: FastMap<u32, u32>,
174 blips: Vec<BlipSource>,
175 stream_slides: Vec<usize>,
178 pub slides_recovered_by_scan: bool,
180 pub slide_size: Option<(Emu, Emu)>,
181 pub notes_size: Option<(Emu, Emu)>,
182 pub slide_size_kind: Option<&'static str>,
183 pub first_slide_number: i32,
184}
185
186pub struct LegacySlide {
188 pub id: u32,
189 pub hidden: bool,
190 pub name: Option<String>,
191 pub content: SlideContent,
192}
193
194pub struct LegacyPicture {
196 pub content_type: &'static str,
197 pub bytes: Vec<u8>,
198}
199
200pub fn is_presentation(compound: &Compound) -> bool {
202 compound.has_stream("PowerPoint Document")
203 || compound.has_stream("PP97_DUALSTORAGE/PowerPoint Document")
204}
205
206impl LegacyDeck {
207 pub fn open(compound: &Compound) -> Result<Self> {
208 let prefix = match compound.has_stream("PP97_DUALSTORAGE/PowerPoint Document") {
209 true => "PP97_DUALSTORAGE/",
210 false => "",
211 };
212 let document = compound
213 .stream(&format!("{prefix}PowerPoint Document"))
214 .ok_or_else(|| ppt_error("no PowerPoint Document stream"))?;
215 let current_user = compound
216 .stream(&format!("{prefix}Current User"))
217 .ok_or_else(|| ppt_error("no Current User stream"))?;
218 let pictures = compound
219 .stream(&format!("{prefix}Pictures"))
220 .unwrap_or_default();
221 let cu = header_at(¤t_user, 0)
222 .filter(|rec| rec.kind == RT_CURRENT_USER_ATOM)
223 .ok_or_else(|| {
224 Error::Unsupported("PowerPoint 95 or unknown presentation format".into())
225 })?;
226 let header_token = u32_at(¤t_user, cu.start + 4).unwrap_or(0);
227 if header_token == HEADER_TOKEN_ENCRYPTED {
228 return Err(Error::Encrypted(
229 "legacy .ppt encrypted with a password".into(),
230 ));
231 }
232 let mut offset = u32_at(¤t_user, cu.start + 8).unwrap_or(0) as usize;
233 let mut directories: Vec<Vec<(u32, usize)>> = Vec::new();
234 let mut doc_persist = 0u32;
235 let mut encrypt_ref = None;
236 let mut edits = 0usize;
237 while offset != 0 {
238 let edit = header_at(&document, offset)
239 .filter(|rec| rec.kind == RT_USER_EDIT_ATOM)
240 .ok_or_else(|| ppt_error("user edit atom missing"))?;
241 let last_edit = u32_at(&document, edit.start + 8).unwrap_or(0) as usize;
242 let dir_offset = u32_at(&document, edit.start + 12).unwrap_or(0) as usize;
243 if edits == 0 {
244 doc_persist = u32_at(&document, edit.start + 16).unwrap_or(0);
245 if edit.len >= 0x20 {
246 encrypt_ref = u32_at(&document, edit.start + 28).filter(|r| *r != 0);
247 }
248 }
249 let dir = header_at(&document, dir_offset)
250 .filter(|rec| rec.kind == RT_PERSIST_DIRECTORY_ATOM)
251 .ok_or_else(|| ppt_error("persist directory missing"))?;
252 let mut entries = Vec::new();
253 let mut cursor = dir.start;
254 while cursor + 4 <= dir.end().min(document.len()) {
255 let word = u32_at(&document, cursor).unwrap_or(0);
256 let persist_id = word & 0x000f_ffff;
257 let count = (word >> 20) & 0xfff;
258 cursor += 4;
259 if count == 0 {
260 break;
261 }
262 for i in 0..count {
263 let Some(value) = u32_at(&document, cursor) else {
264 break;
265 };
266 entries.push((persist_id + i, value as usize));
267 cursor += 4;
268 }
269 }
270 directories.push(entries);
271 edits += 1;
272 if last_edit >= offset || edits > 4096 {
273 break;
274 }
275 offset = last_edit;
276 }
277 let mut directory = FastMap::default();
278 for entries in directories.iter().rev() {
279 for (id, value) in entries {
280 directory.insert(*id, *value);
281 }
282 }
283 if let Some(reference) = encrypt_ref {
284 let encrypted = directory
285 .get(&reference)
286 .and_then(|&at| header_at(&document, at))
287 .is_some_and(|rec| rec.kind == RT_CRYPT_SESSION10_CONTAINER);
288 if encrypted {
289 return Err(Error::Encrypted(
290 "legacy .ppt encrypted with a password".into(),
291 ));
292 }
293 }
294 let doc_offset = *directory
295 .get(&doc_persist)
296 .ok_or_else(|| ppt_error("document container not in the persist directory"))?;
297 let doc_rec = header_at(&document, doc_offset)
298 .filter(|rec| rec.kind == RT_DOCUMENT && rec.is_container())
299 .ok_or_else(|| ppt_error("document container missing"))?;
300
301 let mut deck = LegacyDeck {
302 document,
303 pictures,
304 directory,
305 slides: Vec::new(),
306 notes: FastMap::default(),
307 blips: Vec::new(),
308 stream_slides: Vec::new(),
309 slides_recovered_by_scan: false,
310 slide_size: None,
311 notes_size: None,
312 slide_size_kind: None,
313 first_slide_number: 1,
314 };
315 let doc_children = children(&deck.document, &doc_rec);
316 let mut notes_persists: Vec<u32> = Vec::new();
317 for child in &doc_children {
318 match child.kind {
319 RT_DOCUMENT_ATOM => deck.read_document_atom(child),
320 RT_DRAWING_GROUP => deck.read_drawing_group(child),
321 RT_SLIDE_LIST_WITH_TEXT => match child.instance {
322 0 => deck.read_slide_list(child),
323 2 => {
324 for atom in children(&deck.document, child) {
325 if atom.kind == RT_SLIDE_PERSIST_ATOM {
326 if let Some(persist) = u32_at(&deck.document, atom.start) {
327 notes_persists.push(persist);
328 }
329 }
330 }
331 }
332 _ => {}
333 },
334 _ => {}
335 }
336 }
337 deck.recover_missing_slides();
338 for persist in notes_persists {
339 let Some(&at) = deck.directory.get(&persist) else {
340 continue;
341 };
342 let Some(rec) = header_at(&deck.document, at).filter(|r| r.kind == RT_NOTES) else {
343 continue;
344 };
345 let slide_id = children(&deck.document, &rec)
346 .iter()
347 .find(|c| c.kind == RT_NOTES_ATOM)
348 .and_then(|atom| u32_at(&deck.document, atom.start))
349 .unwrap_or(0);
350 if slide_id != 0 {
351 deck.notes.insert(slide_id, persist);
352 }
353 }
354 Ok(deck)
355 }
356
357 fn read_document_atom(&mut self, rec: &Rec) {
358 let data = &self.document;
359 let width = i32_at(data, rec.start).unwrap_or(0);
360 let height = i32_at(data, rec.start + 4).unwrap_or(0);
361 if width > 0 && height > 0 {
362 self.slide_size = Some((
363 master_units_to_emu(i64::from(width)),
364 master_units_to_emu(i64::from(height)),
365 ));
366 }
367 let notes_width = i32_at(data, rec.start + 8).unwrap_or(0);
368 let notes_height = i32_at(data, rec.start + 12).unwrap_or(0);
369 if notes_width > 0 && notes_height > 0 {
370 self.notes_size = Some((
371 master_units_to_emu(i64::from(notes_width)),
372 master_units_to_emu(i64::from(notes_height)),
373 ));
374 }
375 self.first_slide_number = i32::from(u16_at(data, rec.start + 32).unwrap_or(1));
376 self.slide_size_kind = match u16_at(data, rec.start + 34).unwrap_or(6) {
377 0 => Some("screen4x3"),
378 1 => Some("letter"),
379 2 => Some("A4"),
380 3 => Some("35mm"),
381 4 => Some("overhead"),
382 5 => Some("banner"),
383 _ => None,
384 };
385 }
386
387 fn read_drawing_group(&mut self, rec: &Rec) {
389 let data = &self.document;
390 for dgg in children(data, rec)
391 .iter()
392 .filter(|c| c.kind == OA_DGG_CONTAINER)
393 {
394 for store in children(data, dgg)
395 .iter()
396 .filter(|c| c.kind == OA_BSTORE_CONTAINER)
397 {
398 for block in children(data, store) {
399 let source = match block.kind {
400 OA_FBSE => {
401 let name_len =
402 data.get(block.start + 33).copied().unwrap_or(0) as usize;
403 let delay = u32_at(data, block.start + 28).unwrap_or(0xffff_ffff);
404 let embedded_at = block.start + 36 + name_len;
405 match embedded_at + 8 <= block.end() {
406 true => BlipSource::Embedded(embedded_at),
407 false if delay != 0xffff_ffff => BlipSource::Delay(delay as usize),
408 false => BlipSource::Missing,
409 }
410 }
411 0xf018..=0xf117 => BlipSource::Embedded(block.start - 8),
412 _ => BlipSource::Missing,
413 };
414 self.blips.push(source);
415 }
416 }
417 }
418 }
419
420 fn read_slide_list(&mut self, rec: &Rec) {
422 let data = &self.document;
423 let items = children(data, rec);
424 let mut current: Option<SlideEntry> = None;
425 let mut index = 0;
426 while index < items.len() {
427 let item = items[index];
428 if item.kind == RT_SLIDE_PERSIST_ATOM {
429 if let Some(entry) = current.take() {
430 self.slides.push(entry);
431 }
432 current = Some(SlideEntry {
433 persist: u32_at(data, item.start).unwrap_or(0),
434 slide_id: u32_at(data, item.start + 12).unwrap_or(0),
435 outline: Vec::new(),
436 });
437 index += 1;
438 continue;
439 }
440 if item.kind == RT_TEXT_HEADER_ATOM {
441 let text_type = u32_at(data, item.start).unwrap_or(4);
442 let mut end = index + 1;
443 while end < items.len()
444 && !matches!(items[end].kind, RT_TEXT_HEADER_ATOM | RT_SLIDE_PERSIST_ATOM)
445 {
446 end += 1;
447 }
448 let body = parse_text_body(data, &items[index + 1..end]);
449 if let Some(entry) = current.as_mut() {
450 entry.outline.push(OutlineText { text_type, body });
451 }
452 index = end;
453 continue;
454 }
455 index += 1;
456 }
457 if let Some(entry) = current {
458 self.slides.push(entry);
459 }
460 }
461
462 fn recover_missing_slides(&mut self) {
466 let missing: Vec<usize> = self
467 .slides
468 .iter()
469 .enumerate()
470 .filter(|(_, slide)| {
471 !self
472 .directory
473 .get(&slide.persist)
474 .and_then(|&at| header_at(&self.document, at))
475 .is_some_and(|rec| rec.kind == RT_SLIDE && rec.is_container())
476 })
477 .map(|(index, _)| index)
478 .collect();
479 if missing.is_empty() {
480 return;
481 }
482 let referenced: Vec<usize> = self.directory.values().copied().collect();
483 self.stream_slides = records(&self.document, 0, self.document.len())
484 .iter()
485 .filter(|rec| rec.kind == RT_SLIDE && rec.is_container())
486 .map(|rec| rec.start - 8)
487 .filter(|at| !referenced.contains(at))
488 .collect();
489 let mut spare = self.stream_slides.iter().copied();
490 let mut next_id = 0x00f0_0000u32;
491 for index in missing {
492 let Some(at) = spare.next() else {
493 break;
494 };
495 while self.directory.contains_key(&next_id) {
496 next_id += 1;
497 }
498 self.directory.insert(next_id, at);
499 self.slides[index].persist = next_id;
500 self.slides_recovered_by_scan = true;
501 }
502 }
503
504 pub fn slide_count(&self) -> usize {
505 self.slides.len()
506 }
507
508 pub fn slide_ids(&self) -> Vec<u32> {
510 self.slides.iter().map(|slide| slide.slide_id).collect()
511 }
512
513 pub fn slide(&self, index: usize) -> Result<LegacySlide> {
515 let entry = self.slides.get(index).ok_or(Error::SlideNotFound(index))?;
516 let at = *self
517 .directory
518 .get(&entry.persist)
519 .ok_or_else(|| ppt_error("slide persist id not in the directory"))?;
520 let rec = header_at(&self.document, at)
521 .filter(|rec| rec.kind == RT_SLIDE && rec.is_container())
522 .ok_or_else(|| ppt_error("slide container missing"))?;
523 let mut hidden = false;
524 let mut name = None;
525 let mut shapes = Vec::new();
526 for child in children(&self.document, &rec) {
527 match child.kind {
528 RT_SLIDE_SHOW_SLIDE_INFO_ATOM => {
529 hidden = u16_at(&self.document, child.start + 10).unwrap_or(0) & 0x0004 != 0;
530 }
531 RT_CSTRING if child.instance == 3 => {
532 name = Some(utf16_string(&self.document[child.start..child.end()]));
533 }
534 RT_DRAWING => {
535 shapes = self.drawing_shapes(&child, &entry.outline);
536 }
537 _ => {}
538 }
539 }
540 Ok(LegacySlide {
541 id: entry.slide_id,
542 hidden,
543 name: name.clone(),
544 content: SlideContent {
545 kind: SlideKind::Slide,
546 name,
547 show: !hidden,
548 shapes,
549 },
550 })
551 }
552
553 pub fn has_notes(&self, index: usize) -> bool {
555 self.slides
556 .get(index)
557 .is_some_and(|slide| self.notes.contains_key(&slide.slide_id))
558 }
559
560 pub fn notes(&self, index: usize) -> Result<Option<TextBody>> {
563 let entry = self.slides.get(index).ok_or(Error::SlideNotFound(index))?;
564 let Some(persist) = self.notes.get(&entry.slide_id) else {
565 return Ok(None);
566 };
567 let Some(&at) = self.directory.get(persist) else {
568 return Ok(None);
569 };
570 let Some(rec) = header_at(&self.document, at).filter(|r| r.kind == RT_NOTES) else {
571 return Ok(None);
572 };
573 let drawing = children(&self.document, &rec)
574 .into_iter()
575 .find(|c| c.kind == RT_DRAWING);
576 let Some(drawing) = drawing else {
577 return Ok(None);
578 };
579 let shapes = self.drawing_shapes(&drawing, &[]);
580 let content = SlideContent {
581 kind: SlideKind::Notes,
582 name: None,
583 show: true,
584 shapes,
585 };
586 let body_placeholder = content
587 .walk()
588 .find(|shape| {
589 shape
590 .placeholder
591 .as_ref()
592 .is_some_and(|ph| ph.kind == PlaceholderKind::Body)
593 })
594 .and_then(|shape| shape.text_body().cloned());
595 if let Some(body) = body_placeholder {
596 return Ok(Some(body));
597 }
598 let mut merged = TextBody::default();
599 for shape in content.walk() {
600 let skip = shape
601 .placeholder
602 .as_ref()
603 .is_some_and(|ph| ph.kind == PlaceholderKind::SlideImage || ph.kind.is_furniture());
604 if skip {
605 continue;
606 }
607 if let Some(body) = shape.text_body() {
608 merged.paragraphs.extend(body.paragraphs.iter().cloned());
609 }
610 }
611 Ok(Some(merged))
612 }
613
614 fn drawing_shapes(&self, drawing: &Rec, outline: &[OutlineText]) -> Vec<Shape> {
616 let data = &self.document;
617 let mut shapes = Vec::new();
618 for dg in children(data, drawing)
619 .iter()
620 .filter(|c| c.kind == OA_DG_CONTAINER)
621 {
622 for child in children(data, dg) {
623 match child.kind {
624 OA_SPGR_CONTAINER => {
625 let members = children(data, &child);
626 for (position, member) in members.iter().enumerate() {
627 if position == 0 {
628 continue;
629 }
630 if let Some(shape) = self.shape_of(member, outline) {
631 shapes.push(shape);
632 }
633 }
634 }
635 OA_SP_CONTAINER => {
636 if let Some(shape) = self.shape_of(&child, outline) {
637 shapes.push(shape);
638 }
639 }
640 _ => {}
641 }
642 }
643 }
644 shapes
645 }
646
647 fn shape_of(&self, rec: &Rec, outline: &[OutlineText]) -> Option<Shape> {
649 let data = &self.document;
650 if rec.kind == OA_SPGR_CONTAINER {
651 let members = children(data, rec);
652 let group_shape = members
653 .first()
654 .and_then(|first| self.shape_of(first, outline));
655 let children_shapes: Vec<Shape> = members
656 .iter()
657 .skip(1)
658 .filter_map(|member| self.shape_of(member, outline))
659 .collect();
660 let mut group = group_shape.unwrap_or_else(|| blank_shape(0));
661 group.content = Content::Group(children_shapes, None);
662 return Some(group);
663 }
664 if rec.kind != OA_SP_CONTAINER {
665 return None;
666 }
667 let parts = children(data, rec);
668 let fsp = parts.iter().find(|p| p.kind == OA_FSP)?;
669 let spid = u32_at(data, fsp.start).unwrap_or(0);
670 let flags = u32_at(data, fsp.start + 4).unwrap_or(0);
671 let deleted = flags & 0x0008 != 0;
672 let patriarch = flags & 0x0004 != 0;
673 if deleted || patriarch {
674 return None;
675 }
676 let ole = flags & 0x0010 != 0;
677 let connector = flags & 0x0100 != 0;
678 let flip_h = flags & 0x0040 != 0;
679 let flip_v = flags & 0x0080 != 0;
680 let shape_type = fsp.instance;
681 let mut shape = blank_shape(spid);
682 shape.text_box = shape_type == MSOSPT_TEXT_BOX;
683 let mut pib = None;
684 for fopt in parts
685 .iter()
686 .filter(|p| matches!(p.kind, OA_FOPT | OA_SECONDARY_FOPT | OA_TERTIARY_FOPT))
687 {
688 let props = parse_properties(data, fopt);
689 for prop in props {
690 match prop.id {
691 0x0104 if !prop.complex => pib = Some(prop.value),
692 0x0380 => {
693 if let Some(name) = prop.text(data) {
694 shape.name = name;
695 }
696 }
697 0x0381 => {
698 shape.description = prop.text(data).filter(|text| !text.is_empty());
699 }
700 _ => {}
701 }
702 }
703 }
704 let mut transform = None;
705 for anchor in parts
706 .iter()
707 .filter(|p| matches!(p.kind, OA_CLIENT_ANCHOR | OA_CHILD_ANCHOR))
708 {
709 let (top, left, right, bottom) = match anchor.len {
710 8 => (
711 i64::from(u16_at(data, anchor.start).unwrap_or(0) as i16),
712 i64::from(u16_at(data, anchor.start + 2).unwrap_or(0) as i16),
713 i64::from(u16_at(data, anchor.start + 4).unwrap_or(0) as i16),
714 i64::from(u16_at(data, anchor.start + 6).unwrap_or(0) as i16),
715 ),
716 16 => match anchor.kind {
717 OA_CLIENT_ANCHOR => (
718 i64::from(i32_at(data, anchor.start).unwrap_or(0)),
719 i64::from(i32_at(data, anchor.start + 4).unwrap_or(0)),
720 i64::from(i32_at(data, anchor.start + 8).unwrap_or(0)),
721 i64::from(i32_at(data, anchor.start + 12).unwrap_or(0)),
722 ),
723 _ => (
724 i64::from(i32_at(data, anchor.start + 4).unwrap_or(0)),
725 i64::from(i32_at(data, anchor.start).unwrap_or(0)),
726 i64::from(i32_at(data, anchor.start + 8).unwrap_or(0)),
727 i64::from(i32_at(data, anchor.start + 12).unwrap_or(0)),
728 ),
729 },
730 _ => continue,
731 };
732 if anchor.kind == OA_CLIENT_ANCHOR || transform.is_none() {
733 transform = Some(Transform {
734 x: master_units_to_emu(left),
735 y: master_units_to_emu(top),
736 cx: master_units_to_emu(right - left),
737 cy: master_units_to_emu(bottom - top),
738 rot: 0,
739 flip_h,
740 flip_v,
741 });
742 }
743 }
744 shape.transform = transform;
745 let mut placeholder = None;
746 for client_data in parts.iter().filter(|p| p.kind == OA_CLIENT_DATA) {
747 for atom in children(data, client_data) {
748 if atom.kind != RT_PLACEHOLDER_ATOM {
749 continue;
750 }
751 let position = u32_at(data, atom.start).unwrap_or(NO_PLACEHOLDER);
752 if position == NO_PLACEHOLDER {
753 continue;
754 }
755 let placement = data.get(atom.start + 4).copied().unwrap_or(0);
756 placeholder = Some(Placeholder {
757 kind: placeholder_kind(placement),
758 idx: position,
759 });
760 }
761 }
762 let mut body: Option<(u32, TextBody)> = None;
763 for textbox in parts.iter().filter(|p| p.kind == OA_CLIENT_TEXTBOX) {
764 let atoms = children(data, textbox);
765 if let Some(reference) = atoms.iter().find(|a| a.kind == RT_OUTLINE_TEXT_REF_ATOM) {
766 let index = i32_at(data, reference.start).unwrap_or(-1);
767 if index >= 0 {
768 if let Some(text) = outline.get(index as usize) {
769 body = Some((text.text_type, text.body.clone()));
770 }
771 }
772 continue;
773 }
774 if let Some(position) = atoms.iter().position(|a| a.kind == RT_TEXT_HEADER_ATOM) {
775 let text_type = u32_at(data, atoms[position].start).unwrap_or(4);
776 let parsed = parse_text_body(data, &atoms[position + 1..]);
777 body = Some((text_type, parsed));
778 }
779 }
780 if placeholder.is_none() {
781 if let Some((text_type, _)) = &body {
782 placeholder = match text_type {
783 0 => Some(Placeholder {
784 kind: PlaceholderKind::Title,
785 idx: 0,
786 }),
787 6 => Some(Placeholder {
788 kind: PlaceholderKind::CenterTitle,
789 idx: 0,
790 }),
791 _ => None,
792 };
793 }
794 }
795 shape.placeholder = placeholder;
796 let picture = pib.filter(|index| *index > 0).map(|index| Picture {
797 embed: Some(index.to_string()),
798 link: None,
799 media: None,
800 });
801 shape.content = match (body, picture, ole, connector) {
802 (Some((_, text)), _, _, _) if !text.is_empty() => Content::Text(text),
803 (_, Some(picture), true, _) => Content::Ole(OleObject {
804 prog_id: None,
805 rel_id: None,
806 preview: Some(picture),
807 }),
808 (_, Some(picture), false, _) => Content::Picture(picture),
809 (_, None, true, _) => Content::Ole(OleObject {
810 prog_id: None,
811 rel_id: None,
812 preview: None,
813 }),
814 (_, None, false, true) => Content::Connector,
815 (Some((_, text)), None, false, false) => Content::Text(text),
816 (None, None, false, false) => Content::Text(TextBody::default()),
817 };
818 Some(shape)
819 }
820
821 pub fn picture(&self, index: usize) -> Result<Option<LegacyPicture>> {
823 let Some(source) = index
824 .checked_sub(1)
825 .and_then(|position| self.blips.get(position))
826 else {
827 return Ok(None);
828 };
829 let (data, at): (&[u8], usize) = match source {
830 BlipSource::Delay(at) => (&self.pictures, *at),
831 BlipSource::Embedded(at) => (&self.document, *at),
832 BlipSource::Missing => return Ok(None),
833 };
834 let Some(rec) = header_at(data, at) else {
835 return Ok(None);
836 };
837 let end = rec.end().min(data.len());
838 if rec.start > end {
839 return Ok(None);
840 }
841 let payload = &data[rec.start..end];
842 Ok(decode_blip(rec.kind, rec.instance, payload))
843 }
844
845 pub fn picture_content_type(&self, index: usize) -> Option<&'static str> {
847 let source = self.blips.get(index.checked_sub(1)?)?;
848 let (data, at): (&[u8], usize) = match source {
849 BlipSource::Delay(at) => (&self.pictures, *at),
850 BlipSource::Embedded(at) => (&self.document, *at),
851 BlipSource::Missing => return None,
852 };
853 blip_content_type(header_at(data, at)?.kind)
854 }
855}
856
857fn blank_shape(id: u32) -> Shape {
858 Shape {
859 id,
860 name: format!("Shape {id}"),
861 hidden: false,
862 description: None,
863 hyperlink: None,
864 placeholder: None,
865 transform: None,
866 text_box: false,
867 content: Content::Text(TextBody::default()),
868 }
869}
870
871fn placeholder_kind(placement: u8) -> PlaceholderKind {
873 match placement {
874 0x01 | 0x0d | 0x11 => PlaceholderKind::Title,
875 0x03 | 0x0f => PlaceholderKind::CenterTitle,
876 0x04 | 0x10 => PlaceholderKind::Subtitle,
877 0x02 | 0x06 | 0x0c | 0x0e | 0x12 => PlaceholderKind::Body,
878 0x07 => PlaceholderKind::DateTime,
879 0x08 => PlaceholderKind::SlideNumber,
880 0x09 => PlaceholderKind::Footer,
881 0x0a => PlaceholderKind::Header,
882 0x05 | 0x0b => PlaceholderKind::SlideImage,
883 0x13 | 0x19 => PlaceholderKind::Object,
884 0x14 => PlaceholderKind::Chart,
885 0x15 => PlaceholderKind::Table,
886 0x16 => PlaceholderKind::ClipArt,
887 0x17 => PlaceholderKind::Diagram,
888 0x18 => PlaceholderKind::Media,
889 0x1a => PlaceholderKind::Picture,
890 _ => PlaceholderKind::Other,
891 }
892}
893
894struct Property {
896 id: u16,
897 complex: bool,
898 value: u32,
899 data: Option<(usize, usize)>,
901}
902
903impl Property {
904 fn text(&self, stream: &[u8]) -> Option<String> {
906 let (start, len) = self.data?;
907 let raw = stream.get(start..start + len)?;
908 Some(utf16_string(raw))
909 }
910}
911
912fn utf16_string(raw: &[u8]) -> String {
913 let (pairs, _) = raw.as_chunks::<2>();
914 let units: Vec<u16> = pairs
915 .iter()
916 .map(|pair| u16::from_le_bytes(*pair))
917 .take_while(|&unit| unit != 0)
918 .collect();
919 String::from_utf16_lossy(&units)
920}
921
922fn parse_properties(data: &[u8], fopt: &Rec) -> Vec<Property> {
923 let count = usize::from(fopt.instance);
924 let mut props = Vec::with_capacity(count);
925 let mut cursor = fopt.start;
926 for _ in 0..count {
927 if cursor + 6 > fopt.end() {
928 break;
929 }
930 let word = u16_at(data, cursor).unwrap_or(0);
931 props.push(Property {
932 id: word & 0x3fff,
933 complex: word & 0x8000 != 0,
934 value: u32_at(data, cursor + 2).unwrap_or(0),
935 data: None,
936 });
937 cursor += 6;
938 }
939 for prop in props.iter_mut().filter(|p| p.complex) {
940 let len = prop.value as usize;
941 if cursor + len > fopt.end() {
942 break;
943 }
944 prop.data = Some((cursor, len));
945 cursor += len;
946 }
947 props
948}
949
950struct Spans {
952 level: Vec<u8>,
953 bullet: Vec<Bullet>,
954 bold: Vec<Option<bool>>,
955 italic: Vec<Option<bool>>,
956 underline: Vec<Option<bool>>,
957}
958
959impl Spans {
960 fn new(len: usize) -> Self {
961 Self {
962 level: vec![0; len],
963 bullet: vec![Bullet::Inherited; len],
964 bold: vec![None; len],
965 italic: vec![None; len],
966 underline: vec![None; len],
967 }
968 }
969}
970
971fn parse_text_body(data: &[u8], atoms: &[Rec]) -> TextBody {
973 let mut chars: Vec<char> = Vec::new();
974 for atom in atoms {
975 match atom.kind {
976 RT_TEXT_CHARS_ATOM => {
977 let raw = &data[atom.start..atom.end().min(data.len())];
978 let (pairs, _) = raw.as_chunks::<2>();
979 let units: Vec<u16> = pairs.iter().map(|pair| u16::from_le_bytes(*pair)).collect();
980 chars = char::decode_utf16(units)
981 .map(|unit| unit.unwrap_or(char::REPLACEMENT_CHARACTER))
982 .collect();
983 break;
984 }
985 RT_TEXT_BYTES_ATOM => {
986 chars = data[atom.start..atom.end().min(data.len())]
987 .iter()
988 .map(|&byte| char::from(byte))
989 .collect();
990 break;
991 }
992 _ => {}
993 }
994 }
995 let total = chars.len() + 1;
996 let mut spans = Spans::new(total);
997 if let Some(style) = atoms.iter().find(|a| a.kind == RT_STYLE_TEXT_PROP_ATOM) {
998 apply_style_runs(data, style, &mut spans);
999 } else if let Some(master) = atoms.iter().find(|a| a.kind == RT_MASTER_TEXT_PROP_ATOM) {
1000 let mut cursor = master.start;
1001 let mut position = 0usize;
1002 while cursor + 6 <= master.end().min(data.len()) && position < total {
1003 let count = u32_at(data, cursor).unwrap_or(0) as usize;
1004 let level = u16_at(data, cursor + 4).unwrap_or(0).min(8) as u8;
1005 let end = (position + count).min(total);
1006 for slot in &mut spans.level[position..end] {
1007 *slot = level;
1008 }
1009 position = end;
1010 cursor += 6;
1011 }
1012 }
1013 let mut body = TextBody::default();
1014 let mut start = 0usize;
1015 let mut position = 0usize;
1016 while position <= chars.len() {
1017 let at_end = position == chars.len();
1018 if at_end || chars[position] == '\r' {
1019 body.paragraphs
1020 .push(paragraph_from(&chars[start..position], start, &spans));
1021 start = position + 1;
1022 }
1023 position += 1;
1024 }
1025 if body.paragraphs.len() > 1
1026 && body.paragraphs.last().is_some_and(Paragraph::is_empty)
1027 && chars.last() == Some(&'\r')
1028 {
1029 body.paragraphs.pop();
1030 }
1031 body
1032}
1033
1034fn paragraph_from(chars: &[char], offset: usize, spans: &Spans) -> Paragraph {
1037 let level = spans.level.get(offset).copied().unwrap_or(0);
1038 let bullet = spans
1039 .bullet
1040 .get(offset)
1041 .cloned()
1042 .unwrap_or(Bullet::Inherited);
1043 let mut runs: Vec<Run> = Vec::new();
1044 let mut current = String::new();
1045 let mut current_props: Option<RunProps> = None;
1046 let flush = |runs: &mut Vec<Run>, text: &mut String, props: &Option<RunProps>| {
1047 if text.is_empty() {
1048 return;
1049 }
1050 runs.push(Run {
1051 kind: RunKind::Text,
1052 text: std::mem::take(text),
1053 props: props.clone().unwrap_or_default(),
1054 });
1055 };
1056 for (i, &ch) in chars.iter().enumerate() {
1057 let position = offset + i;
1058 if ch == '\u{b}' {
1059 flush(&mut runs, &mut current, ¤t_props);
1060 runs.push(Run {
1061 kind: RunKind::LineBreak,
1062 text: String::new(),
1063 props: RunProps::default(),
1064 });
1065 continue;
1066 }
1067 if ch.is_control() && ch != '\t' {
1068 continue;
1069 }
1070 let props = RunProps {
1071 bold: spans.bold.get(position).copied().flatten(),
1072 italic: spans.italic.get(position).copied().flatten(),
1073 underline: spans.underline.get(position).copied().flatten(),
1074 ..RunProps::default()
1075 };
1076 if current_props.as_ref() != Some(&props) {
1077 flush(&mut runs, &mut current, ¤t_props);
1078 current_props = Some(props);
1079 }
1080 current.push(ch);
1081 }
1082 flush(&mut runs, &mut current, ¤t_props);
1083 Paragraph {
1084 level,
1085 bullet,
1086 runs,
1087 }
1088}
1089
1090fn apply_style_runs(data: &[u8], atom: &Rec, spans: &mut Spans) {
1092 let end = atom.end().min(data.len());
1093 let total = spans.level.len();
1094 let mut cursor = atom.start;
1095 let mut position = 0usize;
1096 while position < total && cursor + 6 <= end {
1097 let count = u32_at(data, cursor).unwrap_or(0) as usize;
1098 let level = u16_at(data, cursor + 4).unwrap_or(0).min(8) as u8;
1099 let masks = u32_at(data, cursor + 6).unwrap_or(0);
1100 cursor += 10;
1101 let mut bullet = Bullet::Inherited;
1102 let mut bullet_char = None;
1103 let mut has_bullet = None;
1104 if masks & 0x0000_000f != 0 {
1105 let flags = u16_at(data, cursor).unwrap_or(0);
1106 if masks & 0x0000_0001 != 0 {
1107 has_bullet = Some(flags & 0x0001 != 0);
1108 }
1109 cursor += 2;
1110 }
1111 if masks & 0x0000_0080 != 0 {
1112 bullet_char = u16_at(data, cursor).and_then(|unit| char::from_u32(u32::from(unit)));
1113 cursor += 2;
1114 }
1115 if masks & 0x0000_0010 != 0 {
1116 cursor += 2;
1117 }
1118 if masks & 0x0000_0040 != 0 {
1119 cursor += 2;
1120 }
1121 if masks & 0x0000_0020 != 0 {
1122 cursor += 4;
1123 }
1124 for bit in [0x0800u32, 0x1000, 0x2000, 0x4000, 0x0100, 0x0400, 0x8000] {
1125 if masks & bit != 0 {
1126 cursor += 2;
1127 }
1128 }
1129 if masks & 0x0010_0000 != 0 {
1130 let tabs = u16_at(data, cursor).unwrap_or(0) as usize;
1131 cursor += 2 + tabs * 4;
1132 }
1133 if masks & 0x0001_0000 != 0 {
1134 cursor += 2;
1135 }
1136 if masks & 0x000e_0000 != 0 {
1137 cursor += 2;
1138 }
1139 if masks & 0x0020_0000 != 0 {
1140 cursor += 2;
1141 }
1142 match has_bullet {
1143 Some(true) => bullet = Bullet::Char(bullet_char.unwrap_or('\u{2022}').to_string()),
1144 Some(false) => bullet = Bullet::None,
1145 None => {}
1146 }
1147 let run_end = (position + count).min(total);
1148 for i in position..run_end {
1149 spans.level[i] = level;
1150 spans.bullet[i] = bullet.clone();
1151 }
1152 position = run_end;
1153 if count == 0 {
1154 break;
1155 }
1156 }
1157 position = 0;
1158 while position < total && cursor + 8 <= end {
1159 let count = u32_at(data, cursor).unwrap_or(0) as usize;
1160 let masks = u32_at(data, cursor + 4).unwrap_or(0);
1161 cursor += 8;
1162 let mut bold = None;
1163 let mut italic = None;
1164 let mut underline = None;
1165 if masks & 0x0000_3ea7 != 0 {
1166 let style = u16_at(data, cursor).unwrap_or(0);
1167 if masks & 0x0000_0001 != 0 {
1168 bold = Some(style & 0x0001 != 0);
1169 }
1170 if masks & 0x0000_0002 != 0 {
1171 italic = Some(style & 0x0002 != 0);
1172 }
1173 if masks & 0x0000_0004 != 0 {
1174 underline = Some(style & 0x0004 != 0);
1175 }
1176 cursor += 2;
1177 }
1178 for bit in [
1179 0x0001_0000u32,
1180 0x0020_0000,
1181 0x0040_0000,
1182 0x0080_0000,
1183 0x0002_0000,
1184 ] {
1185 if masks & bit != 0 {
1186 cursor += 2;
1187 }
1188 }
1189 if masks & 0x0004_0000 != 0 {
1190 cursor += 4;
1191 }
1192 if masks & 0x0008_0000 != 0 {
1193 cursor += 2;
1194 }
1195 let run_end = (position + count).min(total);
1196 for i in position..run_end {
1197 spans.bold[i] = bold;
1198 spans.italic[i] = italic;
1199 spans.underline[i] = underline;
1200 }
1201 position = run_end;
1202 if count == 0 {
1203 break;
1204 }
1205 }
1206}
1207
1208fn blip_content_type(kind: u16) -> Option<&'static str> {
1209 Some(match kind {
1210 0xf01a => "image/x-emf",
1211 0xf01b => "image/x-wmf",
1212 0xf01c => "image/x-pict",
1213 0xf01d | 0xf02a => "image/jpeg",
1214 0xf01e => "image/png",
1215 0xf01f => "image/bmp",
1216 0xf029 => "image/tiff",
1217 _ => return None,
1218 })
1219}
1220
1221fn decode_blip(kind: u16, instance: u16, payload: &[u8]) -> Option<LegacyPicture> {
1224 let content_type = blip_content_type(kind)?;
1225 let metafile = matches!(kind, 0xf01a..=0xf01c);
1226 let two_uids = match kind {
1227 0xf01a => instance == 0x3d5,
1228 0xf01b => instance == 0x217,
1229 0xf01c => instance == 0x543,
1230 0xf01d | 0xf02a => matches!(instance, 0x46b | 0x6e3),
1231 0xf01e => instance == 0x6e1,
1232 0xf01f => instance == 0x7a9,
1233 0xf029 => instance == 0x6e5,
1234 _ => false,
1235 };
1236 let uid_len = match two_uids {
1237 true => 32,
1238 false => 16,
1239 };
1240 if metafile {
1241 let header = payload.get(uid_len..uid_len + 34)?;
1242 let uncompressed =
1243 u32::from_le_bytes([header[0], header[1], header[2], header[3]]) as usize;
1244 let compression = header[32];
1245 let body = payload.get(uid_len + 34..)?;
1246 let bytes = match compression {
1247 0x00 => {
1248 let stream = body.get(2..)?;
1249 let mut out = Vec::new();
1250 crate::inflate::inflate(stream, uncompressed, &mut out).ok()?;
1251 out
1252 }
1253 _ => body.to_vec(),
1254 };
1255 return Some(LegacyPicture {
1256 content_type,
1257 bytes,
1258 });
1259 }
1260 let body = payload.get(uid_len + 1..)?;
1261 let bytes = match kind {
1262 0xf01f => dib_to_bmp(body),
1263 _ => body.to_vec(),
1264 };
1265 Some(LegacyPicture {
1266 content_type,
1267 bytes,
1268 })
1269}
1270
1271fn dib_to_bmp(dib: &[u8]) -> Vec<u8> {
1273 let header_size = u32_at(dib, 0).unwrap_or(40) as usize;
1274 let bit_count = u16_at(dib, 14).unwrap_or(24);
1275 let compression = u32_at(dib, 16).unwrap_or(0);
1276 let colors_used = u32_at(dib, 32).unwrap_or(0) as usize;
1277 let palette_entries = match colors_used {
1278 0 => 1usize << bit_count.min(8),
1279 n => n,
1280 };
1281 let palette = match bit_count {
1282 1..=8 => palette_entries * 4,
1283 _ => 0,
1284 };
1285 let masks = match (compression, header_size) {
1286 (3, 40) => 12,
1287 _ => 0,
1288 };
1289 let offset = 14 + header_size + palette + masks;
1290 let mut out = Vec::with_capacity(14 + dib.len());
1291 out.extend_from_slice(b"BM");
1292 out.extend_from_slice(&((14 + dib.len()) as u32).to_le_bytes());
1293 out.extend_from_slice(&0u32.to_le_bytes());
1294 out.extend_from_slice(&(offset as u32).to_le_bytes());
1295 out.extend_from_slice(dib);
1296 out
1297}
1298
1299pub fn open_compound(compound: &Compound) -> Result<Arc<LegacyDeck>> {
1301 LegacyDeck::open(compound).map(Arc::new)
1302}
1303
1304#[cfg(test)]
1305mod tests {
1306 use super::*;
1307
1308 #[test]
1309 fn text_bodies_split_paragraphs_and_read_levels_and_bullets() {
1310 let text = b"First\rSecond\x0bline";
1312 let mut stream = Vec::new();
1313 let text_at = stream.len();
1314 stream.extend_from_slice(&[0x00, 0x00, 0xa8, 0x0f]);
1315 stream.extend_from_slice(&(text.len() as u32).to_le_bytes());
1316 stream.extend_from_slice(text);
1317 let style_at = stream.len();
1318 let mut style = Vec::new();
1319 style.extend_from_slice(&6u32.to_le_bytes());
1321 style.extend_from_slice(&0u16.to_le_bytes());
1322 style.extend_from_slice(&0x0000_0001u32.to_le_bytes());
1323 style.extend_from_slice(&0u16.to_le_bytes());
1324 style.extend_from_slice(&12u32.to_le_bytes());
1326 style.extend_from_slice(&1u16.to_le_bytes());
1327 style.extend_from_slice(&0x0000_0081u32.to_le_bytes());
1328 style.extend_from_slice(&1u16.to_le_bytes());
1329 style.extend_from_slice(&(b'-' as u16).to_le_bytes());
1330 style.extend_from_slice(&5u32.to_le_bytes());
1332 style.extend_from_slice(&0x0000_0001u32.to_le_bytes());
1333 style.extend_from_slice(&0x0001u16.to_le_bytes());
1334 style.extend_from_slice(&13u32.to_le_bytes());
1335 style.extend_from_slice(&0x0000_0001u32.to_le_bytes());
1336 style.extend_from_slice(&0x0000u16.to_le_bytes());
1337 stream.extend_from_slice(&[0x00, 0x00, 0xa1, 0x0f]);
1338 stream.extend_from_slice(&(style.len() as u32).to_le_bytes());
1339 stream.extend_from_slice(&style);
1340 let atoms = records(&stream, 0, stream.len());
1341 assert_eq!(atoms.len(), 2);
1342 assert_eq!(atoms[0].start, text_at + 8);
1343 assert_eq!(atoms[1].start, style_at + 8);
1344 let body = parse_text_body(&stream, &atoms);
1345 assert_eq!(body.paragraphs.len(), 2);
1346 assert_eq!(body.paragraphs[0].text(), "First");
1347 assert_eq!(body.paragraphs[0].level, 0);
1348 assert_eq!(body.paragraphs[0].bullet, Bullet::None);
1349 assert_eq!(body.paragraphs[0].runs[0].props.bold, Some(true));
1350 assert_eq!(body.paragraphs[1].text(), "Second\nline");
1351 assert_eq!(body.paragraphs[1].level, 1);
1352 assert_eq!(body.paragraphs[1].bullet, Bullet::Char("-".into()));
1353 assert_eq!(body.paragraphs[1].runs[0].props.bold, Some(false));
1354 }
1355
1356 #[test]
1357 fn property_tables_read_complex_strings_after_the_fixed_entries() {
1358 let name: Vec<u8> = "Box\0"
1359 .encode_utf16()
1360 .flat_map(|u| u.to_le_bytes())
1361 .collect();
1362 let mut stream = vec![0x33, 0x00, 0x0b, 0xf0];
1363 let body_len = 12 + name.len();
1364 stream.extend_from_slice(&(body_len as u32).to_le_bytes());
1365 stream.extend_from_slice(&(0x0104u16 | 0x4000).to_le_bytes());
1366 stream.extend_from_slice(&3u32.to_le_bytes());
1367 stream.extend_from_slice(&(0x0380u16 | 0x8000).to_le_bytes());
1368 stream.extend_from_slice(&(name.len() as u32).to_le_bytes());
1369 stream.extend_from_slice(&name);
1370 let mut fopt = header_at(&stream, 0).unwrap();
1371 fopt.instance = 2;
1372 let props = parse_properties(&stream, &fopt);
1373 assert_eq!(props.len(), 2);
1374 assert_eq!(props[0].id, 0x0104);
1375 assert_eq!(props[0].value, 3);
1376 assert!(!props[0].complex);
1377 assert_eq!(props[1].text(&stream).as_deref(), Some("Box"));
1378 }
1379
1380 #[test]
1381 fn master_units_convert_to_emu() {
1382 assert_eq!(master_units_to_emu(576), 914_400);
1383 assert_eq!(master_units_to_emu(5760), 9_144_000);
1384 }
1385
1386 #[test]
1387 fn dib_gets_a_file_header() {
1388 let mut dib = vec![0u8; 40];
1389 dib[0] = 40;
1390 dib[14] = 24;
1391 dib.extend_from_slice(&[1, 2, 3]);
1392 let bmp = dib_to_bmp(&dib);
1393 assert_eq!(&bmp[..2], b"BM");
1394 assert_eq!(u32::from_le_bytes([bmp[10], bmp[11], bmp[12], bmp[13]]), 54);
1395 assert_eq!(bmp.len(), 14 + dib.len());
1396 }
1397}