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