Skip to content

Commit 01ac1e8

Browse files
greenhat616Copilot
andauthored
Support image extraction for the OLE formats (#4)
* feat(docx): add to-stream writer for docx package * feat(ole): support paragraph runs image extraction * fix(ole): use apache poi based chp parser * fix(ole): store data stream for accessing image data * refactor(ole): use builtin blip parser chore: up * fix: adviced comments and add static assert for PictureFields fix: adviced comments and add static assert for PictureFields * tests: add document image extraction test * Update src/images/extractor.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/ole/doc/image.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: make clippy happy * fix: make clippy happy again --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 521e6d8 commit 01ac1e8

10 files changed

Lines changed: 842 additions & 63 deletions

File tree

src/images/blip.rs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,14 @@
88
// - [MS-ODRAW] 2.2.23: OfficeArtBlip records
99
// - https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-odraw/5dc1b9ed-818c-436f-8a4f-905a7ebb1ba9
1010

11-
use crate::common::binary::{read_u16_le, read_u32_le};
12-
use crate::common::error::{Error, Result};
11+
use crate::{
12+
common::binary::{read_u16_le, read_u32_le},
13+
ole::escher::EscherRecord,
14+
};
15+
use crate::{
16+
common::error::{Error, Result},
17+
ole::escher::EscherRecordType,
18+
};
1319
use std::borrow::Cow;
1420
use std::io::Read;
1521

@@ -480,6 +486,30 @@ impl<'data> Blip<'data> {
480486
}
481487
}
482488

