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