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::{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("Presentation has no slides".into()));
90        }
91        create_pptx_with_settings(&self.title, &self.slides, self.settings.clone())
92            .map_err(|e| PptxError::Generic(e.to_string()))
93    }
94
95    /// Consume the presentation and build PPTX bytes without cloning slide data.
96    pub fn into_bytes(self) -> Result<Vec<u8>> {
97        if self.slides.is_empty() {
98            return Err(PptxError::InvalidState("Presentation has no slides".into()));
99        }
100        create_pptx_with_settings(&self.title, &self.slides, self.settings)
101            .map_err(|e| PptxError::Generic(e.to_string()))
102    }
103
104    /// Save the presentation to a file
105    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
106        let data = self.build()?;
107        std::fs::write(path, data)?;
108        Ok(())
109    }
110
111    /// Create a presentation from a PPTX file
112    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
113        let path_str = path.as_ref().to_string_lossy();
114        import_pptx(&path_str)
115    }
116
117    /// Export the presentation to HTML
118    pub fn save_as_html<P: AsRef<Path>>(&self, path: P) -> Result<()> {
119        let html = export_to_html(self)?;
120        std::fs::write(path, html)?;
121        Ok(())
122    }
123
124    /// Export the presentation to PDF using LibreOffice
125    ///
126    /// Requires LibreOffice to be installed and available via `soffice` command.
127    /// On macOS, it also checks `/Applications/LibreOffice.app/Contents/MacOS/soffice`.
128    pub fn save_as_pdf<P: AsRef<Path>>(&self, output_path: P) -> Result<()> {
129        // Create a temp file
130        let temp_dir = std::env::temp_dir();
131        let temp_filename = format!("ppt_rs_{}.pptx", uuid::Uuid::new_v4());
132        let temp_path = temp_dir.join(&temp_filename);
133
134        // Save current presentation to temp file
135        self.save(&temp_path)?;
136
137        // Try to find soffice
138        let soffice_cmd = if cfg!(target_os = "macos") {
139            if Path::new("/Applications/LibreOffice.app/Contents/MacOS/soffice").exists() {
140                "/Applications/LibreOffice.app/Contents/MacOS/soffice"
141            } else {
142                "soffice"
143            }
144        } else {
145            "soffice"
146        };
147
148        // Get output directory
149        let output_parent = output_path.as_ref().parent().unwrap_or(Path::new("."));
150
151        // Run conversion
152        // soffice --headless --convert-to pdf <temp_path> --outdir <output_dir>
153        let result = Command::new(soffice_cmd)
154            .arg("--headless")
155            .arg("--convert-to")
156            .arg("pdf")
157            .arg(&temp_path)
158            .arg("--outdir")
159            .arg(output_parent)
160            .output();
161
162        // Clean up temp file (ignore error)
163        let _ = std::fs::remove_file(&temp_path);
164
165        match result {
166            Ok(output) => {
167                if !output.status.success() {
168                    let stderr = String::from_utf8_lossy(&output.stderr);
169                    return Err(PptxError::Generic(format!(
170                        "LibreOffice conversion failed: {}",
171                        stderr
172                    )));
173                }
174            }
175            Err(e) => {
176                return Err(PptxError::Generic(format!(
177                    "Failed to execute libreoffice: {}",
178                    e
179                )));
180            }
181        }
182
183        // LibreOffice creates file with same basename but .pdf extension in outdir
184        // The generated file will be temp_filename.pdf (since input was temp_filename.pptx)
185        let generated_pdf_name = temp_filename.replace(".pptx", ".pdf");
186        let generated_pdf_path = output_parent.join(&generated_pdf_name);
187
188        if generated_pdf_path.exists() {
189            std::fs::rename(&generated_pdf_path, output_path.as_ref())?;
190            Ok(())
191        } else {
192            Err(PptxError::Generic("PDF output file not found".to_string()))
193        }
194    }
195
196    /// Export slides to PNG images
197    ///
198    /// Requires LibreOffice (for PDF conversion) and `pdftoppm` (from poppler).
199    /// Images will be named `slide-1.png`, `slide-2.png`, etc. in the output directory.
200    pub fn save_as_png<P: AsRef<Path>>(&self, output_dir: P) -> Result<()> {
201        let output_dir = output_dir.as_ref();
202        if !output_dir.exists() {
203            std::fs::create_dir_all(output_dir)?;
204        }
205
206        // Create temp PDF
207        let temp_dir = std::env::temp_dir();
208        let temp_pdf_name = format!("ppt_rs_temp_{}.pdf", uuid::Uuid::new_v4());
209        let temp_pdf_path = temp_dir.join(&temp_pdf_name);
210
211        // Convert to PDF first
212        self.save_as_pdf(&temp_pdf_path)?;
213
214        // Convert PDF to PNGs using pdftoppm
215        // pdftoppm -png <pdf_file> <image_prefix>
216        let prefix = output_dir.join("slide");
217
218        let status = Command::new("pdftoppm")
219            .arg("-png")
220            .arg(&temp_pdf_path)
221            .arg(&prefix)
222            .status()
223            .map_err(|e| PptxError::Generic(format!("Failed to execute pdftoppm: {}", e)))?;
224
225        // Cleanup temp PDF
226        let _ = std::fs::remove_file(&temp_pdf_path);
227
228        if !status.success() {
229            return Err(PptxError::Generic("pdftoppm conversion failed".to_string()));
230        }
231
232        Ok(())
233    }
234
235    /// Create a presentation from a PDF file (each page becomes a slide)
236    ///
237    /// Requires `pdftoppm` (from poppler) to be installed.
238    pub fn from_pdf<P: AsRef<Path>>(path: P) -> Result<Self> {
239        let path = path.as_ref();
240        if !path.exists() {
241            return Err(PptxError::NotFound(format!(
242                "PDF file not found: {}",
243                path.display()
244            )));
245        }
246
247        // Create temp dir for images
248        let temp_dir = std::env::temp_dir().join(format!("ppt_rs_import_{}", uuid::Uuid::new_v4()));
249        std::fs::create_dir_all(&temp_dir)?;
250
251        // Convert PDF to PNGs
252        let prefix = temp_dir.join("page");
253
254        let status = Command::new("pdftoppm")
255            .arg("-png")
256            .arg(path)
257            .arg(&prefix)
258            .status()
259            .map_err(|e| PptxError::Generic(format!("Failed to execute pdftoppm: {}", e)))?;
260
261        if !status.success() {
262            let _ = std::fs::remove_dir_all(&temp_dir);
263            return Err(PptxError::Generic("pdftoppm failed".to_string()));
264        }
265
266        // Read images and create slides
267        let mut pres = Presentation::new();
268        // Set title from filename
269        if let Some(stem) = path.file_stem() {
270            pres = pres.title(&stem.to_string_lossy());
271        }
272
273        // Read dir
274        let mut entries: Vec<_> = std::fs::read_dir(&temp_dir)?
275            .filter_map(|e| e.ok())
276            .collect();
277
278        // Sort by filename to ensure page order
279        // pdftoppm names files like page-1.png, page-2.png... page-10.png
280        // Default string sort might put page-10 before page-2
281        // 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)
282        // pdftoppm default is -1, -2... -10.
283        // So page-1.png, page-10.png, page-2.png.
284        // We need natural sort.
285        entries.sort_by_key(|e| {
286            let name = e.file_name().to_string_lossy().to_string();
287            // Extract number from end
288            // "page-1.png" -> 1
289            if let Some(start) = name.rfind('-') {
290                if let Some(end) = name.rfind('.') {
291                    if start < end {
292                        if let Ok(num) = name[start + 1..end].parse::<u32>() {
293                            return num;
294                        }
295                    }
296                }
297            }
298            0 // Fallback
299        });
300
301        for entry in entries {
302            let path = entry.path();
303            if path.extension().map_or(false, |e| e == "png") {
304                // Create slide with full screen image
305                let image = Image::from_path(&path).map_err(|e| PptxError::Generic(e))?;
306
307                // Add image to slide
308                // Use a default layout?
309                // Just create a slide with this image
310                // We'll center it.
311                // Assuming standard 16:9 slide (10x5.625 inches) -> 9144000 x 5143500 EMU
312                // But we don't know image dimensions here easily without reading it.
313                // Image builder defaults to auto size?
314                // Let's just add it.
315
316                let mut slide = SlideContent::new("");
317                slide.images.push(image);
318                pres = pres.add_slide(slide);
319            }
320        }
321
322        let _ = std::fs::remove_dir_all(&temp_dir);
323        Ok(pres)
324    }
325
326    /// Export the presentation to Markdown format
327    ///
328    /// # Arguments
329    /// * `path` - Output file path
330    ///
331    /// # Example
332    /// ```
333    /// # use ppt_rs::api::Presentation;
334    /// # use ppt_rs::generator::SlideContent;
335    /// # let pres = Presentation::with_title("My Presentation")
336    /// #     .add_slide(SlideContent::new("Slide 1").add_bullet("Point 1"));
337    /// # // pres.save_as_markdown("output.md").unwrap();
338    /// ```
339    pub fn save_as_markdown<P: AsRef<Path>>(&self, path: P) -> Result<()> {
340        use crate::export::md::export_to_markdown;
341        let md = export_to_markdown(self)?;
342        std::fs::write(path, md)?;
343        Ok(())
344    }
345
346    /// Export the presentation to Markdown with custom options
347    pub fn save_as_markdown_with_options<P: AsRef<Path>>(
348        &self,
349        path: P,
350        options: &crate::export::md::MarkdownOptions,
351    ) -> Result<()> {
352        use crate::export::md::export_to_markdown_with_options;
353        let md = export_to_markdown_with_options(self, options)?;
354        std::fs::write(path, md)?;
355        Ok(())
356    }
357
358    /// Export slides to image files (PNG/JPEG)
359    ///
360    /// Uses LibreOffice for rendering. Requires LibreOffice to be installed.
361    ///
362    /// # Arguments
363    /// * `output_dir` - Directory to save images
364    /// * `options` - Image export options (format, DPI, quality)
365    ///
366    /// # Returns
367    /// Vector of paths to generated image files
368    pub fn save_as_images<P: AsRef<Path>>(
369        &self,
370        output_dir: P,
371        options: &crate::export::image_export::ImageExportOptions,
372    ) -> Result<Vec<std::path::PathBuf>> {
373        use crate::export::image_export::export_to_images;
374        export_to_images(self, output_dir, options)
375    }
376
377    /// Export a specific slide to an image file
378    ///
379    /// # Arguments
380    /// * `slide_number` - 1-based slide number
381    /// * `output_path` - Output file path
382    /// * `options` - Image export options
383    pub fn save_slide_as_image<P: AsRef<Path>>(
384        &self,
385        slide_number: usize,
386        output_path: P,
387        options: &crate::export::image_export::ImageExportOptions,
388    ) -> Result<std::path::PathBuf> {
389        use crate::export::image_export::export_slide_to_image;
390        export_slide_to_image(self, slide_number, output_path, options)
391    }
392
393    /// Render a thumbnail of the first slide
394    ///
395    /// # Arguments
396    /// * `output_path` - Output file path
397    /// * `width` - Desired width in pixels
398    pub fn save_thumbnail<P: AsRef<Path>>(&self, output_path: P, width: u32) -> Result<std::path::PathBuf> {
399        use crate::export::image_export::render_thumbnail;
400        render_thumbnail(self, output_path, width)
401    }
402
403    /// Compress and optimize the presentation
404    ///
405    /// Saves a compressed version with reduced file size.
406    ///
407    /// # Arguments
408    /// * `output_path` - Path for compressed PPTX file
409    /// * `options` - Compression options (level, features to remove)
410    ///
411    /// # Returns
412    /// Compression result with statistics
413    ///
414    /// # Example
415    /// ```
416    /// # use ppt_rs::api::Presentation;
417    /// # use ppt_rs::opc::compress::CompressionOptions;
418    /// # let pres = Presentation::with_title("Large Presentation");
419    /// # let options = CompressionOptions::web();
420    /// # // let result = pres.compress("optimized.pptx", &options).unwrap();
421    /// # // println!("Reduced by {:.1}%", result.reduction_percent);
422    /// ```
423    pub fn compress<P: AsRef<Path>>(
424        &self,
425        output_path: P,
426        options: &crate::opc::compress::CompressionOptions,
427    ) -> Result<crate::opc::compress::CompressionResult> {
428        // First save to temp file
429        let temp_dir = std::env::temp_dir();
430        let temp_path = temp_dir.join(format!("compress_{}.pptx", uuid::Uuid::new_v4()));
431        self.save(&temp_path)?;
432
433        // Compress
434        let result = crate::opc::compress::compress_pptx(&temp_path, output_path, options);
435
436        // Cleanup
437        let _ = std::fs::remove_file(&temp_path);
438
439        result
440    }
441
442    /// Get file size analysis
443    ///
444    /// Returns analysis of what contributes to file size.
445    pub fn analyze_size(&self) -> Result<crate::opc::compress::PptxAnalysis> {
446        // Save to temp file for analysis
447        let temp_dir = std::env::temp_dir();
448        let temp_path = temp_dir.join(format!("analyze_{}.pptx", uuid::Uuid::new_v4()));
449        self.save(&temp_path)?;
450
451        let analysis = crate::opc::compress::analyze_pptx(&temp_path);
452
453        // Cleanup
454        let _ = std::fs::remove_file(&temp_path);
455
456        analysis
457    }
458}
459
460#[cfg(test)]
461mod tests {
462    use super::*;
463
464    #[test]
465    fn test_presentation_builder() {
466        let pres = Presentation::with_title("Test")
467            .add_slide(SlideContent::new("Slide 1").add_bullet("Point 1"));
468
469        assert_eq!(pres.get_title(), "Test");
470        assert_eq!(pres.slide_count(), 1);
471    }
472
473    #[test]
474    fn test_presentation_build() {
475        let pres = Presentation::with_title("Test").add_slide(SlideContent::new("Slide 1"));
476
477        let result = pres.build();
478        assert!(result.is_ok());
479    }
480}