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#" "#, width, height, width, height ); let bg_color = slide.background.color.as_deref().unwrap_or("#ffffff"); svg.push_str(&format!( r#" "#, 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#" {} "#, 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#" "#, 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#" "#, cx, cy, rx, ry, fill, stroke, stroke_width )); } "line" => { svg.push_str(&format!( r#" "#, 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#" "#, x1, y1, x2, y2, x3, y3, fill, stroke, stroke_width )); } _ => {} } } "image" => { if let Some(ref src) = element.content.src { svg.push_str(&format!( r#" "#, element.x, element.y, element.width, element.height, src )); } } _ => {} } } svg.push_str(""); 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!("![Image]({})\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, } }