489+
pub fn try_from_escher_record(blip_record: &EscherRecord) -> Result<Self> {
490+
// Reconstruct full BLIP record
491+
match blip_record.record_type {
492+
EscherRecordType::BlipEmf
493+
| EscherRecordType::BlipWmf
494+
| EscherRecordType::BlipPict
495+
| EscherRecordType::BlipJpeg
496+
| EscherRecordType::BlipPng
497+
| EscherRecordType::BlipDib
498+
| EscherRecordType::BlipTiff => {
499+
// Reconstruct full BLIP record
500+
let mut full_data = Vec::with_capacity(8 + blip_record.data.len());
501+
let ver_inst = (blip_record.instance << 4) | (blip_record.version as u16);
502+
full_data.extend_from_slice(&ver_inst.to_le_bytes());
503+
full_data.extend_from_slice(&blip_record.record_type_raw.to_le_bytes());
504+
full_data.extend_from_slice(&blip_record.length.to_le_bytes());
505+
full_data.extend_from_slice(blip_record.data);
506+
507+
Blip::parse(&full_data).map(|blip| blip.into_owned())
508+
},
509+
_ => Err(Error::ParseError("Not a BLIP record".into())),
510+
}
511+
}
512+
483513
/// Get the BLIP type
484514
pub fn blip_type(&self) -> Option<BlipType> {
485515
match self {

src/images/extractor.rs

Lines changed: 154 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
use crate::common::error::Result;
77
use crate::images::{Blip, BlipStore, BlipStoreEntry};
8+
use crate::ole::escher::EscherRecord;
89
use crate::ole::ppt::escher::{EscherContainer, EscherParser, EscherRecordType};
910
use std::borrow::Cow;
1011

@@ -208,6 +209,112 @@ impl ImageExtractor {
208209
Ok(store)
209210
}
210211

212+
/// Extract BLIP from BSE record, handling both embedded and delay-loaded cases.
213+
///
214+
/// # Arguments
215+
/// * `bse` - Parsed BSE record
216+
/// * `record_data` - Raw BSE record data (without Escher header)
217+
/// * `delay_stream` - Optional data stream for delay-loaded BLIPs
218+
///
219+
/// # Returns
220+
/// Owned BLIP data
221+
fn blip_from_bse(
222+
bse: &BlipStoreEntry<'_>,
223+
bse_record: &EscherRecord<'_>,
224+
delay_stream: Option<&[u8]>,
225+
) -> Result<Blip<'static>> {
226+
let is_in_delayed_stream = bse.is_delay_loaded() && bse_record.length <= 36;
227+
let blip_data = if is_in_delayed_stream {
228+
let stream = delay_stream.ok_or_else(|| {
229+
crate::common::error::Error::ParseError(
230+
"BSE record is delay-loaded but no data stream was provided".into(),
231+
)
232+
})?;
233+
let offset = bse.offset as usize;
234+
if offset >= stream.len() {
235+
return Err(crate::common::error::Error::ParseError(
236+
"BSE delay stream offset is out of bounds".into(),
237+
));
238+
}
239+
&stream[offset..]
240+
} else {
241+
if bse_record.data.len() <= 36 {
242+
return Err(crate::common::error::Error::ParseError(
243+
"BSE record data is too short for embedded BLIP".into(),
244+
));
245+
}
246+
&bse_record.data[36..]
247+
};
248+
249+
if blip_data.len() < 8 {
250+
return Err(crate::common::error::Error::ParseError(
251+
"Insufficient data for BLIP".into(),
252+
));
253+
}
254+
255+
Blip::parse(blip_data).map(|b| b.into_owned())
256+
}
257+
258+
/// Extract image from a single Escher record
259+
///
260+
/// This method handles both BSE (0xF007) and BLIP type records directly.
261+
/// For BSE records, it extracts the embedded BLIP data.
262+
/// For BLIP records (0xF01A-0xF029), it parses the image directly.
263+
///
264+
/// # Arguments
265+
/// * `record` - An Escher record that is either BSE or a BLIP type
266+
///
267+
/// # Returns
268+
/// ExtractedImage if the record contains valid image data
269+
pub fn extract_from_escher_record(
270+
record: &EscherRecord<'_>,
271+
) -> Result<ExtractedImage<'static>> {
272+
Self::extract_from_escher_record_with_stream(record, None)
273+
}
274+
275+
/// Extract image from a single Escher record with optional data stream.
276+
///
277+
/// This method handles both BSE (0xF007) and BLIP type records directly.
278+
/// For BSE records with delay-loaded BLIPs (offset != 0), the delay_stream
279+
/// parameter must be provided to locate the BLIP data.
280+
///
281+
/// # Arguments
282+
/// * `record` - An Escher record that is either BSE or a BLIP type
283+
/// * `delay_stream` - Optional main stream for delay-loaded BLIPs
284+
///
285+
/// # Returns
286+
/// ExtractedImage if the record contains valid image data
287+
pub fn extract_from_escher_record_with_stream(
288+
record: &EscherRecord<'_>,
289+
delay_stream: Option<&[u8]>,
290+
) -> Result<ExtractedImage<'static>> {
291+
match record.record_type {
292+
// BLIP type records - parse directly
293+
EscherRecordType::BlipEmf
294+
| EscherRecordType::BlipWmf
295+
| EscherRecordType::BlipPict
296+
| EscherRecordType::BlipJpeg
297+
| EscherRecordType::BlipPng
298+
| EscherRecordType::BlipDib
299+
| EscherRecordType::BlipTiff => {
300+
let blip = Blip::try_from_escher_record(record)?;
301+
Ok(ExtractedImage::new(blip, None, 0))
302+
},
303+
304+
// BSE record - extract embedded or delay-loaded BLIP
305+
EscherRecordType::BSE => {
306+
let bse = BlipStoreEntry::parse(record.data)?;
307+
let name = bse.name.as_ref().map(|n| n.to_string());
308+
let blip = Self::blip_from_bse(&bse, record, delay_stream)?;
309+
Ok(ExtractedImage::new(blip, name, 0))
310+
},
311+
_ => Err(crate::common::error::Error::ParseError(format!(
312+
"Record type 0x{:04X} is not a supported image record",
313+
record.record_type_raw
314+
))),
315+
}
316+
}
317+
211318
/// Extract all BLIPs from Escher drawing data
212319
///
213320
/// This extracts actual BLIP records (image data) from the drawing layer.
@@ -361,40 +468,48 @@ impl ImageExtractor {
361468
/// part of a document (e.g., a specific slide in PPT).
362469
pub fn extract_from_container(
363470
container: &EscherContainer,
471+
) -> Result<Vec<ExtractedImage<'static>>> {
472+
Self::extract_from_container_with_stream(container, None)
473+
}
474+
475+
/// Extract images from a specific Escher container with optional data stream.
476+
///
477+
/// # Arguments
478+
/// * `container` - The Escher container to extract from
479+
/// * `delay_stream` - Optional data stream for delay-loaded BLIPs
480+
pub fn extract_from_container_with_stream(
481+
container: &EscherContainer,
482+
delay_stream: Option<&[u8]>,
364483
) -> Result<Vec<ExtractedImage<'static>>> {
365484
let mut images = Vec::new();
366485
let mut index = 0;
367486

