1use crate::api::Presentation;
9use crate::exc::{PptxError, Result};
10use std::path::Path;
11use std::process::Command;
12
13#[derive(Debug, Clone, Copy, PartialEq)]
15#[derive(Default)]
16pub enum ImageFormat {
17 #[default]
19 Png,
20 Jpeg,
22}
23
24impl ImageFormat {
25 pub fn extension(&self) -> &'static str {
27 match self {
28 ImageFormat::Png => "png",
29 ImageFormat::Jpeg => "jpg",
30 }
31 }
32
33 pub fn mime_type(&self) -> &'static str {
35 match self {
36 ImageFormat::Png => "image/png",
37 ImageFormat::Jpeg => "image/jpeg",
38 }
39 }
40}
41
42
43#[derive(Debug, Clone)]
45pub struct ImageExportOptions {
46 pub format: ImageFormat,
48 pub dpi: u32,
50 pub jpeg_quality: u8,
52 pub width: u32,
54 pub height: u32,
56 pub slide_number: usize,
58}
59
60impl Default for ImageExportOptions {
61 fn default() -> Self {
62 Self {
63 format: ImageFormat::Png,
64 dpi: 150,
65 jpeg_quality: 90,
66 width: 0,
67 height: 0,
68 slide_number: 0,
69 }
70 }
71}
72
73impl ImageExportOptions {
74 pub fn new() -> Self {
76 Self::default()
77 }
78
79 pub fn with_format(mut self, format: ImageFormat) -> Self {
81 self.format = format;
82 self
83 }
84
85 pub fn with_dpi(mut self, dpi: u32) -> Self {
87 self.dpi = dpi;
88 self
89 }
90
91 pub fn with_jpeg_quality(mut self, quality: u8) -> Self {
93 self.jpeg_quality = quality.min(100);
94 self
95 }
96
97 pub fn with_dimensions(mut self, width: u32, height: u32) -> Self {
99 self.width = width;
100 self.height = height;
101 self
102 }
103
104 pub fn with_slide(mut self, slide: usize) -> Self {
106 self.slide_number = slide;
107 self
108 }
109
110 pub fn high_quality() -> Self {
112 Self {
113 format: ImageFormat::Png,
114 dpi: 300,
115 jpeg_quality: 95,
116 width: 0,
117 height: 0,
118 slide_number: 0,
119 }
120 }
121
122 pub fn web_optimized() -> Self {
124 Self {
125 format: ImageFormat::Jpeg,
126 dpi: 96,
127 jpeg_quality: 85,
128 width: 0,
129 height: 0,
130 slide_number: 0,
131 }
132 }
133}
134
135pub fn export_to_images<P: AsRef<Path>>(
147 presentation: &Presentation,
148 output_dir: P,
149 options: &ImageExportOptions,
150) -> Result<Vec<std::path::PathBuf>> {
151 let temp_dir = std::env::temp_dir();
153 let temp_pptx = temp_dir.join("temp_export.pptx");
154 presentation.save(&temp_pptx)?;
155
156 let output_dir = output_dir.as_ref();
157 std::fs::create_dir_all(output_dir)?;
158
159 let result = export_pptx_to_images(&temp_pptx, output_dir, options);
161
162 let _ = std::fs::remove_file(&temp_pptx);
164
165 result
166}
167
168pub fn export_slide_to_image<P: AsRef<Path>>(
170 presentation: &Presentation,
171 slide_number: usize,
172 output_path: P,
173 options: &ImageExportOptions,
174) -> Result<std::path::PathBuf> {
175 if slide_number == 0 || slide_number > presentation.slide_count() {
176 return Err(PptxError::InvalidOperation(format!(
177 "Invalid slide number: {} (presentation has {} slides)",
178 slide_number,
179 presentation.slide_count()
180 )));
181 }
182
183 let mut slide_options = options.clone();
184 slide_options.slide_number = slide_number;
185
186 let output_dir = output_path
187 .as_ref()
188 .parent()
189 .unwrap_or(std::path::Path::new("."));
190
191 let paths = export_to_images(presentation, output_dir, &slide_options)?;
192
193 let expected_name = format!("Slide{}.{}", slide_number, slide_options.format.extension());
195 for path in paths {
196 if let Some(name) = path.file_name().and_then(|n| n.to_str())
197 && (name == expected_name || name.contains(&format!("Slide{}", slide_number))) {
198 if path != output_path.as_ref() {
200 std::fs::rename(&path, &output_path)?;
201 return Ok(output_path.as_ref().to_path_buf());
202 }
203 return Ok(path);
204 }
205 }
206
207 Err(PptxError::Generic(String::from("Export failed")))
208}
209
210fn export_pptx_to_images<P: AsRef<Path>, Q: AsRef<Path>>(
212 pptx_path: P,
213 output_dir: Q,
214 options: &ImageExportOptions,
215) -> Result<Vec<std::path::PathBuf>> {
216 let pptx_path = pptx_path.as_ref();
217 let output_dir = output_dir.as_ref();
218
219 if !is_libreoffice_available() {
221 return Err(PptxError::Generic(String::from(
222 "LibreOffice not found"
223 )));
224 }
225
226 let ext = options.format.extension();
228 let convert_opt = format!("{}:ExportNotesPages=false", ext);
229
230 let mut cmd = Command::new("soffice");
231 cmd.arg("--headless")
232 .arg("--convert-to")
233 .arg(&convert_opt)
234 .arg("--outdir")
235 .arg(output_dir)
236 .arg(pptx_path);
237
238 let output = cmd.output().map_err(|e| {
240 PptxError::Generic(format!("Failed to execute LibreOffice: {}", e))
241 })?;
242
243 if !output.status.success() {
244 let stderr = String::from_utf8_lossy(&output.stderr);
245 return Err(PptxError::Generic(format!(
246 "LibreOffice conversion failed: {}",
247 stderr
248 )));
249 }
250
251 let mut image_files = Vec::new();
253 let file_stem = pptx_path.file_stem().and_then(|s| s.to_str()).unwrap_or("slide");
254
255 for entry in std::fs::read_dir(output_dir)? {
256 let entry = entry?;
257 let path = entry.path();
258 if let Some(file_ext) = path.extension().and_then(|e| e.to_str())
259 && file_ext.eq_ignore_ascii_case(ext) {
260 if let Some(name) = path.file_stem().and_then(|s| s.to_str())
262 && (name.starts_with(file_stem) || name.starts_with("Slide")) {
263 image_files.push(path);
264 }
265 }
266 }
267
268 image_files.sort_by(|a, b| {
270 let a_num = extract_slide_number(a);
271 let b_num = extract_slide_number(b);
272 a_num.cmp(&b_num)
273 });
274
275 Ok(image_files)
276}
277
278fn is_libreoffice_available() -> bool {
280 Command::new("soffice").arg("--version").output().is_ok()
281}
282
283fn extract_slide_number(path: &std::path::Path) -> usize {
285 path.file_stem()
286 .and_then(|s| s.to_str())
287 .and_then(|name| {
288 let digits: String = name.chars().filter(|c| c.is_ascii_digit()).collect();
290 digits.parse().ok()
291 })
292 .unwrap_or(0)
293}
294
295pub fn render_thumbnail<P: AsRef<Path>>(
297 presentation: &Presentation,
298 output_path: P,
299 width: u32,
300) -> Result<std::path::PathBuf> {
301 let options = ImageExportOptions::new()
302 .with_format(ImageFormat::Png)
303 .with_slide(1);
304
305 let dpi = (width as f32 / 10.0) as u32;
307
308 let options = ImageExportOptions {
309 dpi,
310 ..options
311 };
312
313 export_slide_to_image(presentation, 1, output_path, &options)
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319
320 #[test]
321 fn test_image_format_extension() {
322 assert_eq!(ImageFormat::Png.extension(), "png");
323 assert_eq!(ImageFormat::Jpeg.extension(), "jpg");
324 }
325
326 #[test]
327 fn test_image_export_options() {
328 let opts = ImageExportOptions::new()
329 .with_format(ImageFormat::Jpeg)
330 .with_dpi(200)
331 .with_jpeg_quality(85);
332
333 assert_eq!(opts.format, ImageFormat::Jpeg);
334 assert_eq!(opts.dpi, 200);
335 assert_eq!(opts.jpeg_quality, 85);
336 }
337
338 #[test]
339 fn test_high_quality_preset() {
340 let opts = ImageExportOptions::high_quality();
341 assert_eq!(opts.dpi, 300);
342 assert_eq!(opts.format, ImageFormat::Png);
343 }
344
345 #[test]
346 fn test_web_optimized_preset() {
347 let opts = ImageExportOptions::web_optimized();
348 assert_eq!(opts.dpi, 96);
349 assert_eq!(opts.format, ImageFormat::Jpeg);
350 assert_eq!(opts.jpeg_quality, 85);
351 }
352
353 #[test]
354 fn test_extract_slide_number() {
355 let path = std::path::Path::new("Slide1.png");
356 assert_eq!(extract_slide_number(path), 1);
357
358 let path = std::path::Path::new("slide12.jpg");
359 assert_eq!(extract_slide_number(path), 12);
360
361 let path = std::path::Path::new("temp_export5.png");
362 assert_eq!(extract_slide_number(path), 5);
363 }
364}