use crate::formats::doc::text_extractor::DocParagraph;
use crate::formats::odraw::{
blip_hash_name, decode_blip_record_at, decode_fbse_blip, find_record, is_blip_record,
parse_odraw_header, DecodedBlip, RT_BSTORE_CONTAINER, RT_FBSE,
};
use super::cfb_reader;
use super::records::{parse_header, RT_MAIN_MASTER, RT_NOTES_CONTAINER, RT_SLIDE_CONTAINER,
REC_VER_CONTAINER};
use super::text_extractor;
#[derive(Debug, Clone)]
pub struct PptImage {
pub hash_name: String,
pub bytes: Vec<u8>,
pub slide_idx: Option<usize>,
}
fn decode_bstore(doc_stream: &[u8], pictures: Option<&[u8]>) -> Vec<Option<DecodedBlip>> {
let Some(bstore) = find_record(doc_stream, 0, doc_stream.len(), RT_BSTORE_CONTAINER) else {
return Vec::new();
};
let mut entries = Vec::new();
let mut pos = bstore.body_start;
while let Some((hdr, next)) = parse_odraw_header(doc_stream, pos, bstore.body_end) {
if next <= pos {
break;
}
let body = &doc_stream[hdr.body_start..hdr.body_end];
let decoded = if hdr.rec_type == RT_FBSE {
decode_fbse_blip(body, pictures)
} else if is_blip_record(hdr.rec_type) {
decode_blip_record_at(doc_stream, pos)
} else {
None
};
entries.push(decoded);
pos = next;
}
entries
}
fn collect_slide_pibs(data: &[u8], start: usize, end: usize, out: &mut Vec<Vec<u32>>) {
let mut pos = start;
while let Some((hdr, next)) = parse_header(data, pos, end) {
if next <= pos {
break;
}
match hdr.rec_type {
RT_SLIDE_CONTAINER => {
let mut pibs = Vec::new();
crate::formats::odraw::collect_pib_values(
data,
hdr.body_start,
hdr.body_end,
&mut pibs,
);
out.push(pibs);
}
RT_NOTES_CONTAINER | RT_MAIN_MASTER => {}
_ => {
if hdr.rec_ver == REC_VER_CONTAINER {
collect_slide_pibs(data, hdr.body_start, hdr.body_end, out);
}
}
}
pos = next;
}
}
fn decode_pictures_stream(pictures: &[u8]) -> Vec<DecodedBlip> {
let mut out = Vec::new();
let mut pos = 0usize;
while let Some((hdr, next)) = parse_odraw_header(pictures, pos, pictures.len()) {
if next <= pos {
break;
}
if let Some(decoded) = decode_blip_record_at(pictures, pos) {
out.push(decoded);
} else if hdr.rec_type == RT_FBSE {
if let Some(decoded) =
decode_fbse_blip(&pictures[hdr.body_start..hdr.body_end], None)
{
out.push(decoded);
}
}
pos = next;
}
out
}
pub fn extract_ppt_images(file_path: &str) -> Result<Vec<PptImage>, String> {
let bytes = std::fs::read(file_path).map_err(|e| format!("Failed to read .ppt file: {e}"))?;
extract_ppt_images_bytes(&bytes)
}
pub fn extract_ppt_images_bytes(bytes: &[u8]) -> Result<Vec<PptImage>, String> {
let doc_stream = cfb_reader::read_powerpoint_document_stream(bytes)?;
let pictures = cfb_reader::read_pictures_stream(bytes)?;
let bstore = decode_bstore(&doc_stream, pictures.as_deref());
let mut slide_pibs: Vec<Vec<u32>> = Vec::new();
collect_slide_pibs(&doc_stream, 0, doc_stream.len(), &mut slide_pibs);
let slide_count = text_extractor::extract_slides(&doc_stream).len();
let aligned = slide_pibs.len() == slide_count;
let mut out: Vec<PptImage> = Vec::new();
for (drawing_idx, pibs) in slide_pibs.iter().enumerate() {
for pib in pibs {
let idx = (*pib as usize).wrapping_sub(1);
if let Some(Some(decoded)) = bstore.get(idx) {
out.push(PptImage {
hash_name: blip_hash_name(&decoded.bytes, decoded.ext),
bytes: decoded.bytes.clone(),
slide_idx: if aligned { Some(drawing_idx) } else { None },
});
}
}
}
if out.is_empty() {
if let Some(ref pics) = pictures {
for decoded in decode_pictures_stream(pics) {
out.push(PptImage {
hash_name: blip_hash_name(&decoded.bytes, decoded.ext),
bytes: decoded.bytes,
slide_idx: None,
});
}
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::formats::odraw::{FBSE_FO_DELAY_OFFSET, FBSE_HEADER_LEN};
const PNG_MAGIC: [u8; 8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
fn record(ver_inst: u16, rec_type: u16, body: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(8 + body.len());
out.extend_from_slice(&ver_inst.to_le_bytes());
out.extend_from_slice(&rec_type.to_le_bytes());
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
out.extend_from_slice(body);
out
}
fn png_blip_record() -> Vec<u8> {
let mut body = vec![0u8; 17];
body.extend_from_slice(&PNG_MAGIC);
body.extend_from_slice(b"data");
record(0x6E0 << 4, crate::formats::odraw::RT_BLIP_PNG, &body)
}
#[test]
fn decode_bstore_resolves_delay_stream_blip() {
let mut pictures = vec![0u8; 4];
let fo_delay = pictures.len() as u32;
pictures.extend_from_slice(&png_blip_record());
let mut fbse = vec![0u8; FBSE_HEADER_LEN];
fbse[FBSE_FO_DELAY_OFFSET..FBSE_FO_DELAY_OFFSET + 4]
.copy_from_slice(&fo_delay.to_le_bytes());
let fbse_rec = record(0, RT_FBSE, &fbse);
let bstore = record((1 << 4) | 0xF, RT_BSTORE_CONTAINER, &fbse_rec);
let dgg = record(0xF, crate::formats::odraw::RT_DGG_CONTAINER, &bstore);
let entries = decode_bstore(&dgg, Some(&pictures));
assert_eq!(entries.len(), 1);
assert!(entries[0].is_some());
assert_eq!(entries[0].as_ref().unwrap().ext, ".png");
}
#[test]
fn collect_slide_pibs_groups_by_slide_container() {
let mut opt_body = Vec::new();
opt_body.extend_from_slice(&0x4104u16.to_le_bytes());
opt_body.extend_from_slice(&1u32.to_le_bytes());
let opt = record((1 << 4) | 0x3, crate::formats::odraw::RT_OPT, &opt_body);
let slide = record(0xF, RT_SLIDE_CONTAINER, &opt);
let mut pibs = Vec::new();
collect_slide_pibs(&slide, 0, slide.len(), &mut pibs);
assert_eq!(pibs, vec![vec![1]]);
}
#[test]
fn decode_pictures_stream_walks_records() {
let mut stream = png_blip_record();
stream.extend_from_slice(&png_blip_record());
let decoded = decode_pictures_stream(&stream);
assert_eq!(decoded.len(), 2);
}
}
pub(super) fn chunk_with_images_impl(
file_path: &str,
build: impl FnOnce(Vec<DocParagraph>) -> Vec<crate::formats::doc::structural::ChunkRecord>,
) -> Result<(Vec<crate::chunk::Chunk>, Vec<(String, Vec<u8>)>), String> {
super::structural::validate_ppt_path(file_path)?;
let bytes = std::fs::read(file_path).map_err(|e| format!("Failed to read .ppt file: {e}"))?;
chunk_with_images_impl_bytes(&bytes, file_path, build)
}
pub(super) fn chunk_with_images_impl_bytes(
bytes: &[u8],
source: &str,
build: impl FnOnce(Vec<DocParagraph>) -> Vec<crate::formats::doc::structural::ChunkRecord>,
) -> Result<(Vec<crate::chunk::Chunk>, Vec<(String, Vec<u8>)>), String> {
let file_path = source;
let paragraphs = super::structural::load_ppt_paragraphs_bytes(bytes)?;
let images = extract_ppt_images_bytes(bytes).unwrap_or_default();
let text_chunks = build(paragraphs);
let total = images.len() + text_chunks.len();
let mut chunk_list: Vec<crate::chunk::Chunk> = Vec::with_capacity(total);
let mut image_out: Vec<(String, Vec<u8>)> = Vec::new();
for (i, img) in images.iter().enumerate() {
if !image_out.iter().any(|(n, _)| n == &img.hash_name) {
image_out.push((img.hash_name.clone(), img.bytes.clone()));
}
chunk_list.push(crate::chunk::Chunk::new(
img.hash_name.clone(),
"image",
serde_json::json!({
"source": file_path,
"chunk_index": i,
"total_chunks": total,
"paragraph_type": "image",
"heading_level": serde_json::Value::Null,
"page_number": img.slide_idx.map(|s| s + 1),
"image_name": img.hash_name,
}),
));
}
let offset = images.len();
for chunk in &text_chunks {
chunk_list.push(crate::chunk::Chunk::new(
chunk.content.clone(),
chunk.content_type,
serde_json::json!({
"source": file_path,
"chunk_index": chunk.chunk_index + offset,
"total_chunks": total,
"paragraph_type": chunk.paragraph_type,
"heading_level": chunk.heading_level,
"page_number": serde_json::Value::Null,
}),
));
}
Ok((chunk_list, image_out))
}