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::{Result, PptxError};
6use crate::opc::Package;
7use crate::generator::{SlideContent, create_pptx_with_content, Image};
8use crate::import::import_pptx;
9use crate::export::html::export_to_html;
10use std::io::{Read, Seek};
11use std::path::Path;
12use std::process::Command;
13
14/// Represents a PowerPoint presentation
15#[derive(Debug, Clone, Default)]
16pub struct Presentation {
17    title: String,
18    slides: Vec<SlideContent>,
19}
20
21impl Presentation {
22    /// Create a new empty presentation
23    pub fn new() -> Self {
24        Presentation {
25            title: String::new(),
26            slides: Vec::new(),
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        }
36    }
37
38    /// Set the presentation title
39    pub fn title(mut self, title: &str) -> Self {
40        self.title = title.to_string();
41        self
42    }
43
44    /// Add a slide to the presentation
45    pub fn add_slide(mut self, slide: SlideContent) -> Self {
46        self.slides.push(slide);
47        self
48    }
49
50    /// Append slides from another presentation
51    pub fn add_presentation(mut self, other: Presentation) -> Self {
52        self.slides.extend(other.slides);
53        self
54    }
55
56    /// Get the number of slides
57    pub fn slide_count(&self) -> usize {
58        self.slides.len()
59    }
60
61    /// Get the slides in the presentation
62    pub fn slides(&self) -> &[SlideContent] {
63        &self.slides
64    }
65
66    /// Get the presentation title
67    pub fn get_title(&self) -> &str {
68        &self.title
69    }
70
71    /// Build the presentation as PPTX bytes
72    pub fn build(&self) -> Result<Vec<u8>> {
73        if self.slides.is_empty() {
74            return Err(PptxError::InvalidState("Presentation has no slides".into()));
75        }
76        create_pptx_with_content(&self.title, self.slides.clone())
77            .map_err(|e| PptxError::Generic(e.to_string()))
78    }
79
80    /// Save the presentation to a file
81    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
82        let data = self.build()?;
83        std::fs::write(path, data)?;
84        Ok(())
85    }
86
87    /// Create a presentation from a PPTX file
88    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
89        let path_str = path.as_ref().to_string_lossy();
90        import_pptx(&path_str)
91    }
92
93    /// Export the presentation to HTML
94    pub fn save_as_html<P: AsRef<Path>>(&self, path: P) -> Result<()> {
95        let html = export_to_html(self)?;
96        std::fs::write(path, html)?;
97        Ok(())
98    }
99
100    /// Export the presentation to PDF using LibreOffice
101    /// 
102    /// Requires LibreOffice to be installed and available via `soffice` command.
103    /// On macOS, it also checks `/Applications/LibreOffice.app/Contents/MacOS/soffice`.
104    pub fn save_as_pdf<P: AsRef<Path>>(&self, output_path: P) -> Result<()> {
105        // Create a temp file
106        let temp_dir = std::env::temp_dir();
107        let temp_filename = format!("ppt_rs_{}.pptx", uuid::Uuid::new_v4());
108        let temp_path = temp_dir.join(&temp_filename);
109        
110        // Save current presentation to temp file
111        self.save(&temp_path)?;
112        
113        // Try to find soffice
114        let soffice_cmd = if cfg!(target_os = "macos") {
115            if Path::new("/Applications/LibreOffice.app/Contents/MacOS/soffice").exists() {
116                "/Applications/LibreOffice.app/Contents/MacOS/soffice"
117            } else {
118                "soffice"
119            }
120        } else {
121            "soffice"
122        };
123
124        // Get output directory
125        let output_parent = output_path.as_ref().parent().unwrap_or(Path::new("."));
126        
127        // Run conversion
128        // soffice --headless --convert-to pdf <temp_path> --outdir <output_dir>
129        let result = Command::new(soffice_cmd)
130            .arg("--headless")
131            .arg("--convert-to")
132            .arg("pdf")
133            .arg(&temp_path)
134            .arg("--outdir")
135            .arg(output_parent)
136            .output();
137
138        // Clean up temp file (ignore error)
139        let _ = std::fs::remove_file(&temp_path);
140
141        match result {
142            Ok(output) => {
143                if !output.status.success() {
144                    let stderr = String::from_utf8_lossy(&output.stderr);
145                    return Err(PptxError::Generic(format!("LibreOffice conversion failed: {}", stderr)));
146                }
147            },
148            Err(e) => {
149                return Err(PptxError::Generic(format!("Failed to execute libreoffice: {}", e)));
150            }
151        }
152        
153        // LibreOffice creates file with same basename but .pdf extension in outdir
154        // The generated file will be temp_filename.pdf (since input was temp_filename.pptx)
155        let generated_pdf_name = temp_filename.replace(".pptx", ".pdf");
156        let generated_pdf_path = output_parent.join(&generated_pdf_name);
157        
158        if generated_pdf_path.exists() {
159             std::fs::rename(&generated_pdf_path, output_path.as_ref())?;
160             Ok(())
161        } else {
162             Err(PptxError::Generic("PDF output file not found".to_string()))
163        }
164    }
165
166    /// Export slides to PNG images
167    /// 
168    /// Requires LibreOffice (for PDF conversion) and `pdftoppm` (from poppler).
169    /// Images will be named `slide-1.png`, `slide-2.png`, etc. in the output directory.
170    pub fn save_as_png<P: AsRef<Path>>(&self, output_dir: P) -> Result<()> {
171        let output_dir = output_dir.as_ref();
172        if !output_dir.exists() {
173            std::fs::create_dir_all(output_dir)?;
174        }
175
176        // Create temp PDF
177        let temp_dir = std::env::temp_dir();
178        let temp_pdf_name = format!("ppt_rs_temp_{}.pdf", uuid::Uuid::new_v4());
179        let temp_pdf_path = temp_dir.join(&temp_pdf_name);
180        
181        // Convert to PDF first
182        self.save_as_pdf(&temp_pdf_path)?;
183        
184        // Convert PDF to PNGs using pdftoppm
185        // pdftoppm -png <pdf_file> <image_prefix>
186        let prefix = output_dir.join("slide");
187        
188        let status = Command::new("pdftoppm")
189            .arg("-png")
190            .arg(&temp_pdf_path)
191            .arg(&prefix)
192            .status()
193            .map_err(|e| PptxError::Generic(format!("Failed to execute pdftoppm: {}", e)))?;
194            
195        // Cleanup temp PDF
196        let _ = std::fs::remove_file(&temp_pdf_path);
197        
198        if !status.success() {
199            return Err(PptxError::Generic("pdftoppm conversion failed".to_string()));
200        }
201        
202        Ok(())
203    }
204
205    /// Create a presentation from a PDF file (each page becomes a slide)
206    /// 
207    /// Requires `pdftoppm` (from poppler) to be installed.
208    pub fn from_pdf<P: AsRef<Path>>(path: P) -> Result<Self> {
209        let path = path.as_ref();
210        if !path.exists() {
211            return Err(PptxError::NotFound(format!("PDF file not found: {}", path.display())));
212        }
213
214        // Create temp dir for images
215        let temp_dir = std::env::temp_dir().join(format!("ppt_rs_import_{}", uuid::Uuid::new_v4()));
216        std::fs::create_dir_all(&temp_dir)?;
217        
218        // Convert PDF to PNGs
219        let prefix = temp_dir.join("page");
220        
221        let status = Command::new("pdftoppm")
222            .arg("-png")
223            .arg(path)
224            .arg(&prefix)
225            .status()
226            .map_err(|e| PptxError::Generic(format!("Failed to execute pdftoppm: {}", e)))?;
227            
228        if !status.success() {
229            let _ = std::fs::remove_dir_all(&temp_dir);
230            return Err(PptxError::Generic("pdftoppm failed".to_string()));
231        }
232        
233        // Read images and create slides
234        let mut pres = Presentation::new();
235        // Set title from filename
236        if let Some(stem) = path.file_stem() {
237            pres = pres.title(&stem.to_string_lossy());
238        }
239
240        // Read dir
241        let mut entries: Vec<_> = std::fs::read_dir(&temp_dir)?
242            .filter_map(|e| e.ok())
243            .collect();
244            
245        // Sort by filename to ensure page order
246        // pdftoppm names files like page-1.png, page-2.png... page-10.png
247        // Default string sort might put page-10 before page-2
248        // 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)
249        // pdftoppm default is -1, -2... -10.
250        // So page-1.png, page-10.png, page-2.png.
251        // We need natural sort.
252        entries.sort_by_key(|e| {
253            let name = e.file_name().to_string_lossy().to_string();
254            // Extract number from end
255            // "page-1.png" -> 1
256            if let Some(start) = name.rfind('-') {
257                if let Some(end) = name.rfind('.') {
258                    if start < end {
259                        if let Ok(num) = name[start+1..end].parse::<u32>() {
260                            return num;
261                        }
262                    }
263                }
264            }
265            0 // Fallback
266        });
267        
268        for entry in entries {
269            let path = entry.path();
270            if path.extension().map_or(false, |e| e == "png") {
271                // Create slide with full screen image
272                let image = Image::from_path(&path)
273                    .map_err(|e| PptxError::Generic(e))?;
274                
275                // Add image to slide
276                // Use a default layout?
277                // Just create a slide with this image
278                // We'll center it.
279                // Assuming standard 16:9 slide (10x5.625 inches) -> 9144000 x 5143500 EMU
280                // But we don't know image dimensions here easily without reading it.
281                // Image builder defaults to auto size?
282                // Let's just add it.
283                
284                let mut slide = SlideContent::new("");
285                slide.images.push(image);
286                pres = pres.add_slide(slide);
287            }
288        }
289        
290        let _ = std::fs::remove_dir_all(&temp_dir);
291        Ok(pres)
292    }
293}
294
295/// Open a presentation from a file path
296pub fn open<P: AsRef<Path>>(path: P) -> Result<Package> {
297    Package::open(path)
298}
299
300/// Open a presentation from a reader
301pub fn open_reader<R: Read + Seek>(reader: R) -> Result<Package> {
302    Package::open_reader(reader)
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308
309    #[test]
310    fn test_presentation_builder() {
311        let pres = Presentation::with_title("Test")
312            .add_slide(SlideContent::new("Slide 1").add_bullet("Point 1"));
313        
314        assert_eq!(pres.get_title(), "Test");
315        assert_eq!(pres.slide_count(), 1);
316    }
317
318    #[test]
319    fn test_presentation_build() {
320        let pres = Presentation::with_title("Test")
321            .add_slide(SlideContent::new("Slide 1"));
322        
323        let result = pres.build();
324        assert!(result.is_ok());
325    }
326}