generalbots/src/slides/utils.rs
Rodrigo Rodriguez (Pragmatismo) 3e75bbff97 MS Office 100% Compatibility - Phase 1 Implementation
- Add rust_xlsxwriter for Excel export with formatting support
- Add docx-rs for Word document import/export with HTML conversion
- Add PPTX export support with slides, shapes, and text elements
- Refactor sheet module into 7 files (types, formulas, handlers, etc)
- Refactor docs module into 6 files (types, handlers, storage, etc)
- Refactor slides module into 6 files (types, handlers, storage, etc)
- Fix collaboration modules (borrow issues, rand compatibility)
- Add ooxmlsdk dependency for future Office 2021 features
- Fix type mismatches in slides storage
- Update security protection API router type

Features:
- Excel: Read xlsx/xlsm/xls, write xlsx with styles
- Word: Read/write docx with formatting preservation
- PowerPoint: Write pptx with slides, shapes, text
- Real-time collaboration via WebSocket (already working)
- Theme-aware UI with --sentient-* CSS variables
2026-01-11 09:56:15 -03:00

314 lines
10 KiB
Rust

use crate::slides::types::{
ElementContent, ElementStyle, PresentationTheme, Slide, SlideBackground, SlideElement,
ThemeColors, ThemeFonts,
};
use uuid::Uuid;
pub fn create_default_theme() -> PresentationTheme {
PresentationTheme {
name: "Default".to_string(),
colors: ThemeColors {
primary: "#1a73e8".to_string(),
secondary: "#34a853".to_string(),
accent: "#ea4335".to_string(),
background: "#ffffff".to_string(),
text: "#202124".to_string(),
text_light: "#5f6368".to_string(),
},
fonts: ThemeFonts {
heading: "Arial".to_string(),
body: "Arial".to_string(),
},
}
}
pub fn create_title_slide(theme: &PresentationTheme) -> Slide {
Slide {
id: Uuid::new_v4().to_string(),
layout: "title".to_string(),
elements: vec![
SlideElement {
id: Uuid::new_v4().to_string(),
element_type: "text".to_string(),
x: 100.0,
y: 200.0,
width: 760.0,
height: 100.0,
rotation: 0.0,
content: ElementContent {
text: Some("Presentation Title".to_string()),
html: Some("<h1>Presentation Title</h1>".to_string()),
src: None,
shape_type: None,
chart_data: None,
table_data: None,
},
style: ElementStyle {
fill: None,
stroke: None,
stroke_width: None,
opacity: None,
shadow: None,
font_family: Some(theme.fonts.heading.clone()),
font_size: Some(44.0),
font_weight: Some("bold".to_string()),
font_style: None,
text_align: Some("center".to_string()),
vertical_align: Some("middle".to_string()),
color: Some(theme.colors.text.clone()),
line_height: None,
border_radius: None,
},
animations: vec![],
z_index: 1,
locked: false,
},
SlideElement {
id: Uuid::new_v4().to_string(),
element_type: "text".to_string(),
x: 100.0,
y: 320.0,
width: 760.0,
height: 60.0,
rotation: 0.0,
content: ElementContent {
text: Some("Subtitle".to_string()),
html: Some("<p>Subtitle</p>".to_string()),
src: None,
shape_type: None,
chart_data: None,
table_data: None,
},
style: ElementStyle {
fill: None,
stroke: None,
stroke_width: None,
opacity: None,
shadow: None,
font_family: Some(theme.fonts.body.clone()),
font_size: Some(24.0),
font_weight: None,
font_style: None,
text_align: Some("center".to_string()),
vertical_align: Some("middle".to_string()),
color: Some(theme.colors.text_light.clone()),
line_height: None,
border_radius: None,
},
animations: vec![],
z_index: 2,
locked: false,
},
],
background: SlideBackground {
bg_type: "solid".to_string(),
color: Some(theme.colors.background.clone()),
gradient: None,
image_url: None,
image_fit: None,
},
notes: None,
transition: None,
}
}
pub fn create_content_slide(theme: &PresentationTheme) -> Slide {
Slide {
id: Uuid::new_v4().to_string(),
layout: "content".to_string(),
elements: vec![
SlideElement {
id: Uuid::new_v4().to_string(),
element_type: "text".to_string(),
x: 50.0,
y: 40.0,
width: 860.0,
height: 60.0,
rotation: 0.0,
content: ElementContent {
text: Some("Slide Title".to_string()),
html: Some("<h2>Slide Title</h2>".to_string()),
src: None,
shape_type: None,
chart_data: None,
table_data: None,
},
style: ElementStyle {
fill: None,
stroke: None,
stroke_width: None,
opacity: None,
shadow: None,
font_family: Some(theme.fonts.heading.clone()),
font_size: Some(32.0),
font_weight: Some("bold".to_string()),
font_style: None,
text_align: Some("left".to_string()),
vertical_align: Some("middle".to_string()),
color: Some(theme.colors.text.clone()),
line_height: None,
border_radius: None,
},
animations: vec![],
z_index: 1,
locked: false,
},
SlideElement {
id: Uuid::new_v4().to_string(),
element_type: "text".to_string(),
x: 50.0,
y: 120.0,
width: 860.0,
height: 400.0,
rotation: 0.0,
content: ElementContent {
text: Some("Content goes here...".to_string()),
html: Some("<p>Content goes here...</p>".to_string()),
src: None,
shape_type: None,
chart_data: None,
table_data: None,
},
style: ElementStyle {
fill: None,
stroke: None,
stroke_width: None,
opacity: None,
shadow: None,
font_family: Some(theme.fonts.body.clone()),
font_size: Some(18.0),
font_weight: None,
font_style: None,
text_align: Some("left".to_string()),
vertical_align: Some("top".to_string()),
color: Some(theme.colors.text.clone()),
line_height: Some(1.5),
border_radius: None,
},
animations: vec![],
z_index: 2,
locked: false,
},
],
background: SlideBackground {
bg_type: "solid".to_string(),
color: Some(theme.colors.background.clone()),
gradient: None,
image_url: None,
image_fit: None,
},
notes: None,
transition: None,
}
}
pub fn create_blank_slide(theme: &PresentationTheme) -> Slide {
Slide {
id: Uuid::new_v4().to_string(),
layout: "blank".to_string(),
elements: vec![],
background: SlideBackground {
bg_type: "solid".to_string(),
color: Some(theme.colors.background.clone()),
gradient: None,
image_url: None,
image_fit: None,
},
notes: None,
transition: None,
}
}
pub fn get_user_presentations_path(user_id: &str) -> String {
format!("users/{}/presentations", user_id)
}
pub fn generate_presentation_id() -> String {
Uuid::new_v4().to_string()
}
pub fn export_to_html(presentation: &crate::slides::types::Presentation) -> String {
let mut html = String::from(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>"#,
);
html.push_str(&presentation.name);
html.push_str(
r#"</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: Arial, sans-serif; background: #000; }
.slide {
width: 960px;
height: 540px;
margin: 20px auto;
position: relative;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
}
.element { position: absolute; }
.element-text { white-space: pre-wrap; }
</style>
</head>
<body>
"#,
);
for slide in &presentation.slides {
let bg_color = slide
.background
.color
.as_deref()
.unwrap_or("#ffffff");
html.push_str(&format!(
r#" <div class="slide" style="background-color: {};">
"#,
bg_color
));
for element in &slide.elements {
let style = format!(
"left: {}px; top: {}px; width: {}px; height: {}px;",
element.x, element.y, element.width, element.height
);
let content = element
.content
.html
.as_deref()
.or(element.content.text.as_deref())
.unwrap_or("");
html.push_str(&format!(
r#" <div class="element element-{}" style="{}">{}</div>
"#,
element.element_type, style, content
));
}
html.push_str(" </div>\n");
}
html.push_str("</body>\n</html>");
html
}
pub fn sanitize_filename(name: &str) -> String {
name.chars()
.map(|c| {
if c.is_alphanumeric() || c == '-' || c == '_' || c == '.' {
c
} else if c == ' ' {
'_'
} else {
'_'
}
})
.collect::<String>()
.trim_matches('_')
.to_string()
}