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
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 #[test]
306 fn test_presentation_builder() {
307 let pres = Presentation::with_title("Test")
308 .add_slide(SlideContent::new("Slide 1").add_bullet("Point 1"));
309
310 assert_eq!(pres.get_title(), "Test");
311 assert_eq!(pres.slide_count(), 1);
312 }
313
314 #[test]
315 fn test_presentation_build() {
316 let pres = Presentation::with_title("Test").add_slide(SlideContent::new("Slide 1"));
317
318 let result = pres.build();
319 assert!(result.is_ok());
320 }
321}