368487
// Recursively search for BLIP records
369-
Self::extract_from_container_recursive(container, &mut images, &mut index)?;
488+
Self::extract_from_container_recursive_with_stream(
489+
container,
490+
&mut images,
491+
&mut index,
492+
delay_stream,
493+
)?;
370494

371495
Ok(images)
372496
}
373497

374-
/// Recursively extract BLIPs from a container and its children
375-
fn extract_from_container_recursive(
498+
/// Recursively extract BLIPs from a container with optional data stream.
499+
fn extract_from_container_recursive_with_stream(
376500
container: &EscherContainer,
377501
images: &mut Vec<ExtractedImage<'static>>,
378502
index: &mut usize,
503+
delay_stream: Option<&[u8]>,
379504
) -> Result<()> {
380505
// Check if this container has BLIP records
381506
for child_result in container.children() {
382507
let child = match child_result {
383508
Ok(c) => c,
384509
Err(_) => continue, // Skip invalid records
385510
};
386-
let is_blip = matches!(
387-
child.record_type,
388-
EscherRecordType::BlipEmf
389-
| EscherRecordType::BlipWmf
390-
| EscherRecordType::BlipPict
391-
| EscherRecordType::BlipJpeg
392-
| EscherRecordType::BlipPng
393-
| EscherRecordType::BlipDib
394-
| EscherRecordType::BlipTiff
395-
);
396511

397-
if is_blip {
512+
if child.record_type.is_blip() {
398513
// Reconstruct full BLIP record
399514
let mut full_data = Vec::with_capacity(8 + child.data.len());
400515
let ver_inst = (child.instance << 4) | (child.version as u16);
@@ -412,7 +527,31 @@ impl ImageExtractor {
412527
} else if child.is_container() {
413528
// Recurse into child containers
414529
let child_container = EscherContainer::new(child);
415-
Self::extract_from_container_recursive(&child_container, images, index)?;
530+
Self::extract_from_container_recursive_with_stream(
531+
&child_container,
532+
images,
533+
index,
534+
delay_stream,
535+
)?;
536+
} else if child.record_type == EscherRecordType::BSE {
537+
// BSE records can contain embedded or delay-loaded BLIP data
538+
match BlipStoreEntry::parse(child.data) {
539+
Ok(bse) => {
540+
let name = bse.name.as_ref().map(|n| n.to_string());
541+
match Self::blip_from_bse(&bse, &child, delay_stream) {
542+
Ok(blip) => {
543+
images.push(ExtractedImage::new(blip, name, *index));
544+
*index += 1;
545+
},
546+
Err(_) => {
547+
continue;
548+
},
549+
}
550+
},
551+
Err(_) => {
552+
continue;
553+
},
554+
}
416555
}
417556
}
418557

0 commit comments

Comments
 (0)