1#[derive(Debug, Clone)]
7pub struct InlineImage {
8 pub bytes: Vec<u8>,
9 pub extension: &'static str,
10 pub content_type: &'static str,
11 pub width_emu: u64,
12 pub height_emu: u64,
13}
14
15fn read_u16(bytes: &[u8], offset: usize) -> Option<u16> {
16 bytes
17 .get(offset..offset.checked_add(2)?)
18 .map(|pair| u16::from_le_bytes([pair[0], pair[1]]))
19}
20
21fn read_u32(bytes: &[u8], offset: usize) -> Option<u32> {
22 bytes
23 .get(offset..offset.checked_add(4)?)
24 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
25}
26
27pub fn extract_image(data_stream: &[u8], fc: u32) -> Option<InlineImage> {
28 let data = data_stream.get(fc as usize..)?;
29 let lcb = read_u32(data, 0)? as usize;
30 let cb_header = read_u16(data, 4)? as usize;
31 if cb_header < 0x2C || lcb < cb_header {
32 return None;
33 }
34 let data = data.get(..lcb)?;
35 let mm = read_u16(data, 6)?;
36
37 let dxa_goal = read_u16(data, 28)? as i64;
39 let dya_goal = read_u16(data, 30)? as i64;
40 let mx = read_u16(data, 32)? as i64;
41 let my = read_u16(data, 34)? as i64;
42 let scale = |goal: i64, factor: i64| -> u64 {
43 let factor = if factor == 0 { 1000 } else { factor };
44 let twips = goal * factor / 1000;
45 (twips.max(0) as u64) * 635 };
47 let width_emu = scale(dxa_goal, mx);
48 let height_emu = scale(dya_goal, my);
49
50 let payload = data.get(cb_header..)?;
51
52 if mm != 0x64 && mm != 0x66 {
55 return None;
56 }
57
58 let payload = if mm == 0x66 {
59 payload.get(1 + usize::from(*payload.first()?)..)?
60 } else {
61 payload
62 };
63 let (bytes, extension, content_type) = find_blip(payload, 0)?;
64 Some(InlineImage {
65 bytes,
66 extension,
67 content_type,
68 width_emu,
69 height_emu,
70 })
71}
72
73fn find_blip(bytes: &[u8], depth: usize) -> Option<(Vec<u8>, &'static str, &'static str)> {
76 if depth > 8 {
77 return None;
78 }
79 let mut pos = 0usize;
80 while bytes.len().saturating_sub(pos) >= 8 {
81 let ver_inst = read_u16(bytes, pos)?;
82 let rec_type = read_u16(bytes, pos + 2)?;
83 let rec_len = read_u32(bytes, pos + 4)? as usize;
84 let body_start = pos + 8;
85 let body_end = body_start.checked_add(rec_len)?;
86 let body = bytes.get(body_start..body_end)?;
87 let is_container = ver_inst & 0x000F == 0x000F;
88 let instance = ver_inst >> 4;
89
90 if (0xF018..=0xF117).contains(&rec_type) {
91 if let Some(result) = decode_blip(rec_type, instance, body) {
92 return Some(result);
93 }
94 } else if rec_type == 0xF007 {
95 if rec_len > 36 {
97 let name_len = usize::from(*body.get(33)?);
98 let body = body.get(36 + name_len..)?;
99 if let Some(result) = find_blip(body, depth + 1) {
100 return Some(result);
101 }
102 }
103 } else if is_container {
104 if let Some(result) = find_blip(body, depth + 1) {
105 return Some(result);
106 }
107 }
108 pos = body_end;
109 }
110 None
111}
112
113fn decode_blip(
114 rec_type: u16,
115 instance: u16,
116 body: &[u8],
117) -> Option<(Vec<u8>, &'static str, &'static str)> {
118 let tagged = |base_instance: u16,
121 extension: &'static str,
122 content_type: &'static str|
123 -> Option<(Vec<u8>, &'static str, &'static str)> {
124 let uid_count = if instance == base_instance + 1 { 2 } else { 1 };
125 let data_start = uid_count * 16 + 1;
126 let data = body.get(data_start..)?;
127 if data.is_empty() {
128 return None;
129 }
130 Some((data.to_vec(), extension, content_type))
131 };
132
133 match rec_type {
134 0xF01D => tagged(
135 if matches!(instance, 0x6E2 | 0x6E3) {
136 0x6E2
137 } else {
138 0x46A
139 },
140 "jpeg",
141 "image/jpeg",
142 ),
143 0xF02A => tagged(0x6E2, "jpeg", "image/jpeg"), 0xF01E => tagged(0x6E0, "png", "image/png"),
145 0xF029 => tagged(0x6E4, "tiff", "image/tiff"),
146 0xF01F => {
147 let uid_count = if instance == 0x7A9 { 2 } else { 1 };
149 let data_start = uid_count * 16 + 1;
150 let dib = body.get(data_start..)?;
151 Some((dib_to_bmp(dib)?, "bmp", "image/bmp"))
152 }
153 0xF01A | 0xF01B | 0xF01C => {
154 let (base_instance, extension, mime) = match rec_type {
155 0xF01A => (0x3D4, "emf", "image/x-emf"),
156 0xF01B => (0x216, "wmf", "image/x-wmf"),
157 _ => (0x542, "pict", "image/x-pict"),
158 };
159 let offset = if instance == base_instance + 1 {
160 32
161 } else {
162 16
163 };
164 let size = read_u32(body, offset)? as usize;
165 if size == 0 || size > 64 * 1024 * 1024 {
166 return None;
167 }
168 let saved = read_u32(body, offset + 28)? as usize;
169 let data_start = offset + 34;
170 let data = body.get(data_start..data_start.checked_add(saved)?)?;
171 let data = match *body.get(offset + 32)? {
172 0 => miniz_oxide::inflate::decompress_to_vec_zlib_with_limit(data, size).ok()?,
173 0xFE if saved == size => data.to_vec(),
174 _ => return None,
175 };
176 if data.len() != size {
177 return None;
178 }
179 Some((data, extension, mime))
180 }
181 _ => None,
182 }
183}
184
185fn dib_to_bmp(dib: &[u8]) -> Option<Vec<u8>> {
186 let bi_size = read_u32(dib, 0)? as usize;
187 if !matches!(bi_size, 12 | 40 | 52 | 56 | 108 | 124) || dib.len() < bi_size {
188 return None;
189 }
190 let (width, height, planes, bit_count, compression, clr_used, palette_stride) = if bi_size == 12
191 {
192 (
193 usize::from(read_u16(dib, 4)?),
194 usize::from(read_u16(dib, 6)?),
195 read_u16(dib, 8)?,
196 usize::from(read_u16(dib, 10)?),
197 0,
198 0,
199 3,
200 )
201 } else {
202 let width = read_u32(dib, 4)? as i32;
203 let height = read_u32(dib, 8)? as i32;
204 if width <= 0 || height == 0 {
205 return None;
206 }
207 (
208 width as usize,
209 height.checked_abs()? as usize,
210 read_u16(dib, 12)?,
211 usize::from(read_u16(dib, 14)?),
212 read_u32(dib, 16)?,
213 read_u32(dib, 32)? as usize,
214 4,
215 )
216 };
217 if width == 0 || height == 0 || planes != 1 {
218 return None;
219 }
220 match compression {
221 0 if matches!(bit_count, 1 | 2 | 4 | 8 | 16 | 24 | 32) => {}
222 1 if bit_count == 8 => {}
223 2 if bit_count == 4 => {}
224 3 | 6 if matches!(bit_count, 16 | 32) => {}
225 4 | 5 if bit_count == 0 => {}
226 _ => return None,
227 }
228 let palette_entries = if clr_used != 0 {
229 clr_used
230 } else if (1..=8).contains(&bit_count) {
231 1usize << bit_count
232 } else {
233 0
234 };
235 if (1..=8).contains(&bit_count) && palette_entries > 1usize << bit_count {
236 return None;
237 }
238 let mask_end = match compression {
239 3 => bi_size.max(52),
240 6 => bi_size.max(56),
241 _ => bi_size,
242 };
243 let pixel_offset = mask_end.checked_add(palette_entries.checked_mul(palette_stride)?)?;
244 let pixels = dib.get(pixel_offset..)?;
245 if matches!(compression, 0 | 3 | 6) {
246 let stride = width.checked_mul(bit_count)?.checked_add(31)? / 32 * 4;
247 pixels.get(..stride.checked_mul(height)?)?;
248 } else {
249 let image_size = read_u32(dib, 20)? as usize;
250 if image_size == 0 {
251 return None;
252 }
253 pixels.get(..image_size)?;
254 }
255 let file_size = u32::try_from(dib.len().checked_add(14)?).ok()?;
256 let file_pixel_offset = u32::try_from(pixel_offset.checked_add(14)?).ok()?;
257
258 let mut bmp = Vec::with_capacity(file_size as usize);
259 bmp.extend_from_slice(b"BM");
260 bmp.extend_from_slice(&file_size.to_le_bytes());
261 bmp.extend_from_slice(&0u32.to_le_bytes());
262 bmp.extend_from_slice(&file_pixel_offset.to_le_bytes());
263 bmp.extend_from_slice(dib);
264 Some(bmp)
265}
266
267pub(super) struct FloatingImage {
268 pub anchor: super::textboxes::Spa,
269 pub image: InlineImage,
270 pub tiled: bool,
271}
272
273type Blip = (Vec<u8>, &'static str, &'static str);
274
275fn records(mut bytes: &[u8]) -> impl Iterator<Item = (u16, u16, &[u8])> {
276 std::iter::from_fn(move || {
277 let header = read_u16(bytes, 0)?;
278 let kind = read_u16(bytes, 2)?;
279 let len = read_u32(bytes, 4)? as usize;
280 let body = bytes.get(8..8usize.checked_add(len)?)?;
281 bytes = &bytes[8 + len..];
282 Some((header, kind, body))
283 })
284}
285
286fn bstore(bytes: &[u8], word: &[u8], depth: usize, blips: &mut Vec<Option<Blip>>) {
287 if depth > 16 {
288 return;
289 }
290 for (header, kind, body) in records(bytes) {
291 if kind == 0xF007 {
292 let image = body
293 .get(33)
294 .and_then(|name_len| find_blip(body.get(36 + usize::from(*name_len)..)?, 0))
295 .or_else(|| {
296 let offset = read_u32(body, 28)? as usize;
297 let (header, kind, body) = records(word.get(offset..)?).next()?;
298 decode_blip(kind, header >> 4, body)
299 });
300 blips.push(image);
301 } else if header & 15 == 15 {
302 bstore(body, word, depth + 1, blips);
303 }
304 }
305}
306
307fn picture_shapes(
308 bytes: &[u8],
309 depth: usize,
310 pictures: &mut std::collections::HashMap<u32, (usize, bool)>,
311) {
312 if depth > 16 {
313 return;
314 }
315 for (header, kind, body) in records(bytes) {
316 if kind == 0xF004 {
317 let mut id = None;
318 let mut pib = None;
319 let mut fill = None;
320 let mut fill_type = 0;
321 for (header, kind, data) in records(body) {
322 match kind {
323 0xF00A => id = read_u32(data, 0),
324 0xF00B | 0xF121 | 0xF122 => {
325 for property in data.chunks_exact(6).take((header >> 4) as usize) {
326 let op = read_u16(property, 0).unwrap_or(0);
327 if op & 0xBFFF == 0x0104 {
328 pib = read_u32(property, 2).and_then(|v| v.checked_sub(1));
329 } else if op & 0xBFFF == 0x0186 {
330 fill = read_u32(property, 2).and_then(|v| v.checked_sub(1));
331 } else if op == 0x0180 {
332 fill_type = read_u32(property, 2).unwrap_or(0);
333 }
334 }
335 }
336 _ => {}
337 }
338 }
339 if let (Some(id), Some((index, tiled))) = (
340 id,
341 pib.map(|p| (p, false))
342 .or_else(|| fill.map(|p| (p, matches!(fill_type, 1 | 2)))),
343 ) {
344 pictures.insert(id, (index as usize, tiled));
345 }
346 } else if header & 15 == 15 {
347 picture_shapes(body, depth + 1, pictures);
348 }
349 }
350}
351
352fn shape_anchor(
353 bytes: &[u8],
354 parent: Option<(super::textboxes::Spa, [i32; 4])>,
355 anchors: &mut std::collections::HashMap<u32, super::textboxes::Spa>,
356) -> Option<super::textboxes::Spa> {
357 let id = records(bytes)
358 .find(|r| r.1 == 0xF00A)
359 .and_then(|r| read_u32(r.2, 0))?;
360 if let Some(anchor) = anchors.get(&id) {
361 return Some(*anchor);
362 }
363 let (mut anchor, bounds) = parent?;
364 let child = records(bytes).find(|r| r.1 == 0xF00F)?.2;
365 let rect: Vec<i32> = child
366 .chunks_exact(4)
367 .take(4)
368 .map(|b| i32::from_le_bytes(b.try_into().unwrap()))
369 .collect();
370 if rect.len() != 4 {
371 return None;
372 }
373 let map = |value: i32, origin: i32, end: i32, target: i32, target_end: i32| -> Option<i32> {
374 let extent = i64::from(end) - i64::from(origin);
375 if extent == 0 {
376 return None;
377 }
378 let delta = (i128::from(value) - i128::from(origin))
379 * (i128::from(target_end) - i128::from(target));
380 i32::try_from(i128::from(target) + delta / i128::from(extent)).ok()
381 };
382 let left = map(rect[0], bounds[0], bounds[2], anchor.left, anchor.right)?;
383 let right = map(rect[2], bounds[0], bounds[2], anchor.left, anchor.right)?;
384 let top = map(rect[1], bounds[1], bounds[3], anchor.top, anchor.bottom)?;
385 let bottom = map(rect[3], bounds[1], bounds[3], anchor.top, anchor.bottom)?;
386 anchor.shape_id = id;
387 anchor.left = left;
388 anchor.right = right;
389 anchor.top = top;
390 anchor.bottom = bottom;
391 anchors.insert(id, anchor);
392 Some(anchor)
393}
394
395fn group_anchors(
396 bytes: &[u8],
397 depth: usize,
398 parent: Option<(super::textboxes::Spa, [i32; 4])>,
399 anchors: &mut std::collections::HashMap<u32, super::textboxes::Spa>,
400) {
401 if depth > 16 {
402 return;
403 }
404 for (header, kind, body) in records(bytes) {
405 if kind == 0xF003 {
406 let group = records(body).find(|r| r.1 == 0xF004).map(|r| r.2);
407 let frame = group.and_then(|group| {
408 let anchor = shape_anchor(group, parent, anchors)?;
409 let bounds = records(group).find(|r| r.1 == 0xF009)?.2;
410 let mut rect = [0i32; 4];
411 for (index, value) in rect.iter_mut().enumerate() {
412 *value = read_u32(bounds, index * 4)? as i32;
413 }
414 Some((anchor, rect))
415 });
416 group_anchors(body, depth + 1, frame, anchors);
417 } else if kind == 0xF004 {
418 shape_anchor(body, parent, anchors);
419 } else if header & 15 == 15 {
420 group_anchors(body, depth + 1, parent, anchors);
421 }
422 }
423}
424
425pub(super) fn shape_anchors(
426 doc: &super::DocFile,
427 header: bool,
428) -> std::collections::HashMap<u32, super::textboxes::Spa> {
429 use super::fib::fcidx;
430 let (index, base, len) = if header {
431 (
432 fcidx::PLCF_SPA_HDR,
433 doc.fib.ccp.hdd_start(),
434 doc.fib.ccp.hdd,
435 )
436 } else {
437 (fcidx::PLCF_SPA_MOM, 0, doc.fib.ccp.text)
438 };
439 let spas = doc
440 .fib
441 .table_slice(&doc.table, index)
442 .and_then(|plc| super::textboxes::parse_spas(plc, base, len).ok())
443 .unwrap_or_default();
444 let mut anchors = spas.into_iter().map(|spa| (spa.shape_id, spa)).collect();
445 let Some(data) = doc.fib.table_slice(&doc.table, fcidx::DGG_INFO) else {
446 return anchors;
447 };
448 let Some((_, _, global)) = records(data).next() else {
449 return anchors;
450 };
451 let mut cursor = 8 + global.len();
452 while let Some(&story) = data.get(cursor) {
453 cursor += 1;
454 let Some((_, kind, drawing)) = records(&data[cursor..]).next() else {
455 break;
456 };
457 cursor += 8 + drawing.len();
458 if kind != 0xF002 {
459 break;
460 }
461 if story == u8::from(header) {
462 group_anchors(drawing, 0, None, &mut anchors);
463 }
464 }
465 anchors
466}
467
468pub(super) fn floating_images(doc: &super::DocFile) -> Vec<FloatingImage> {
469 use super::fib::fcidx;
470 let Some(data) = doc.fib.table_slice(&doc.table, fcidx::DGG_INFO) else {
471 return Vec::new();
472 };
473 let Some((_, kind, global)) = records(data).next() else {
474 return Vec::new();
475 };
476 if kind != 0xF000 {
477 return Vec::new();
478 }
479 let mut blips = Vec::new();
480 bstore(global, &doc.word, 0, &mut blips);
481 let mut cursor = 8 + global.len();
482 let mut images = Vec::new();
483 while let Some(&story) = data.get(cursor) {
484 cursor += 1;
485 let Some((_, kind, drawing)) = records(&data[cursor..]).next() else {
486 break;
487 };
488 cursor += 8 + drawing.len();
489 if kind != 0xF002 {
490 break;
491 }
492 if story > 1 {
493 continue;
494 }
495 let anchors = shape_anchors(doc, story == 1);
496 let mut pictures = std::collections::HashMap::new();
497 picture_shapes(drawing, 0, &mut pictures);
498 for anchor in anchors.into_values() {
499 let Some(&(index, tiled)) = pictures.get(&anchor.shape_id) else {
500 continue;
501 };
502 let Some(Some((bytes, extension, content_type))) = blips.get(index) else {
503 continue;
504 };
505 let width = i64::from(anchor.right) - i64::from(anchor.left);
506 let height = i64::from(anchor.bottom) - i64::from(anchor.top);
507 if width <= 0 || height <= 0 {
508 continue;
509 }
510 images.push(FloatingImage {
511 anchor,
512 tiled,
513 image: InlineImage {
514 bytes: bytes.clone(),
515 extension,
516 content_type,
517 width_emu: width as u64 * 635,
518 height_emu: height as u64 * 635,
519 },
520 });
521 }
522 }
523 images.sort_by_key(|image| (image.anchor.anchor_cp, image.anchor.shape_id));
524 images
525}
526
527#[cfg(test)]
528mod tests {
529 use super::*;
530
531 fn build_picf_with_png(png: &[u8]) -> Vec<u8> {
532 let mut blip = Vec::new();
534 blip.extend_from_slice(&((0x6E0u16 << 4) | 0x0).to_le_bytes());
535 blip.extend_from_slice(&0xF01Eu16.to_le_bytes());
536 blip.extend_from_slice(&((16 + 1 + png.len()) as u32).to_le_bytes());
537 blip.extend_from_slice(&[0u8; 16]); blip.push(0xFF); blip.extend_from_slice(png);
540
541 let mut container = Vec::new();
542 container.extend_from_slice(&0x000Fu16.to_le_bytes());
543 container.extend_from_slice(&0xF004u16.to_le_bytes());
544 container.extend_from_slice(&(blip.len() as u32).to_le_bytes());
545 container.extend_from_slice(&blip);
546
547 let cb_header = 0x44usize;
548 let mut picf = vec![0u8; cb_header];
549 let lcb = (cb_header + container.len()) as u32;
550 picf[..4].copy_from_slice(&lcb.to_le_bytes());
551 picf[4..6].copy_from_slice(&(cb_header as u16).to_le_bytes());
552 picf[6..8].copy_from_slice(&0x64u16.to_le_bytes()); picf[28..30].copy_from_slice(&1440u16.to_le_bytes()); picf[30..32].copy_from_slice(&720u16.to_le_bytes()); picf[32..34].copy_from_slice(&1000u16.to_le_bytes()); picf[34..36].copy_from_slice(&500u16.to_le_bytes()); picf.extend_from_slice(&container);
558 picf
559 }
560
561 #[test]
562 fn extracts_png_blip_with_scaling() {
563 let png = b"\x89PNG\r\n\x1a\nfakepngdata";
564 let data_stream = build_picf_with_png(png);
565 let image = extract_image(&data_stream, 0).expect("image");
566 assert_eq!(image.extension, "png");
567 assert_eq!(image.bytes, png);
568 assert_eq!(image.width_emu, 1440 * 635);
569 assert_eq!(image.height_emu, 360 * 635);
570 }
571
572 #[test]
573 fn rejects_unsupported_storage() {
574 let mut picf = vec![0u8; 0x44];
575 picf[..4].copy_from_slice(&0x44u32.to_le_bytes());
576 picf[4..6].copy_from_slice(&0x44u16.to_le_bytes());
577 picf[6..8].copy_from_slice(&0x08u16.to_le_bytes());
578 assert!(extract_image(&picf, 0).is_none());
579 }
580}