use super::{Result, Slide};
use crate::constants::{
COMMENTS_NAMESPACE, NOTES_SLIDE_NAMESPACE, SLIDE_LAYOUT_NAMESPACE, SLIDE_MASTER_NAMESPACE,
};
use crate::metadata::{parse_pptx_metadata, render_presentation_markdown};
use crate::parse_rels::{parse_hyperlink_rels, parse_relationships};
use crate::parse_xml::{extract_inherited_positions, InheritedPositions};
use crate::parser_config::ParserConfig;
use crate::PresentationMetadata;
use rayon::prelude::*;
use std::sync::Arc;
use std::{collections::HashMap, io::Read, path::Path};
pub struct PptxContainer {
pub config: ParserConfig,
archive: zip::ZipArchive<std::fs::File>,
pub slide_paths: Vec<String>,
pub slide_count: u32,
metadata: PresentationMetadata,
}
impl PptxContainer {
pub fn open(path: &Path, config: ParserConfig) -> Result<Self> {
let file = std::fs::File::open(path)?;
let mut archive = zip::ZipArchive::new(file)?;
let mut slide_paths: Vec<String> = Vec::new();
let mut slide_count = 0;
for i in 0..archive.len() {
let file = archive.by_index(i)?;
let name = file.name().to_string();
if name.starts_with("ppt/slides/slide") && name.ends_with(".xml") {
slide_paths.push(name);
slide_count += 1;
}
}
sort_slide_paths(&mut slide_paths);
let core_xml = read_optional_archive_file(&mut archive, "docProps/core.xml")?;
let metadata = parse_pptx_metadata(core_xml.as_deref())?;
Ok(Self {
archive,
slide_paths,
config,
slide_count,
metadata,
})
}
pub fn parse_all(&mut self) -> Result<Vec<Slide>> {
let mut slides = Vec::new();
let count = self.slide_paths.len();
for i in 0..count {
let path = &self.slide_paths[i].clone();
if let Some(slide) = self.load_slide(path)? {
slides.push(slide);
}
}
Ok(slides)
}
pub fn metadata(&self) -> &PresentationMetadata {
&self.metadata
}
pub fn convert_to_md(&mut self) -> Result<String> {
let slides = self.parse_all()?;
render_presentation_markdown(
&self.metadata,
self.config.include_presentation_metadata,
slides,
)
}
pub fn convert_to_md_multi_threaded(&mut self) -> Result<String> {
let slides = self.parse_all_multi_threaded()?;
render_presentation_markdown(
&self.metadata,
self.config.include_presentation_metadata,
slides,
)
}
pub fn parse_all_multi_threaded(&mut self) -> Result<Vec<Slide>> {
let slide_paths = self.slide_paths.clone();
let config = self.config.clone();
let mut raw_data = Vec::with_capacity(slide_paths.len());
let mut all_image_data = HashMap::new();
for slide_path in &slide_paths {
let slide_xml = self.read_file_from_archive(slide_path)?;
let rels_path = self.get_slide_rels_path(slide_path);
let rels_data = self.read_file_from_archive(&rels_path).ok();
let hyperlinks = rels_data
.as_deref()
.map(parse_hyperlink_rels)
.transpose()?
.unwrap_or_default();
let slide_number = Slide::extract_slide_number(slide_path).unwrap_or(0);
let inherited_positions =
self.resolve_inherited_positions(slide_path, rels_data.as_deref())?;
let speaker_notes = self.resolve_speaker_notes(slide_path, rels_data.as_deref())?;
let comments = self.resolve_comments(slide_path, rels_data.as_deref())?;
let mut slide_images = Vec::new();
if config.extract_images {
if let Some(ref data) = rels_data {
slide_images = crate::parse_rels::parse_slide_rels(data)?;
}
for img_ref in &slide_images {
let path = PptxContainer::resolve_target_path(slide_path, &img_ref.target);
let data = self.read_file_from_archive(&path)?;
all_image_data.entry(img_ref.target.clone()).or_insert(data);
}
}
raw_data.push((
slide_path.clone(),
slide_number,
slide_xml,
slide_images,
inherited_positions,
speaker_notes,
comments,
hyperlinks,
));
}
let shared_image_data = Arc::new(all_image_data);
let slides: Result<Vec<_>> = raw_data
.into_par_iter()
.map(
|(
path,
number,
xml,
images,
inherited_positions,
speaker_notes,
comments,
hyperlinks,
)| {
let elements = crate::parse_xml::parse_slide_xml_with_hyperlinks(
&xml,
&inherited_positions,
&hyperlinks,
)?;
let mut image_map = HashMap::new();
if config.extract_images {
for img_ref in &images {
if let Some(data) = shared_image_data.get(&img_ref.target) {
image_map.insert(img_ref.id.clone(), data.clone());
}
}
}
let mut slide = Slide::new(
path,
number,
elements,
speaker_notes,
comments,
images,
image_map,
config.clone(),
);
slide.link_images();
Ok(slide)
},
)
.collect();
slides
}
pub fn iter_slides(&mut self) -> SlideIterator<'_> {
SlideIterator::new(self)
}
pub fn load_slide(&mut self, slide_path: &str) -> Result<Option<Slide>> {
let slide_data = self.read_file_from_archive(slide_path)?;
let rels_path = self.get_slide_rels_path(slide_path);
let rels_data = self.read_file_from_archive(&rels_path).ok();
let hyperlinks = rels_data
.as_deref()
.map(parse_hyperlink_rels)
.transpose()?
.unwrap_or_default();
let slide_number = Slide::extract_slide_number(slide_path).unwrap_or(0);
let inherited_positions =
self.resolve_inherited_positions(slide_path, rels_data.as_deref())?;
let speaker_notes = self.resolve_speaker_notes(slide_path, rels_data.as_deref())?;
let comments = self.resolve_comments(slide_path, rels_data.as_deref())?;
let elements = crate::parse_xml::parse_slide_xml_with_hyperlinks(
&slide_data,
&inherited_positions,
&hyperlinks,
)?;
let mut images = Vec::new();
let mut image_data = HashMap::new();
if self.config.extract_images {
if let Some(ref rels_bytes) = rels_data {
images = crate::parse_rels::parse_slide_rels(rels_bytes)?;
}
for img_ref in &images {
let img_path = Self::resolve_target_path(slide_path, &img_ref.target);
if let Ok(data) = self.read_file_from_archive(&img_path) {
image_data.insert(img_ref.id.clone(), data);
}
}
}
let config = self.config.clone();
let mut slide = Slide::new(
slide_path.to_string(),
slide_number,
elements,
speaker_notes,
comments,
images,
image_data,
config,
);
slide.link_images();
Ok(Some(slide))
}
pub fn read_file_from_archive(&mut self, path: &str) -> Result<Vec<u8>> {
let mut file = self.archive.by_name(path)?;
let mut content = Vec::new();
file.read_to_end(&mut content)?;
Ok(content)
}
pub fn get_slide_rels_path(&self, slide_path: &str) -> String {
let mut rels_path = slide_path.to_string();
if let Some(pos) = rels_path.rfind('/') {
rels_path.insert_str(pos + 1, "_rels/");
}
rels_path.push_str(".rels");
rels_path
}
fn resolve_inherited_positions(
&mut self,
slide_path: &str,
slide_rels_data: Option<&[u8]>,
) -> Result<InheritedPositions> {
let Some(slide_rels_data) = slide_rels_data else {
return Ok(InheritedPositions::default());
};
let slide_relationships = parse_relationships(slide_rels_data)?;
let Some(layout_target) = slide_relationships
.iter()
.find(|rel| rel.rel_type == SLIDE_LAYOUT_NAMESPACE)
.map(|rel| rel.target.as_str())
else {
return Ok(InheritedPositions::default());
};
let layout_path = Self::resolve_target_path(slide_path, layout_target);
let layout_xml = self.read_file_from_archive(&layout_path)?;
let layout_rels_path = self.get_slide_rels_path(&layout_path);
let layout_rels_data = self.read_file_from_archive(&layout_rels_path).ok();
let master_positions = if let Some(layout_rels_data) = layout_rels_data.as_deref() {
let layout_relationships = parse_relationships(layout_rels_data)?;
if let Some(master_target) = layout_relationships
.iter()
.find(|rel| rel.rel_type == SLIDE_MASTER_NAMESPACE)
.map(|rel| rel.target.as_str())
{
let master_path = Self::resolve_target_path(&layout_path, master_target);
let master_xml = self.read_file_from_archive(&master_path)?;
extract_inherited_positions(&master_xml, &InheritedPositions::default())?
} else {
InheritedPositions::default()
}
} else {
InheritedPositions::default()
};
extract_inherited_positions(&layout_xml, &master_positions)
}
fn resolve_speaker_notes(
&mut self,
slide_path: &str,
slide_rels_data: Option<&[u8]>,
) -> Result<Vec<crate::TextElement>> {
let Some(slide_rels_data) = slide_rels_data else {
return Ok(Vec::new());
};
let relationships = parse_relationships(slide_rels_data)?;
let Some(notes_target) = relationships
.iter()
.find(|rel| rel.rel_type == NOTES_SLIDE_NAMESPACE)
.map(|rel| rel.target.as_str())
else {
return Ok(Vec::new());
};
let notes_path = Self::resolve_target_path(slide_path, notes_target);
let notes_xml = self.read_file_from_archive(¬es_path)?;
let notes_rels = self
.read_file_from_archive(&self.get_slide_rels_path(¬es_path))
.ok();
let hyperlinks = notes_rels
.as_deref()
.map(parse_hyperlink_rels)
.transpose()?
.unwrap_or_default();
crate::parse_xml::parse_speaker_notes_xml_with_hyperlinks(¬es_xml, &hyperlinks)
}
fn resolve_comments(
&mut self,
slide_path: &str,
slide_rels_data: Option<&[u8]>,
) -> Result<Vec<crate::TextElement>> {
let Some(slide_rels_data) = slide_rels_data else {
return Ok(Vec::new());
};
let relationships = parse_relationships(slide_rels_data)?;
let Some(comment_target) = relationships
.iter()
.find(|rel| rel.rel_type == COMMENTS_NAMESPACE || rel.rel_type.ends_with("/comments"))
.map(|rel| rel.target.as_str())
else {
return Ok(Vec::new());
};
let comment_path = Self::resolve_target_path(slide_path, comment_target);
let comment_xml = self.read_file_from_archive(&comment_path)?;
let comment_rels = self
.read_file_from_archive(&self.get_slide_rels_path(&comment_path))
.ok();
let hyperlinks = comment_rels
.as_deref()
.map(parse_hyperlink_rels)
.transpose()?
.unwrap_or_default();
crate::parse_xml::parse_comments_xml_with_hyperlinks(&comment_xml, &hyperlinks)
}
pub fn resolve_target_path(base_path: &str, target: &str) -> String {
let mut parts: Vec<&str> = if target.starts_with('/') {
Vec::new()
} else {
let mut parts: Vec<&str> = base_path.split('/').collect();
let _ = parts.pop();
parts
};
for part in target.split('/') {
match part {
"" | "." => {}
".." => {
let _ = parts.pop();
}
_ => parts.push(part),
}
}
parts.join("/")
}
}
fn read_optional_archive_file(
archive: &mut zip::ZipArchive<std::fs::File>,
path: &str,
) -> Result<Option<Vec<u8>>> {
let mut file = match archive.by_name(path) {
Ok(file) => file,
Err(zip::result::ZipError::FileNotFound) => return Ok(None),
Err(error) => return Err(error.into()),
};
let mut content = Vec::new();
file.read_to_end(&mut content)?;
Ok(Some(content))
}
fn sort_slide_paths(slide_paths: &mut [String]) {
slide_paths.sort_by(|left, right| {
Slide::extract_slide_number(left)
.cmp(&Slide::extract_slide_number(right))
.then_with(|| left.cmp(right))
});
}
pub struct SlideIterator<'a> {
container: &'a mut PptxContainer,
current_paths: Vec<String>, current_index: usize,
}
impl<'a> SlideIterator<'a> {
fn new(container: &'a mut PptxContainer) -> Self {
let current_paths = container.slide_paths.clone();
Self {
container,
current_paths,
current_index: 0,
}
}
}
impl<'a> Iterator for SlideIterator<'a> {
type Item = Result<Slide>;
fn next(&mut self) -> Option<Self::Item> {
if self.current_index >= self.current_paths.len() {
return None;
}
let slide_path = &self.current_paths[self.current_index];
self.current_index += 1;
match self.container.load_slide(slide_path) {
Ok(Some(slide)) => Some(Ok(slide)),
Ok(None) => self.next(), Err(e) => Some(Err(e)),
}
}
}
#[cfg(test)]
#[path = "../tests/unit/container.rs"]
mod tests;