habulaj's picture
Update routers/video.py
7130523 verified
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)}")