1use 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#[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(
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 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 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 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 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 pub fn save_as_pdf<P: AsRef<Path>>(&self, output_path: P) -> Result<()> {
133 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 self.save(&temp_path)?;
140
141 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 let output_parent = output_path.as_ref().parent().unwrap_or(Path::new("."));
154
155 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 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 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 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 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 self.save_as_pdf(&temp_pdf_path)?;
217
218 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 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 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 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 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 let mut pres = Presentation::new();
272 if let Some(stem) = path.file_stem() {
274 pres = pres.title(&stem.to_string_lossy());
275 }
276
277 let mut entries: Vec<_> = std::fs::read_dir(&temp_dir)?
279 .filter_map(|e| e.ok())
280 .collect();
281
282 entries.sort_by_key(|e| {
290 let name = e.file_name().to_string_lossy().to_string();
291 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 });
304
305 for entry in entries {
306 let path = entry.path();
307 if path.extension().map_or(false, |e| e == "png") {
308 let image = Image::from_path(&path).map_err(|e| PptxError::Generic(e))?;
310
311 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 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 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 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 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 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 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 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 let result = crate::opc::compress::compress_pptx(&temp_path, output_path, options);
439
440 let _ = std::fs::remove_file(&temp_path);
442
443 result
444 }
445
446 pub fn analyze_size(&self) -> Result<crate::opc::compress::PptxAnalysis> {
450 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 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}