Spaces:
Running
Running
feat: support many template in image generation (#13)
Browse files- feat: support many template in image generation (547fbf9dcedd839f06720760776af450e5633062)
- .gitattributes +2 -0
- app.py +9 -3
- assets/template_b_avon.png +3 -0
- assets/template_b_natura.png +3 -0
- image_generator_tool.py +17 -55
- template_system.py +359 -0
.gitattributes
CHANGED
|
@@ -38,3 +38,5 @@ assets/Montserrat-Regular.ttf filter=lfs diff=lfs merge=lfs -text
|
|
| 38 |
assets/template_natura_empty.jpg filter=lfs diff=lfs merge=lfs -text
|
| 39 |
assets/template_1.png filter=lfs diff=lfs merge=lfs -text
|
| 40 |
assets/template_2.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 38 |
assets/template_natura_empty.jpg filter=lfs diff=lfs merge=lfs -text
|
| 39 |
assets/template_1.png filter=lfs diff=lfs merge=lfs -text
|
| 40 |
assets/template_2.png filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
assets/template_b_avon.png filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
assets/template_b_natura.png filter=lfs diff=lfs merge=lfs -text
|
app.py
CHANGED
|
@@ -132,6 +132,11 @@ with gr.Blocks() as demo:
|
|
| 132 |
image_original_price_input = gr.Number(label="Original Price", placeholder="Enter original price...")
|
| 133 |
image_final_price_input = gr.Number(label="Final Price", placeholder="Enter final price...")
|
| 134 |
image_coupon_code_input = gr.Textbox(label="Coupon Code", placeholder="Enter coupon code...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
gen_image_btn = gr.Button("Generate Image")
|
| 136 |
with gr.Column():
|
| 137 |
image_output = gr.Image(label="Generated Image", height=500, type="filepath", interactive=False, show_share_button=True)
|
|
@@ -168,7 +173,7 @@ with gr.Blocks() as demo:
|
|
| 168 |
|
| 169 |
|
| 170 |
|
| 171 |
-
def generate_image(product_image_url, product_name, original_price, final_price, coupon_code):
|
| 172 |
tool = GenerateImageTool()
|
| 173 |
original_price_str = f"{original_price:.2f}".replace('.', ',')
|
| 174 |
final_price_str = f"{final_price:.2f}".replace('.', ',')
|
|
@@ -180,7 +185,8 @@ with gr.Blocks() as demo:
|
|
| 180 |
product_name=product_name,
|
| 181 |
original_price=original_price_str,
|
| 182 |
final_price=final_price_str,
|
| 183 |
-
coupon_code=coupon_code
|
|
|
|
| 184 |
)
|
| 185 |
|
| 186 |
yield gr.update(interactive=True, value="Generate Image"), image_path, gr.update(interactive=True)
|
|
@@ -207,7 +213,7 @@ with gr.Blocks() as demo:
|
|
| 207 |
inputs=[fragrantica_url_input, openai_key_input, natura_token_input, openai_base_url_input, openai_model_name_input],
|
| 208 |
outputs=fragrantica_output)
|
| 209 |
gen_image_btn.click(generate_image,
|
| 210 |
-
inputs=[image_product_url_input, image_product_name_input, image_original_price_input, image_final_price_input, image_coupon_code_input],
|
| 211 |
outputs=[gen_image_btn, image_output, share_button])
|
| 212 |
share_button.click(fn=process_image_for_sharing,
|
| 213 |
inputs=[image_output],
|
|
|
|
| 132 |
image_original_price_input = gr.Number(label="Original Price", placeholder="Enter original price...")
|
| 133 |
image_final_price_input = gr.Number(label="Final Price", placeholder="Enter final price...")
|
| 134 |
image_coupon_code_input = gr.Textbox(label="Coupon Code", placeholder="Enter coupon code...")
|
| 135 |
+
template_selection = gr.Radio(
|
| 136 |
+
choices=["lidi_promo", "natura", "avon"],
|
| 137 |
+
value="lidi_promo",
|
| 138 |
+
label="Template Selection"
|
| 139 |
+
)
|
| 140 |
gen_image_btn = gr.Button("Generate Image")
|
| 141 |
with gr.Column():
|
| 142 |
image_output = gr.Image(label="Generated Image", height=500, type="filepath", interactive=False, show_share_button=True)
|
|
|
|
| 173 |
|
| 174 |
|
| 175 |
|
| 176 |
+
def generate_image(product_image_url, product_name, original_price, final_price, coupon_code, template_name):
|
| 177 |
tool = GenerateImageTool()
|
| 178 |
original_price_str = f"{original_price:.2f}".replace('.', ',')
|
| 179 |
final_price_str = f"{final_price:.2f}".replace('.', ',')
|
|
|
|
| 185 |
product_name=product_name,
|
| 186 |
original_price=original_price_str,
|
| 187 |
final_price=final_price_str,
|
| 188 |
+
coupon_code=coupon_code,
|
| 189 |
+
template_name=template_name
|
| 190 |
)
|
| 191 |
|
| 192 |
yield gr.update(interactive=True, value="Generate Image"), image_path, gr.update(interactive=True)
|
|
|
|
| 213 |
inputs=[fragrantica_url_input, openai_key_input, natura_token_input, openai_base_url_input, openai_model_name_input],
|
| 214 |
outputs=fragrantica_output)
|
| 215 |
gen_image_btn.click(generate_image,
|
| 216 |
+
inputs=[image_product_url_input, image_product_name_input, image_original_price_input, image_final_price_input, image_coupon_code_input, template_selection],
|
| 217 |
outputs=[gen_image_btn, image_output, share_button])
|
| 218 |
share_button.click(fn=process_image_for_sharing,
|
| 219 |
inputs=[image_output],
|
assets/template_b_avon.png
ADDED
|
Git LFS Details
|
assets/template_b_natura.png
ADDED
|
Git LFS Details
|
image_generator_tool.py
CHANGED
|
@@ -1,9 +1,6 @@
|
|
| 1 |
from crewai.tools import BaseTool
|
| 2 |
from pydantic import BaseModel, Field
|
| 3 |
-
from
|
| 4 |
-
import requests
|
| 5 |
-
from io import BytesIO
|
| 6 |
-
import base64
|
| 7 |
|
| 8 |
class GenerateImageToolInput(BaseModel):
|
| 9 |
"""Input for the Generate Image Tool."""
|
|
@@ -12,63 +9,28 @@ class GenerateImageToolInput(BaseModel):
|
|
| 12 |
original_price: str = Field(..., description="Original price of the product.")
|
| 13 |
final_price: str = Field(..., description="Final price of the product.")
|
| 14 |
coupon_code: str = Field(..., description="Coupon code to be displayed on the image.")
|
| 15 |
-
|
| 16 |
-
import tempfile
|
| 17 |
|
| 18 |
class GenerateImageTool(BaseTool):
|
| 19 |
name: str = "Generate Image Tool"
|
| 20 |
description: str = "Generates a promotional image for a product using a template and returns the file path."
|
| 21 |
args_schema = GenerateImageToolInput
|
| 22 |
|
| 23 |
-
def _run(self, product_image_url: str, product_name: str, original_price: str, final_price: str, coupon_code: str) -> str:
|
| 24 |
-
template_path = 'assets/template_1.png'
|
| 25 |
-
|
| 26 |
try:
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
template_image.paste(product_image_resized, paste_position, product_image_resized)
|
| 43 |
-
|
| 44 |
-
draw = ImageDraw.Draw(template_image)
|
| 45 |
-
|
| 46 |
-
try:
|
| 47 |
-
font_name = ImageFont.truetype("assets/Montserrat-Bold.ttf", 47)
|
| 48 |
-
font_price_from = ImageFont.truetype("assets/Montserrat-Regular.ttf", 28)
|
| 49 |
-
font_price = ImageFont.truetype("assets/Montserrat-Bold.ttf", 47)
|
| 50 |
-
font_cupom = ImageFont.truetype("assets/Montserrat-Bold.ttf", 33)
|
| 51 |
-
except IOError:
|
| 52 |
-
print("Arial font not found. Using default font.")
|
| 53 |
-
font_name = ImageFont.load_default()
|
| 54 |
-
font_price_from = ImageFont.load_default()
|
| 55 |
-
font_price = ImageFont.load_default()
|
| 56 |
-
font_cupom = ImageFont.load_default()
|
| 57 |
-
|
| 58 |
-
white_color = "#FFFFFF"
|
| 59 |
-
yellow_color = "#FEE161"
|
| 60 |
-
black_color = "#000000"
|
| 61 |
-
|
| 62 |
-
draw.text((360, 710), product_name, font=font_name, fill=white_color, anchor="ms")
|
| 63 |
-
draw.text((360, 800), f"De: R$ {original_price}", font=font_price_from, fill=white_color, anchor="ms")
|
| 64 |
-
draw.text((360, 860), f"Por: R$ {final_price}", font=font_price, fill=yellow_color, anchor="ms")
|
| 65 |
-
draw.text((360, 993), coupon_code, font=font_cupom, fill=black_color, anchor="ms")
|
| 66 |
-
|
| 67 |
-
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp_file:
|
| 68 |
-
template_image.save(temp_file.name)
|
| 69 |
-
return temp_file.name
|
| 70 |
-
|
| 71 |
-
except FileNotFoundError:
|
| 72 |
-
return f"Error: The template file '{template_path}' was not found."
|
| 73 |
except Exception as e:
|
| 74 |
return f"An error occurred: {e}"
|
|
|
|
| 1 |
from crewai.tools import BaseTool
|
| 2 |
from pydantic import BaseModel, Field
|
| 3 |
+
from template_system import TemplateRegistry
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
class GenerateImageToolInput(BaseModel):
|
| 6 |
"""Input for the Generate Image Tool."""
|
|
|
|
| 9 |
original_price: str = Field(..., description="Original price of the product.")
|
| 10 |
final_price: str = Field(..., description="Final price of the product.")
|
| 11 |
coupon_code: str = Field(..., description="Coupon code to be displayed on the image.")
|
| 12 |
+
template_name: str = Field(default="lidi_promo", description="Name of the template to use for generating the image.")
|
|
|
|
| 13 |
|
| 14 |
class GenerateImageTool(BaseTool):
|
| 15 |
name: str = "Generate Image Tool"
|
| 16 |
description: str = "Generates a promotional image for a product using a template and returns the file path."
|
| 17 |
args_schema = GenerateImageToolInput
|
| 18 |
|
| 19 |
+
def _run(self, product_image_url: str, product_name: str, original_price: str, final_price: str, coupon_code: str, template_name: str = "lidi_promo") -> str:
|
|
|
|
|
|
|
| 20 |
try:
|
| 21 |
+
# Get the template instance
|
| 22 |
+
template = TemplateRegistry.get_template(template_name)
|
| 23 |
+
|
| 24 |
+
# Generate the image using the template
|
| 25 |
+
return template.generate_image(
|
| 26 |
+
product_image_url=product_image_url,
|
| 27 |
+
product_name=product_name,
|
| 28 |
+
original_price=original_price,
|
| 29 |
+
final_price=final_price,
|
| 30 |
+
coupon_code=coupon_code
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
except ValueError as e:
|
| 34 |
+
return f"Error: {e}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
except Exception as e:
|
| 36 |
return f"An error occurred: {e}"
|
template_system.py
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from abc import ABC, abstractmethod
|
| 2 |
+
from typing import Dict, Any
|
| 3 |
+
from PIL import Image, ImageDraw, ImageFont
|
| 4 |
+
import requests
|
| 5 |
+
from io import BytesIO
|
| 6 |
+
import tempfile
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class Template(ABC):
|
| 10 |
+
"""
|
| 11 |
+
Abstract base class for image templates.
|
| 12 |
+
Each template defines its own configuration for box sizes, positions, colors, and fonts.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
def __init__(self, template_path: str = None):
|
| 16 |
+
self.template_path = template_path
|
| 17 |
+
|
| 18 |
+
@abstractmethod
|
| 19 |
+
def get_box_config(self) -> Dict[str, Any]:
|
| 20 |
+
"""Return box configuration including size and position for product image."""
|
| 21 |
+
pass
|
| 22 |
+
|
| 23 |
+
@abstractmethod
|
| 24 |
+
def get_text_config(self) -> Dict[str, Dict[str, Any]]:
|
| 25 |
+
"""Return text configuration including positions, colors, and fonts for all text elements."""
|
| 26 |
+
pass
|
| 27 |
+
|
| 28 |
+
@abstractmethod
|
| 29 |
+
def get_font_config(self) -> Dict[str, Dict[str, Any]]:
|
| 30 |
+
"""Return font configuration for different text elements."""
|
| 31 |
+
pass
|
| 32 |
+
|
| 33 |
+
def load_template_image(self) -> Image.Image:
|
| 34 |
+
"""Load and return the template image."""
|
| 35 |
+
return Image.open(self.template_path).convert("RGBA")
|
| 36 |
+
|
| 37 |
+
def load_fonts(self) -> Dict[str, ImageFont.FreeTypeFont]:
|
| 38 |
+
"""Load and return all required fonts."""
|
| 39 |
+
fonts = {}
|
| 40 |
+
font_config = self.get_font_config()
|
| 41 |
+
|
| 42 |
+
for font_name, config in font_config.items():
|
| 43 |
+
try:
|
| 44 |
+
fonts[font_name] = ImageFont.truetype(config['path'], config['size'])
|
| 45 |
+
except IOError:
|
| 46 |
+
print(f"Font {config['path']} not found. Using default font.")
|
| 47 |
+
fonts[font_name] = ImageFont.load_default()
|
| 48 |
+
|
| 49 |
+
return fonts
|
| 50 |
+
|
| 51 |
+
def generate_image(self, product_image_url: str, product_name: str,
|
| 52 |
+
original_price: str, final_price: str, coupon_code: str) -> str:
|
| 53 |
+
"""
|
| 54 |
+
Generate the promotional image using this template's configuration.
|
| 55 |
+
|
| 56 |
+
Args:
|
| 57 |
+
product_image_url: URL of the product image
|
| 58 |
+
product_name: Name of the product
|
| 59 |
+
original_price: Original price of the product
|
| 60 |
+
final_price: Final price of the product
|
| 61 |
+
coupon_code: Coupon code to display
|
| 62 |
+
|
| 63 |
+
Returns:
|
| 64 |
+
Path to the generated image file
|
| 65 |
+
"""
|
| 66 |
+
try:
|
| 67 |
+
# Load template and fonts
|
| 68 |
+
template_image = self.load_template_image()
|
| 69 |
+
fonts = self.load_fonts()
|
| 70 |
+
|
| 71 |
+
# Fetch and process product image
|
| 72 |
+
response = requests.get(product_image_url)
|
| 73 |
+
product_image_data = BytesIO(response.content)
|
| 74 |
+
product_image = Image.open(product_image_data).convert("RGBA")
|
| 75 |
+
|
| 76 |
+
# Get box configuration
|
| 77 |
+
box_config = self.get_box_config()
|
| 78 |
+
box_size = box_config['size']
|
| 79 |
+
box_position = box_config['position']
|
| 80 |
+
|
| 81 |
+
# Resize product image to fit within box while preserving aspect ratio
|
| 82 |
+
product_image_resized = product_image.copy()
|
| 83 |
+
product_image_resized.thumbnail(box_size)
|
| 84 |
+
|
| 85 |
+
# Calculate position to center the image in the box
|
| 86 |
+
paste_x = box_position[0] + (box_size[0] - product_image_resized.width) // 2
|
| 87 |
+
paste_y = box_position[1] + (box_size[1] - product_image_resized.height) // 2
|
| 88 |
+
paste_position = (paste_x, paste_y)
|
| 89 |
+
|
| 90 |
+
# Paste product image onto template
|
| 91 |
+
template_image.paste(product_image_resized, paste_position, product_image_resized)
|
| 92 |
+
|
| 93 |
+
# Draw text elements
|
| 94 |
+
draw = ImageDraw.Draw(template_image)
|
| 95 |
+
text_config = self.get_text_config()
|
| 96 |
+
|
| 97 |
+
# Draw each text element
|
| 98 |
+
for element_name, config in text_config.items():
|
| 99 |
+
text_content = self._get_text_content(element_name, product_name,
|
| 100 |
+
original_price, final_price, coupon_code)
|
| 101 |
+
position = config['position']
|
| 102 |
+
color = config['color']
|
| 103 |
+
font_name = config['font']
|
| 104 |
+
anchor = config.get('anchor', 'ms')
|
| 105 |
+
|
| 106 |
+
draw.text(position, text_content, font=fonts[font_name],
|
| 107 |
+
fill=color, anchor=anchor)
|
| 108 |
+
|
| 109 |
+
# Save the result
|
| 110 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp_file:
|
| 111 |
+
template_image.save(temp_file.name)
|
| 112 |
+
return temp_file.name
|
| 113 |
+
|
| 114 |
+
except FileNotFoundError:
|
| 115 |
+
return f"Error: The template file '{self.template_path}' was not found."
|
| 116 |
+
except Exception as e:
|
| 117 |
+
return f"An error occurred: {e}"
|
| 118 |
+
|
| 119 |
+
def _get_text_content(self, element_name: str, product_name: str,
|
| 120 |
+
original_price: str, final_price: str, coupon_code: str) -> str:
|
| 121 |
+
"""Get the actual text content for each text element."""
|
| 122 |
+
content_map = {
|
| 123 |
+
'product_name': product_name,
|
| 124 |
+
'original_price': f"De: R$ {original_price}",
|
| 125 |
+
'final_price': f"Por: R$ {final_price}",
|
| 126 |
+
'coupon_code': coupon_code
|
| 127 |
+
}
|
| 128 |
+
return content_map.get(element_name, '')
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
class TemplateRegistry:
|
| 132 |
+
"""Registry for managing different template types."""
|
| 133 |
+
|
| 134 |
+
_templates = {}
|
| 135 |
+
|
| 136 |
+
@classmethod
|
| 137 |
+
def register(cls, name: str, template_class):
|
| 138 |
+
"""Register a template class."""
|
| 139 |
+
cls._templates[name] = template_class
|
| 140 |
+
|
| 141 |
+
@classmethod
|
| 142 |
+
def get_template(cls, name: str) -> Template:
|
| 143 |
+
"""Get a template instance by name."""
|
| 144 |
+
if name not in cls._templates:
|
| 145 |
+
raise ValueError(f"Template '{name}' not found")
|
| 146 |
+
template_instance = cls._templates[name]()
|
| 147 |
+
return template_instance
|
| 148 |
+
|
| 149 |
+
@classmethod
|
| 150 |
+
def list_templates(cls) -> list:
|
| 151 |
+
"""List all registered template names."""
|
| 152 |
+
return list(cls._templates.keys())
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
class LidiPromoTemplate(Template):
|
| 156 |
+
"""
|
| 157 |
+
Template implementation for Lidi promotional images.
|
| 158 |
+
Uses the original hardcoded values from the existing implementation.
|
| 159 |
+
"""
|
| 160 |
+
|
| 161 |
+
def __init__(self, template_path: str = None):
|
| 162 |
+
super().__init__(template_path or "assets/template_1.png")
|
| 163 |
+
|
| 164 |
+
def get_box_config(self) -> Dict[str, Any]:
|
| 165 |
+
"""Return box configuration for product image."""
|
| 166 |
+
return {
|
| 167 |
+
'size': (442, 353),
|
| 168 |
+
'position': (140, 280) # (x, y) from top-left corner
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
def get_text_config(self) -> Dict[str, Dict[str, Any]]:
|
| 172 |
+
"""Return text configuration for all text elements."""
|
| 173 |
+
return {
|
| 174 |
+
'product_name': {
|
| 175 |
+
'position': (360, 710),
|
| 176 |
+
'color': '#FFFFFF',
|
| 177 |
+
'font': 'font_name',
|
| 178 |
+
'anchor': 'ms'
|
| 179 |
+
},
|
| 180 |
+
'original_price': {
|
| 181 |
+
'position': (360, 800),
|
| 182 |
+
'color': '#FFFFFF',
|
| 183 |
+
'font': 'font_price_from',
|
| 184 |
+
'anchor': 'ms'
|
| 185 |
+
},
|
| 186 |
+
'final_price': {
|
| 187 |
+
'position': (360, 860),
|
| 188 |
+
'color': '#FEE161', # Yellow color from original design
|
| 189 |
+
'font': 'font_price',
|
| 190 |
+
'anchor': 'ms'
|
| 191 |
+
},
|
| 192 |
+
'coupon_code': {
|
| 193 |
+
'position': (360, 993),
|
| 194 |
+
'color': '#000000',
|
| 195 |
+
'font': 'font_cupom',
|
| 196 |
+
'anchor': 'ms'
|
| 197 |
+
}
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
def get_font_config(self) -> Dict[str, Dict[str, Any]]:
|
| 201 |
+
"""Return font configuration for different text elements."""
|
| 202 |
+
return {
|
| 203 |
+
'font_name': {
|
| 204 |
+
'path': 'assets/Montserrat-Bold.ttf',
|
| 205 |
+
'size': 47
|
| 206 |
+
},
|
| 207 |
+
'font_price_from': {
|
| 208 |
+
'path': 'assets/Montserrat-Regular.ttf',
|
| 209 |
+
'size': 28
|
| 210 |
+
},
|
| 211 |
+
'font_price': {
|
| 212 |
+
'path': 'assets/Montserrat-Bold.ttf',
|
| 213 |
+
'size': 47
|
| 214 |
+
},
|
| 215 |
+
'font_cupom': {
|
| 216 |
+
'path': 'assets/Montserrat-Bold.ttf',
|
| 217 |
+
'size': 33
|
| 218 |
+
}
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
class NaturaTemplate(Template):
|
| 223 |
+
"""
|
| 224 |
+
Template implementation for Natura promotional images.
|
| 225 |
+
Uses template_b_natura.png with different configuration.
|
| 226 |
+
"""
|
| 227 |
+
|
| 228 |
+
def __init__(self, template_path: str = None):
|
| 229 |
+
super().__init__(template_path or "assets/template_b_natura.png")
|
| 230 |
+
|
| 231 |
+
def get_box_config(self) -> Dict[str, Any]:
|
| 232 |
+
"""Return box configuration for product image."""
|
| 233 |
+
return {
|
| 234 |
+
'size': (602, 424),
|
| 235 |
+
'position': (54, 254) # (x, y) from top-left corner
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
def get_text_config(self) -> Dict[str, Dict[str, Any]]:
|
| 239 |
+
"""Return text configuration for all text elements."""
|
| 240 |
+
return {
|
| 241 |
+
'product_name': {
|
| 242 |
+
'position': (72, 727),
|
| 243 |
+
'color': '#000000',
|
| 244 |
+
'font': 'font_name',
|
| 245 |
+
'anchor': 'ls'
|
| 246 |
+
},
|
| 247 |
+
'original_price': {
|
| 248 |
+
'position': (72, 765),
|
| 249 |
+
'color': '#666666',
|
| 250 |
+
'font': 'font_price_from',
|
| 251 |
+
'anchor': 'ls'
|
| 252 |
+
},
|
| 253 |
+
'final_price': {
|
| 254 |
+
'position': (90, 837),
|
| 255 |
+
'color': '#FFFFFF',
|
| 256 |
+
'font': 'font_price',
|
| 257 |
+
'anchor': 'lm'
|
| 258 |
+
},
|
| 259 |
+
'coupon_code': {
|
| 260 |
+
'position': (461, 957),
|
| 261 |
+
'color': '#FFFFFF',
|
| 262 |
+
'font': 'font_cupom',
|
| 263 |
+
'anchor': 'ms'
|
| 264 |
+
}
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
def get_font_config(self) -> Dict[str, Dict[str, Any]]:
|
| 268 |
+
"""Return font configuration for different text elements."""
|
| 269 |
+
return {
|
| 270 |
+
'font_name': {
|
| 271 |
+
'path': 'assets/Montserrat-Bold.ttf',
|
| 272 |
+
'size': 32
|
| 273 |
+
},
|
| 274 |
+
'font_price_from': {
|
| 275 |
+
'path': 'assets/Montserrat-Regular.ttf',
|
| 276 |
+
'size': 22
|
| 277 |
+
},
|
| 278 |
+
'font_price': {
|
| 279 |
+
'path': 'assets/Montserrat-Bold.ttf',
|
| 280 |
+
'size': 40
|
| 281 |
+
},
|
| 282 |
+
'font_cupom': {
|
| 283 |
+
'path': 'assets/Montserrat-Bold.ttf',
|
| 284 |
+
'size': 42
|
| 285 |
+
}
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
class AvonTemplate(Template):
|
| 290 |
+
"""
|
| 291 |
+
Template implementation for Avon promotional images.
|
| 292 |
+
Uses template_b_avon.png with Avon-specific configuration.
|
| 293 |
+
"""
|
| 294 |
+
|
| 295 |
+
def __init__(self, template_path: str = None):
|
| 296 |
+
super().__init__(template_path or "assets/template_b_avon.png")
|
| 297 |
+
|
| 298 |
+
def get_box_config(self) -> Dict[str, Any]:
|
| 299 |
+
"""Return box configuration for product image."""
|
| 300 |
+
return {
|
| 301 |
+
'size': (602, 424),
|
| 302 |
+
'position': (54, 254) # (x, y) from top-left corner
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
def get_text_config(self) -> Dict[str, Dict[str, Any]]:
|
| 306 |
+
"""Return text configuration for all text elements."""
|
| 307 |
+
return {
|
| 308 |
+
'product_name': {
|
| 309 |
+
'position': (72, 727),
|
| 310 |
+
'color': '#000000',
|
| 311 |
+
'font': 'font_name',
|
| 312 |
+
'anchor': 'ls'
|
| 313 |
+
},
|
| 314 |
+
'original_price': {
|
| 315 |
+
'position': (72, 765),
|
| 316 |
+
'color': '#666666',
|
| 317 |
+
'font': 'font_price_from',
|
| 318 |
+
'anchor': 'ls'
|
| 319 |
+
},
|
| 320 |
+
'final_price': {
|
| 321 |
+
'position': (90, 837),
|
| 322 |
+
'color': '#FFFFFF',
|
| 323 |
+
'font': 'font_price',
|
| 324 |
+
'anchor': 'lm'
|
| 325 |
+
},
|
| 326 |
+
'coupon_code': {
|
| 327 |
+
'position': (461, 957),
|
| 328 |
+
'color': '#FFFFFF',
|
| 329 |
+
'font': 'font_cupom',
|
| 330 |
+
'anchor': 'ms'
|
| 331 |
+
}
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
def get_font_config(self) -> Dict[str, Dict[str, Any]]:
|
| 335 |
+
"""Return font configuration for different text elements."""
|
| 336 |
+
return {
|
| 337 |
+
'font_name': {
|
| 338 |
+
'path': 'assets/Montserrat-Bold.ttf',
|
| 339 |
+
'size': 32
|
| 340 |
+
},
|
| 341 |
+
'font_price_from': {
|
| 342 |
+
'path': 'assets/Montserrat-Regular.ttf',
|
| 343 |
+
'size': 22
|
| 344 |
+
},
|
| 345 |
+
'font_price': {
|
| 346 |
+
'path': 'assets/Montserrat-Bold.ttf',
|
| 347 |
+
'size': 40
|
| 348 |
+
},
|
| 349 |
+
'font_cupom': {
|
| 350 |
+
'path': 'assets/Montserrat-Bold.ttf',
|
| 351 |
+
'size': 42
|
| 352 |
+
}
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
|
| 356 |
+
# Register additional templates
|
| 357 |
+
TemplateRegistry.register('lidi_promo', LidiPromoTemplate)
|
| 358 |
+
TemplateRegistry.register('natura', NaturaTemplate)
|
| 359 |
+
TemplateRegistry.register('avon', AvonTemplate)
|