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_content, Image, 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}
18
19impl Presentation {
20    /// Create a new empty presentation
21    pub fn new() -> Self {
22        Presentation {
23            title: String::new(),
24            slides: Vec::new(),
25        }
26    }
27
28    /// Create a presentation with a title
29    pub fn with_title(title: &str) -> Self {
30        Presentation {
31            title: title.to_string(),
32            slides: Vec::new(),
33        }
34    }
35
36    /// Set the presentation title
37    pub fn title(mut self, title: &str) -> Self {
38        self.title = title.to_string();
39        self
40    }
41
42    /// Add a slide to the presentation
43    pub fn add_slide(mut self, slide: SlideContent) -> Self {
44        self.slides.push(slide);
45        self
46    }
47
48    /// Append slides from another presentation
49    pub fn add_presentation(mut self, other: Presentation) -> Self {
50        self.slides.extend(other.slides);
51        self
52    }
53
54    /// Get the number of slides
55    pub fn slide_count(&self) -> usize {
56        self.slides.len()
57    }
58
59    /// Get the slides in the presentation
60    pub fn slides(&self) -> &[SlideContent] {
61        &self.slides
62    }
63
64    /// Get the presentation title
65    pub fn get_title(&self) -> &str {
66        &self.title
67    }
68
69    /// Build the presentation as PPTX bytes
70    pub fn build(&self) -> Result<Vec<u8>> {
71        if self.slides.is_empty() {
72            return Err(PptxError::InvalidState("Presentation has no slides".into()));
73        }
74        create_pptx_with_content(&self.title, self.slides.clone())
75            .map_err(|e| PptxError::Generic(e.to_string()))
76    }
77
78    /// Save the presentation to a file
79    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
80        let data = self.build()?;
81        std::fs::write(path, data)?;
82        Ok(())
83    }
84
85    /// Create a presentation from a PPTX file
86    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
87        let path_str = path.as_ref().to_string_lossy();
88        import_pptx(&path_str)
89    }
90
91    /// Export the presentation to HTML
92    pub fn save_as_html<P: AsRef<Path>>(&self, path: P) -> Result<()> {
93        let html = export_to_html(self)?;
94        std::fs::write(path, html)?;
95        Ok(())
96    }
97
98    /// Export the presentation to PDF using LibreOffice
99    ///
100    /// Requires LibreOffice to be installed and available via `soffice` command.
101    /// On macOS, it also checks `/Applications/LibreOffice.app/Contents/MacOS/soffice`.
102    pub fn save_as_pdf<P: AsRef<Path>>(&self, output_path: P) -> Result<()> {
103        // Create a temp file
104        let temp_dir = std::env::temp_dir();
105        let temp_filename = format!("ppt_rs_{}.pptx", uuid::Uuid::new_v4());
106        let temp_path = temp_dir.join(&temp_filename);
107
108        // Save current presentation to temp file
109        self.save(&temp_path)?;
110
111        // Try to find soffice
112        let soffice_cmd = if cfg!(target_os = "macos") {
113            if Path::new("/Applications/LibreOffice.app/Contents/MacOS/soffice").exists() {
114                "/Applications/LibreOffice.app/Contents/MacOS/soffice"
115            } else {
116                "soffice"
117            }
118        } else {
119            "soffice"
120        };
121
122        // Get output directory
123        let output_parent = output_path.as_ref().parent().unwrap_or(Path::new("."));
124
125        // Run conversion
126        // soffice --headless --convert-to pdf <temp_path> --outdir <output_dir>
127        let result = Command::new(soffice_cmd)
128            .arg("--headless")
129            .arg("--convert-to")
130            .arg("pdf")
131            .arg(&temp_path)
132            .arg("--outdir")
133            .arg(output_parent)
134            .output();
135
136        // Clean up temp file (ignore error)
137        let _ = std::fs::remove_file(&temp_path);
138
139        match result {
140            Ok(output) => {
141                if !output.status.success() {
142                    let stderr = String::from_utf8_lossy(&output.stderr);
143                    return Err(PptxError::Generic(format!(
144                        "LibreOffice conversion failed: {}",
145                        stderr
146                    )));
147                }
148            }
149            Err(e) => {
150                return Err(PptxError::Generic(format!(
151                    "Failed to execute libreoffice: {}",
152                    e
153                )));
154            }
155        }
156
157        // LibreOffice creates file with same basename but .pdf extension in outdir
158        // The generated file will be temp_filename.pdf (since input was temp_filename.pptx)
159        let generated_pdf_name = temp_filename.replace(".pptx", ".pdf");
160        let generated_pdf_path = output_parent.join(&generated_pdf_name);
161
162        if generated_pdf_path.exists() {
163            std::fs::rename(&generated_pdf_path, output_path.as_ref())?;
164            Ok(())
165        } else {
166            Err(PptxError::Generic("PDF output file not found".to_string()))
167        }
168    }
169
170    /// Export slides to PNG images
171    ///
172    /// Requires LibreOffice (for PDF conversion) and `pdftoppm` (from poppler).
173    /// Images will be named `slide-1.png`, `slide-2.png`, etc. in the output directory.
174    pub fn save_as_png<P: AsRef<Path>>(&self, output_dir: P) -> Result<()> {
175        let output_dir = output_dir.as_ref();
176        if !output_dir.exists() {
177            std::fs::create_dir_all(output_dir)?;
178        }
179
180        // Create temp PDF
181        let temp_dir = std::env::temp_dir();
182        let temp_pdf_name = format!("ppt_rs_temp_{}.pdf", uuid::Uuid::new_v4());
183        let temp_pdf_path = temp_dir.join(&temp_pdf_name);
184
185        // Convert to PDF first
186        self.save_as_pdf(&temp_pdf_path)?;
187
188        // Convert PDF to PNGs using pdftoppm
189        // pdftoppm -png <pdf_file> <image_prefix>
190        let prefix = output_dir.join("slide");
191
192        let status = Command::new("pdftoppm")
193            .arg("-png")
194            .arg(&temp_pdf_path)
195            .arg(&prefix)
196            .status()
197            .map_err(|e| PptxError::Generic(format!("Failed to execute pdftoppm: {}", e)))?;
198
199        // Cleanup temp PDF
200        let _ = std::fs::remove_file(&temp_pdf_path);
201
202        if !status.success() {
203            return Err(PptxError::Generic("pdftoppm conversion failed".to_string()));
204        }
205
206        Ok(())
207    }
208
209    /// Create a presentation from a PDF file (each page becomes a slide)
210    ///
211    /// Requires `pdftoppm` (from poppler) to be installed.
212    pub fn from_pdf<P: AsRef<Path>>(path: P) -> Result<Self> {
213        let path = path.as_ref();
214        if !path.exists() {
215            return Err(PptxError::NotFound(format!(
216                "PDF file not found: {}",
217                path.display()
218            )));
219        }
220
221        // Create temp dir for images
222        let temp_dir = std::env::temp_dir().join(format!("ppt_rs_import_{}", uuid::Uuid::new_v4()));
223        std::fs::create_dir_all(&temp_dir)?;
224
225        // Convert PDF to PNGs
226        let prefix = temp_dir.join("page");
227
228        let status = Command::new("pdftoppm")
229            .arg("-png")
230            .arg(path)
231            .arg(&prefix)
232            .status()
233            .map_err(|e| PptxError::Generic(format!("Failed to execute pdftoppm: {}", e)))?;
234
235        if !status.success() {
236            let _ = std::fs::remove_dir_all(&temp_dir);
237            return Err(PptxError::Generic("pdftoppm failed".to_string()));
238        }
239
240        // Read images and create slides
241        let mut pres = Presentation::new();
242        // Set title from filename
243        if let Some(stem) = path.file_stem() {
244            pres = pres.title(&stem.to_string_lossy());
245        }
246
247        // Read dir
248        let mut entries: Vec<_> = std::fs::read_dir(&temp_dir)?
249            .filter_map(|e| e.ok())
250            .collect();
251
252        // Sort by filename to ensure page order
253        // pdftoppm names files like page-1.png, page-2.png... page-10.png
254        // Default string sort might put page-10 before page-2
255        // 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)
256        // pdftoppm default is -1, -2... -10.
257        // So page-1.png, page-10.png, page-2.png.
258        // We need natural sort.
259        entries.sort_by_key(|e| {
260            let name = e.file_name().to_string_lossy().to_string();
261            // Extract number from end
262            // "page-1.png" -> 1
263            if let Some(start) = name.rfind('-') {
264                if let Some(end) = name.rfind('.') {
265                    if start < end {
266                        if let Ok(num) = name[start + 1..end].parse::<u32>() {
267                            return num;
268                        }
269                    }
270                }
271            }
272            0 // Fallback
273        });
274
275        for entry in entries {
276            let path = entry.path();
277            if path.extension().map_or(false, |e| e == "png") {
278                // Create slide with full screen image
279                let image = Image::from_path(&path).map_err(|e| PptxError::Generic(e))?;
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 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}