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 @router.get("/cover/memoriam") 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)}")