Spaces:
Running
Running
| 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}") | |
| 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)}") |