from abc import ABC, abstractmethod from typing import Dict, Any from PIL import Image, ImageDraw, ImageFont import requests from io import BytesIO import tempfile class Template(ABC): """ Abstract base class for image templates. Each template defines its own configuration for box sizes, positions, colors, and fonts. """ def __init__(self, template_path: str = None): self.template_path = template_path @abstractmethod def get_box_config(self) -> Dict[str, Any]: """Return box configuration including size and position for product image.""" pass @abstractmethod def get_text_config(self) -> Dict[str, Dict[str, Any]]: """Return text configuration including positions, colors, and fonts for all text elements.""" pass @abstractmethod def get_font_config(self) -> Dict[str, Dict[str, Any]]: """Return font configuration for different text elements.""" pass def load_template_image(self) -> Image.Image: """Load and return the template image.""" return Image.open(self.template_path).convert("RGBA") def load_fonts(self) -> Dict[str, ImageFont.FreeTypeFont]: """Load and return all required fonts.""" fonts = {} font_config = self.get_font_config() for font_name, config in font_config.items(): try: fonts[font_name] = ImageFont.truetype(config['path'], config['size']) except IOError: print(f"Font {config['path']} not found. Using default font.") fonts[font_name] = ImageFont.load_default() return fonts def generate_image(self, product_image_url: str, product_name: str, original_price: str, final_price: str, coupon_code: str) -> str: """ Generate the promotional image using this template's configuration. Args: product_image_url: URL of the product image product_name: Name of the product original_price: Original price of the product final_price: Final price of the product coupon_code: Coupon code to display Returns: Path to the generated image file """ try: # Load template and fonts template_image = self.load_template_image() fonts = self.load_fonts() # Fetch and process product image response = requests.get(product_image_url) product_image_data = BytesIO(response.content) product_image = Image.open(product_image_data).convert("RGBA") # Get box configuration box_config = self.get_box_config() box_size = box_config['size'] box_position = box_config['position'] # Resize product image to fit within box while preserving aspect ratio product_image_resized = product_image.copy() product_image_resized.thumbnail(box_size) # Calculate position to center the image in the box paste_x = box_position[0] + (box_size[0] - product_image_resized.width) // 2 paste_y = box_position[1] + (box_size[1] - product_image_resized.height) // 2 paste_position = (paste_x, paste_y) # Paste product image onto template template_image.paste(product_image_resized, paste_position, product_image_resized) # Draw text elements draw = ImageDraw.Draw(template_image) text_config = self.get_text_config() # Draw each text element for element_name, config in text_config.items(): text_content = self._get_text_content(element_name, product_name, original_price, final_price, coupon_code) position = config['position'] color = config['color'] font_name = config['font'] anchor = config.get('anchor', 'ms') draw.text(position, text_content, font=fonts[font_name], fill=color, anchor=anchor) # Save the result with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp_file: template_image.save(temp_file.name) return temp_file.name except FileNotFoundError: return f"Error: The template file '{self.template_path}' was not found." except Exception as e: return f"An error occurred: {e}" def _get_text_content(self, element_name: str, product_name: str, original_price: str, final_price: str, coupon_code: str) -> str: """Get the actual text content for each text element.""" content_map = { 'product_name': product_name, 'original_price': f"De: R$ {original_price}", 'final_price': f"Por: R$ {final_price}", 'coupon_code': coupon_code } return content_map.get(element_name, '') class TemplateRegistry: """Registry for managing different template types.""" _templates = {} @classmethod def register(cls, name: str, template_class): """Register a template class.""" cls._templates[name] = template_class @classmethod def get_template(cls, name: str) -> Template: """Get a template instance by name.""" if name not in cls._templates: raise ValueError(f"Template '{name}' not found") template_instance = cls._templates[name]() return template_instance @classmethod def list_templates(cls) -> list: """List all registered template names.""" return list(cls._templates.keys()) class LidiPromoTemplate(Template): """ Template implementation for Lidi promotional images. Uses the original hardcoded values from the existing implementation. """ def __init__(self, template_path: str = None): super().__init__(template_path or "assets/template_1.png") def get_box_config(self) -> Dict[str, Any]: """Return box configuration for product image.""" return { 'size': (442, 353), 'position': (140, 280) # (x, y) from top-left corner } def get_text_config(self) -> Dict[str, Dict[str, Any]]: """Return text configuration for all text elements.""" return { 'product_name': { 'position': (360, 710), 'color': '#FFFFFF', 'font': 'font_name', 'anchor': 'ms' }, 'original_price': { 'position': (360, 800), 'color': '#FFFFFF', 'font': 'font_price_from', 'anchor': 'ms' }, 'final_price': { 'position': (360, 860), 'color': '#FEE161', # Yellow color from original design 'font': 'font_price', 'anchor': 'ms' }, 'coupon_code': { 'position': (360, 993), 'color': '#000000', 'font': 'font_cupom', 'anchor': 'ms' } } def get_font_config(self) -> Dict[str, Dict[str, Any]]: """Return font configuration for different text elements.""" return { 'font_name': { 'path': 'assets/Montserrat-Bold.ttf', 'size': 47 }, 'font_price_from': { 'path': 'assets/Montserrat-Regular.ttf', 'size': 28 }, 'font_price': { 'path': 'assets/Montserrat-Bold.ttf', 'size': 47 }, 'font_cupom': { 'path': 'assets/Montserrat-Bold.ttf', 'size': 33 } } class NaturaClienteTemplate(Template): """ Template implementation for Natura promotional images. Uses template_b_natura.png with different configuration. """ def __init__(self, template_path: str = None): super().__init__(template_path or "assets/template_b_natura.png") def get_box_config(self) -> Dict[str, Any]: """Return box configuration for product image.""" return { 'size': (602, 424), 'position': (54, 254) # (x, y) from top-left corner } def get_text_config(self) -> Dict[str, Dict[str, Any]]: """Return text configuration for all text elements.""" return { 'product_name': { 'position': (72, 727), 'color': '#000000', 'font': 'font_name', 'anchor': 'ls' }, 'original_price': { 'position': (72, 765), 'color': '#666666', 'font': 'font_price_from', 'anchor': 'ls' }, 'final_price': { 'position': (90, 837), 'color': '#FFFFFF', 'font': 'font_price', 'anchor': 'lm' }, 'coupon_code': { 'position': (461, 957), 'color': '#FFFFFF', 'font': 'font_cupom', 'anchor': 'ms' } } def get_font_config(self) -> Dict[str, Dict[str, Any]]: """Return font configuration for different text elements.""" return { 'font_name': { 'path': 'assets/Montserrat-Bold.ttf', 'size': 32 }, 'font_price_from': { 'path': 'assets/Montserrat-Regular.ttf', 'size': 22 }, 'font_price': { 'path': 'assets/Montserrat-Bold.ttf', 'size': 40 }, 'font_cupom': { 'path': 'assets/Montserrat-Bold.ttf', 'size': 42 } } class AvonTemplate(Template): """ Template implementation for Avon promotional images. Uses template_b_avon.png with Avon-specific configuration. """ def __init__(self, template_path: str = None): super().__init__(template_path or "assets/template_b_avon.png") def get_box_config(self) -> Dict[str, Any]: """Return box configuration for product image.""" return { 'size': (602, 424), 'position': (54, 254) # (x, y) from top-left corner } def get_text_config(self) -> Dict[str, Dict[str, Any]]: """Return text configuration for all text elements.""" return { 'product_name': { 'position': (72, 727), 'color': '#000000', 'font': 'font_name', 'anchor': 'ls' }, 'original_price': { 'position': (72, 765), 'color': '#666666', 'font': 'font_price_from', 'anchor': 'ls' }, 'final_price': { 'position': (90, 837), 'color': '#FFFFFF', 'font': 'font_price', 'anchor': 'lm' }, 'coupon_code': { 'position': (461, 957), 'color': '#FFFFFF', 'font': 'font_cupom', 'anchor': 'ms' } } def get_font_config(self) -> Dict[str, Dict[str, Any]]: """Return font configuration for different text elements.""" return { 'font_name': { 'path': 'assets/Montserrat-Bold.ttf', 'size': 32 }, 'font_price_from': { 'path': 'assets/Montserrat-Regular.ttf', 'size': 22 }, 'font_price': { 'path': 'assets/Montserrat-Bold.ttf', 'size': 40 }, 'font_cupom': { 'path': 'assets/Montserrat-Bold.ttf', 'size': 42 } } class TemplateC(Template): """ Template implementation for Avon promotional images. Uses template_b_avon.png with Avon-specific configuration. """ def __init__(self, template_path: str): super().__init__(template_path) def get_box_config(self) -> Dict[str, Any]: """Return box configuration for product image.""" return { 'size': (602, 434), 'position': (54, 304) # (x, y) from top-left corner } def get_text_config(self) -> Dict[str, Dict[str, Any]]: """Return text configuration for all text elements.""" return { 'product_name': { 'position': (72, 777), 'color': '#000000', 'font': 'font_name', 'anchor': 'ls' }, 'original_price': { 'position': (72, 815), 'color': '#666666', 'font': 'font_price_from', 'anchor': 'ls' }, 'final_price': { 'position': (90, 887), 'color': '#FFFFFF', 'font': 'font_price', 'anchor': 'lm' }, 'coupon_code': { 'position': (471, 1020), 'color': '#FFFFFF', 'font': 'font_cupom', 'anchor': 'ms' } } def get_font_config(self) -> Dict[str, Dict[str, Any]]: """Return font configuration for different text elements.""" return { 'font_name': { 'path': 'assets/Montserrat-Bold.ttf', 'size': 32 }, 'font_price_from': { 'path': 'assets/Montserrat-Regular.ttf', 'size': 22 }, 'font_price': { 'path': 'assets/Montserrat-Bold.ttf', 'size': 40 }, 'font_cupom': { 'path': 'assets/Montserrat-Bold.ttf', 'size': 42 } } class TemplateOutros(TemplateC): def __init__(self, template_path: str = None): super().__init__("assets/template_c_outros.png") class TemplateOutrosSemCupom(TemplateC): def __init__(self, template_path: str = None): super().__init__("assets/template_c_outros_sem_cupom.png") class TemplateNatura(TemplateC): def __init__(self, template_path: str = None): super().__init__("assets/template_c_natura.png") # Register additional templates TemplateRegistry.register('lidi_promo', LidiPromoTemplate) TemplateRegistry.register('natura', TemplateNatura) TemplateRegistry.register('avon', AvonTemplate) TemplateRegistry.register('outros', TemplateOutros) TemplateRegistry.register('outros_sem_cupom', TemplateOutrosSemCupom) TemplateRegistry.register('natura_cliente', NaturaClienteTemplate)