""" Image generation module for creating infographic images """ import io import os import logging from typing import Dict, List, Tuple, Optional from PIL import Image, ImageDraw, ImageFont, ImageFilter import matplotlib.pyplot as plt import matplotlib.patches as mpatches from matplotlib import font_manager import numpy as np logger = logging.getLogger(__name__) class ImageGenerator: """Generate infographic images from layout data""" def __init__(self): """Initialize image generator""" self.default_font_path = self._get_default_font() self.generated_images = [] logger.info("Image generator initialized") def create_infographic(self, layout_data: Dict) -> str: """ Create infographic image from layout data Args: layout_data: Complete layout specification Returns: Path to generated image file """ try: canvas = layout_data.get('canvas', {}) elements = layout_data.get('elements', []) # Create PIL image img = Image.new('RGB', (canvas.get('width', 1080), canvas.get('height', 1920)), canvas.get('background', '#ffffff')) draw = ImageDraw.Draw(img) # Draw each element for element in elements: self._draw_element(draw, element, img) # Apply post-processing effects img = self._apply_effects(img, layout_data) # Save image output_path = f"/tmp/infographic_{id(layout_data)}.png" img.save(output_path, 'PNG', quality=95, dpi=(300, 300)) logger.info(f"Infographic generated successfully: {output_path}") self.generated_images.append(output_path) return output_path except Exception as e: logger.error(f"Failed to generate infographic: {e}") return self._create_error_image() def _draw_element(self, draw: ImageDraw.Draw, element: Dict, img: Image.Image): """Draw individual element on the canvas""" element_type = element.get('type', 'text') position = element.get('position', {'x': 0, 'y': 0}) size = element.get('size', {'width': 100, 'height': 50}) styling = element.get('styling', {}) content = element.get('content', '') if element_type == 'title': self._draw_title(draw, content, position, size, styling) elif element_type == 'section': self._draw_section(draw, content, position, size, styling) elif element_type == 'icon': self._draw_icon(draw, content, position, size, styling) elif element_type == 'chart': self._draw_chart(draw, img, content, position, size, styling) elif element_type == 'divider': self._draw_divider(draw, position, size, styling) def _draw_title(self, draw: ImageDraw.Draw, text: str, position: Dict, size: Dict, styling: Dict): """Draw title element""" font_info = styling.get('font', ('Arial', 32, 'bold')) color = styling.get('color', '#2c3e50') alignment = styling.get('alignment', 'left') try: font = ImageFont.truetype(self.default_font_path, font_info[1]) except: font = ImageFont.load_default() # Calculate text position for alignment bbox = draw.textbbox((0, 0), text, font=font) text_width = bbox[2] - bbox[0] text_height = bbox[3] - bbox[1] if alignment == 'center': x = position['x'] + (size['width'] - text_width) // 2 elif alignment == 'right': x = position['x'] + size['width'] - text_width else: x = position['x'] y = position['y'] # Add background if specified bg_color = styling.get('background_color') if bg_color and bg_color != 'transparent': draw.rectangle([ x - 10, y - 5, x + text_width + 10, y + text_height + 5 ], fill=bg_color) # Draw text draw.text((x, y), text, fill=color, font=font) def _draw_section(self, draw: ImageDraw.Draw, text: str, position: Dict, size: Dict, styling: Dict): """Draw section element""" font_info = styling.get('font', ('Arial', 16, 'normal')) color = styling.get('color', '#2c3e50') bg_color = styling.get('background_color', 'transparent') border_radius = styling.get('border_radius', 0) padding = styling.get('padding', 20) x, y = position['x'], position['y'] width, height = size['width'], size['height'] # Draw background if bg_color and bg_color != 'transparent': if border_radius > 0: self._draw_rounded_rectangle(draw, [x, y, x + width, y + height], bg_color, border_radius) else: draw.rectangle([x, y, x + width, y + height], fill=bg_color) # Draw text with wrapping try: font = ImageFont.truetype(self.default_font_path, font_info[1]) except: font = ImageFont.load_default() # Text wrapping wrapped_text = self._wrap_text(text, font, width - padding * 2) text_y = y + padding for line in wrapped_text: draw.text((x + padding, text_y), line, fill=color, font=font) text_y += font_info[1] + 6 def _draw_icon(self, draw: ImageDraw.Draw, description: str, position: Dict, size: Dict, styling: Dict): """Draw icon placeholder (simplified implementation)""" color = styling.get('color', '#3498db') icon_size = min(size['width'], size['height']) # Draw simple geometric shape as icon placeholder center_x = position['x'] + size['width'] // 2 center_y = position['y'] + size['height'] // 2 radius = icon_size // 3 # Different shapes based on description keywords if any(word in description.lower() for word in ['chart', 'data', 'graph']): # Draw bar chart icon bar_width = radius // 2 for i in range(3): bar_height = radius * (0.5 + i * 0.3) bar_x = center_x - radius + i * bar_width bar_y = center_y + radius - bar_height draw.rectangle([bar_x, bar_y, bar_x + bar_width - 2, center_y + radius], fill=color) elif any(word in description.lower() for word in ['process', 'flow', 'step']): # Draw arrow icon draw.polygon([ (center_x - radius, center_y), (center_x, center_y - radius//2), (center_x + radius, center_y), (center_x, center_y + radius//2) ], fill=color) else: # Draw circle icon draw.ellipse([ center_x - radius, center_y - radius, center_x + radius, center_y + radius ], fill=color) def _draw_chart(self, draw: ImageDraw.Draw, img: Image.Image, description: str, position: Dict, size: Dict, styling: Dict): """Draw chart element using matplotlib""" try: # Create matplotlib figure fig, ax = plt.subplots(figsize=(size['width']/100, size['height']/100), dpi=100) # Sample data for demonstration categories = ['A', 'B', 'C', 'D'] values = [23, 45, 56, 78] colors = [styling.get('color', '#3498db')] * len(categories) # Create simple bar chart ax.bar(categories, values, color=colors) ax.set_title(description[:30], fontsize=12) ax.set_facecolor('white') fig.patch.set_facecolor('white') # Convert matplotlib figure to PIL image buf = io.BytesIO() plt.savefig(buf, format='png', bbox_inches='tight', dpi=100) buf.seek(0) chart_img = Image.open(buf) plt.close(fig) # Resize and paste onto main image chart_img = chart_img.resize((size['width'], size['height'])) img.paste(chart_img, (position['x'], position['y'])) buf.close() except Exception as e: logger.error(f"Failed to draw chart: {e}") # Draw placeholder rectangle draw.rectangle([ position['x'], position['y'], position['x'] + size['width'], position['y'] + size['height'] ], outline=styling.get('color', '#3498db'), width=2) def _draw_divider(self, draw: ImageDraw.Draw, position: Dict, size: Dict, styling: Dict): """Draw divider line""" color = styling.get('color', '#bdc3c7') thickness = styling.get('thickness', 2) draw.line([ position['x'], position['y'] + size['height'] // 2, position['x'] + size['width'], position['y'] + size['height'] // 2 ], fill=color, width=thickness) def _draw_rounded_rectangle(self, draw: ImageDraw.Draw, coords: List[int], fill_color: str, radius: int): """Draw rounded rectangle""" x1, y1, x2, y2 = coords # Draw main rectangle draw.rectangle([x1 + radius, y1, x2 - radius, y2], fill=fill_color) draw.rectangle([x1, y1 + radius, x2, y2 - radius], fill=fill_color) # Draw corners draw.pieslice([x1, y1, x1 + 2*radius, y1 + 2*radius], 180, 270, fill=fill_color) draw.pieslice([x2 - 2*radius, y1, x2, y1 + 2*radius], 270, 360, fill=fill_color) draw.pieslice([x1, y2 - 2*radius, x1 + 2*radius, y2], 90, 180, fill=fill_color) draw.pieslice([x2 - 2*radius, y2 - 2*radius, x2, y2], 0, 90, fill=fill_color) def _wrap_text(self, text: str, font: ImageFont.ImageFont, max_width: int) -> List[str]: """Wrap text to fit within specified width""" words = text.split() lines = [] current_line = [] for word in words: test_line = ' '.join(current_line + [word]) bbox = font.getbbox(test_line) line_width = bbox[2] - bbox[0] if line_width <= max_width: current_line.append(word) else: if current_line: lines.append(' '.join(current_line)) current_line = [word] else: # Single word too long, force it lines.append(word) if current_line: lines.append(' '.join(current_line)) return lines def _apply_effects(self, img: Image.Image, layout_data: Dict) -> Image.Image: """Apply post-processing effects""" canvas = layout_data.get('canvas', {}) # Apply subtle blur for depth (optional) # img = img.filter(ImageFilter.UnsharpMask(radius=1, percent=110, threshold=2)) return img def _get_default_font(self) -> str: """Get path to default font""" try: # Try to find Arial or similar system font available_fonts = font_manager.findSystemFonts() for font_path in available_fonts: if 'arial' in font_path.lower() or 'liberation' in font_path.lower(): return font_path # Fallback to first available font return available_fonts[0] if available_fonts else None except: return None def _create_error_image(self) -> str: """Create error image when generation fails""" img = Image.new('RGB', (1080, 1920), '#f8f9fa') draw = ImageDraw.Draw(img) # Draw error message try: font = ImageFont.truetype(self.default_font_path, 24) except: font = ImageFont.load_default() error_text = "Error generating infographic\nPlease try again" bbox = draw.textbbox((0, 0), error_text, font=font) text_width = bbox[2] - bbox[0] text_height = bbox[3] - bbox[1] x = (1080 - text_width) // 2 y = (1920 - text_height) // 2 draw.text((x, y), error_text, fill='#e74c3c', font=font) error_path = "/tmp/error_infographic.png" img.save(error_path, 'PNG') return error_path def create_multiple_variations(self, layout_data: Dict, count: int = 3) -> List[str]: """Create multiple variations of the same infographic""" variations = [] for i in range(count): # Modify layout slightly for each variation variation_data = self._create_variation(layout_data, i) variation_path = self.create_infographic(variation_data) variations.append(variation_path) return variations def _create_variation(self, layout_data: Dict, variation_index: int) -> Dict: """Create a variation of the layout""" variation = layout_data.copy() # Modify colors slightly if variation_index == 1: # Darker variation variation['canvas']['background'] = '#f5f6fa' elif variation_index == 2: # Lighter variation variation['canvas']['background'] = '#ffffff' return variation def export_to_pdf(self, image_path: str) -> str: """Convert PNG to PDF""" try: from reportlab.pdfgen import canvas from reportlab.lib.utils import ImageReader img = Image.open(image_path) pdf_path = image_path.replace('.png', '.pdf') # Create PDF c = canvas.Canvas(pdf_path, pagesize=(img.width, img.height)) c.drawImage(ImageReader(img), 0, 0, width=img.width, height=img.height) c.save() return pdf_path except Exception as e: logger.error(f"PDF export failed: {e}") return image_path def export_to_svg(self, layout_data: Dict) -> str: """Export layout as SVG""" try: canvas = layout_data.get('canvas', {}) elements = layout_data.get('elements', []) svg_content = f''' ''' # Add elements as SVG for element in elements: svg_content += self._element_to_svg(element) svg_content += '' svg_path = f"/tmp/infographic_{id(layout_data)}.svg" with open(svg_path, 'w', encoding='utf-8') as f: f.write(svg_content) return svg_path except Exception as e: logger.error(f"SVG export failed: {e}") return "" def _element_to_svg(self, element: Dict) -> str: """Convert element to SVG markup""" element_type = element.get('type', 'text') position = element.get('position', {'x': 0, 'y': 0}) size = element.get('size', {'width': 100, 'height': 50}) styling = element.get('styling', {}) content = element.get('content', '') if element_type in ['title', 'section']: color = styling.get('color', '#2c3e50') font_size = styling.get('font', ['Arial', 16, 'normal'])[1] return f'''{content}\n''' elif element_type == 'icon': color = styling.get('color', '#3498db') cx = position['x'] + size['width'] // 2 cy = position['y'] + size['height'] // 2 r = min(size['width'], size['height']) // 3 return f'''\n''' return "" def cleanup_temp_files(self): """Clean up temporary generated files""" for file_path in self.generated_images: try: if os.path.exists(file_path): os.remove(file_path) except: pass self.generated_images.clear()