Spaces:
Running
Running
from fastapi import APIRouter, Query, HTTPException | |
from fastapi.responses import StreamingResponse | |
from PIL import Image, ImageDraw, ImageEnhance, ImageFont | |
from io import BytesIO | |
import requests | |
from typing import Optional | |
import logging | |
# Configurar logging | |
logging.basicConfig(level=logging.INFO) | |
log = logging.getLogger("memoriam-api") | |
router = APIRouter() | |
def download_image_from_url(url: str) -> Image.Image: | |
headers = { | |
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36" | |
} | |
response = requests.get(url, headers=headers) | |
if response.status_code != 200: | |
raise HTTPException(status_code=400, detail=f"Imagem não pôde ser baixada. Código {response.status_code}") | |
try: | |
return Image.open(BytesIO(response.content)).convert("RGB") | |
except Exception as e: | |
raise HTTPException(status_code=400, detail=f"Erro ao abrir imagem: {str(e)}") | |
def resize_and_crop_to_fill(img: Image.Image, target_width: int, target_height: int) -> Image.Image: | |
img_ratio = img.width / img.height | |
target_ratio = target_width / target_height | |
if img_ratio > target_ratio: | |
scale_height = target_height | |
scale_width = int(scale_height * img_ratio) | |
else: | |
scale_width = target_width | |
scale_height = int(scale_width / img_ratio) | |
img_resized = img.resize((scale_width, scale_height), Image.LANCZOS) | |
left = (scale_width - target_width) // 2 | |
top = (scale_height - target_height) // 2 | |
right = left + target_width | |
bottom = top + target_height | |
return img_resized.crop((left, top, right, bottom)) | |
def create_bottom_black_gradient(width: int, height: int) -> Image.Image: | |
"""Cria um gradiente preto suave que vai do topo transparente até a metade da imagem preto""" | |
gradient = Image.new("RGBA", (width, height), color=(0, 0, 0, 0)) | |
draw = ImageDraw.Draw(gradient) | |
for y in range(height): | |
# Gradiente mais suave que começa transparente e vai até metade da imagem | |
ratio = y / height | |
if ratio <= 0.6: | |
# Primeira parte: totalmente transparente | |
alpha = 0 | |
elif ratio <= 0.75: | |
# Transição muito suave (60% a 75% da altura) | |
alpha = int(80 * (ratio - 0.6) / 0.15) | |
else: | |
# Final suave (75% a 100% da altura) | |
alpha = int(80 + 50 * (ratio - 0.75) / 0.25) | |
# Usar preto puro (0, 0, 0) com alpha mais baixo | |
draw.line([(0, y), (width, y)], fill=(0, 0, 0, alpha)) | |
return gradient | |
def create_top_black_gradient(width: int, height: int) -> Image.Image: | |
"""Cria um gradiente preto suave que vai do fundo transparente até a metade da imagem preto""" | |
gradient = Image.new("RGBA", (width, height), color=(0, 0, 0, 0)) | |
draw = ImageDraw.Draw(gradient) | |
for y in range(height): | |
# Gradiente mais suave que começa preto e vai até metade da imagem | |
ratio = y / height | |
if ratio <= 0.25: | |
# Primeira parte suave (0% a 25% da altura) | |
alpha = int(80 + 50 * (0.25 - ratio) / 0.25) | |
elif ratio <= 0.4: | |
# Transição muito suave (25% a 40% da altura) | |
alpha = int(80 * (0.4 - ratio) / 0.15) | |
else: | |
# Segunda parte: totalmente transparente | |
alpha = 0 | |
# Usar preto puro (0, 0, 0) com alpha mais baixo | |
draw.line([(0, y), (width, y)], fill=(0, 0, 0, alpha)) | |
return gradient | |
def draw_text_left_aligned(draw: ImageDraw.Draw, text: str, x: int, y: int, font_path: str, font_size: int): | |
"""Desenha texto alinhado à esquerda com especificações exatas""" | |
try: | |
font = ImageFont.truetype(font_path, font_size) | |
except Exception: | |
font = ImageFont.load_default() | |
# Espaçamento entre letras 0% e cor branca | |
draw.text((x, y), text, font=font, fill=(255, 255, 255), spacing=0) | |
def create_canvas(image_url: Optional[str], name: Optional[str], birth: Optional[str], death: Optional[str], text_position: str = "bottom") -> BytesIO: | |
# Dimensões fixas para Instagram | |
width = 1080 | |
height = 1350 | |
canvas = Image.new("RGBA", (width, height), (0, 0, 0, 0)) # Fundo transparente | |
# Carregar e processar imagem de fundo se fornecida | |
if image_url: | |
try: | |
img = download_image_from_url(image_url) | |
img_bw = ImageEnhance.Color(img).enhance(0.0).convert("RGBA") | |
filled_img = resize_and_crop_to_fill(img_bw, width, height) | |
canvas.paste(filled_img, (0, 0)) | |
except Exception as e: | |
log.warning(f"Erro ao carregar imagem: {e}") | |
# Aplicar gradiente baseado na posição do texto | |
if text_position.lower() == "top": | |
gradient_overlay = create_top_black_gradient(width, height) | |
else: # bottom | |
gradient_overlay = create_bottom_black_gradient(width, height) | |
canvas = Image.alpha_composite(canvas, gradient_overlay) | |
# Adicionar logo no canto inferior direito com opacidade | |
try: | |
logo = Image.open("recurve.png").convert("RGBA") | |
logo_resized = logo.resize((120, 22)) | |
# Aplicar opacidade à logo | |
logo_with_opacity = Image.new("RGBA", logo_resized.size) | |
logo_with_opacity.paste(logo_resized, (0, 0)) | |
# Reduzir opacidade | |
logo_alpha = logo_with_opacity.split()[-1].point(lambda x: int(x * 0.42)) # 42% de opacidade | |
logo_with_opacity.putalpha(logo_alpha) | |
logo_padding = 40 | |
logo_x = width - 120 - logo_padding | |
logo_y = height - 22 - logo_padding | |
canvas.paste(logo_with_opacity, (logo_x, logo_y), logo_with_opacity) | |
except Exception as e: | |
log.warning(f"Erro ao carregar a logo: {e}") | |
draw = ImageDraw.Draw(canvas) | |
# Configurar posições baseadas no text_position | |
text_x = 80 # Alinhamento à esquerda com margem | |
if text_position.lower() == "top": | |
dates_y = 100 | |
name_y = dates_y + 36 + 6 # Ano + espaçamento de 6px + nome | |
else: # bottom | |
dates_y = height - 250 | |
name_y = dates_y + 36 + 6 # Ano + espaçamento de 6px + nome | |
# Desenhar datas primeiro (se fornecidas) | |
if birth or death: | |
font_path_regular = "fonts/AGaramondPro-Regular.ttf" | |
# Construir texto das datas | |
dates_text = "" | |
if birth and death: | |
dates_text = f"{birth} - {death}" | |
elif birth: | |
dates_text = f"{birth}" | |
elif death: | |
dates_text = f"- {death}" | |
if dates_text: | |
draw_text_left_aligned(draw, dates_text, text_x, dates_y, font_path_regular, 36) | |
# Desenhar nome abaixo das datas | |
if name: | |
font_path = "fonts/AGaramondPro-BoldItalic.ttf" | |
draw_text_left_aligned(draw, name, text_x, name_y, font_path, 87) | |
buffer = BytesIO() | |
canvas.save(buffer, format="PNG") | |
buffer.seek(0) | |
return buffer | |
def get_memoriam_image( | |
image_url: Optional[str] = Query(None, description="URL da imagem de fundo"), | |
name: Optional[str] = Query(None, description="Nome (será exibido em maiúsculas)"), | |
birth: Optional[str] = Query(None, description="Ano de nascimento (ex: 1943)"), | |
death: Optional[str] = Query(None, description="Ano de falecimento (ex: 2023)"), | |
text_position: str = Query("bottom", description="Posição do texto: 'top' ou 'bottom'") | |
): | |
""" | |
Gera imagem de memoriam no formato 1080x1350 (Instagram). | |
Todos os parâmetros são opcionais, mas recomenda-se fornecer pelo menos o nome. | |
O gradiente será aplicado baseado na posição do texto (top ou bottom). | |
""" | |
try: | |
buffer = create_canvas(image_url, name, birth, death, text_position) | |
return StreamingResponse(buffer, media_type="image/png") | |
except Exception as e: | |
raise HTTPException(status_code=500, detail=f"Erro ao gerar imagem: {str(e)}") |