Skip to main content

rust_docx/doc/
images.rs

1//! Inline pictures: a run with fSpec and character 0x01 carries
2//! sprmCPicLocation, an offset to a PICF structure ([MS-DOC] 2.9.190) in the
3//! Data stream. Word 97+ stores the actual bitmap inside an OfficeArt
4//! ([MS-ODRAW]) record stream following the PICF header.
5
6#[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    // Display size: dxaGoal/dyaGoal twips scaled by mx/my (per-mille).
38    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 // twips -> EMU
46    };
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    // mm 0x64/0x66: OfficeArt inline shape; otherwise legacy metafile storage
53    // we do not support.
54    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
73/// Walks an OfficeArt record stream (descending into containers) until a
74/// supported blip payload is found.
75fn 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            // OfficeArtFBSE: 36-byte header, embedded blip may follow.
96            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    // Tagged bitmap blips: N UIDs (16 bytes each) + 1 tag byte + data. The
119    // "+1" instance variant carries a second UID.
120    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"), // JPEG (CMYK variant record)
144        0xF01E => tagged(0x6E0, "png", "image/png"),
145        0xF029 => tagged(0x6E4, "tiff", "image/tiff"),
146        0xF01F => {
147            // DIB: BMP without the 14-byte file header; reconstruct it.
148            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        // Escher: OfficeArtSpContainer (0xF004) wrapping a PNG blip (0xF01E).
533        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]); // UID
538        blip.push(0xFF); // tag
539        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()); // mm = MM_SHAPE
553        picf[28..30].copy_from_slice(&1440u16.to_le_bytes()); // dxaGoal 1 inch
554        picf[30..32].copy_from_slice(&720u16.to_le_bytes()); // dyaGoal 0.5 inch
555        picf[32..34].copy_from_slice(&1000u16.to_le_bytes()); // mx
556        picf[34..36].copy_from_slice(&500u16.to_le_bytes()); // my -> 0.25 inch
557        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}