1use crate::exc::{Result, PptxError};
6use crate::opc::Package;
7use crate::generator::{SlideContent, create_pptx_with_content, Image};
8use crate::import::import_pptx;
9use crate::export::html::export_to_html;
10use std::io::{Read, Seek};
11use std::path::Path;
12use std::process::Command;
13
14#[derive(Debug, Clone, Default)]
16pub struct Presentation {
17 title: String,
18 slides: Vec<SlideContent>,
19}
20
21impl Presentation {
22 pub fn new() -> Self {
24 Presentation {
25 title: String::new(),
26 slides: Vec::new(),
27 }
28 }
29
30 pub fn with_title(title: &str) -> Self {
32 Presentation {
33 title: title.to_string(),
34 slides: Vec::new(),
35 }
36 }
37
38 pub fn title(mut self, title: &str) -> Self {
40 self.title = title.to_string();
41 self
42 }
43
44 pub fn add_slide(mut self, slide: SlideContent) -> Self {
46 self.slides.push(slide);
47 self
48 }
49
50 pub fn add_presentation(mut self, other: Presentation) -> Self {
52 self.slides.extend(other.slides);
53 self
54 }
55
56 pub fn slide_count(&self) -> usize {
58 self.slides.len()
59 }
60
61 pub fn slides(&self) -> &[SlideContent] {
63 &self.slides
64 }
65
66 pub fn get_title(&self) -> &str {
68 &self.title
69 }
70
71 pub fn build(&self) -> Result<Vec<u8>> {
73 if self.slides.is_empty() {
74 return Err(PptxError::InvalidState("Presentation has no slides".into()));
75 }
76 create_pptx_with_content(&self.title, self.slides.clone())
77 .map_err(|e| PptxError::Generic(e.to_string()))
78 }
79
80 pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
82 let data = self.build()?;
83 std::fs::write(path, data)?;
84 Ok(())
85 }
86
87 pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
89 let path_str = path.as_ref().to_string_lossy();
90 import_pptx(&path_str)
91 }
92
93 pub fn save_as_html<P: AsRef<Path>>(&self, path: P) -> Result<()> {
95 let html = export_to_html(self)?;
96 std::fs::write(path, html)?;
97 Ok(())
98 }
99
100 pub fn save_as_pdf<P: AsRef<Path>>(&self, output_path: P) -> Result<()> {
105 let temp_dir = std::env::temp_dir();
107 let temp_filename = format!("ppt_rs_{}.pptx", uuid::Uuid::new_v4());
108 let temp_path = temp_dir.join(&temp_filename);
109
110 self.save(&temp_path)?;
112
113 let soffice_cmd = if cfg!(target_os = "macos") {
115 if Path::new("/Applications/LibreOffice.app/Contents/MacOS/soffice").exists() {
116 "/Applications/LibreOffice.app/Contents/MacOS/soffice"
117 } else {
118 "soffice"
119 }
120 } else {
121 "soffice"
122 };
123
124 let output_parent = output_path.as_ref().parent().unwrap_or(Path::new("."));
126
127 let result = Command::new(soffice_cmd)
130 .arg("--headless")
131 .arg("--convert-to")
132 .arg("pdf")
133 .arg(&temp_path)
134 .arg("--outdir")
135 .arg(output_parent)
136 .output();
137
138 let _ = std::fs::remove_file(&temp_path);
140
141 match result {
142 Ok(output) => {
143 if !output.status.success() {
144 let stderr = String::from_utf8_lossy(&output.stderr);
145 return Err(PptxError::Generic(format!("LibreOffice conversion failed: {}", stderr)));
146 }
147 },
148 Err(e) => {
149 return Err(PptxError::Generic(format!("Failed to execute libreoffice: {}", e)));
150 }
151 }
152
153 let generated_pdf_name = temp_filename.replace(".pptx", ".pdf");
156 let generated_pdf_path = output_parent.join(&generated_pdf_name);
157
158 if generated_pdf_path.exists() {
159 std::fs::rename(&generated_pdf_path, output_path.as_ref())?;
160 Ok(())
161 } else {
162 Err(PptxError::Generic("PDF output file not found".to_string()))
163 }
164 }
165
166 pub fn save_as_png<P: AsRef<Path>>(&self, output_dir: P) -> Result<()> {
171 let output_dir = output_dir.as_ref();
172 if !output_dir.exists() {
173 std::fs::create_dir_all(output_dir)?;
174 }
175
176 let temp_dir = std::env::temp_dir();
178 let temp_pdf_name = format!("ppt_rs_temp_{}.pdf", uuid::Uuid::new_v4());
179 let temp_pdf_path = temp_dir.join(&temp_pdf_name);
180
181 self.save_as_pdf(&temp_pdf_path)?;
183
184 let prefix = output_dir.join("slide");
187
188 let status = Command::new("pdftoppm")
189 .arg("-png")
190 .arg(&temp_pdf_path)
191 .arg(&prefix)
192 .status()
193 .map_err(|e| PptxError::Generic(format!("Failed to execute pdftoppm: {}", e)))?;
194
195 let _ = std::fs::remove_file(&temp_pdf_path);
197
198 if !status.success() {
199 return Err(PptxError::Generic("pdftoppm conversion failed".to_string()));
200 }
201
202 Ok(())
203 }
204
205 pub fn from_pdf<P: AsRef<Path>>(path: P) -> Result<Self> {
209 let path = path.as_ref();
210 if !path.exists() {
211 return Err(PptxError::NotFound(format!("PDF file not found: {}", path.display())));
212 }
213
214 let temp_dir = std::env::temp_dir().join(format!("ppt_rs_import_{}", uuid::Uuid::new_v4()));
216 std::fs::create_dir_all(&temp_dir)?;
217
218 let prefix = temp_dir.join("page");
220
221 let status = Command::new("pdftoppm")
222 .arg("-png")
223 .arg(path)
224 .arg(&prefix)
225 .status()
226 .map_err(|e| PptxError::Generic(format!("Failed to execute pdftoppm: {}", e)))?;
227
228 if !status.success() {
229 let _ = std::fs::remove_dir_all(&temp_dir);
230 return Err(PptxError::Generic("pdftoppm failed".to_string()));
231 }
232
233 let mut pres = Presentation::new();
235 if let Some(stem) = path.file_stem() {
237 pres = pres.title(&stem.to_string_lossy());
238 }
239
240 let mut entries: Vec<_> = std::fs::read_dir(&temp_dir)?
242 .filter_map(|e| e.ok())
243 .collect();
244
245 entries.sort_by_key(|e| {
253 let name = e.file_name().to_string_lossy().to_string();
254 if let Some(start) = name.rfind('-') {
257 if let Some(end) = name.rfind('.') {
258 if start < end {
259 if let Ok(num) = name[start+1..end].parse::<u32>() {
260 return num;
261 }
262 }
263 }
264 }
265 0 });
267
268 for entry in entries {
269 let path = entry.path();
270 if path.extension().map_or(false, |e| e == "png") {
271 let image = Image::from_path(&path)
273 .map_err(|e| PptxError::Generic(e))?;
274
275 let mut slide = SlideContent::new("");
285 slide.images.push(image);
286 pres = pres.add_slide(slide);
287 }
288 }
289
290 let _ = std::fs::remove_dir_all(&temp_dir);
291 Ok(pres)
292 }
293}
294
295pub fn open<P: AsRef<Path>>(path: P) -> Result<Package> {
297 Package::open(path)
298}
299
300pub fn open_reader<R: Read + Seek>(reader: R) -> Result<Package> {
302 Package::open_reader(reader)
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 #[test]
310 fn test_presentation_builder() {
311 let pres = Presentation::with_title("Test")
312 .add_slide(SlideContent::new("Slide 1").add_bullet("Point 1"));
313
314 assert_eq!(pres.get_title(), "Test");
315 assert_eq!(pres.slide_count(), 1);
316 }
317
318 #[test]
319 fn test_presentation_build() {
320 let pres = Presentation::with_title("Test")
321 .add_slide(SlideContent::new("Slide 1"));
322
323 let result = pres.build();
324 assert!(result.is_ok());
325 }
326}