Skip to main content

pptx_to_md/
presentation.rs

1use crate::container::SlideIterator;
2use crate::odp::{OdpContainer, OdpSlideIterator};
3use crate::{ParserConfig, PptxContainer, Presentation, PresentationMetadata, Result, Slide};
4use std::io::Read;
5use std::path::Path;
6
7/// The presentation format detected by [`PresentationContainer`].
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9#[non_exhaustive]
10pub enum PresentationFormat {
11    Pptx,
12    Odp,
13}
14
15enum ContainerInner {
16    Pptx(PptxContainer),
17    Odp(OdpContainer),
18}
19
20/// Opens either a PowerPoint or an OpenDocument presentation.
21///
22/// This is additive to [`PptxContainer`], which continues to provide the
23/// existing PPTX-only API unchanged.
24pub struct PresentationContainer {
25    format: PresentationFormat,
26    inner: ContainerInner,
27}
28
29impl PresentationContainer {
30    /// Opens a `.pptx` or `.odp` presentation by detecting the file format from
31    /// the package contents.
32    ///
33    /// Use this as the default entry point when the input may be either format.
34    pub fn open(path: &Path, config: ParserConfig) -> Result<Self> {
35        let format = detect_format(path)?;
36        Self::open_as(path, config, format)
37    }
38
39    /// Opens a presentation as the explicitly provided format.
40    ///
41    /// Use this when the caller already knows the format and wants to skip
42    /// auto-detection, or when a file extension is not a reliable signal.
43    pub fn open_as(path: &Path, config: ParserConfig, format: PresentationFormat) -> Result<Self> {
44        let inner = match format {
45            PresentationFormat::Pptx => ContainerInner::Pptx(PptxContainer::open(path, config)?),
46            PresentationFormat::Odp => ContainerInner::Odp(OdpContainer::open(path, config)?),
47        };
48
49        Ok(Self { format, inner })
50    }
51
52    pub fn format(&self) -> PresentationFormat {
53        self.format
54    }
55
56    pub fn metadata(&self) -> &PresentationMetadata {
57        match &self.inner {
58            ContainerInner::Pptx(container) => container.metadata(),
59            ContainerInner::Odp(container) => container.metadata(),
60        }
61    }
62
63    pub fn parse_all(&mut self) -> Result<Vec<Slide>> {
64        match &mut self.inner {
65            ContainerInner::Pptx(container) => container.parse_all(),
66            ContainerInner::Odp(container) => container.parse_all(),
67        }
68    }
69
70    /// Parses the complete presentation into the semantic document model.
71    pub fn parse_document(&mut self) -> Result<Presentation> {
72        let metadata = self.metadata().clone();
73        let slides = self.parse_all()?;
74        let diagnostics = slides
75            .iter()
76            .flat_map(|slide| slide.diagnostics.iter().cloned())
77            .collect();
78        Ok(Presentation {
79            metadata,
80            slides,
81            diagnostics,
82        })
83    }
84
85    pub fn parse_all_multi_threaded(&mut self) -> Result<Vec<Slide>> {
86        match &mut self.inner {
87            ContainerInner::Pptx(container) => container.parse_all_multi_threaded(),
88            // ODP stores all pages in one content.xml, so there is no independent
89            // slide XML to preload as there is for PPTX.
90            ContainerInner::Odp(container) => container.parse_all(),
91        }
92    }
93
94    pub fn convert_to_md(&mut self) -> Result<String> {
95        match &mut self.inner {
96            ContainerInner::Pptx(container) => container.convert_to_md(),
97            ContainerInner::Odp(container) => container.convert_to_md(),
98        }
99    }
100
101    pub fn convert_to_md_multi_threaded(&mut self) -> Result<String> {
102        match &mut self.inner {
103            ContainerInner::Pptx(container) => container.convert_to_md_multi_threaded(),
104            ContainerInner::Odp(container) => container.convert_to_md(),
105        }
106    }
107
108    pub fn iter_slides(&mut self) -> PresentationSlideIterator<'_> {
109        let inner = match &mut self.inner {
110            ContainerInner::Pptx(container) => {
111                PresentationIteratorInner::Pptx(container.iter_slides())
112            }
113            ContainerInner::Odp(container) => {
114                PresentationIteratorInner::Odp(container.iter_slides())
115            }
116        };
117        PresentationSlideIterator { inner }
118    }
119}
120
121fn detect_format(path: &Path) -> Result<PresentationFormat> {
122    let file = std::fs::File::open(path)?;
123    let mut archive = zip::ZipArchive::new(file)?;
124
125    if let Ok(mut mimetype) = archive.by_name("mimetype") {
126        let mut value = String::new();
127        mimetype.read_to_string(&mut value)?;
128        if value.trim() == "application/vnd.oasis.opendocument.presentation" {
129            return Ok(PresentationFormat::Odp);
130        }
131    }
132
133    if archive.by_name("[Content_Types].xml").is_ok()
134        && archive.by_name("ppt/presentation.xml").is_ok()
135    {
136        return Ok(PresentationFormat::Pptx);
137    }
138
139    Err(crate::Error::ParseError("Unsupported presentation format"))
140}
141
142/// Iterator returned by [`PresentationContainer::iter_slides`].
143pub struct PresentationSlideIterator<'a> {
144    inner: PresentationIteratorInner<'a>,
145}
146
147enum PresentationIteratorInner<'a> {
148    Pptx(SlideIterator<'a>),
149    Odp(OdpSlideIterator<'a>),
150}
151
152impl Iterator for PresentationSlideIterator<'_> {
153    type Item = Result<Slide>;
154
155    fn next(&mut self) -> Option<Self::Item> {
156        match &mut self.inner {
157            PresentationIteratorInner::Pptx(iterator) => iterator.next(),
158            PresentationIteratorInner::Odp(iterator) => iterator.next(),
159        }
160    }
161}