1use 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#[derive(Debug, Clone, Default)]
14pub struct Presentation {
15 title: String,
16 slides: Vec<SlideContent>,
17 settings: Option<PresentationSettings>,
18}
19
20impl Presentation {
21 pub fn new() -> Self {
23 Presentation {
24 title: String::new(),
25 slides: Vec::new(),
26 settings: None,
27 }
28 }
29
30 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 pub fn title(mut self, title: &str) -> Self {
41 self.title = title.to_string();
42 self
43 }
44
45 pub fn add_slide(mut self, slide: SlideContent) -> Self {
47 self.slides.push(slide);
48 self
49 }
50
51 pub fn add_presentation(mut self, other: Presentation) -> Self {
53 self.slides.extend(other.slides);
54 self
55 }
56
57 pub fn slide_count(&self) -> usize {
59 self.slides.len()
60 }
61
62 pub fn slides(&self) -> &[SlideContent] {
64 &self.slides
65 }
66
67 pub fn get_title(&self) -> &str {
69 &self.title
70 }
71
72 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 pub fn with_settings(mut self, settings: PresentationSettings) -> Self {
82 self.settings = Some(settings);
83 self
84 }
85
86 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 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 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 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 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 pub fn save_as_pdf<P: AsRef<Path>>(&self, output_path: P) -> Result<()> {
129 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 self.save(&temp_path)?;
136
137 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 let output_parent = output_path.as_ref().parent().unwrap_or(Path::new("."));
150
151 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 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 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 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 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 self.save_as_pdf(&temp_pdf_path)?;
213
214 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 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 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 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 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 let mut pres = Presentation::new();
268 if let Some(stem) = path.file_stem() {
270 pres = pres.title(&stem.to_string_lossy());
271 }
272
273 let mut entries: Vec<_> = std::fs::read_dir(&temp_dir)?
275 .filter_map(|e| e.ok())
276 .collect();
277
278 entries.sort_by_key(|e| {
286 let name = e.file_name().to_string_lossy().to_string();
287 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 });
300
301 for entry in entries {
302 let path = entry.path();
303 if path.extension().map_or(false, |e| e == "png") {
304 let image = Image::from_path(&path).map_err(|e| PptxError::Generic(e))?;
306
307 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 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 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 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 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 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 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 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 let result = crate::opc::compress::compress_pptx(&temp_path, output_path, options);
435
436 let _ = std::fs::remove_file(&temp_path);
438
439 result
440 }
441
442 pub fn analyze_size(&self) -> Result<crate::opc::compress::PptxAnalysis> {
446 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 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}