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