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<()> {
134 crate::export::slide_render::render_to_pdf(self, output_path)?;
135 Ok(())
136 }
137
138 #[cfg(feature = "pdf-native")]
148 pub fn save_as_pdf_via_pdfrs<P: AsRef<Path>>(
149 &self,
150 output_path: P,
151 options: &crate::export::pdf_export::PdfExportOptions,
152 ) -> Result<()> {
153 crate::export::pdf_export::export_to_pdf(self, output_path, options)?;
154 Ok(())
155 }
156
157 #[cfg(feature = "pdf-native")]
165 pub fn to_pdf_bytes(
166 &self,
167 options: &crate::export::pdf_export::PdfExportOptions,
168 ) -> Result<Vec<u8>> {
169 crate::export::pdf_export::export_to_pdf_bytes(self, options)
170 }
171
172 pub fn save_as_png<P: AsRef<Path>>(&self, output_dir: P) -> Result<()> {
177 let output_dir = output_dir.as_ref();
178 if !output_dir.exists() {
179 std::fs::create_dir_all(output_dir)?;
180 }
181
182 let temp_dir = std::env::temp_dir();
184 let temp_pdf_name = format!("ppt_rs_temp_{}.pdf", uuid::Uuid::new_v4());
185 let temp_pdf_path = temp_dir.join(&temp_pdf_name);
186
187 let bytes = crate::export::slide_render::render_to_pdf_bytes(self)?;
189 std::fs::write(&temp_pdf_path, &bytes)?;
190
191 let prefix = output_dir.join("slide");
194
195 let status = Command::new("pdftoppm")
196 .arg("-png")
197 .arg(&temp_pdf_path)
198 .arg(&prefix)
199 .status()
200 .map_err(|e| PptxError::Generic(format!("Failed to execute pdftoppm: {}", e)))?;
201
202 let _ = std::fs::remove_file(&temp_pdf_path);
204
205 if !status.success() {
206 return Err(PptxError::Generic("pdftoppm conversion failed".to_string()));
207 }
208
209 Ok(())
210 }
211
212 pub fn from_pdf<P: AsRef<Path>>(path: P) -> Result<Self> {
216 let path = path.as_ref();
217 if !path.exists() {
218 return Err(PptxError::NotFound(format!(
219 "PDF file not found: {}",
220 path.display()
221 )));
222 }
223
224 let temp_dir = std::env::temp_dir().join(format!("ppt_rs_import_{}", uuid::Uuid::new_v4()));
226 std::fs::create_dir_all(&temp_dir)?;
227
228 let prefix = temp_dir.join("page");
230
231 let status = Command::new("pdftoppm")
232 .arg("-png")
233 .arg(path)
234 .arg(&prefix)
235 .status()
236 .map_err(|e| PptxError::Generic(format!("Failed to execute pdftoppm: {}", e)))?;
237
238 if !status.success() {
239 let _ = std::fs::remove_dir_all(&temp_dir);
240 return Err(PptxError::Generic("pdftoppm failed".to_string()));
241 }
242
243 let mut pres = Presentation::new();
245 if let Some(stem) = path.file_stem() {
247 pres = pres.title(&stem.to_string_lossy());
248 }
249
250 let mut entries: Vec<_> = std::fs::read_dir(&temp_dir)?
252 .filter_map(|e| e.ok())
253 .collect();
254
255 entries.sort_by_key(|e| {
263 let name = e.file_name().to_string_lossy().to_string();
264 if let Some(start) = name.rfind('-')
267 && let Some(end) = name.rfind('.')
268 && start < end
269 && let Ok(num) = name[start + 1..end].parse::<u32>() {
270 return num;
271 }
272 0 });
274
275 for entry in entries {
276 let path = entry.path();
277 if path.extension().is_some_and(|e| e == "png") {
278 let image = Image::from_path(&path).map_err(PptxError::Generic)?;
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}