1use 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#[derive(Debug, Clone, Default)]
14pub struct Presentation {
15 title: String,
16 slides: Vec<SlideContent>,
17}
18
19impl Presentation {
20 pub fn new() -> Self {
22 Presentation {
23 title: String::new(),
24 slides: Vec::new(),
25 }
26 }
27
28 pub fn with_title(title: &str) -> Self {
30 Presentation {
31 title: title.to_string(),
32 slides: Vec::new(),
33 }
34 }
35
36 pub fn title(mut self, title: &str) -> Self {
38 self.title = title.to_string();
39 self
40 }
41
42 pub fn add_slide(mut self, slide: SlideContent) -> Self {
44 self.slides.push(slide);
45 self
46 }
47
48 pub fn add_presentation(mut self, other: Presentation) -> Self {
50 self.slides.extend(other.slides);
51 self
52 }
53
54 pub fn slide_count(&self) -> usize {
56 self.slides.len()
57 }
58
59 pub fn slides(&self) -> &[SlideContent] {
61 &self.slides
62 }
63
64 pub fn get_title(&self) -> &str {
66 &self.title
67 }
68
69 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 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 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 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 pub fn save_as_pdf<P: AsRef<Path>>(&self, output_path: P) -> Result<()> {
103 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 self.save(&temp_path)?;
110
111 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 let output_parent = output_path.as_ref().parent().unwrap_or(Path::new("."));
124
125 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 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 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 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 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 self.save_as_pdf(&temp_pdf_path)?;
187
188 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 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 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 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 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 let mut pres = Presentation::new();
242 if let Some(stem) = path.file_stem() {
244 pres = pres.title(&stem.to_string_lossy());
245 }
246
247 let mut entries: Vec<_> = std::fs::read_dir(&temp_dir)?
249 .filter_map(|e| e.ok())
250 .collect();
251
252 entries.sort_by_key(|e| {
260 let name = e.file_name().to_string_lossy().to_string();
261 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 });
274
275 for entry in entries {
276 let path = entry.path();
277 if path.extension().map_or(false, |e| e == "png") {
278 let image = Image::from_path(&path).map_err(|e| PptxError::Generic(e))?;
280
281 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 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 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 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 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 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 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 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 let result = crate::opc::compress::compress_pptx(&temp_path, output_path, options);
409
410 let _ = std::fs::remove_file(&temp_path);
412
413 result
414 }
415
416 pub fn analyze_size(&self) -> Result<crate::opc::compress::PptxAnalysis> {
420 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 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}