use crate::api::Presentation;
use crate::exc::{PptxError, Result};
use std::path::Path;
use std::process::Command;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ImageFormat {
Png,
Jpeg,
}
impl ImageFormat {
pub fn extension(&self) -> &'static str {
match self {
ImageFormat::Png => "png",
ImageFormat::Jpeg => "jpg",
}
}
pub fn mime_type(&self) -> &'static str {
match self {
ImageFormat::Png => "image/png",
ImageFormat::Jpeg => "image/jpeg",
}
}
}
impl Default for ImageFormat {
fn default() -> Self {
ImageFormat::Png
}
}
#[derive(Debug, Clone)]
pub struct ImageExportOptions {
pub format: ImageFormat,
pub dpi: u32,
pub jpeg_quality: u8,
pub width: u32,
pub height: u32,
pub slide_number: usize,
}
impl Default for ImageExportOptions {
fn default() -> Self {
Self {
format: ImageFormat::Png,
dpi: 150,
jpeg_quality: 90,
width: 0,
height: 0,
slide_number: 0,
}
}
}
impl ImageExportOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_format(mut self, format: ImageFormat) -> Self {
self.format = format;
self
}
pub fn with_dpi(mut self, dpi: u32) -> Self {
self.dpi = dpi;
self
}
pub fn with_jpeg_quality(mut self, quality: u8) -> Self {
self.jpeg_quality = quality.min(100);
self
}
pub fn with_dimensions(mut self, width: u32, height: u32) -> Self {
self.width = width;
self.height = height;
self
}
pub fn with_slide(mut self, slide: usize) -> Self {
self.slide_number = slide;
self
}
pub fn high_quality() -> Self {
Self {
format: ImageFormat::Png,
dpi: 300,
jpeg_quality: 95,
width: 0,
height: 0,
slide_number: 0,
}
}
pub fn web_optimized() -> Self {
Self {
format: ImageFormat::Jpeg,
dpi: 96,
jpeg_quality: 85,
width: 0,
height: 0,
slide_number: 0,
}
}
}
pub fn export_to_images<P: AsRef<Path>>(
presentation: &Presentation,
output_dir: P,
options: &ImageExportOptions,
) -> Result<Vec<std::path::PathBuf>> {
let temp_dir = std::env::temp_dir();
let temp_pptx = temp_dir.join("temp_export.pptx");
presentation.save(&temp_pptx)?;
let output_dir = output_dir.as_ref();
std::fs::create_dir_all(output_dir)?;
let result = export_pptx_to_images(&temp_pptx, output_dir, options);
let _ = std::fs::remove_file(&temp_pptx);
result
}
pub fn export_slide_to_image<P: AsRef<Path>>(
presentation: &Presentation,
slide_number: usize,
output_path: P,
options: &ImageExportOptions,
) -> Result<std::path::PathBuf> {
if slide_number == 0 || slide_number > presentation.slide_count() {
return Err(PptxError::InvalidOperation(format!(
"Invalid slide number: {} (presentation has {} slides)",
slide_number,
presentation.slide_count()
)));
}
let mut slide_options = options.clone();
slide_options.slide_number = slide_number;
let output_dir = output_path
.as_ref()
.parent()
.unwrap_or(std::path::Path::new("."));
let file_stem = output_path
.as_ref()
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("slide");
let paths = export_to_images(presentation, output_dir, &slide_options)?;
let expected_name = format!("Slide{}.{}", slide_number, slide_options.format.extension());
for path in paths {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if name == expected_name || name.contains(&format!("Slide{}", slide_number)) {
if path != output_path.as_ref() {
std::fs::rename(&path, &output_path)?;
return Ok(output_path.as_ref().to_path_buf());
}
return Ok(path);
}
}
}
Err(PptxError::Generic(String::from("Export failed")))
}
fn export_pptx_to_images<P: AsRef<Path>, Q: AsRef<Path>>(
pptx_path: P,
output_dir: Q,
options: &ImageExportOptions,
) -> Result<Vec<std::path::PathBuf>> {
let pptx_path = pptx_path.as_ref();
let output_dir = output_dir.as_ref();
if !is_libreoffice_available() {
return Err(PptxError::Generic(String::from(
"LibreOffice not found"
)));
}
let ext = options.format.extension();
let convert_opt = format!("{}:ExportNotesPages=false", ext);
let mut cmd = Command::new("soffice");
cmd.arg("--headless")
.arg("--convert-to")
.arg(&convert_opt)
.arg("--outdir")
.arg(output_dir)
.arg(pptx_path);
let output = cmd.output().map_err(|e| {
PptxError::Generic(format!("Failed to execute LibreOffice: {}", e))
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(PptxError::Generic(format!(
"LibreOffice conversion failed: {}",
stderr
)));
}
let mut image_files = Vec::new();
let file_stem = pptx_path.file_stem().and_then(|s| s.to_str()).unwrap_or("slide");
for entry in std::fs::read_dir(output_dir)? {
let entry = entry?;
let path = entry.path();
if let Some(file_ext) = path.extension().and_then(|e| e.to_str()) {
if file_ext.eq_ignore_ascii_case(ext) {
if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
if name.starts_with(file_stem) || name.starts_with("Slide") {
image_files.push(path);
}
}
}
}
}
image_files.sort_by(|a, b| {
let a_num = extract_slide_number(a);
let b_num = extract_slide_number(b);
a_num.cmp(&b_num)
});
Ok(image_files)
}
fn is_libreoffice_available() -> bool {
Command::new("soffice").arg("--version").output().is_ok()
}
fn extract_slide_number(path: &std::path::Path) -> usize {
path.file_stem()
.and_then(|s| s.to_str())
.and_then(|name| {
let digits: String = name.chars().filter(|c| c.is_ascii_digit()).collect();
digits.parse().ok()
})
.unwrap_or(0)
}
pub fn render_thumbnail<P: AsRef<Path>>(
presentation: &Presentation,
output_path: P,
width: u32,
) -> Result<std::path::PathBuf> {
let options = ImageExportOptions::new()
.with_format(ImageFormat::Png)
.with_slide(1);
let dpi = (width as f32 / 10.0) as u32;
let options = ImageExportOptions {
dpi,
..options
};
export_slide_to_image(presentation, 1, output_path, &options)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::generator::SlideContent;
#[test]
fn test_image_format_extension() {
assert_eq!(ImageFormat::Png.extension(), "png");
assert_eq!(ImageFormat::Jpeg.extension(), "jpg");
}
#[test]
fn test_image_export_options() {
let opts = ImageExportOptions::new()
.with_format(ImageFormat::Jpeg)
.with_dpi(200)
.with_jpeg_quality(85);
assert_eq!(opts.format, ImageFormat::Jpeg);
assert_eq!(opts.dpi, 200);
assert_eq!(opts.jpeg_quality, 85);
}
#[test]
fn test_high_quality_preset() {
let opts = ImageExportOptions::high_quality();
assert_eq!(opts.dpi, 300);
assert_eq!(opts.format, ImageFormat::Png);
}
#[test]
fn test_web_optimized_preset() {
let opts = ImageExportOptions::web_optimized();
assert_eq!(opts.dpi, 96);
assert_eq!(opts.format, ImageFormat::Jpeg);
assert_eq!(opts.jpeg_quality, 85);
}
#[test]
fn test_extract_slide_number() {
let path = std::path::Path::new("Slide1.png");
assert_eq!(extract_slide_number(path), 1);
let path = std::path::Path::new("slide12.jpg");
assert_eq!(extract_slide_number(path), 12);
let path = std::path::Path::new("temp_export5.png");
assert_eq!(extract_slide_number(path), 5);
}
}