Slide_gator / visual_elements.py
cryogenic22's picture
Update visual_elements.py
96815db verified
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"""
<div class="chart-container" style="height: 150px; margin-bottom: 15px; border: 1px dashed {colors['accent']}; text-align: center; line-height: 150px;">
<div style="display: inline-block; vertical-align: middle;">
<div style="font-weight: bold;">{chart_type.capitalize()} Chart</div>
<div style="font-size: 12px; color: {colors['accent']};">Based on slide content analysis</div>
</div>
</div>
"""
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"""
<div class="image-container" style="height: 150px; margin-bottom: 15px; border: 1px dashed {colors['accent']}; text-align: center; line-height: 150px;">
<div style="display: inline-block; vertical-align: middle;">
<div style="font-weight: bold;">Image: "{image_query}"</div>
<div style="font-size: 12px; color: {colors['accent']};">Click 'Add Image' to select</div>
</div>
</div>
"""
visual_html += image_html
# Handle icons if needed
if visual_analysis['icon_suggestions']:
icons = visual_analysis['icon_suggestions']
icon_html = f"""
<div class="icon-container" style="margin-bottom: 15px; text-align: center;">
<div style="display: inline-block; margin: 0 5px; font-size: 24px;">{"⚙️"}</div>
<div style="display: inline-block; margin: 0 5px; font-size: 24px;">{"📈"}</div>
<div style="display: inline-block; margin: 0 5px; font-size: 24px;">{"🔍"}</div>
</div>
"""
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"""
<div class="row" style="display: flex; flex-direction: row;">
<div class="col" style="flex: 1; padding-right: 10px;">
<ul>
{"".join(f'<li style="margin-bottom: 8px;">{item}</li>' for item in left_content)}
</ul>
</div>
<div class="col" style="flex: 1; padding-left: 10px;">
<ul>
{"".join(f'<li style="margin-bottom: 8px;">{item}</li>' for item in right_content)}
</ul>
</div>
</div>
"""
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"""
<div class="quote-container" style="text-align: center; padding: 20px;">
<blockquote style="font-size: 24px; font-style: italic; color: {colors['accent']};">
"{quote_text}"
</blockquote>
{f'<div style="text-align: right; font-size: 18px;">— {author}</div>' if author else ''}
</div>
"""
else:
content_html = '<div class="quote-container" style="text-align: center;"><p>No quote content</p></div>'
elif "picture" in layout_type:
# Picture with caption layout
caption = content[0] if content else "Image caption"
content_html = f"""
<div class="picture-container" style="text-align: center;">
<div class="image-placeholder" style="height: 150px; border: 1px dashed {colors['accent']}; margin-bottom: 10px; line-height: 150px;">
<span>Image Area</span>
</div>
<div class="caption" style="font-style: italic; color: {colors['text']};">{caption}</div>
</div>
"""
else:
# Standard layout with bullet points
content_html = f"""
<ul>
{"".join(f'<li style="margin-bottom: 8px;">{item}</li>' for item in content)}
</ul>
"""
# Create the full HTML preview
html = f"""
<div class="slide-preview" style="background-color: {colors['secondary']}; border: 1px solid #dee2e6; border-radius: 4px; padding: 20px; height: 300px; overflow: auto; font-family: {fonts['body']};">
<div class="slide-title" style="color: {colors['text']}; font-family: {fonts['title']}; font-size: 24px; margin-bottom: 20px; border-bottom: 2px solid {colors['primary']}; padding-bottom: 10px;">
{title}
</div>
<div class="slide-visuals">
{visual_html}
</div>
<div class="slide-content" style="color: {colors['text']}; font-family: {fonts['body']}; font-size: 16px;">
{content_html}
</div>
</div>
"""
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 = '''
<svg width="400" height="300" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#f0f0f0"/>
<text x="50%" y="50%" font-family="Arial" font-size="20" fill="#666" text-anchor="middle">
AI Generated Image
</text>
</svg>
'''
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