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
|
|
|
use crate::slides::types::{
|
2026-01-11 12:22:14 -03:00
|
|
|
ElementContent, ElementStyle, Presentation, PresentationTheme, Slide, SlideBackground,
|
|
|
|
|
SlideElement, ThemeColors, ThemeFonts,
|
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
|
|
|
};
|
|
|
|
|
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,
|
2026-01-11 18:49:04 -03:00
|
|
|
transition_config: None,
|
|
|
|
|
media: None,
|
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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
2026-01-11 18:49:04 -03:00
|
|
|
transition_config: None,
|
|
|
|
|
media: None,
|
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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
2026-01-11 18:49:04 -03:00
|
|
|
transition_config: None,
|
|
|
|
|
media: None,
|
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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
}
|
2026-01-11 12:22:14 -03:00
|
|
|
|
|
|
|
|
pub fn export_to_svg(slide: &Slide, width: u32, height: u32) -> String {
|
|
|
|
|
let mut svg = format!(
|
|
|
|
|
r#"<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="{}" height="{}" viewBox="0 0 {} {}">
|
|
|
|
|
"#,
|
|
|
|
|
width, height, width, height
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let bg_color = slide.background.color.as_deref().unwrap_or("#ffffff");
|
|
|
|
|
svg.push_str(&format!(
|
|
|
|
|
r#" <rect width="100%" height="100%" fill="{}"/>
|
|
|
|
|
"#,
|
|
|
|
|
bg_color
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
for element in &slide.elements {
|
|
|
|
|
match element.element_type.as_str() {
|
|
|
|
|
"text" => {
|
|
|
|
|
let text = element.content.text.as_deref().unwrap_or("");
|
|
|
|
|
let font_size = element.style.font_size.unwrap_or(18.0);
|
|
|
|
|
let color = element.style.color.as_deref().unwrap_or("#000000");
|
|
|
|
|
let font_family = element.style.font_family.as_deref().unwrap_or("Arial");
|
|
|
|
|
let font_weight = element.style.font_weight.as_deref().unwrap_or("normal");
|
|
|
|
|
|
|
|
|
|
svg.push_str(&format!(
|
|
|
|
|
r#" <text x="{}" y="{}" font-family="{}" font-size="{}" font-weight="{}" fill="{}">{}</text>
|
|
|
|
|
"#,
|
|
|
|
|
element.x,
|
|
|
|
|
element.y + font_size,
|
|
|
|
|
font_family,
|
|
|
|
|
font_size,
|
|
|
|
|
font_weight,
|
|
|
|
|
color,
|
|
|
|
|
xml_escape(text)
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
"shape" => {
|
|
|
|
|
let shape_type = element.content.shape_type.as_deref().unwrap_or("rectangle");
|
|
|
|
|
let fill = element.style.fill.as_deref().unwrap_or("#cccccc");
|
|
|
|
|
let stroke = element.style.stroke.as_deref().unwrap_or("none");
|
|
|
|
|
let stroke_width = element.style.stroke_width.unwrap_or(1.0);
|
|
|
|
|
|
|
|
|
|
match shape_type {
|
|
|
|
|
"rectangle" | "rect" => {
|
|
|
|
|
let rx = element.style.border_radius.unwrap_or(0.0);
|
|
|
|
|
svg.push_str(&format!(
|
|
|
|
|
r#" <rect x="{}" y="{}" width="{}" height="{}" rx="{}" fill="{}" stroke="{}" stroke-width="{}"/>
|
|
|
|
|
"#,
|
|
|
|
|
element.x, element.y, element.width, element.height, rx, fill, stroke, stroke_width
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
"circle" | "ellipse" => {
|
|
|
|
|
let cx = element.x + element.width / 2.0;
|
|
|
|
|
let cy = element.y + element.height / 2.0;
|
|
|
|
|
let rx = element.width / 2.0;
|
|
|
|
|
let ry = element.height / 2.0;
|
|
|
|
|
svg.push_str(&format!(
|
|
|
|
|
r#" <ellipse cx="{}" cy="{}" rx="{}" ry="{}" fill="{}" stroke="{}" stroke-width="{}"/>
|
|
|
|
|
"#,
|
|
|
|
|
cx, cy, rx, ry, fill, stroke, stroke_width
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
"line" => {
|
|
|
|
|
svg.push_str(&format!(
|
|
|
|
|
r#" <line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{}" stroke-width="{}"/>
|
|
|
|
|
"#,
|
|
|
|
|
element.x,
|
|
|
|
|
element.y,
|
|
|
|
|
element.x + element.width,
|
|
|
|
|
element.y + element.height,
|
|
|
|
|
stroke,
|
|
|
|
|
stroke_width
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
"triangle" => {
|
|
|
|
|
let x1 = element.x + element.width / 2.0;
|
|
|
|
|
let y1 = element.y;
|
|
|
|
|
let x2 = element.x;
|
|
|
|
|
let y2 = element.y + element.height;
|
|
|
|
|
let x3 = element.x + element.width;
|
|
|
|
|
let y3 = element.y + element.height;
|
|
|
|
|
svg.push_str(&format!(
|
|
|
|
|
r#" <polygon points="{},{} {},{} {},{}" fill="{}" stroke="{}" stroke-width="{}"/>
|
|
|
|
|
"#,
|
|
|
|
|
x1, y1, x2, y2, x3, y3, fill, stroke, stroke_width
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
"image" => {
|
|
|
|
|
if let Some(ref src) = element.content.src {
|
|
|
|
|
svg.push_str(&format!(
|
|
|
|
|
r#" <image x="{}" y="{}" width="{}" height="{}" href="{}"/>
|
|
|
|
|
"#,
|
|
|
|
|
element.x, element.y, element.width, element.height, src
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
svg.push_str("</svg>");
|
|
|
|
|
svg
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn xml_escape(s: &str) -> String {
|
|
|
|
|
s.replace('&', "&")
|
|
|
|
|
.replace('<', "<")
|
|
|
|
|
.replace('>', ">")
|
|
|
|
|
.replace('"', """)
|
|
|
|
|
.replace('\'', "'")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn export_slide_to_png_placeholder(slide: &Slide, width: u32, height: u32) -> Vec<u8> {
|
|
|
|
|
let svg = export_to_svg(slide, width, height);
|
|
|
|
|
svg.into_bytes()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn export_to_odp_content(presentation: &Presentation) -> String {
|
|
|
|
|
let mut xml = String::from(r#"<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
|
<office:document-content xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
|
|
|
|
|
xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0"
|
|
|
|
|
xmlns:presentation="urn:oasis:names:tc:opendocument:xmlns:presentation:1.0"
|
|
|
|
|
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"
|
|
|
|
|
xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0"
|
|
|
|
|
office:version="1.2">
|
|
|
|
|
<office:body>
|
|
|
|
|
<office:presentation>
|
|
|
|
|
"#);
|
|
|
|
|
|
|
|
|
|
for (idx, slide) in presentation.slides.iter().enumerate() {
|
|
|
|
|
xml.push_str(&format!(
|
|
|
|
|
"<draw:page draw:name=\"Slide{}\" draw:style-name=\"dp1\">\n",
|
|
|
|
|
idx + 1
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
for element in &slide.elements {
|
|
|
|
|
match element.element_type.as_str() {
|
|
|
|
|
"text" => {
|
|
|
|
|
let text = element.content.text.as_deref().unwrap_or("");
|
|
|
|
|
xml.push_str(&format!(
|
|
|
|
|
r#" <draw:frame svg:x="{}pt" svg:y="{}pt" svg:width="{}pt" svg:height="{}pt">
|
|
|
|
|
<draw:text-box>
|
|
|
|
|
<text:p>{}</text:p>
|
|
|
|
|
</draw:text-box>
|
|
|
|
|
</draw:frame>
|
|
|
|
|
"#,
|
|
|
|
|
element.x, element.y, element.width, element.height, xml_escape(text)
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
"shape" => {
|
|
|
|
|
let shape_type = element.content.shape_type.as_deref().unwrap_or("rectangle");
|
|
|
|
|
let fill = element.style.fill.as_deref().unwrap_or("#cccccc");
|
|
|
|
|
|
|
|
|
|
match shape_type {
|
|
|
|
|
"rectangle" | "rect" => {
|
|
|
|
|
xml.push_str(&format!(
|
|
|
|
|
r#" <draw:rect svg:x="{}pt" svg:y="{}pt" svg:width="{}pt" svg:height="{}pt" draw:fill-color="{}"/>
|
|
|
|
|
"#,
|
|
|
|
|
element.x, element.y, element.width, element.height, fill
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
"circle" | "ellipse" => {
|
|
|
|
|
xml.push_str(&format!(
|
|
|
|
|
r#" <draw:ellipse svg:x="{}pt" svg:y="{}pt" svg:width="{}pt" svg:height="{}pt" draw:fill-color="{}"/>
|
|
|
|
|
"#,
|
|
|
|
|
element.x, element.y, element.width, element.height, fill
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
"image" => {
|
|
|
|
|
if let Some(ref src) = element.content.src {
|
|
|
|
|
xml.push_str(&format!(
|
|
|
|
|
r#" <draw:frame svg:x="{}pt" svg:y="{}pt" svg:width="{}pt" svg:height="{}pt">
|
|
|
|
|
<draw:image xlink:href="{}"/>
|
|
|
|
|
</draw:frame>
|
|
|
|
|
"#,
|
|
|
|
|
element.x, element.y, element.width, element.height, src
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
xml.push_str("</draw:page>\n");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
xml.push_str("</office:presentation>\n</office:body>\n</office:document-content>");
|
|
|
|
|
xml
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn export_to_json(presentation: &Presentation) -> String {
|
|
|
|
|
serde_json::to_string_pretty(presentation).unwrap_or_default()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn export_to_markdown(presentation: &Presentation) -> String {
|
|
|
|
|
let mut md = format!("# {}\n\n", presentation.name);
|
|
|
|
|
|
|
|
|
|
for (idx, slide) in presentation.slides.iter().enumerate() {
|
|
|
|
|
md.push_str(&format!("---\n\n## Slide {}\n\n", idx + 1));
|
|
|
|
|
|
|
|
|
|
for element in &slide.elements {
|
|
|
|
|
if element.element_type == "text" {
|
|
|
|
|
if let Some(ref text) = element.content.text {
|
|
|
|
|
let font_size = element.style.font_size.unwrap_or(18.0);
|
|
|
|
|
if font_size >= 32.0 {
|
|
|
|
|
md.push_str(&format!("### {}\n\n", text));
|
|
|
|
|
} else {
|
|
|
|
|
md.push_str(&format!("{}\n\n", text));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if element.element_type == "image" {
|
|
|
|
|
if let Some(ref src) = element.content.src {
|
|
|
|
|
md.push_str(&format!("\n\n", src));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(ref notes) = slide.notes {
|
|
|
|
|
md.push_str(&format!("**Speaker Notes:**\n{}\n\n", notes));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
md
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn slides_from_markdown(md: &str) -> Vec<Slide> {
|
|
|
|
|
let theme = create_default_theme();
|
|
|
|
|
let mut slides = Vec::new();
|
|
|
|
|
let sections: Vec<&str> = md.split("\n---\n").collect();
|
|
|
|
|
|
|
|
|
|
for section in sections {
|
|
|
|
|
let lines: Vec<&str> = section.lines().filter(|l| !l.trim().is_empty()).collect();
|
|
|
|
|
if lines.is_empty() {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut slide = create_blank_slide(&theme);
|
|
|
|
|
let mut y_offset = 50.0;
|
|
|
|
|
|
|
|
|
|
for line in lines {
|
|
|
|
|
let trimmed = line.trim();
|
|
|
|
|
if trimmed.starts_with("# ") {
|
|
|
|
|
slide.elements.push(create_text_element(
|
|
|
|
|
&trimmed[2..],
|
|
|
|
|
50.0,
|
|
|
|
|
y_offset,
|
|
|
|
|
860.0,
|
|
|
|
|
60.0,
|
|
|
|
|
44.0,
|
|
|
|
|
true,
|
|
|
|
|
&theme,
|
|
|
|
|
));
|
|
|
|
|
y_offset += 80.0;
|
|
|
|
|
} else if trimmed.starts_with("## ") {
|
|
|
|
|
slide.elements.push(create_text_element(
|
|
|
|
|
&trimmed[3..],
|
|
|
|
|
50.0,
|
|
|
|
|
y_offset,
|
|
|
|
|
860.0,
|
|
|
|
|
50.0,
|
|
|
|
|
32.0,
|
|
|
|
|
true,
|
|
|
|
|
&theme,
|
|
|
|
|
));
|
|
|
|
|
y_offset += 60.0;
|
|
|
|
|
} else if trimmed.starts_with("### ") {
|
|
|
|
|
slide.elements.push(create_text_element(
|
|
|
|
|
&trimmed[4..],
|
|
|
|
|
50.0,
|
|
|
|
|
y_offset,
|
|
|
|
|
860.0,
|
|
|
|
|
40.0,
|
|
|
|
|
24.0,
|
|
|
|
|
true,
|
|
|
|
|
&theme,
|
|
|
|
|
));
|
|
|
|
|
y_offset += 50.0;
|
|
|
|
|
} else if trimmed.starts_with("![") {
|
|
|
|
|
if let Some(start) = trimmed.find('(') {
|
|
|
|
|
if let Some(end) = trimmed.find(')') {
|
|
|
|
|
let src = &trimmed[start + 1..end];
|
|
|
|
|
slide.elements.push(SlideElement {
|
|
|
|
|
id: Uuid::new_v4().to_string(),
|
|
|
|
|
element_type: "image".to_string(),
|
|
|
|
|
x: 50.0,
|
|
|
|
|
y: y_offset,
|
|
|
|
|
width: 400.0,
|
|
|
|
|
height: 300.0,
|
|
|
|
|
rotation: 0.0,
|
|
|
|
|
content: ElementContent {
|
|
|
|
|
text: None,
|
|
|
|
|
html: None,
|
|
|
|
|
src: Some(src.to_string()),
|
|
|
|
|
shape_type: None,
|
|
|
|
|
chart_data: None,
|
|
|
|
|
table_data: None,
|
|
|
|
|
},
|
|
|
|
|
style: ElementStyle::default(),
|
|
|
|
|
animations: vec![],
|
|
|
|
|
z_index: slide.elements.len() as i32,
|
|
|
|
|
locked: false,
|
|
|
|
|
});
|
|
|
|
|
y_offset += 320.0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if !trimmed.is_empty() {
|
|
|
|
|
slide.elements.push(create_text_element(
|
|
|
|
|
trimmed,
|
|
|
|
|
50.0,
|
|
|
|
|
y_offset,
|
|
|
|
|
860.0,
|
|
|
|
|
30.0,
|
|
|
|
|
18.0,
|
|
|
|
|
false,
|
|
|
|
|
&theme,
|
|
|
|
|
));
|
|
|
|
|
y_offset += 40.0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
slides.push(slide);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if slides.is_empty() {
|
|
|
|
|
slides.push(create_title_slide(&theme));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
slides
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn create_text_element(
|
|
|
|
|
text: &str,
|
|
|
|
|
x: f64,
|
|
|
|
|
y: f64,
|
|
|
|
|
width: f64,
|
|
|
|
|
height: f64,
|
|
|
|
|
font_size: f64,
|
|
|
|
|
bold: bool,
|
|
|
|
|
theme: &PresentationTheme,
|
|
|
|
|
) -> SlideElement {
|
|
|
|
|
SlideElement {
|
|
|
|
|
id: Uuid::new_v4().to_string(),
|
|
|
|
|
element_type: "text".to_string(),
|
|
|
|
|
x,
|
|
|
|
|
y,
|
|
|
|
|
width,
|
|
|
|
|
height,
|
|
|
|
|
rotation: 0.0,
|
|
|
|
|
content: ElementContent {
|
|
|
|
|
text: Some(text.to_string()),
|
|
|
|
|
html: Some(format!("<p>{}</p>", text)),
|
|
|
|
|
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(font_size),
|
|
|
|
|
font_weight: if bold { Some("bold".to_string()) } else { None },
|
|
|
|
|
font_style: None,
|
|
|
|
|
text_align: Some("left".to_string()),
|
|
|
|
|
vertical_align: Some("top".to_string()),
|
|
|
|
|
color: Some(theme.colors.text.clone()),
|
|
|
|
|
line_height: None,
|
|
|
|
|
border_radius: None,
|
|
|
|
|
},
|
|
|
|
|
animations: vec![],
|
|
|
|
|
z_index: 0,
|
|
|
|
|
locked: false,
|
|
|
|
|
}
|
|
|
|
|
}
|