from fastapi import APIRouter, HTTPException, Query from fastapi.responses import StreamingResponse from PIL import Image, ImageDraw, ImageFont, ImageFilter from io import BytesIO import requests from typing import Optional router = APIRouter() def render_headline_text(draw: ImageDraw.Draw, text: str, x: int, y: int, max_width: int, max_lines: int = 5) -> None: """ Renderiza headline com fonte Montserrat-ExtraBold. Args: draw: Objeto ImageDraw para desenhar text: Texto do headline x: Posição X (centro) y: Posição Y (base - texto alinhado de baixo para cima) max_width: Largura máxima do texto max_lines: Número máximo de linhas (padrão: 5) """ if not text.strip(): return # Converter texto para maiúsculas text = text.upper() # Carregar fonte Montserrat-ExtraBold try: font_path = "fonts/Montserrat-ExtraBold.ttf" base_font = ImageFont.truetype(font_path, 70) except (OSError, IOError): # Fallback para fonte padrão se não encontrar a fonte base_font = ImageFont.load_default() # Dividir texto em palavras words = text.split() if not words: return # Função para quebrar texto em linhas def wrap_text(text, font_size, max_width): try: test_font = ImageFont.truetype(font_path, font_size) except (OSError, IOError): test_font = ImageFont.load_default() lines = [] current_line = [] for word in words: test_line = " ".join(current_line + [word]) bbox = draw.textbbox((0, 0), test_line, font=test_font) line_width = bbox[2] - bbox[0] if line_width <= max_width: current_line.append(word) else: if current_line: lines.append(" ".join(current_line)) current_line = [word] else: # Palavra muito longa, adiciona mesmo assim lines.append(word) if current_line: lines.append(" ".join(current_line)) return lines # Encontrar o tamanho de fonte ideal font_size = 70 # Tamanho inicial min_font_size = 20 # Tamanho mínimo # Tentar diferentes tamanhos de fonte até encontrar um que caiba em max_lines while font_size >= min_font_size: lines = wrap_text(text, font_size, max_width) # Se o texto cabe em max_lines ou menos, usar este tamanho if len(lines) <= max_lines: break # Reduzir o tamanho da fonte font_size -= 2 # Garantir que não seja menor que o tamanho mínimo font_size = max(font_size, min_font_size) # Carregar fonte final try: final_font = ImageFont.truetype(font_path, font_size) except (OSError, IOError): final_font = ImageFont.load_default() # Quebrar texto final lines = wrap_text(text, font_size, max_width) # Se ainda não couber em max_lines, forçar quebra nas primeiras max_lines if len(lines) > max_lines: combined_text = " ".join(lines[:max_lines-1] + [" ".join(lines[max_lines-1:])]) lines = wrap_text(combined_text, font_size, max_width) lines = lines[:max_lines] # Calcular altura total do texto line_heights = [] for line in lines: bbox = draw.textbbox((0, 0), line, font=final_font) line_height = bbox[3] - bbox[1] line_heights.append(line_height) # Calcular posição inicial (Y é a posição da base - última linha) # Começar de baixo para cima current_y = y # Desenhar cada linha (de baixo para cima) for i, line in enumerate(reversed(lines)): line_height = line_heights[len(lines) - 1 - i] # Calcular posição X centralizada para esta linha bbox = draw.textbbox((0, 0), line, font=final_font) line_width = bbox[2] - bbox[0] centered_x = x - (line_width // 2) # Ajustar Y para que a base da linha fique na posição correta # O textbbox retorna (left, top, right, bottom), então precisamos ajustar text_bbox = draw.textbbox((0, 0), line, font=final_font) text_height = text_bbox[3] - text_bbox[1] adjusted_y = current_y - text_height # Desenhar texto principal (branco) draw.text((centered_x, adjusted_y), line, fill=(0, 0, 0, 89), font=final_font) # Texto com opacidade para sombra # Atualizar posição para próxima linha (subindo) current_y -= int(line_height * 1.4) # Line height de 140% def render_headline_text_white(draw: ImageDraw.Draw, text: str, x: int, y: int, max_width: int, max_lines: int = 5) -> None: """ Renderiza headline branco com fonte Montserrat-ExtraBold. """ if not text.strip(): return # Converter texto para maiúsculas text = text.upper() # Carregar fonte Montserrat-ExtraBold try: font_path = "fonts/Montserrat-ExtraBold.ttf" except (OSError, IOError): font_path = None # Dividir texto em palavras words = text.split() if not words: return # Função para quebrar texto em linhas def wrap_text(text, font_size, max_width): try: test_font = ImageFont.truetype(font_path, font_size) except (OSError, IOError): test_font = ImageFont.load_default() lines = [] current_line = [] for word in words: test_line = " ".join(current_line + [word]) bbox = draw.textbbox((0, 0), test_line, font=test_font) line_width = bbox[2] - bbox[0] if line_width <= max_width: current_line.append(word) else: if current_line: lines.append(" ".join(current_line)) current_line = [word] else: lines.append(word) if current_line: lines.append(" ".join(current_line)) return lines # Encontrar o tamanho de fonte ideal font_size = 70 min_font_size = 20 while font_size >= min_font_size: lines = wrap_text(text, font_size, max_width) if len(lines) <= max_lines: break font_size -= 2 font_size = max(font_size, min_font_size) # Carregar fonte final try: final_font = ImageFont.truetype(font_path, font_size) except (OSError, IOError): final_font = ImageFont.load_default() # Quebrar texto final lines = wrap_text(text, font_size, max_width) if len(lines) > max_lines: combined_text = " ".join(lines[:max_lines-1] + [" ".join(lines[max_lines-1:])]) lines = wrap_text(combined_text, font_size, max_width) lines = lines[:max_lines] # Calcular altura total do texto line_heights = [] for line in lines: bbox = draw.textbbox((0, 0), line, font=final_font) line_height = bbox[3] - bbox[1] line_heights.append(line_height) # Calcular posição inicial (Y é a posição da base - última linha) # Começar de baixo para cima current_y = y # Desenhar cada linha (branco) - de baixo para cima for i, line in enumerate(reversed(lines)): line_height = line_heights[len(lines) - 1 - i] # Calcular posição X centralizada para esta linha bbox = draw.textbbox((0, 0), line, font=final_font) line_width = bbox[2] - bbox[0] centered_x = x - (line_width // 2) # Ajustar Y para que a base da linha fique na posição correta text_bbox = draw.textbbox((0, 0), line, font=final_font) text_height = text_bbox[3] - text_bbox[1] adjusted_y = current_y - text_height # Desenhar texto principal (branco) draw.text((centered_x, adjusted_y), line, fill=(255, 255, 255, 255), font=final_font) # Atualizar posição para próxima linha (subindo) current_y -= int(line_height * 1.4) # Line height de 140% def calculate_text_height(draw: ImageDraw.Draw, text: str, max_width: int, max_lines: int = 5) -> int: """ Calcula a altura real do texto considerando o tamanho de fonte final que será usado. Args: draw: Objeto ImageDraw para calcular dimensões text: Texto do headline max_width: Largura máxima do texto max_lines: Número máximo de linhas (padrão: 5) Returns: Altura total do texto em pixels """ if not text.strip(): return 0 # Converter texto para maiúsculas text = text.upper() # Carregar fonte Montserrat-ExtraBold try: font_path = "fonts/Montserrat-ExtraBold.ttf" base_font = ImageFont.truetype(font_path, 70) except (OSError, IOError): # Fallback para fonte padrão se não encontrar a fonte base_font = ImageFont.load_default() # Dividir texto em palavras words = text.split() if not words: return 0 # Função para quebrar texto em linhas def wrap_text(text, font_size, max_width): try: test_font = ImageFont.truetype(font_path, font_size) except (OSError, IOError): test_font = ImageFont.load_default() lines = [] current_line = [] for word in words: test_line = " ".join(current_line + [word]) bbox = draw.textbbox((0, 0), test_line, font=test_font) line_width = bbox[2] - bbox[0] if line_width <= max_width: current_line.append(word) else: if current_line: lines.append(" ".join(current_line)) current_line = [word] else: # Palavra muito longa, adiciona mesmo assim lines.append(word) if current_line: lines.append(" ".join(current_line)) return lines # Encontrar o tamanho de fonte ideal (mesmo algoritmo da função render) font_size = 70 # Tamanho inicial min_font_size = 20 # Tamanho mínimo # Tentar diferentes tamanhos de fonte até encontrar um que caiba em max_lines while font_size >= min_font_size: lines = wrap_text(text, font_size, max_width) # Se o texto cabe em max_lines ou menos, usar este tamanho if len(lines) <= max_lines: break # Reduzir o tamanho da fonte font_size -= 2 # Garantir que não seja menor que o tamanho mínimo font_size = max(font_size, min_font_size) # Carregar fonte final try: final_font = ImageFont.truetype(font_path, font_size) except (OSError, IOError): final_font = ImageFont.load_default() # Quebrar texto final lines = wrap_text(text, font_size, max_width) # Se ainda não couber em max_lines, forçar quebra nas primeiras max_lines if len(lines) > max_lines: combined_text = " ".join(lines[:max_lines-1] + [" ".join(lines[max_lines-1:])]) lines = wrap_text(combined_text, font_size, max_width) lines = lines[:max_lines] # Calcular altura total do texto total_height = 0 for line in lines: bbox = draw.textbbox((0, 0), line, font=final_font) line_height = bbox[3] - bbox[1] total_height += int(line_height * 1.4) # Line height de 140% return total_height def render_badge(image: Image.Image, x: int, y: int) -> None: """ Renderiza o badge.png acima do título. Args: image: Imagem principal onde o badge será desenhado x: Posição X centralizada y: Posição Y (topo do badge) """ try: # Carregar o badge badge = Image.open("badge.png") # Redimensionar para as dimensões especificadas (L: 210, Altura: 29) badge = badge.resize((210, 29), Image.Resampling.LANCZOS) # Converter para RGBA se necessário if badge.mode != 'RGBA': badge = badge.convert('RGBA') # Calcular posição centralizada badge_x = x - (210 // 2) # Centralizar horizontalmente # Colar o badge na imagem principal image.paste(badge, (badge_x, y), badge) except (OSError, IOError) as e: # Se não conseguir carregar o badge, apenas ignora print(f"Erro ao carregar badge: {e}") @router.get("/cover/video") def get_video_cover( headline: Optional[str] = Query(None, description="Headline a ser exibido") ): """ Endpoint que retorna uma imagem de fundo com dimensões 1080x1920. """ try: # Dimensões especificadas: largura 1080, altura 1920 width, height = 1080, 1920 # Criar fundo transparente background_image = Image.new("RGBA", (width, height), color=(0, 0, 0, 0)) # Adicionar headline se fornecido if headline and headline.strip(): # Calcular altura total do texto para posicionar o badge # Criar uma imagem temporária para calcular a altura temp_img = Image.new('RGBA', background_image.size, (0, 0, 0, 0)) temp_draw = ImageDraw.Draw(temp_img) # Calcular altura real do texto considerando o tamanho de fonte final total_text_height = calculate_text_height(temp_draw, headline, 720, 5) # Posição do badge: 28px acima do topo do texto badge_y = 1270 - total_text_height - 28 # Renderizar o badge render_badge(background_image, 540, badge_y) # Criar uma nova imagem para o texto com transparência text_img = Image.new('RGBA', background_image.size, (0, 0, 0, 0)) text_draw = ImageDraw.Draw(text_img) # Renderizar o texto na imagem temporária (para sombra) render_headline_text( text_draw, headline, x=540, # Centro da imagem (1080/2) y=1270, max_width=720, max_lines=5 ) # Aplicar desfoque na sombra (aumentado) shadow_img = text_img.copy() shadow_img = shadow_img.filter(ImageFilter.GaussianBlur(radius=25)) # Aplicar offset de 5,5 na sombra (aumentado) shadow_offset = Image.new('RGBA', background_image.size, (0, 0, 0, 0)) shadow_offset.paste(shadow_img, (5, 5)) # Combinar sombra com imagem de fundo background_image = Image.alpha_composite(background_image, shadow_offset) # Criar imagem para texto principal (branco) text_white_img = Image.new('RGBA', background_image.size, (0, 0, 0, 0)) text_white_draw = ImageDraw.Draw(text_white_img) # Renderizar texto branco render_headline_text_white(text_white_draw, headline, 540, 1270, 720, 5) # Combinar texto principal background_image = Image.alpha_composite(background_image, text_white_img) # Converter para BytesIO (mantendo transparência) buffer = BytesIO() background_image.save(buffer, format="PNG") buffer.seek(0) return StreamingResponse(buffer, media_type="image/png") except Exception as e: raise HTTPException(status_code=500, detail=f"Erro ao gerar imagem: {str(e)}")