import os import requests import streamlit as st from io import BytesIO from PIL import Image from pptx.enum.shapes import MSO_SHAPE from pptx.enum.shapes import MSO_SHAPE_TYPE from pptx.dml.color import RGBColor from pptx.util import Inches, Pt import random import json import base64 # Define standard icons and shapes that come with python-pptx STANDARD_SHAPES = { "rectangle": MSO_SHAPE.RECTANGLE, "rounded_rectangle": MSO_SHAPE.ROUNDED_RECTANGLE, "oval": MSO_SHAPE.OVAL, "triangle": MSO_SHAPE.ISOSCELES_TRIANGLE, "right_triangle": MSO_SHAPE.RIGHT_TRIANGLE, "diamond": MSO_SHAPE.DIAMOND, "pentagon": MSO_SHAPE.PENTAGON, "hexagon": MSO_SHAPE.HEXAGON, "heptagon": MSO_SHAPE.HEPTAGON, "octagon": MSO_SHAPE.OCTAGON, "star": MSO_SHAPE.STAR_5_POINT, "arrow": MSO_SHAPE.RIGHT_ARROW, "up_arrow": MSO_SHAPE.UP_ARROW, "down_arrow": MSO_SHAPE.DOWN_ARROW, "left_arrow": MSO_SHAPE.LEFT_ARROW, "curved_arrow": MSO_SHAPE.CURVED_RIGHT_ARROW, "curved_up_arrow": MSO_SHAPE.CURVED_UP_ARROW, "curved_down_arrow": MSO_SHAPE.CURVED_DOWN_ARROW, "curved_left_arrow": MSO_SHAPE.CURVED_LEFT_ARROW, "smiley_face": MSO_SHAPE.SMILEY_FACE, "heart": MSO_SHAPE.HEART, "lightning_bolt": MSO_SHAPE.LIGHTNING_BOLT, "sun": MSO_SHAPE.SUN, "moon": MSO_SHAPE.MOON, "cloud": MSO_SHAPE.CLOUD, "arc": MSO_SHAPE.ARC, "bracket": MSO_SHAPE.LEFT_BRACKET, "brace": MSO_SHAPE.LEFT_BRACE, "can": MSO_SHAPE.CAN, "cube": MSO_SHAPE.CUBE, "gear": MSO_SHAPE.GEAR_6, "donut": MSO_SHAPE.DONUT, "chart": MSO_SHAPE.PIE, "plus": MSO_SHAPE.MATH_PLUS, "minus": MSO_SHAPE.MATH_MINUS, "multiply": MSO_SHAPE.MATH_MULTIPLY, "divide": MSO_SHAPE.MATH_DIVIDE, "equal": MSO_SHAPE.MATH_EQUAL, "not_equal": MSO_SHAPE.MATH_NOT_EQUAL } # Stock icon categories with predefined colors ICON_CATEGORIES = { "business": ["chart", "gear", "cube", "can", "plus", "minus", "equal"], "arrows": ["arrow", "up_arrow", "down_arrow", "left_arrow", "curved_arrow"], "basic": ["rectangle", "oval", "triangle", "diamond", "star"], "nature": ["sun", "moon", "cloud", "lightning_bolt"], "emotion": ["smiley_face", "heart"], "math": ["plus", "minus", "multiply", "divide", "equal", "not_equal"] } # Local image library path IMAGE_LIBRARY_PATH = "images" os.makedirs(IMAGE_LIBRARY_PATH, exist_ok=True) # Pexels API for stock images (need to get a free API key) PEXELS_API_KEY = os.getenv("PEXELS_API_KEY", "") def search_stock_images(query, per_page=10): """Search for stock images using Pexels API""" if not PEXELS_API_KEY: st.warning("Pexels API key not set. Using placeholder images.") return [{"url": f"https://via.placeholder.com/800x600?text=Placeholder+{i}", "thumbnail": f"https://via.placeholder.com/100x100?text=Thumbnail+{i}"} for i in range(1, 6)] try: headers = {"Authorization": PEXELS_API_KEY} response = requests.get( f"https://api.pexels.com/v1/search?query={query}&per_page={per_page}", headers=headers ) if response.status_code == 200: data = response.json() return [{"url": photo["src"]["large"], "thumbnail": photo["src"]["tiny"]} for photo in data.get("photos", [])] else: st.error(f"Error fetching stock images: {response.status_code}") return [] except Exception as e: st.error(f"Error in stock image search: {str(e)}") return [] def download_image(url, filename=None): """Download image from URL and save to local library""" try: response = requests.get(url) if response.status_code == 200: image_data = BytesIO(response.content) if filename: filepath = os.path.join(IMAGE_LIBRARY_PATH, filename) with open(filepath, 'wb') as f: f.write(response.content) return filepath else: return image_data else: st.error(f"Error downloading image: {response.status_code}") return None except Exception as e: st.error(f"Error downloading image: {str(e)}") return None def get_icon_suggestions(slide_content): """Get icon suggestions based on slide content""" content_text = "" if isinstance(slide_content.get('content', []), list): content_text = " ".join(slide_content.get('content', [])) else: content_text = str(slide_content.get('content', '')) keywords = { "growth": ["up_arrow", "chart"], "decline": ["down_arrow"], "innovation": ["gear", "lightning_bolt"], "success": ["star", "smiley_face"], "challenge": ["triangle"], "balance": ["equal"], "addition": ["plus"], "subtraction": ["minus"], "division": ["divide"], "partnership": ["heart"], "business": ["cube", "can"], "nature": ["sun", "cloud"], "direction": ["arrow", "curved_arrow"] } suggested_icons = [] for keyword, icons in keywords.items(): if keyword.lower() in content_text.lower(): suggested_icons.extend(icons) # If no specific matches, return some general business icons if not suggested_icons: suggested_icons = random.sample(ICON_CATEGORIES["business"], min(3, len(ICON_CATEGORIES["business"]))) # Deduplicate return list(set(suggested_icons)) def get_shape_by_name(shape_name): """Get shape enum from name""" return STANDARD_SHAPES.get(shape_name.lower(), MSO_SHAPE.RECTANGLE) def add_shape_to_slide(slide, shape_name, left=Inches(1), top=Inches(1), width=Inches(1), height=Inches(1), fill_color=None): """Add a shape to a slide""" shape_type = get_shape_by_name(shape_name) shape = slide.shapes.add_shape(shape_type, left, top, width, height) if fill_color: # Convert hex color to RGB if fill_color.startswith('#'): r = int(fill_color[1:3], 16) g = int(fill_color[3:5], 16) b = int(fill_color[5:7], 16) shape.fill.solid() shape.fill.fore_color.rgb = RGBColor(r, g, b) return shape def add_image_to_slide(slide, image_stream, left=Inches(1), top=Inches(1), width=Inches(4), height=Inches(3)): """Add an image to a slide""" try: picture = slide.shapes.add_picture(image_stream, left, top, width, height) return picture except Exception as e: st.error(f"Error adding image to slide: {str(e)}") return None def create_chart_based_on_content(slide, chart_type, data, left=Inches(1), top=Inches(1), width=Inches(6), height=Inches(4)): """Create a chart based on the slide content""" try: # Basic chart types supported by python-pptx chart_types = { "bar": 1, # MSO_CHART_TYPE.BAR_CLUSTERED "column": 0, # MSO_CHART_TYPE.COLUMN_CLUSTERED "line": 4, # MSO_CHART_TYPE.LINE "pie": 5, # MSO_CHART_TYPE.PIE "scatter": 15, # MSO_CHART_TYPE.SCATTER "area": 8, # MSO_CHART_TYPE.AREA } chart_type_id = chart_types.get(chart_type.lower(), 0) # Default to column chart # Create sample data if not provided if not data: data = { "categories": ["Category 1", "Category 2", "Category 3", "Category 4"], "series": [ { "name": "Series 1", "values": [4.3, 2.5, 3.5, 4.5] }, { "name": "Series 2", "values": [2.4, 4.4, 1.8, 2.8] } ] } # Add chart to slide chart = slide.shapes.add_chart( chart_type_id, left, top, width, height ).chart # Set chart data chart_data = chart.chart_data # Set categories chart_data.categories = data.get("categories", ["Category 1", "Category 2", "Category 3"]) # Add series for i, series in enumerate(data.get("series", [])): if i == 0: # First series already exists chart_data.series[0].name = series.get("name", f"Series {i+1}") chart_data.series[0].values = series.get("values", [1, 2, 3]) else: # Add additional series new_series = chart_data.series.add() new_series.name = series.get("name", f"Series {i+1}") new_series.values = series.get("values", [1, 2, 3]) return chart except Exception as e: st.error(f"Error creating chart: {str(e)}") return None def analyze_slide_for_visuals(slide_content): """Analyze slide content to suggest appropriate visuals""" content_text = "" if isinstance(slide_content.get('content', []), list): content_text = " ".join(slide_content.get('content', [])) else: content_text = str(slide_content.get('content', '')) title_text = slide_content.get('title', '') purpose = slide_content.get('purpose', '') # Analyze for data visualization needs data_viz_keywords = [ 'compare', 'comparison', 'versus', 'vs', 'trend', 'growth', 'decline', 'increase', 'decrease', 'percentage', 'proportion', 'distribution', 'data', 'statistics', 'numbers', 'metrics', 'kpi' ] # Chart type suggestions based on content chart_suggestions = { 'comparison': 'bar', 'trend': 'line', 'growth': 'line', 'decline': 'line', 'percentage': 'pie', 'proportion': 'pie', 'distribution': 'column', 'correlation': 'scatter', } need_chart = False suggested_chart_type = 'column' for keyword in data_viz_keywords: if keyword.lower() in content_text.lower() or keyword.lower() in title_text.lower(): need_chart = True # Find most appropriate chart type for chart_keyword, chart_type in chart_suggestions.items(): if chart_keyword.lower() in content_text.lower() or chart_keyword.lower() in title_text.lower(): suggested_chart_type = chart_type break # Analyze for image needs image_keywords = [ 'show', 'display', 'picture', 'image', 'photo', 'visual', 'illustration', 'diagram', 'screenshot', 'graphic' ] need_image = False image_search_query = "" for keyword in image_keywords: if keyword.lower() in content_text.lower() or keyword.lower() in purpose.lower(): need_image = True # Try to extract the subject of the image text_parts = content_text.split() keyword_index = -1 for i, part in enumerate(text_parts): if keyword.lower() in part.lower(): keyword_index = i break if keyword_index != -1 and keyword_index < len(text_parts) - 1: # Take the next few words as the image subject image_search_query = " ".join(text_parts[keyword_index+1:keyword_index+4]) else: # Use the title as a fallback image_search_query = title_text # If no specific image query was found, use title if not image_search_query: image_search_query = title_text # Analyze for icon needs icon_suggestions = get_icon_suggestions(slide_content) # Analyze layout needs layout_keywords = { 'compare': 'Two Column', 'versus': 'Two Column', 'vs': 'Two Column', 'quote': 'Quote', 'testimonial': 'Quote', 'picture': 'Picture with Caption', 'image': 'Picture with Caption', 'photo': 'Picture with Caption', 'overview': 'Title Only', 'list': 'Standard' } suggested_layout = 'Standard' for keyword, layout in layout_keywords.items(): if (keyword.lower() in content_text.lower() or keyword.lower() in title_text.lower() or keyword.lower() in purpose.lower()): suggested_layout = layout break return { 'need_chart': need_chart, 'chart_type': suggested_chart_type, 'need_image': need_image, 'image_query': image_search_query, 'icon_suggestions': icon_suggestions, 'suggested_layout': suggested_layout } def get_random_placeholder_image(): """Generate a random placeholder image""" width = random.randint(800, 1200) height = random.randint(600, 800) image_url = f"https://via.placeholder.com/{width}x{height}?text=SlideGator+Placeholder" return download_image(image_url) def get_default_icon_set(): """Return a set of default icons for common topics""" return { "business": ["cube", "gear", "chart"], "growth": ["up_arrow", "chart"], "teamwork": ["heart", "smiley_face"], "innovation": ["lightning_bolt", "star"], "nature": ["sun", "cloud"], "time": ["clock", "hourglass"] } def generate_html_preview_with_visuals(slide, template_name): """Generate HTML preview that includes visual representations of charts, icons, images""" from utils import TEMPLATES # Get template info template = TEMPLATES.get(template_name, TEMPLATES["professional"]) colors = template["colors"] fonts = template["fonts"] # Prepare content title = slide.get('title', 'Untitled Slide') if isinstance(slide.get('content', []), list): content = slide.get('content', []) else: content_text = slide.get('content', '') content = content_text.split('\n') if content_text else [] # Determine layout based on slide design or content layout_type = "standard" if "design" in slide and "layout" in slide["design"]: layout_type = slide["design"]["layout"].lower() # Check for visual elements visual_elements = slide.get('visual_elements', []) if isinstance(visual_elements, str): visual_elements = [visual_elements] # Analyze what visuals would be appropriate visual_analysis = analyze_slide_for_visuals(slide) # Prepare visual HTML visual_html = "" # Handle charts if needed if visual_analysis['need_chart'] or any('chart' in str(ve).lower() for ve in visual_elements): chart_type = visual_analysis['chart_type'] chart_html = f"""
{chart_type.capitalize()} Chart
Based on slide content analysis
""" visual_html += chart_html # Handle images if needed if visual_analysis['need_image'] or any('image' in str(ve).lower() for ve in visual_elements): image_query = visual_analysis['image_query'] image_html = f"""
Image: "{image_query}"
Click 'Add Image' to select
""" visual_html += image_html # Handle icons if needed if visual_analysis['icon_suggestions']: icons = visual_analysis['icon_suggestions'] icon_html = f"""
{"⚙️"}
{"📈"}
{"🔍"}
""" visual_html += icon_html # Generate HTML based on layout type if "two column" in layout_type or "comparison" in layout_type: # Two-column layout mid_point = len(content) // 2 left_content = content[:mid_point] right_content = content[mid_point:] content_html = f"""
""" elif "quote" in layout_type: # Quote layout if content: quote_text = content[0] if isinstance(content, list) and len(content) > 0 else "Quote text" author = content[1] if isinstance(content, list) and len(content) > 1 else "" content_html = f"""
"{quote_text}"
{f'
— {author}
' if author else ''}
""" else: content_html = '

No quote content

' elif "picture" in layout_type: # Picture with caption layout caption = content[0] if content else "Image caption" content_html = f"""
Image Area
{caption}
""" else: # Standard layout with bullet points content_html = f""" """ # Create the full HTML preview html = f"""
{title}
{visual_html}
{content_html}
""" return html def apply_visuals_to_pptx_slide(slide, slide_content, template_colors): """Apply the suggested visuals to an actual PowerPoint slide""" # Get content areas content_placeholders = [shape for shape in slide.placeholders if shape.placeholder_format.type != 1] # 1 is title # No content placeholders, can't continue if not content_placeholders: return slide # Analyze visual needs visual_analysis = analyze_slide_for_visuals(slide_content) visual_elements = slide_content.get('visual_elements', []) # Handle charts if visual_analysis['need_chart'] or any('chart' in str(ve).lower() for ve in visual_elements): # Getting a reference to the content placeholder placeholder = content_placeholders[0] # Extract placeholder dimensions left = placeholder.left top = placeholder.top width = placeholder.width height = placeholder.height / 2 # Use half the height for the chart # Sample chart data data = { "categories": ["Q1", "Q2", "Q3", "Q4"], "series": [ { "name": "Series 1", "values": [4.3, 2.5, 3.5, 4.5] }, { "name": "Series 2", "values": [2.4, 4.4, 1.8, 2.8] } ] } create_chart_based_on_content(slide, visual_analysis['chart_type'], data, left, top, width, height) # Adjust content placeholder position to make room for chart placeholder.top = top + height + Inches(0.5) placeholder.height = height # Handle icons if requested if visual_analysis['icon_suggestions']: icons = visual_analysis['icon_suggestions'][:3] # Limit to 3 icons # Get last shape for reference if len(slide.shapes) > 0: last_shape = slide.shapes[-1] left = Inches(0.5) top = last_shape.top + last_shape.height + Inches(0.2) else: left = Inches(0.5) top = Inches(3) # Add icons for i, icon_name in enumerate(icons): if icon_name in STANDARD_SHAPES: icon_left = left + (i * Inches(1.2)) add_shape_to_slide( slide, icon_name, left=icon_left, top=top, width=Inches(0.8), height=Inches(0.8), fill_color=template_colors.get('accent', '#0000FF') ) return slide def get_ai_generated_image_placeholder(): """Create a base64 encoded placeholder for AI-generated images""" # Simple placeholder SVG svg = ''' AI Generated Image ''' encoded = base64.b64encode(svg.encode('utf-8')).decode('utf-8') return f"data:image/svg+xml;base64,{encoded}" def generate_image_prompt(slide_content): """Generate a prompt for AI image generation based on slide content""" title = slide_content.get('title', '') if isinstance(slide_content.get('content', []), list): content_text = " ".join(slide_content.get('content', [])) else: content_text = str(slide_content.get('content', '')) prompt = f"Create an image for a slide titled '{title}' with content about {content_text[:100]}..." return prompt