from fastapi import APIRouter, Query, HTTPException from fastapi.responses import StreamingResponse from PIL import Image, ImageDraw, ImageFont from io import BytesIO import requests from typing import Optional 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" ) } try: response = requests.get(url, headers=headers, timeout=10) response.raise_for_status() return Image.open(BytesIO(response.content)).convert("RGBA") except Exception as e: raise HTTPException(status_code=400, detail=f"Erro ao baixar imagem: {url} ({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_gradient_overlay(width: int, height: int, text_position: str = "bottom") -> Image.Image: """ Cria gradiente overlay baseado na posição do texto """ gradient = Image.new("RGBA", (width, height)) draw = ImageDraw.Draw(gradient) if text_position.lower() == "bottom": # Gradiente para texto embaixo: posição Y:531, altura 835px gradient_start = 531 gradient_height = 835 for y in range(gradient_height): if y + gradient_start < height: # Gradient: 0% transparent -> 46.63% rgba(0,0,0,0.55) -> 100% rgba(0,0,0,0.7) ratio = y / gradient_height if ratio <= 0.4663: # 0% a 46.63%: de transparente para 0.55 opacity_ratio = ratio / 0.4663 opacity = int(255 * 0.55 * opacity_ratio) else: # 46.63% a 100%: de 0.55 para 0.7 opacity_ratio = (ratio - 0.4663) / (1 - 0.4663) opacity = int(255 * (0.55 + (0.7 - 0.55) * opacity_ratio)) draw.line([(0, y + gradient_start), (width, y + gradient_start)], fill=(0, 0, 0, opacity)) else: # text_position == "top" # Gradiente para texto no topo: posição Y:0, altura 835px # linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.307045) 16.93%, rgba(0,0,0,0.55) 45.57%, rgba(0,0,0,0.7) 100%) # 0deg significa: 0% = bottom, 100% = top gradient_height = 835 for y in range(gradient_height): if y < height: # Inverter a ratio: y=0 (topo) deve ser 100% do gradient, y=835 (bottom) deve ser 0% ratio = (gradient_height - y) / gradient_height if ratio <= 0.1693: # 0% a 16.93%: de 0 (transparente) para 0.307 opacity_ratio = ratio / 0.1693 opacity = int(255 * (0.307 * opacity_ratio)) elif ratio <= 0.4557: # 16.93% a 45.57%: de 0.307 para 0.55 opacity_ratio = (ratio - 0.1693) / (0.4557 - 0.1693) opacity = int(255 * (0.307 + (0.55 - 0.307) * opacity_ratio)) else: # 45.57% a 100%: de 0.55 para 0.7 opacity_ratio = (ratio - 0.4557) / (1 - 0.4557) opacity = int(255 * (0.55 + (0.7 - 0.55) * opacity_ratio)) draw.line([(0, y), (width, y)], fill=(0, 0, 0, opacity)) return gradient def wrap_text(text: str, font: ImageFont.FreeTypeFont, max_width: int, draw: ImageDraw.Draw) -> list[str]: words = text.split() lines = [] current_line = "" for word in words: test_line = f"{current_line} {word}".strip() if draw.textlength(test_line, font=font) <= max_width: current_line = test_line else: if current_line: lines.append(current_line) current_line = word if current_line: lines.append(current_line) return lines def get_responsive_font_and_lines(text: str, font_path: str, max_width: int, max_lines: int = 3, max_font_size: int = 80, min_font_size: int = 20) -> tuple[ImageFont.FreeTypeFont, list[str], int]: """ Retorna a fonte e linhas ajustadas para caber no número máximo de linhas. """ temp_img = Image.new("RGB", (1, 1)) temp_draw = ImageDraw.Draw(temp_img) current_font_size = max_font_size while current_font_size >= min_font_size: try: font = ImageFont.truetype(font_path, current_font_size) except Exception: font = ImageFont.load_default() lines = wrap_text(text, font, max_width, temp_draw) if len(lines) <= max_lines: return font, lines, current_font_size current_font_size -= 1 try: font = ImageFont.truetype(font_path, min_font_size) except Exception: font = ImageFont.load_default() lines = wrap_text(text, font, max_width, temp_draw) return font, lines, min_font_size def get_font_and_lines(text: str, font_path: str, font_size: int, max_width: int) -> tuple[ImageFont.FreeTypeFont, list[str]]: """ Retorna a fonte e linhas com tamanho fixo de fonte. """ try: font = ImageFont.truetype(font_path, font_size) except Exception: font = ImageFont.load_default() temp_img = Image.new("RGB", (1, 1)) temp_draw = ImageDraw.Draw(temp_img) lines = wrap_text(text, font, max_width, temp_draw) return font, lines def get_text_color_rgb(text_color: str) -> tuple[int, int, int]: """ Converte o parâmetro text_color para RGB. """ if text_color.lower() == "black": return (0, 0, 0) else: # white por padrão return (255, 255, 255) def get_device_dimensions(device: str) -> tuple[int, int]: """Retorna as dimensões baseadas no dispositivo""" if device.lower() == "web": return (1280, 720) else: # Instagram por padrão return (1080, 1350) def create_canvas(image_url: Optional[str], headline: Optional[str], device: str = "ig", text_position: str = "bottom", text_color: str = "white") -> BytesIO: width, height = get_device_dimensions(device) is_web = device.lower() == "web" text_rgb = get_text_color_rgb(text_color) # Configurações específicas por dispositivo if is_web: padding_x = 40 logo_width, logo_height = 120, 22 logo_padding = 40 else: padding_x = 60 bottom_padding = 80 top_padding = 60 logo_width, logo_height = 121, 23 # Novas dimensões: L:121, A:22.75 (arredondado para 23) max_width = width - 2 * padding_x canvas = Image.new("RGBA", (width, height), color=(255, 255, 255, 255)) # Adicionar imagem de fundo se fornecida if image_url: img = download_image_from_url(image_url) filled_img = resize_and_crop_to_fill(img, width, height) canvas.paste(filled_img, (0, 0)) # Para Instagram: adicionar gradiente e texto if not is_web: # Só aplicar gradiente se o texto for branco if text_color.lower() != "black": gradient_overlay = create_gradient_overlay(width, height, text_position) canvas = Image.alpha_composite(canvas, gradient_overlay) if headline: draw = ImageDraw.Draw(canvas) font_path = "fonts/AGaramondPro-Semibold.ttf" line_height_factor = 1.05 # 105% da altura da linha try: font, lines, font_size = get_responsive_font_and_lines( headline, font_path, max_width, max_lines=3, max_font_size=80, min_font_size=20 ) line_height = int(font_size * line_height_factor) except Exception as e: raise HTTPException(status_code=500, detail=f"Erro ao processar a fonte: {e}") total_text_height = len(lines) * line_height # Posicionar texto baseado no parâmetro text_position if text_position.lower() == "bottom": # Posicionar texto 50px acima da logo (que está em Y:1274) text_end_y = 1274 - 50 start_y = text_end_y - total_text_height else: # text_position == "top" # Posicionar texto no topo com padding start_y = top_padding # Adicionar logo no canto inferior direito (posição fixa) try: logo_path = "recurve.png" logo = Image.open(logo_path).convert("RGBA") logo_resized = logo.resize((logo_width, logo_height)) # Aplicar opacidade de 42% logo_with_opacity = Image.new("RGBA", logo_resized.size) for x in range(logo_resized.width): for y in range(logo_resized.height): r, g, b, a = logo_resized.getpixel((x, y)) new_alpha = int(a * 0.42) # 42% de opacidade logo_with_opacity.putpixel((x, y), (r, g, b, new_alpha)) # Posição fixa: X:891, Y:1274 canvas.paste(logo_with_opacity, (891, 1274), logo_with_opacity) except Exception as e: raise HTTPException(status_code=500, detail=f"Erro ao carregar a logo: {e}") # Adiciona texto com a cor especificada for i, line in enumerate(lines): y = start_y + i * line_height draw.text((padding_x, y), line, font=font, fill=text_rgb) # Para web: apenas logo no canto inferior direito else: try: logo_path = "recurve.png" logo = Image.open(logo_path).convert("RGBA") logo_resized = logo.resize((logo_width, logo_height)) logo_x = width - logo_width - logo_padding logo_y = height - logo_height - logo_padding canvas.paste(logo_resized, (logo_x, logo_y), logo_resized) except Exception as e: raise HTTPException(status_code=500, detail=f"Erro ao carregar a logo: {e}") buffer = BytesIO() canvas.convert("RGB").save(buffer, format="PNG") buffer.seek(0) return buffer @router.get("/cover/news") def get_news_image( image_url: Optional[str] = Query(None, description="URL da imagem de fundo"), headline: Optional[str] = Query(None, description="Texto do título (opcional para IG, ignorado para web)"), device: str = Query("ig", description="Dispositivo: 'ig' para Instagram (1080x1350) ou 'web' para Web (1280x720)"), text_position: str = Query("bottom", description="Posição do texto: 'top' para topo ou 'bottom' para parte inferior"), text_color: str = Query("white", description="Cor do texto: 'white' (padrão) ou 'black'. Se 'black', remove o gradiente de fundo") ): try: buffer = create_canvas(image_url, headline, device, text_position, text_color) return StreamingResponse(buffer, media_type="image/png") except Exception as e: raise HTTPException(status_code=500, detail=f"Erro ao gerar imagem: {str(e)}")