Skip to main content

ppt_rs/
api.rs

1//! Public API module
2//!
3//! High-level API for working with PowerPoint presentations.
4
5use crate::exc::{messages, PptxError, Result};
6use crate::export::html::export_to_html;
7use crate::generator::{create_pptx_with_settings, Image, PresentationSettings, PresentationTheme, SlideContent};
8use crate::import::import_pptx;
9use std::path::Path;
10use std::process::Command;
11
12/// Represents a PowerPoint presentation
13#[derive(Debug, Clone, Default)]
14pub struct Presentation {
15    title: String,
16    slides: Vec<SlideContent>,
17    settings: Option<PresentationSettings>,
18}
19
20impl Presentation {
21    /// Create a new empty presentation
22    pub fn new() -> Self {
23        Presentation {
24            title: String::new(),
25            slides: Vec::new(),
26            settings: None,
27        }
28    }
29
30    /// Create a presentation with a title
31    pub fn with_title(title: &str) -> Self {
32        Presentation {
33            title: title.to_string(),
34            slides: Vec::new(),
35            settings: None,
36        }
37    }
38
39    /// Set the presentation title
40    pub fn title(mut self, title: &str) -> Self {
41        self.title = title.to_string();
42        self
43    }
44
45    /// Add a slide to the presentation
46    pub fn add_slide(mut self, slide: SlideContent) -> Self {
47        self.slides.push(slide);
48        self
49    }
50
51    /// Append slides from another presentation
52    pub fn add_presentation(mut self, other: Presentation) -> Self {
53        self.slides.extend(other.slides);
54        self
55    }
56
57    /// Get the number of slides
58    pub fn slide_count(&self) -> usize {
59        self.slides.len()
60    }
61
62    /// Get the slides in the presentation
63    pub fn slides(&self) -> &[SlideContent] {
64        &self.slides
65    }
66
67    /// Get the presentation title
68    pub fn get_title(&self) -> &str {
69        &self.title
70    }
71
72    /// Apply a custom color/font theme to the generated PPTX
73    pub fn with_theme(mut self, theme: PresentationTheme) -> Self {
74        let mut settings = self.settings.take().unwrap_or_default();
75        settings.theme = Some(theme);
76        self.settings = Some(settings);
77        self
78    }
79
80    /// Set presentation-level settings (theme, slide show, print, etc.)
81    pub fn with_settings(mut self, settings: PresentationSettings) -> Self {
82        self.settings = Some(settings);
83        self
84    }
85
86    /// Build the presentation as PPTX bytes
87    pub fn build(&self) -> Result<Vec<u8>> {
88        if self.slides.is_empty() {
89            return Err(PptxError::InvalidState(
90                messages::must_not_be_empty("presentation slides"),
91            ));
92        }
93        create_pptx_with_settings(&self.title, &self.slides, self.settings.clone())
94            .map_err(|e| PptxError::Generic(e.to_string()))
95    }
96
97    /// Consume the presentation and build PPTX bytes without cloning slide data.
98    pub fn into_bytes(self) -> Result<Vec<u8>> {
99        if self.slides.is_empty() {
100            return Err(PptxError::InvalidState(
101                messages::must_not_be_empty("presentation slides"),
102            ));
103        }
104        create_pptx_with_settings(&self.title, &self.slides, self.settings)
105            .map_err(|e| PptxError::Generic(e.to_string()))
106    }
107
108    /// Save the presentation to a file
109    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
110        let data = self.build()?;
111        std::fs::write(path, data)?;
112        Ok(())
113    }
114
115    /// Create a presentation from a PPTX file
116    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
117        let path_str = path.as_ref().to_string_lossy();
118        import_pptx(&path_str)
119    }
120
121    /// Export the presentation to HTML
122    pub fn save_as_html<P: AsRef<Path>>(&self, path: P) -> Result<()> {
123        let html = export_to_html(self)?;
124        std::fs::write(path, html)?;
125        Ok(())
126    }
127
128    /// Export the presentation to PDF using pure-Rust vector rendering.
129    ///
130    /// Each slide is rendered as a native PDF page with title, bullets,
131    /// and content drawn as vector text and graphics. No external binaries
132    /// (LibreOffice, Poppler, etc.) are required.
133    pub fn save_as_pdf<P: AsRef<Path>>(&self, output_path: P) -> Result<()> {
134        crate::export::slide_render::render_to_pdf(self, output_path)?;
135        Ok(())
136    }
137
138    /// Export the presentation to PDF with custom options using the pure-Rust `pdfrs` engine.
139    ///
140    /// Like [`Presentation::save_as_pdf`] but allows customizing orientation,
141    /// font, font size, and which Markdown sections are included.
142    ///
143    /// # Arguments
144    /// * `output_path` - Path to the PDF file to write.
145    /// * `options` - [`crate::export::pdf_export::PdfExportOptions`] controlling
146    ///   orientation, font, font size, and which Markdown sections are included.
147    #[cfg(feature = "pdf-native")]
148    pub fn save_as_pdf_via_pdfrs<P: AsRef<Path>>(
149        &self,
150        output_path: P,
151        options: &crate::export::pdf_export::PdfExportOptions,
152    ) -> Result<()> {
153        crate::export::pdf_export::export_to_pdf(self, output_path, options)?;
154        Ok(())
155    }
156
157    /// Render the presentation to PDF bytes using the pure-Rust `pdfrs` engine.
158    ///
159    /// Returns the raw PDF byte buffer instead of writing to disk. Useful
160    /// for embedding in HTTP responses, mailing systems, or piping to other
161    /// tools.
162    ///
163    /// Requires the `pdf-native` Cargo feature.
164    #[cfg(feature = "pdf-native")]
165    pub fn to_pdf_bytes(
166        &self,
167        options: &crate::export::pdf_export::PdfExportOptions,
168    ) -> Result<Vec<u8>> {
169        crate::export::pdf_export::export_to_pdf_bytes(self, options)
170    }
171
172    /// Export slides to PNG images
173    ///
174    /// Requires `pdftoppm` (from poppler) to be installed.
175    /// Images will be named `slide-1.png`, `slide-2.png`, etc. in the output directory.
176    pub fn save_as_png<P: AsRef<Path>>(&self, output_dir: P) -> Result<()> {
177        let output_dir = output_dir.as_ref();
178        if !output_dir.exists() {
179            std::fs::create_dir_all(output_dir)?;
180        }
181
182        // Create temp PDF using pure-Rust pdfrs engine
183        let temp_dir = std::env::temp_dir();
184        let temp_pdf_name = format!("ppt_rs_temp_{}.pdf", uuid::Uuid::new_v4());
185        let temp_pdf_path = temp_dir.join(&temp_pdf_name);
186
187        // Convert to PDF first (no LibreOffice needed)
188        let bytes = crate::export::slide_render::render_to_pdf_bytes(self)?;
189        std::fs::write(&temp_pdf_path, &bytes)?;
190
191        // Convert PDF to PNGs using pdftoppm
192        // pdftoppm -png <pdf_file> <image_prefix>
193        let prefix = output_dir.join("slide");
194
195        let status = Command::new("pdftoppm")
196            .arg("-png")
197            .arg(&temp_pdf_path)
198            .arg(&prefix)
199            .status()
200            .map_err(|e| PptxError::Generic(format!("Failed to execute pdftoppm: {}", e)))?;
201
202        // Cleanup temp PDF
203        let _ = std::fs::remove_file(&temp_pdf_path);
204
205        if !status.success() {
206            return Err(PptxError::Generic("pdftoppm conversion failed".to_string()));
207        }
208
209        Ok(())
210    }
211
212    /// Create a presentation from a PDF file (each page becomes a slide)
213    ///
214    /// Requires `pdftoppm` (from poppler) to be installed.
215    pub fn from_pdf<P: AsRef<Path>>(path: P) -> Result<Self> {
216        let path = path.as_ref();
217        if !path.exists() {
218            return Err(PptxError::NotFound(format!(
219                "PDF file not found: {}",
220                path.display()
221            )));
222        }
223
224        // Create temp dir for images
225        let temp_dir = std::env::temp_dir().join(format!("ppt_rs_import_{}", uuid::Uuid::new_v4()));
226        std::fs::create_dir_all(&temp_dir)?;
227
228        // Convert PDF to PNGs
229        let prefix = temp_dir.join("page");
230
231        let status = Command::new("pdftoppm")
232            .arg("-png")
233            .arg(path)
234            .arg(&prefix)
235            .status()
236            .map_err(|e| PptxError::Generic(format!("Failed to execute pdftoppm: {}", e)))?;
237
238        if !status.success() {
239            let _ = std::fs::remove_dir_all(&temp_dir);
240            return Err(PptxError::Generic("pdftoppm failed".to_string()));
241        }
242
243        // Read images and create slides
244        let mut pres = Presentation::new();
245        // Set title from filename
246        if let Some(stem) = path.file_stem() {
247            pres = pres.title(&stem.to_string_lossy());
248        }
249
250        // Read dir
251        let mut entries: Vec<_> = std::fs::read_dir(&temp_dir)?
252            .filter_map(|e| e.ok())
253            .collect();
254
255        // Sort by filename to ensure page order
256        // pdftoppm names files like page-1.png, page-2.png... page-10.png
257        // Default string sort might put page-10 before page-2
258        // We need to sort by length then by name, or rely on pdftoppm zero padding (it usually does -01 if needed, but safer to trust number)
259        // pdftoppm default is -1, -2... -10.
260        // So page-1.png, page-10.png, page-2.png.
261        // We need natural sort.
262        entries.sort_by_key(|e| {
263            let name = e.file_name().to_string_lossy().to_string();
264            // Extract number from end
265            // "page-1.png" -> 1
266            if let Some(start) = name.rfind('-')
267                && let Some(end) = name.rfind('.')
268                    && start < end
269                        && let Ok(num) = name[start + 1..end].parse::<u32>() {
270                            return num;
271                        }
272            0 // Fallback
273        });
274
275        for entry in entries {
276            let path = entry.path();
277            if path.extension().is_some_and(|e| e == "png") {
278                // Create slide with full screen image
279                let image = Image::from_path(&path).map_err(PptxError::Generic)?;
280
281                // Add image to slide
282                // Use a default layout?
283                // Just create a slide with this image
284                // We'll center it.
285                // Assuming standard 16:9 slide (10x5.625 inches) -> 9144000 x 5143500 EMU
286                // But we don't know image dimensions here easily without reading it.
287                // Image builder defaults to auto size?
288                // Let's just add it.
289
290                let mut slide = SlideContent::new("");
291                slide.images.push(image);
292                pres = pres.add_slide(slide);
293            }
294        }
295
296        let _ = std::fs::remove_dir_all(&temp_dir);
297        Ok(pres)
298    }
299
300    /// Export the presentation to Markdown format
301    ///
302    /// # Arguments
303    /// * `path` - Output file path
304    ///
305    /// # Example
306    /// ```
307    /// # use ppt_rs::api::Presentation;
308    /// # use ppt_rs::generator::SlideContent;
309    /// # let pres = Presentation::with_title("My Presentation")
310    /// #     .add_slide(SlideContent::new("Slide 1").add_bullet("Point 1"));
311    /// # // pres.save_as_markdown("output.md").unwrap();
312    /// ```
313    pub fn save_as_markdown<P: AsRef<Path>>(&self, path: P) -> Result<()> {
314        use crate::export::md::export_to_markdown;
315        let md = export_to_markdown(self)?;
316        std::fs::write(path, md)?;
317        Ok(())
318    }
319
320    /// Export the presentation to Markdown with custom options
321    pub fn save_as_markdown_with_options<P: AsRef<Path>>(
322        &self,
323        path: P,
324        options: &crate::export::md::MarkdownOptions,
325    ) -> Result<()> {
326        use crate::export::md::export_to_markdown_with_options;
327        let md = export_to_markdown_with_options(self, options)?;
328        std::fs::write(path, md)?;
329        Ok(())
330    }
331
332    /// Export slides to image files (PNG/JPEG)
333    ///
334    /// Uses LibreOffice for raster image rendering. Requires LibreOffice to be installed.
335    ///
336    /// # Arguments
337    /// * `output_dir` - Directory to save images
338    /// * `options` - Image export options (format, DPI, quality)
339    ///
340    /// # Returns
341    /// Vector of paths to generated image files
342    pub fn save_as_images<P: AsRef<Path>>(
343        &self,
344        output_dir: P,
345        options: &crate::export::image_export::ImageExportOptions,
346    ) -> Result<Vec<std::path::PathBuf>> {
347        use crate::export::image_export::export_to_images;
348        export_to_images(self, output_dir, options)
349    }
350
351    /// Export a specific slide to an image file
352    ///
353    /// # Arguments
354    /// * `slide_number` - 1-based slide number
355    /// * `output_path` - Output file path
356    /// * `options` - Image export options
357    pub fn save_slide_as_image<P: AsRef<Path>>(
358        &self,
359        slide_number: usize,
360        output_path: P,
361        options: &crate::export::image_export::ImageExportOptions,
362    ) -> Result<std::path::PathBuf> {
363        use crate::export::image_export::export_slide_to_image;
364        export_slide_to_image(self, slide_number, output_path, options)
365    }
366
367    /// Render a thumbnail of the first slide
368    ///
369    /// # Arguments
370    /// * `output_path` - Output file path
371    /// * `width` - Desired width in pixels
372    pub fn save_thumbnail<P: AsRef<Path>>(&self, output_path: P, width: u32) -> Result<std::path::PathBuf> {
373        use crate::export::image_export::render_thumbnail;
374        render_thumbnail(self, output_path, width)
375    }
376
377    /// Compress and optimize the presentation
378    ///
379    /// Saves a compressed version with reduced file size.
380    ///
381    /// # Arguments
382    /// * `output_path` - Path for compressed PPTX file
383    /// * `options` - Compression options (level, features to remove)
384    ///
385    /// # Returns
386    /// Compression result with statistics
387    ///
388    /// # Example
389    /// ```
390    /// # use ppt_rs::api::Presentation;
391    /// # use ppt_rs::opc::compress::CompressionOptions;
392    /// # let pres = Presentation::with_title("Large Presentation");
393    /// # let options = CompressionOptions::web();
394    /// # // let result = pres.compress("optimized.pptx", &options).unwrap();
395    /// # // println!("Reduced by {:.1}%", result.reduction_percent);
396    /// ```
397    pub fn compress<P: AsRef<Path>>(
398        &self,
399        output_path: P,
400        options: &crate::opc::compress::CompressionOptions,
401    ) -> Result<crate::opc::compress::CompressionResult> {
402        // First save to temp file
403        let temp_dir = std::env::temp_dir();
404        let temp_path = temp_dir.join(format!("compress_{}.pptx", uuid::Uuid::new_v4()));
405        self.save(&temp_path)?;
406
407        // Compress
408        let result = crate::opc::compress::compress_pptx(&temp_path, output_path, options);
409
410        // Cleanup
411        let _ = std::fs::remove_file(&temp_path);
412
413        result
414    }
415
416    /// Get file size analysis
417    ///
418    /// Returns analysis of what contributes to file size.
419    pub fn analyze_size(&self) -> Result<crate::opc::compress::PptxAnalysis> {
420        // Save to temp file for analysis
421        let temp_dir = std::env::temp_dir();
422        let temp_path = temp_dir.join(format!("analyze_{}.pptx", uuid::Uuid::new_v4()));
423        self.save(&temp_path)?;
424
425        let analysis = crate::opc::compress::analyze_pptx(&temp_path);
426
427        // Cleanup
428        let _ = std::fs::remove_file(&temp_path);
429
430        analysis
431    }
432}
433
434#[cfg(test)]
435mod tests {
436    use super::*;
437
438    #[test]
439    fn test_presentation_builder() {
440        let pres = Presentation::with_title("Test")
441            .add_slide(SlideContent::new("Slide 1").add_bullet("Point 1"));
442
443        assert_eq!(pres.get_title(), "Test");
444        assert_eq!(pres.slide_count(), 1);
445    }
446
447    #[test]
448    fn test_presentation_build() {
449        let pres = Presentation::with_title("Test").add_slide(SlideContent::new("Slide 1"));
450
451        let result = pres.build();
452        assert!(result.is_ok());
453    }
454}