use crate::slides::types::{
ElementContent, ElementStyle, Presentation, 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("
Presentation Title
".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("Subtitle
".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,
transition_config: None,
media: 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("Slide Title
".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("Content goes here...
".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,
transition_config: None,
media: 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,
transition_config: None,
media: 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#"
"#,
);
html.push_str(&presentation.name);
html.push_str(
r#"
"#,
);
for slide in &presentation.slides {
let bg_color = slide
.background
.color
.as_deref()
.unwrap_or("#ffffff");
html.push_str(&format!(
r#"
"#,
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#"
{}
"#,
element.element_type, style, content
));
}
html.push_str("
\n");
}
html.push_str("\n");
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::()
.trim_matches('_')
.to_string()
}
pub fn export_to_svg(slide: &Slide, width: u32, height: u32) -> String {
let mut svg = format!(
r#"
");
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 {
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#"
"#);
for (idx, slide) in presentation.slides.iter().enumerate() {
xml.push_str(&format!(
"\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#"
{}
"#,
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#"
"#,
element.x, element.y, element.width, element.height, fill
));
}
"circle" | "ellipse" => {
xml.push_str(&format!(
r#"
"#,
element.x, element.y, element.width, element.height, fill
));
}
_ => {}
}
}
"image" => {
if let Some(ref src) = element.content.src {
xml.push_str(&format!(
r#"
"#,
element.x, element.y, element.width, element.height, src
));
}
}
_ => {}
}
}
xml.push_str("\n");
}
xml.push_str("\n\n");
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 {
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!("{}
", 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,
}
}