Skip to main content

ppt_rs/export/
html.rs

1use crate::api::Presentation;
2use crate::generator::{SlideContent, Image};
3use crate::exc::Result;
4
5/// Export a presentation to a single HTML file
6pub fn export_to_html(presentation: &Presentation) -> Result<String> {
7    let mut html = String::new();
8    
9    // Header
10    html.push_str("<!DOCTYPE html>\n<html>\n<head>\n");
11    html.push_str("<meta charset=\"UTF-8\">\n");
12    html.push_str("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n");
13    html.push_str(&format!("<title>{}</title>\n", presentation.get_title()));
14    
15    // CSS
16    html.push_str("<style>\n");
17    html.push_str(include_str!("html_style.css"));
18    html.push_str("</style>\n");
19    
20    html.push_str("</head>\n<body>\n");
21    
22    // Title Slide (Presentation Title)
23    html.push_str("<div class=\"slide title-slide\">\n");
24    html.push_str(&format!("<h1>{}</h1>\n", presentation.get_title()));
25    html.push_str("</div>\n");
26    
27    // Slides
28    for (i, slide) in presentation.slides().iter().enumerate() {
29        html.push_str(&render_slide(slide, i + 1));
30    }
31    
32    html.push_str("</body>\n</html>");
33    
34    Ok(html)
35}
36
37fn render_slide(slide: &SlideContent, index: usize) -> String {
38    let mut html = String::new();
39    
40    html.push_str(&format!("<div class=\"slide\" id=\"slide-{}\">\n", index));
41    
42    // Slide Number
43    html.push_str(&format!("<div class=\"slide-number\">{}</div>\n", index));
44    
45    // Title
46    html.push_str(&format!("<h2>{}</h2>\n", slide.title));
47    
48    // Content Container
49    html.push_str("<div class=\"content\">\n");
50    
51    // Bullets / Content
52    if !slide.content.is_empty() {
53        html.push_str("<ul>\n");
54        for item in &slide.content {
55            html.push_str(&format!("<li>{}</li>\n", item));
56        }
57        html.push_str("</ul>\n");
58    }
59    
60    // Images
61    for image in &slide.images {
62        if let Some(img_html) = render_image(image) {
63            html.push_str(&img_html);
64        }
65    }
66    
67    // Code Blocks
68    for code in &slide.code_blocks {
69        html.push_str("<pre><code>");
70        html.push_str(&code.code);
71        html.push_str("</code></pre>\n");
72    }
73    
74    html.push_str("</div>\n"); // content
75    html.push_str("</div>\n"); // slide
76    
77    html
78}
79
80fn render_image(image: &Image) -> Option<String> {
81    let bytes = image.get_bytes()?;
82    let b64 = base64_encode(&bytes);
83    let mime = match image.format.to_lowercase().as_str() {
84        "jpg" | "jpeg" => "image/jpeg",
85        "png" => "image/png",
86        "gif" => "image/gif",
87        "svg" => "image/svg+xml",
88        _ => "application/octet-stream",
89    };
90    
91    // Calculate style for positioning
92    // Converting EMUs to percentage or px is tricky without context of slide size.
93    // For simple HTML export, we might just display images inline or block.
94    // Or we can try to use absolute positioning if we assume a fixed slide size (e.g. 16:9 aspect ratio).
95    // Let's use simple block display for now to be safe.
96    
97    Some(format!(
98        "<div class=\"image-container\"><img src=\"data:{};base64,{}\" alt=\"{}\" /></div>\n",
99        mime, b64, image.filename
100    ))
101}
102
103// Simple base64 encoder
104fn base64_encode(data: &[u8]) -> String {
105    const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
106    let mut output = String::with_capacity(data.len() * 4 / 3 + 4);
107    
108    let mut i = 0;
109    while i < data.len() {
110        let mut buf = [0u8; 3];
111        let mut len = 0;
112        
113        for j in 0..3 {
114            if i + j < data.len() {
115                buf[j] = data[i + j];
116                len += 1;
117            }
118        }
119        
120        let b0 = (buf[0] >> 2) & 0x3F;
121        let b1 = ((buf[0] & 0x03) << 4) | ((buf[1] >> 4) & 0x0F);
122        let b2 = ((buf[1] & 0x0F) << 2) | ((buf[2] >> 6) & 0x03);
123        let b3 = buf[2] & 0x3F;
124        
125        output.push(ALPHABET[b0 as usize] as char);
126        output.push(ALPHABET[b1 as usize] as char);
127        
128        if len > 1 {
129            output.push(ALPHABET[b2 as usize] as char);
130        } else {
131            output.push('=');
132        }
133        
134        if len > 2 {
135            output.push(ALPHABET[b3 as usize] as char);
136        } else {
137            output.push('=');
138        }
139        
140        i += 3;
141    }
142    
143    output
144}