habulaj's picture
Update routers/image.py
047a98b verified
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, List
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:
"""
Redimensiona e corta a imagem para preencher exatamente o espaço alvo (sempre centralizado).
Args:
img: Imagem PIL
target_width: Largura alvo
target_height: Altura alvo
"""
img_ratio = img.width / img.height
target_ratio = target_width / target_height
if img_ratio > target_ratio:
# Imagem é mais larga proporcionalmente - redimensionar baseado na altura
scale_height = target_height
scale_width = int(scale_height * img_ratio)
else:
# Imagem é mais alta proporcionalmente - redimensionar baseado na largura
scale_width = target_width
scale_height = int(scale_width / img_ratio)
img_resized = img.resize((scale_width, scale_height), Image.LANCZOS)
# Centralizar o crop
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, position: str = "bottom") -> Image.Image:
""" Cria o gradiente overlay conforme especificado """
gradient = Image.new("RGBA", (width, height))
draw = ImageDraw.Draw(gradient)
if position == "top":
# Gradiente para topo: começa escuro em cima e vai ficando transparente
# linear-gradient(180deg, rgba(0, 0, 0, 0.7) 0%, rgba(0, 0, 0, 0.55) 45.06%, rgba(0, 0, 0, 0) 100%)
gradient_start = 0
gradient_height = 706
for y in range(gradient_height):
if y < height:
ratio = y / gradient_height
if ratio <= 0.4506: # 0% a 45.06%: de rgba(0,0,0,0.7) para rgba(0,0,0,0.55)
opacity_ratio = ratio / 0.4506
opacity = int(255 * (0.7 - 0.15 * opacity_ratio))
else: # 45.06% a 100%: de rgba(0,0,0,0.55) para transparente
opacity_ratio = (ratio - 0.4506) / (1 - 0.4506)
opacity = int(255 * 0.55 * (1 - opacity_ratio))
draw.line([(0, y), (width, y)], fill=(0, 0, 0, opacity))
else:
# Gradiente para baixo: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.55) 54.94%, rgba(0, 0, 0, 0.7) 100%)
# Posição: left: 0px, top: 644px, width: 1080px, height: 706px
gradient_start = 644
gradient_height = 706
for y in range(gradient_height):
if y + gradient_start < height:
ratio = y / gradient_height
if ratio <= 0.5494: # 0% a 54.94%: de transparente para rgba(0,0,0,0.55)
opacity_ratio = ratio / 0.5494
opacity = int(255 * 0.55 * opacity_ratio)
else: # 54.94% a 100%: de rgba(0,0,0,0.55) para rgba(0,0,0,0.7)
opacity_ratio = (ratio - 0.5494) / (1 - 0.5494)
opacity = int(255 * (0.55 + 0.15 * opacity_ratio))
draw.line([(0, y + gradient_start), (width, y + gradient_start)], fill=(0, 0, 0, opacity))
return gradient
def render_text(draw: ImageDraw.Draw, text: str, x: int, y: int, max_width: int, max_lines: int = 5, align_top: bool = False, text_color: tuple = (255, 255, 255, 255)) -> None:
"""
Renderiza texto com fonte Montserrat Regular, suportando formatação com asteriscos para negrito.
Ajusta automaticamente o tamanho da fonte para caber em max_lines.
Args:
draw: Objeto ImageDraw para desenhar
text: Texto a ser renderizado (suporta *texto* para negrito)
x: Posição X (esquerda)
y: Posição Y (topo do texto se align_top=True, base se align_top=False)
max_width: Largura máxima do texto
max_lines: Número máximo de linhas (padrão: 5)
align_top: Se True, alinha pelo topo; se False, alinha pela base
text_color: Cor do texto como tuple (R, G, B, A)
"""
import re
if not text.strip():
return
# Carregar fontes
try:
regular_font_path = "fonts/Montserrat-Regular.ttf"
bold_font_path = "fonts/Montserrat-Bold.ttf"
except (OSError, IOError):
regular_font_path = None
bold_font_path = None
# Função para processar texto com formatação
def parse_formatted_text(text):
"""Processa texto e retorna lista de tokens (palavras/espaços/pontuação) com formatação"""
tokens = []
current_token = ""
in_bold = False
i = 0
while i < len(text):
if text[i] == '*':
# Salvar token atual se não estiver vazio
if current_token:
tokens.append((current_token, in_bold))
current_token = ""
# Alternar negrito
in_bold = not in_bold
i += 1
else:
current_token += text[i]
i += 1
# Adicionar último token
if current_token:
tokens.append((current_token, in_bold))
return tokens
# Função para tokenizar respeitando espaços e pontuação
def tokenize_with_formatting(tokens):
"""Quebra tokens em palavras individuais, espaços e pontuação mantendo formatação"""
result = []
for token_text, is_bold in tokens:
# Usar regex para dividir em palavras, espaços e pontuação
parts = re.findall(r'\S+|\s+', token_text)
for part in parts:
result.append((part, is_bold))
return result
# Processar texto com formatação
formatted_tokens = parse_formatted_text(text)
detailed_tokens = tokenize_with_formatting(formatted_tokens)
# Função para quebrar texto em linhas considerando formatação
def wrap_formatted_text(tokens, max_width, font_size):
lines = []
current_line = []
# Carregar fontes com tamanho específico
try:
regular_font_size = ImageFont.truetype(regular_font_path, font_size) if regular_font_path else ImageFont.load_default()
bold_font_size = ImageFont.truetype(bold_font_path, font_size) if bold_font_path else ImageFont.load_default()
except (OSError, IOError):
regular_font_size = ImageFont.load_default()
bold_font_size = ImageFont.load_default()
i = 0
while i < len(tokens):
token_text, is_bold = tokens[i]
# Se for espaço em branco, pular para próximo token
if token_text.isspace():
i += 1
continue
# Testar se o token cabe na linha atual
test_line = current_line + [(token_text, is_bold)]
# Calcular largura da linha de teste
test_width = 0
for j, (seg_text, seg_is_bold) in enumerate(test_line):
# Usar fonte com tamanho correto
font_to_use = bold_font_size if seg_is_bold else regular_font_size
bbox = font_to_use.getbbox(seg_text)
test_width += bbox[2] - bbox[0]
# Adicionar espaço entre palavras (apenas se não for pontuação)
if j < len(test_line) - 1:
next_text = test_line[j + 1][0]
# Não adicionar espaço se o próximo token for pontuação
if not re.match(r'^[^\w\s]+$', next_text):
space_bbox = font_to_use.getbbox(" ")
test_width += space_bbox[2] - space_bbox[0]
if test_width <= max_width:
current_line.append((token_text, is_bold))
else:
# Quebrar linha
if current_line:
lines.append(current_line)
current_line = [(token_text, is_bold)]
else:
# Token muito longo, adiciona mesmo assim
lines.append([(token_text, is_bold)])
i += 1
if current_line:
lines.append(current_line)
return lines
# Encontrar o tamanho de fonte ideal
font_size = 32 # Tamanho inicial
min_font_size = 12 # Tamanho mínimo
# Tentar diferentes tamanhos de fonte até encontrar um que caiba em max_lines
while font_size >= min_font_size:
lines = wrap_formatted_text(detailed_tokens, max_width, font_size)
# 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 fontes finais
try:
final_regular_font = ImageFont.truetype(regular_font_path, font_size) if regular_font_path else ImageFont.load_default()
final_bold_font = ImageFont.truetype(bold_font_path, font_size) if bold_font_path else ImageFont.load_default()
except (OSError, IOError):
final_regular_font = ImageFont.load_default()
final_bold_font = ImageFont.load_default()
# Quebrar texto final
lines = wrap_formatted_text(detailed_tokens, max_width, font_size)
# Se ainda não couber em max_lines, forçar quebra nas primeiras max_lines
if len(lines) > max_lines:
# Combinar as linhas restantes na última linha permitida
combined_tokens = []
for i, line in enumerate(lines):
if i < max_lines - 1:
combined_tokens.extend(line)
elif i == max_lines - 1:
# Última linha permitida - combinar com todas as linhas restantes
remaining_tokens = []
for j in range(i, len(lines)):
remaining_tokens.extend(lines[j])
combined_tokens.extend(remaining_tokens)
break
lines = wrap_formatted_text(combined_tokens, max_width, font_size)
lines = lines[:max_lines] # Garantir que não exceda max_lines
# Desenhar texto baseado no alinhamento
if align_top:
# Alinhamento pelo topo: desenhar de cima para baixo
current_y = y
for line in lines:
# Calcular altura da linha (usar altura máxima dos segmentos)
max_line_height = 0
for segment_text, is_bold in line:
font_to_use = final_bold_font if is_bold else final_regular_font
bbox = font_to_use.getbbox(segment_text)
line_height = bbox[3] - bbox[1]
max_line_height = max(max_line_height, line_height)
# Desenhar segmentos da linha
current_x = x
for i, (segment_text, is_bold) in enumerate(line):
font_to_use = final_bold_font if is_bold else final_regular_font
draw.text((current_x, current_y), segment_text, fill=text_color, font=font_to_use)
bbox = font_to_use.getbbox(segment_text)
current_x += bbox[2] - bbox[0]
# Adicionar espaço entre palavras (apenas se não for pontuação)
if i < len(line) - 1:
next_text = line[i + 1][0]
# Não adicionar espaço se o próximo token for pontuação
if not re.match(r'^[^\w\s]+$', next_text):
space_bbox = font_to_use.getbbox(" ")
current_x += space_bbox[2] - space_bbox[0]
# Aplicar line height de 120% (adicionar 20% de espaçamento extra)
current_y += int(max_line_height * 1.20)
else:
# Alinhamento pela base: desenhar de baixo para cima
current_y = y
for line in reversed(lines):
# Calcular altura da linha (usar altura máxima dos segmentos)
max_line_height = 0
for segment_text, is_bold in line:
font_to_use = final_bold_font if is_bold else final_regular_font
bbox = font_to_use.getbbox(segment_text)
line_height = bbox[3] - bbox[1]
max_line_height = max(max_line_height, line_height)
# Aplicar line height de 120% (adicionar 20% de espaçamento extra)
current_y -= int(max_line_height * 1.20)
# Desenhar segmentos da linha
current_x = x
for i, (segment_text, is_bold) in enumerate(line):
font_to_use = final_bold_font if is_bold else final_regular_font
draw.text((current_x, current_y), segment_text, fill=text_color, font=font_to_use)
bbox = font_to_use.getbbox(segment_text)
current_x += bbox[2] - bbox[0]
# Adicionar espaço entre palavras (apenas se não for pontuação)
if i < len(line) - 1:
next_text = line[i + 1][0]
# Não adicionar espaço se o próximo token for pontuação
if not re.match(r'^[^\w\s]+$', next_text):
space_bbox = font_to_use.getbbox(" ")
current_x += space_bbox[2] - space_bbox[0]
def calculate_text_height(text: str, font, max_width: int) -> int:
"""Calcula a altura total do texto quebrado em linhas"""
if not text.strip():
return 0
words = text.split()
if not words:
return 0
# Quebrar texto em linhas
lines = []
current_line = []
for word in words:
test_line = " ".join(current_line + [word])
bbox = font.getbbox(test_line)
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))
# Calcular altura total
total_height = 0
for line in lines:
bbox = font.getbbox(line)
line_height = bbox[3] - bbox[1]
total_height += int(line_height * 1.20) # Aplicar line height de 120%
return total_height
def render_slide_text(draw: ImageDraw.Draw, text: str, x: int, y: int, max_width: int, max_lines: int = 5, center_vertical: bool = False, text_color: tuple = (0, 0, 0, 255)) -> None:
"""
Renderiza texto para slides com fonte padrão, tamanho inicial 32px.
Suporta formatação com asteriscos para negrito.
Args:
draw: Objeto ImageDraw para desenhar
text: Texto a ser renderizado (suporta *texto* para negrito)
x: Posição X (esquerda)
y: Posição Y (topo do texto se center_vertical=False, centro se center_vertical=True)
max_width: Largura máxima do texto
max_lines: Número máximo de linhas (padrão: 5)
center_vertical: Se True, centraliza verticalmente o texto na posição Y
text_color: Cor do texto como tuple (R, G, B, A)
"""
import re
if not text.strip():
return
# Carregar fontes
try:
regular_font_path = "fonts/Montserrat-Regular.ttf"
bold_font_path = "fonts/Montserrat-Bold.ttf"
regular_font = ImageFont.truetype(regular_font_path, 32)
bold_font = ImageFont.truetype(bold_font_path, 32)
except (OSError, IOError):
# Fallback para fonte padrão se as fontes personalizadas não forem encontradas
regular_font = ImageFont.load_default()
bold_font = ImageFont.load_default()
# Função para processar texto com formatação
def parse_formatted_text(text):
"""Processa texto e retorna lista de tokens (palavras/espaços/pontuação) com formatação"""
tokens = []
current_token = ""
in_bold = False
current_font = regular_font
i = 0
while i < len(text):
if text[i] == '*':
# Salvar token atual se não estiver vazio
if current_token:
tokens.append((current_token, current_font))
current_token = ""
# Alternar negrito
in_bold = not in_bold
current_font = bold_font if in_bold else regular_font
i += 1
else:
current_token += text[i]
i += 1
# Adicionar último token
if current_token:
tokens.append((current_token, current_font))
return tokens
# Função para tokenizar respeitando espaços e pontuação
def tokenize_with_formatting(tokens):
"""Quebra tokens em palavras individuais, espaços e pontuação mantendo formatação"""
result = []
for token_text, font in tokens:
# Usar regex para dividir em palavras, espaços e pontuação
parts = re.findall(r'\S+|\s+', token_text)
for part in parts:
result.append((part, font))
return result
# Processar texto com formatação
formatted_tokens = parse_formatted_text(text)
detailed_tokens = tokenize_with_formatting(formatted_tokens)
# Função para quebrar texto em linhas considerando formatação
def wrap_formatted_text(tokens, max_width, font_size):
lines = []
current_line = []
# Carregar fontes com tamanho específico
try:
regular_font_size = ImageFont.truetype(regular_font_path, font_size)
bold_font_size = ImageFont.truetype(bold_font_path, font_size)
except (OSError, IOError):
regular_font_size = ImageFont.load_default()
bold_font_size = ImageFont.load_default()
i = 0
while i < len(tokens):
token_text, token_font = tokens[i]
# Se for espaço em branco, pular para próximo token
if token_text.isspace():
i += 1
continue
# Testar se o token cabe na linha atual
test_line = current_line + [(token_text, token_font)]
# Calcular largura da linha de teste
test_width = 0
for j, (seg_text, seg_font) in enumerate(test_line):
# Usar fonte com tamanho correto
font_to_use = regular_font_size if seg_font == regular_font else bold_font_size
bbox = font_to_use.getbbox(seg_text)
test_width += bbox[2] - bbox[0]
# Adicionar espaço entre palavras (apenas se não for pontuação)
if j < len(test_line) - 1:
next_text = test_line[j + 1][0]
# Não adicionar espaço se o próximo token for pontuação
if not re.match(r'^[^\w\s]+$', next_text):
space_bbox = font_to_use.getbbox(" ")
test_width += space_bbox[2] - space_bbox[0]
if test_width <= max_width:
current_line.append((token_text, token_font))
else:
# Quebrar linha
if current_line:
lines.append(current_line)
current_line = [(token_text, token_font)]
else:
# Token muito longo, adiciona mesmo assim
lines.append([(token_text, token_font)])
i += 1
if current_line:
lines.append(current_line)
return lines
# Encontrar o tamanho de fonte ideal
font_size = 32 # Tamanho inicial
min_font_size = 12 # Tamanho mínimo
# Tentar diferentes tamanhos de fonte até encontrar um que caiba em max_lines
while font_size >= min_font_size:
lines = wrap_formatted_text(detailed_tokens, max_width, font_size)
# 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 fontes finais
try:
final_regular_font = ImageFont.truetype(regular_font_path, font_size)
final_bold_font = ImageFont.truetype(bold_font_path, font_size)
except (OSError, IOError):
final_regular_font = ImageFont.load_default()
final_bold_font = ImageFont.load_default()
# Quebrar texto final
lines = wrap_formatted_text(detailed_tokens, max_width, font_size)
# Se ainda não couber em max_lines, forçar quebra nas primeiras max_lines
if len(lines) > max_lines:
# Combinar as linhas restantes na última linha permitida
combined_tokens = []
for i, line in enumerate(lines):
if i < max_lines - 1:
combined_tokens.extend(line)
elif i == max_lines - 1:
# Última linha permitida - combinar com todas as linhas restantes
remaining_tokens = []
for j in range(i, len(lines)):
remaining_tokens.extend(lines[j])
combined_tokens.extend(remaining_tokens)
break
lines = wrap_formatted_text(combined_tokens, max_width, font_size)
lines = lines[:max_lines] # Garantir que não exceda max_lines
# Calcular altura total do texto para centralização vertical
total_text_height = 0
line_heights = []
for line in lines:
max_line_height = 0
for segment_text, segment_font in line:
font_to_use = final_regular_font if segment_font == regular_font else final_bold_font
bbox = font_to_use.getbbox(segment_text)
line_height = bbox[3] - bbox[1]
max_line_height = max(max_line_height, line_height)
line_heights.append(max_line_height)
total_text_height += int(max_line_height * 1.20) # Aplicar line height de 120%
# Ajustar posição Y baseado na centralização
if center_vertical:
# Y representa o centro vertical do texto, então subtraímos metade da altura total
start_y = y - (total_text_height // 2)
else:
# Y representa o topo do texto
start_y = y
# Desenhar texto de cima para baixo
current_y = start_y
for line_idx, line in enumerate(lines):
# Calcular altura da linha (usar altura máxima dos segmentos)
max_line_height = line_heights[line_idx]
# Desenhar segmentos da linha
current_x = x
for i, (segment_text, segment_font) in enumerate(line):
font_to_use = final_regular_font if segment_font == regular_font else final_bold_font
draw.text((current_x, current_y), segment_text, fill=text_color, font=font_to_use)
bbox = font_to_use.getbbox(segment_text)
current_x += bbox[2] - bbox[0]
# Adicionar espaço entre palavras (apenas se não for pontuação)
if i < len(line) - 1:
next_text = line[i + 1][0]
# Não adicionar espaço se o próximo token for pontuação
if not re.match(r'^[^\w\s]+$', next_text):
space_bbox = font_to_use.getbbox(" ")
current_x += space_bbox[2] - space_bbox[0]
# Aplicar line height de 120% (adicionar 20% de espaçamento extra)
current_y += int(max_line_height * 1.20)
def get_slide_color(slide_color: int) -> tuple:
"""
Retorna a cor do slide baseada no número fornecido.
Args:
slide_color: Número da cor (1-8)
Returns:
Tuple (R, G, B, A) da cor
"""
color_map = {
1: (0, 0, 0, 255), # #000000 - Preto
2: (191, 198, 147, 255), # #BFC693
3: (246, 164, 29, 255), # #f6a41d
4: (245, 244, 239, 255), # #f5f4ef
5: (111, 180, 165, 255), # #6fb4a5
6: (25, 83, 208, 255), # #1953d0
7: (255, 80, 17, 255), # #ff5011
8: (255, 217, 194, 255), # #ffd9c2
}
# Se o número for inválido, usar preto como padrão
return color_map.get(slide_color, (0, 0, 0, 255))
def get_text_color(slide_color: int) -> tuple:
"""
Retorna a cor do texto baseada na cor do slide.
Args:
slide_color: Número da cor do slide (1-8)
Returns:
Tuple (R, G, B, A) da cor do texto
"""
# Cores que devem ter texto branco: 1 (preto), 6 (azul), e cores inválidas (padrão preto)
if slide_color == 1 or slide_color == 6 or slide_color not in [1, 2, 3, 4, 5, 6, 7, 8]:
return (255, 255, 255, 255) # Branco
else:
return (0, 0, 0, 255) # Preto
def get_logo_path(slide_color: int) -> str:
"""
Retorna o caminho da logo baseado na cor do slide.
Args:
slide_color: Número da cor do slide (1-8)
Returns:
Caminho da logo
"""
# Cores que devem usar logo branca: 1 (preto), 6 (azul), e cores inválidas (padrão preto)
if slide_color == 1 or slide_color == 6 or slide_color not in [1, 2, 3, 4, 5, 6, 7, 8]:
return "recurve.png" # Logo branca
else:
return "recurveblack.png" # Logo preta
def create_slide_canvas(image_url: str, slide: int, text: Optional[str] = None, color: int = 1, text_color: str = "white") -> BytesIO:
"""
Cria um canvas para slides com imagem cortada baseada no parâmetro slide.
Args:
image_url: URL da imagem de fundo
slide: 1 para cortar início (880px), 2 para cortar final (400px)
text: Texto opcional para exibir no slide
color: Cor do slide (1-8)
"""
# Dimensões do poster
canvas_width, canvas_height = 1080, 1350
# Obter cor do slide
slide_bg_color = get_slide_color(color)
# Criar canvas com cor do slide
canvas = Image.new("RGBA", (canvas_width, canvas_height), color=slide_bg_color)
# Baixar imagem original
img = download_image_from_url(image_url)
# PRIMEIRO: Usar resize_and_crop_to_fill para adaptar a imagem a 1280x780 (com zoom/crop centralizado)
img_resized = resize_and_crop_to_fill(img, 1280, 780)
# Configurações baseadas no slide
if slide == 1:
# Slide 1: Cortar os primeiros 880px (0 a 880)
crop_left = 0
crop_right = 880
target_width = 880
target_height = 780
x_pos = 200
y_pos = 370 # Ajustado de 425 para 370
round_top_left = True
round_bottom_left = True
round_top_right = False
round_bottom_right = False
elif slide == 2:
# Slide 2: Cortar os últimos 400px (880 a 1280)
crop_left = 880
crop_right = 1280
target_width = 400
target_height = 780
x_pos = 0
y_pos = 370 # Ajustado de 425 para 370
round_top_left = False
round_bottom_left = False
round_top_right = True
round_bottom_right = True
else:
raise HTTPException(status_code=400, detail="Parâmetro slide deve ser 1 ou 2")
# Cortar a imagem (apenas crop, sem redimensionar)
img_final = img_resized.crop((crop_left, 0, crop_right, 780))
# Criar máscara para bordas arredondadas
mask = Image.new("L", (target_width, target_height), 0)
draw_mask = ImageDraw.Draw(mask)
# Desenhar retângulo base
draw_mask.rectangle([(0, 0), (target_width, target_height)], fill=255)
# Arredondar cantos específicos (25px de raio)
radius = 25
if round_top_left:
# Apagar o canto e redesenhar arredondado
draw_mask.rectangle([(0, 0), (radius, radius)], fill=0)
draw_mask.pieslice([(0, 0), (radius * 2, radius * 2)], 180, 270, fill=255)
if round_bottom_left:
draw_mask.rectangle([(0, target_height - radius), (radius, target_height)], fill=0)
draw_mask.pieslice([(0, target_height - radius * 2), (radius * 2, target_height)], 90, 180, fill=255)
if round_top_right:
draw_mask.rectangle([(target_width - radius, 0), (target_width, radius)], fill=0)
draw_mask.pieslice([(target_width - radius * 2, 0), (target_width, radius * 2)], 270, 360, fill=255)
if round_bottom_right:
draw_mask.rectangle([(target_width - radius, target_height - radius), (target_width, target_height)], fill=0)
draw_mask.pieslice([(target_width - radius * 2, target_height - radius * 2), (target_width, target_height)], 0, 90, fill=255)
# Aplicar máscara à imagem
img_with_mask = Image.new("RGBA", (target_width, target_height))
img_with_mask.paste(img_final, (0, 0))
img_with_mask.putalpha(mask)
# Colar imagem no canvas
canvas.paste(img_with_mask, (x_pos, y_pos), img_with_mask)
# Obter cor do texto baseada no parâmetro text_color
if text_color.lower() == "black":
text_color_tuple = (0, 0, 0, 255) # #000000
else: # white (padrão)
text_color_tuple = (255, 255, 255, 255) # branco
# Adicionar texto se slide for 1 e text for fornecido
if slide == 1 and text and text.strip():
draw = ImageDraw.Draw(canvas)
# Renderizar texto com as especificações: X: 200, Y: 155, L: 815, MAX 5 linhas
render_slide_text(draw, text, x=200, y=155, max_width=815, max_lines=5, text_color=text_color_tuple)
# Adicionar texto se slide for 2 e text for fornecido
if slide == 2 and text and text.strip():
draw = ImageDraw.Draw(canvas)
# Renderizar texto com as especificações: X: 445, Y: 370, L: 585, MAX 19 linhas
# A imagem do slide 2 agora vai de Y: 370 a Y: 1150 (altura 780px)
# Para centralizar o texto na área da imagem, o centro é Y: 370 + (780/2) = 760
render_slide_text(draw, text, x=445, y=760, max_width=585, max_lines=19, center_vertical=True, text_color=text_color_tuple)
# Adicionar logo no canto inferior direito
try:
logo_path = get_logo_path(color)
logo_width, logo_height = 121, 23
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: X: 880, Y: 1260 (canto inferior direito)
canvas.paste(logo_with_opacity, (880, 1260), logo_with_opacity)
except Exception as e:
# Se não conseguir carregar a logo, continuar sem ela
pass
buffer = BytesIO()
canvas.convert("RGB").save(buffer, format="PNG")
buffer.seek(0)
return buffer
def create_quote_canvas(quote_text: str, role: Optional[str] = None, name: Optional[str] = None, color: int = 1, quote_background: bool = False, image_url: Optional[str] = None, text_color: str = "white") -> BytesIO:
"""
Cria um canvas para quotes com fundo colorido, imagem quote.png e texto centralizado.
Args:
quote_text: Texto da quote a ser exibido
role: Cargo da pessoa que falou a quote
name: Nome da pessoa que falou a quote
color: Cor do fundo (1-8)
quote_background: Se True, centraliza texto, role/name e símbolo da quote
image_url: URL da imagem de fundo (usado quando quote_background=True)
"""
# Dimensões do poster
canvas_width, canvas_height = 1080, 1350
# Criar canvas baseado no quote_background
if quote_background and image_url:
# Quando quote_background=True, usar image_url como fundo
try:
response = requests.get(image_url, timeout=10)
response.raise_for_status()
background_img = Image.open(BytesIO(response.content)).convert("RGBA")
# Calcular proporções para fazer zoom centralizado
img_width, img_height = background_img.size
canvas_ratio = canvas_width / canvas_height
img_ratio = img_width / img_height
if img_ratio > canvas_ratio:
# Imagem é mais larga - ajustar pela altura
new_height = canvas_height
new_width = int(canvas_height * img_ratio)
else:
# Imagem é mais alta - ajustar pela largura
new_width = canvas_width
new_height = int(canvas_width / img_ratio)
# Redimensionar imagem mantendo proporção
background_img = background_img.resize((new_width, new_height), Image.Resampling.LANCZOS)
# Criar canvas e colar imagem centralizada
canvas = Image.new("RGBA", (canvas_width, canvas_height), (0, 0, 0, 0))
x_offset = (canvas_width - new_width) // 2
y_offset = (canvas_height - new_height) // 2
canvas.paste(background_img, (x_offset, y_offset))
# Adicionar gradiente preto sobre a imagem
gradient = Image.new("RGBA", (canvas_width, canvas_height), (0, 0, 0, 0))
gradient_draw = ImageDraw.Draw(gradient)
# Criar gradiente linear de baixo para cima
# 0% (bottom): #000000 100% opacidade
# 25%: #000000 95% opacidade
# 100% (top): #000000 0% opacidade
for y in range(canvas_height):
# Calcular posição relativa (0 = top, 1 = bottom)
relative_y = (canvas_height - y) / canvas_height
if relative_y <= 0.25: # De 0% a 25% (bottom para cima)
# Transição de 100% a 95% opacidade
alpha = int(255 * (1.0 - 0.05 * (relative_y / 0.25)))
else: # De 25% a 100% (25% para topo)
# Transição de 95% a 0% opacidade
progress = (relative_y - 0.25) / 0.75
alpha = int(255 * 0.95 * (1 - progress))
# Desenhar linha horizontal com opacidade calculada
gradient_draw.line([(0, y), (canvas_width, y)], fill=(0, 0, 0, alpha))
# Aplicar gradiente sobre a imagem
canvas = Image.alpha_composite(canvas, gradient)
except Exception as e:
# Se falhar, usar cor padrão
bg_color = get_slide_color(color)
canvas = Image.new("RGBA", (canvas_width, canvas_height), color=bg_color)
else:
# Quando quote_background=False, usar cor de fundo baseada no parâmetro color
bg_color = get_slide_color(color)
canvas = Image.new("RGBA", (canvas_width, canvas_height), color=bg_color)
draw = ImageDraw.Draw(canvas)
# Configurações do texto
if quote_background:
# Largura maior quando quote_background=True
text_width = 915
# Centralizar texto quando quote_background=True
text_x = (canvas_width - text_width) // 2 # Centralizar horizontalmente
# Posicionar embaixo quando quote_background=True
text_y = 1160 # Base em Y: 1160 (60px mais baixo)
else:
# Largura padrão quando quote_background=False
text_width = 720
# Alinhar à esquerda quando quote_background=False
text_x = 180
text_y = canvas_height // 2 # Centralizar verticalmente
# Obter cor do texto baseada no parâmetro text_color
if text_color.lower() == "black":
text_color_tuple = (0, 0, 0, 255) # #000000
else: # white (padrão)
text_color_tuple = (255, 255, 255, 255) # branco
# Adicionar imagem quote.png ou quoteblank.png acima do texto
try:
# Escolher imagem baseada no quote_background e text_color
if quote_background:
# Quando quote_background=True, sempre usar quotered.png
quote_image_name = "quotered.png"
else:
# Quando quote_background=False, escolher baseado no text_color
if text_color.lower() == "black":
# Se text_color=black, usar quote.png (preta)
quote_image_name = "quote.png"
else:
# Se text_color=white, usar quotered.png (vermelha)
quote_image_name = "quotered.png"
quote_img = Image.open(quote_image_name).convert("RGBA")
quote_width, quote_height = 87, 67.46
# Redimensionar imagem para as dimensões especificadas
quote_resized = quote_img.resize((int(quote_width), int(quote_height)))
# Posição X baseada no parâmetro quote_background
if quote_background:
# Centralizar horizontalmente quando quote_background=True
quote_x = (canvas_width - quote_width) // 2
else:
# Alinhar à esquerda quando quote_background=False
quote_x = 180
# Calcular altura do texto usando o tamanho de fonte real que será usado
# Usar a mesma lógica do render_quote_text para garantir consistência
try:
if quote_background:
regular_font_path = "fonts/Montserrat-SemiBold.ttf"
else:
regular_font_path = "fonts/Montserrat-Regular.ttf"
except (OSError, IOError):
regular_font_path = None
def get_max_lines(font_size):
if quote_background:
# Quando quote_background=True, máximo de 3 linhas independente do tamanho
return 3
else:
# Comportamento original quando quote_background=False
if font_size >= 65:
return 8
elif font_size >= 55:
return 9
elif font_size >= 45:
return 10
elif font_size >= 35:
return 11
elif font_size >= 30:
return 12
elif font_size >= 25:
return 13
elif font_size >= 20:
return 14
elif font_size >= 16:
return 14
else:
return 14
def wrap_text(text, max_width, font_size):
lines = []
current_line = []
try:
font = ImageFont.truetype(regular_font_path, font_size) if regular_font_path else ImageFont.load_default()
except (OSError, IOError):
font = ImageFont.load_default()
words = text.split()
for word in words:
test_line = " ".join(current_line + [word])
bbox = font.getbbox(test_line)
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, font
# Encontrar o tamanho de fonte ideal (mesma lógica do render_quote_text)
if quote_background:
font_size = 42 # Tamanho inicial quando quote_background=True
else:
font_size = 65 # Tamanho inicial quando quote_background=False
min_font_size = 12
while font_size >= min_font_size:
max_lines = get_max_lines(font_size)
lines, font = wrap_text(quote_text, text_width, font_size)
if len(lines) <= max_lines:
break
font_size -= 2
font_size = max(font_size, min_font_size)
# Calcular altura total do texto com o tamanho de fonte real
lines, font = wrap_text(quote_text, text_width, font_size)
total_text_height = 0
for line in lines:
bbox = font.getbbox(line)
line_height = bbox[3] - bbox[1]
total_text_height += int(line_height * 1.20)
# Posicionar imagem baseado no quote_background
if quote_background:
# Quando quote_background=True, posicionar acima do texto (que começa em Y: 1160)
quote_y = text_y - total_text_height - 80 - quote_height
else:
# Espaçamento normal quando quote_background=False
quote_y = text_y - (total_text_height // 2) - 60 - (quote_height // 2)
# Colar imagem no canvas
canvas.paste(quote_resized, (int(quote_x), int(quote_y)), quote_resized)
except Exception as e:
# Se não conseguir carregar a imagem, continuar sem ela
pass
# Adicionar símbolo " no final do texto automaticamente
quote_text_with_symbol = quote_text + '”'
# Renderizar texto com as especificações solicitadas
render_quote_text(draw, quote_text_with_symbol, text_x, text_y, text_width, text_color_tuple, quote_background)
# Renderizar cargo e nome se fornecidos
if role or name:
render_author_info(draw, canvas, role, name, text_x, text_y, text_width, quote_text_with_symbol, text_color_tuple, quote_background)
buffer = BytesIO()
canvas.convert("RGB").save(buffer, format="PNG")
buffer.seek(0)
return buffer
def render_quote_text(draw: ImageDraw.Draw, text: str, x: int, y: int, max_width: int, text_color: tuple = (0, 0, 0, 255), quote_background: bool = False) -> None:
"""
Renderiza texto para quotes com fonte Montserrat Regular, tamanho inicial 65px.
Sistema dinâmico de linhas: 6 linhas em 65px, aumentando até 14 linhas conforme fonte diminui.
Args:
draw: Objeto ImageDraw para desenhar
text: Texto a ser renderizado
x: Posição X (esquerda)
y: Posição Y (centro vertical do texto)
max_width: Largura máxima do texto (720px)
text_color: Cor do texto como tuple (R, G, B, A)
quote_background: Se True, centraliza o texto
"""
if not text.strip():
return
# Carregar fonte baseada no parâmetro quote_background
try:
if quote_background:
regular_font_path = "fonts/Montserrat-SemiBold.ttf"
else:
regular_font_path = "fonts/Montserrat-Regular.ttf"
except (OSError, IOError):
regular_font_path = None
# Função para calcular máximo de linhas baseado no tamanho da fonte
def get_max_lines(font_size):
"""Calcula o máximo de linhas baseado no tamanho da fonte"""
if quote_background:
# Quando quote_background=True, máximo de 3 linhas independente do tamanho
return 3
else:
# Comportamento original quando quote_background=False
if font_size >= 65:
return 8
elif font_size >= 55:
return 9
elif font_size >= 45:
return 10
elif font_size >= 35:
return 11
elif font_size >= 30:
return 12
elif font_size >= 25:
return 13
elif font_size >= 20:
return 14
elif font_size >= 16:
return 14
else:
return 14
# Função para quebrar texto em linhas
def wrap_text(text, max_width, font_size):
lines = []
current_line = []
# Carregar fonte com tamanho específico
try:
font = ImageFont.truetype(regular_font_path, font_size) if regular_font_path else ImageFont.load_default()
except (OSError, IOError):
font = ImageFont.load_default()
words = text.split()
for word in words:
test_line = " ".join(current_line + [word])
bbox = font.getbbox(test_line)
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, font
# Encontrar o tamanho de fonte ideal
if quote_background:
font_size = 42 # Tamanho inicial quando quote_background=True
else:
font_size = 65 # Tamanho inicial quando quote_background=False
min_font_size = 12 # Tamanho mínimo
# Tentar diferentes tamanhos de fonte até encontrar um que caiba no máximo de linhas
while font_size >= min_font_size:
max_lines = get_max_lines(font_size)
lines, font = wrap_text(text, max_width, font_size)
# 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(regular_font_path, font_size) if regular_font_path else ImageFont.load_default()
except (OSError, IOError):
final_font = ImageFont.load_default()
# Quebrar texto final
lines, _ = wrap_text(text, max_width, font_size)
# Se ainda não couber no máximo de linhas, forçar quebra
max_lines = get_max_lines(font_size)
if len(lines) > max_lines:
# Combinar as linhas restantes na última linha permitida
combined_text = " ".join(lines[:max_lines-1]) + " " + " ".join(lines[max_lines-1:])
lines, _ = wrap_text(combined_text, max_width, font_size)
lines = lines[:max_lines] # Garantir que não exceda max_lines
# Calcular altura total do texto para centralização vertical
total_text_height = 0
line_heights = []
for line in lines:
bbox = final_font.getbbox(line)
line_height = bbox[3] - bbox[1]
line_heights.append(line_height)
total_text_height += int(line_height * 1.20) # Aplicar line height de 120%
# Ajustar posição Y baseado no quote_background
if quote_background:
# Alinhar pela base quando quote_background=True
start_y = y - total_text_height
else:
# Centralizar verticalmente quando quote_background=False
start_y = y - (total_text_height // 2)
# Desenhar texto de cima para baixo
current_y = start_y
for line_idx, line in enumerate(lines):
line_height = line_heights[line_idx]
# Calcular posição X da linha
if quote_background:
# Centralizar linha quando quote_background=True
line_bbox = final_font.getbbox(line)
line_width = line_bbox[2] - line_bbox[0]
line_x = x + (max_width - line_width) // 2
else:
# Alinhar à esquerda quando quote_background=False
line_x = x
# Desenhar linha
draw.text((line_x, current_y), line, fill=text_color, font=final_font)
# Aplicar line height de 120% (adicionar 20% de espaçamento extra)
current_y += int(line_height * 1.20)
def wrap_text_simple(text: str, font, max_width: int) -> List[str]:
"""
Quebra texto em linhas respeitando a largura máxima.
Args:
text: Texto a ser quebrado
font: Fonte para calcular largura
max_width: Largura máxima por linha
Returns:
Lista de linhas
"""
if not text.strip():
return []
words = text.split()
if not words:
return []
lines = []
current_line = []
for word in words:
test_line = " ".join(current_line + [word])
bbox = font.getbbox(test_line)
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
def render_author_info(draw: ImageDraw.Draw, canvas: Image.Image, role: Optional[str], name: Optional[str], text_x: int, text_y: int, text_width: int, quote_text: str, text_color: tuple, quote_background: bool) -> None:
"""
Renderiza informações do autor (cargo e nome) abaixo do texto principal.
Args:
draw: Objeto ImageDraw para desenhar
canvas: Canvas da imagem
role: Cargo da pessoa
name: Nome da pessoa
text_x: Posição X do texto principal
text_y: Posição Y do texto principal
text_width: Largura do texto principal
quote_text: Texto da quote
text_color: Cor do texto principal (para determinar cor do role/name)
quote_background: Se True, centraliza role/name
"""
if not role and not name:
return
# Carregar fonte Montserrat Bold
try:
bold_font_path = "fonts/Montserrat-Bold.ttf"
font = ImageFont.truetype(bold_font_path, 32)
except (OSError, IOError):
font = ImageFont.load_default()
# Determinar cores baseadas no text_color
if text_color == (255, 255, 255, 255): # Se texto principal for branco
# Cor do cargo: branco
cargo_color = (255, 255, 255, 255)
# Cor do nome: branco com 65% de opacidade
nome_color = (255, 255, 255, int(255 * 0.65))
else: # Se texto principal for preto
# Cor do cargo: 140F09 (RGB: 20, 15, 9)
cargo_color = (20, 15, 9, 255)
# Cor do nome: 140F09 com 65% de opacidade
nome_color = (20, 15, 9, int(255 * 0.65))
# Calcular altura do texto principal para posicionar o autor
# Usar a mesma lógica do render_quote_text para calcular a altura
try:
if quote_background:
regular_font_path = "fonts/Montserrat-SemiBold.ttf"
else:
regular_font_path = "fonts/Montserrat-Regular.ttf"
except (OSError, IOError):
regular_font_path = None
def get_max_lines(font_size):
if quote_background:
# Quando quote_background=True, máximo de 3 linhas independente do tamanho
return 3
else:
# Comportamento original quando quote_background=False
if font_size >= 65:
return 8
elif font_size >= 55:
return 9
elif font_size >= 45:
return 10
elif font_size >= 35:
return 11
elif font_size >= 30:
return 12
elif font_size >= 25:
return 13
elif font_size >= 20:
return 14
elif font_size >= 16:
return 14
else:
return 14
def wrap_text(text, max_width, font_size):
lines = []
current_line = []
try:
text_font = ImageFont.truetype(regular_font_path, font_size) if regular_font_path else ImageFont.load_default()
except (OSError, IOError):
text_font = ImageFont.load_default()
words = text.split()
for word in words:
test_line = " ".join(current_line + [word])
bbox = text_font.getbbox(test_line)
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, text_font
# Encontrar o tamanho de fonte ideal do texto principal
if quote_background:
font_size = 42 # Tamanho inicial quando quote_background=True
else:
font_size = 65 # Tamanho inicial quando quote_background=False
min_font_size = 12
while font_size >= min_font_size:
max_lines = get_max_lines(font_size)
lines, text_font = wrap_text(quote_text, text_width, font_size)
if len(lines) <= max_lines:
break
font_size -= 2
font_size = max(font_size, min_font_size)
# Calcular altura total do texto principal
lines, text_font = wrap_text(quote_text, text_width, font_size)
total_text_height = 0
for line in lines:
bbox = text_font.getbbox(line)
line_height = bbox[3] - bbox[1]
total_text_height += int(line_height * 1.20)
# Posicionar informações do autor baseado no quote_background
if quote_background:
# Quando quote_background=True, posicionar abaixo do texto (que termina em Y: 1160)
author_y = text_y + 28
else:
# Espaçamento normal quando quote_background=False
author_y = text_y + (total_text_height // 2) + 28
# Renderizar cargo e nome com vírgula
if role and role.strip() and name and name.strip():
# Renderizar cargo + vírgula com quebra de linha
role_text = f"{role},"
role_lines = wrap_text_simple(role_text, font, text_width)
current_y = author_y
for line in role_lines:
if quote_background:
# Centralizar cargo quando quote_background=True
role_bbox = font.getbbox(line)
role_width = role_bbox[2] - role_bbox[0]
role_x = text_x + (text_width - role_width) // 2
else:
# Alinhar à esquerda quando quote_background=False
role_x = text_x
draw.text((role_x, current_y), line, fill=cargo_color, font=font)
bbox = font.getbbox(line)
current_y += bbox[3] - bbox[1] + 5 # Adicionar pequeno espaçamento entre linhas
author_y = current_y + 5 # Adicionar pequeno espaçamento entre cargo e nome
# Renderizar nome com opacidade e quebra de linha
name_lines = wrap_text_simple(name, font, text_width)
for line in name_lines:
# Criar uma imagem temporária para cada linha do nome com opacidade
name_bbox = font.getbbox(line)
name_width = name_bbox[2] - name_bbox[0]
name_height = name_bbox[3] - name_bbox[1]
# Adicionar padding para evitar corte
padding = 10
name_img_width = name_width + (padding * 2)
name_img_height = name_height + (padding * 2)
# Criar imagem temporária para a linha do nome com padding
name_img = Image.new("RGBA", (name_img_width, name_img_height), (0, 0, 0, 0))
name_draw = ImageDraw.Draw(name_img)
name_draw.text((padding, padding), line, fill=nome_color, font=font)
# Calcular posição X da linha do nome
if quote_background:
# Centralizar nome quando quote_background=True
name_x = text_x + (text_width - name_width) // 2 - padding
else:
# Alinhar à esquerda quando quote_background=False
name_x = text_x - padding
# Colar a imagem da linha do nome no canvas principal (ajustar posição considerando o padding)
canvas.paste(name_img, (name_x, author_y - padding), name_img)
# Avançar para próxima linha
author_y += name_height + 5
elif role and role.strip():
# Apenas cargo com quebra de linha
role_lines = wrap_text_simple(role, font, text_width)
current_y = author_y
for line in role_lines:
if quote_background:
# Centralizar cargo quando quote_background=True
role_bbox = font.getbbox(line)
role_width = role_bbox[2] - role_bbox[0]
role_x = text_x + (text_width - role_width) // 2
else:
# Alinhar à esquerda quando quote_background=False
role_x = text_x
draw.text((role_x, current_y), line, fill=cargo_color, font=font)
bbox = font.getbbox(line)
current_y += bbox[3] - bbox[1] + 5 # Adicionar pequeno espaçamento entre linhas
elif name and name.strip():
# Apenas nome com opacidade e quebra de linha
name_lines = wrap_text_simple(name, font, text_width)
current_y = author_y
for line in name_lines:
# Criar uma imagem temporária para cada linha do nome com opacidade
name_bbox = font.getbbox(line)
name_width = name_bbox[2] - name_bbox[0]
name_height = name_bbox[3] - name_bbox[1]
# Adicionar padding para evitar corte
padding = 10
name_img_width = name_width + (padding * 2)
name_img_height = name_height + (padding * 2)
# Criar imagem temporária para a linha do nome com padding
name_img = Image.new("RGBA", (name_img_width, name_img_height), (0, 0, 0, 0))
name_draw = ImageDraw.Draw(name_img)
name_draw.text((padding, padding), line, fill=nome_color, font=font)
# Calcular posição X da linha do nome
if quote_background:
# Centralizar nome quando quote_background=True
name_x = text_x + (text_width - name_width) // 2 - padding
else:
# Alinhar à esquerda quando quote_background=False
name_x = text_x - padding
# Colar a imagem da linha do nome no canvas principal (ajustar posição considerando o padding)
canvas.paste(name_img, (name_x, current_y - padding), name_img)
# Avançar para próxima linha
current_y += name_height + 5
def create_canvas(image_url: str, text: Optional[str] = None,
text_position: str = "bottom", background: bool = False, color: int = 1, background_position: str = "bottom", text_color: str = "white") -> BytesIO:
# Dimensões fixas do Instagram
width, height = 1080, 1350
# Configurações da logo
logo_width, logo_height = 121, 23
# Se background=True, forçar posições baseadas em background_position
if background:
if background_position == "top":
text_position = "top"
else: # bottom
text_position = "bottom"
# Obter cor do fundo
bg_color = get_slide_color(color)
# Criar canvas
canvas = Image.new("RGBA", (width, height), color=bg_color)
# Calcular altura do background se necessário
background_height = 0
background_start_y = 0
if background:
# Calcular altura necessária para o texto
padding_bottom = height - 1260 # Espaço até a logo (90px)
# Calcular altura do texto
text_height = 0
if text and text.strip():
try:
text_font_path = "fonts/Montserrat-Regular.ttf"
text_font = ImageFont.truetype(text_font_path, 32)
except (OSError, IOError):
text_font = ImageFont.load_default()
text_height = calculate_text_height(text, text_font, 924)
# Calcular altura do conteúdo
content_height = text_height
if background_position == "top":
# Background no topo: começar do topo e descer com base no conteúdo
background_start_y = 0
background_height = content_height + 60 + 60 # 60px de padding superior + conteúdo + 60px de padding inferior
# Garantir altura mínima
if background_height < 200:
background_height = 200
else: # bottom
# Background embaixo: usar a mesma posição de referência do modo normal
bottom_y = 1180
# Calcular de onde o background deve começar
# Partir do bottom_y e subir com base no conteúdo
background_start_y = bottom_y - content_height - 60 # 60px de padding superior
# Altura total do background (do início até o fim da página)
background_height = height - background_start_y
# Garantir altura mínima
if background_height < 200:
background_start_y = height - 200
background_height = 200
# Calcular altura da imagem
image_height = height - background_height if background else height
# Adicionar imagem de fundo
img = download_image_from_url(image_url)
filled_img = resize_and_crop_to_fill(img, width, image_height)
if background and background_position == "top":
# Background no topo: colar imagem abaixo do background
canvas.paste(filled_img, (0, background_height))
else:
# Background embaixo ou sem background: colar imagem no topo
canvas.paste(filled_img, (0, 0))
# Adicionar background colorido se necessário
if background:
# Criar retângulo com a cor selecionada
background_rect = Image.new("RGBA", (width, background_height), color=bg_color)
canvas.paste(background_rect, (0, background_start_y))
else:
# Adicionar gradiente apenas se background=False e text_color não for black
# Lógica inteligente para posicionamento do gradiente
# Se text_color=black, não adicionar gradientes pretos
if text_color.lower() != "black":
# Se text_position=bottom, sempre gradiente embaixo (obrigatório)
if text_position == "bottom":
gradient_overlay = create_gradient_overlay(width, height, "bottom")
canvas = Image.alpha_composite(canvas, gradient_overlay)
# Se text_position=top
elif text_position == "top":
# Sempre gradiente em cima quando texto está no topo
gradient_overlay = create_gradient_overlay(width, height, "top")
canvas = Image.alpha_composite(canvas, gradient_overlay)
# Adicionar logo na posição (X: 880, Y: 1260)
try:
# Se background_position for top, sempre usar logo branca
if background and background_position == "top":
logo_path = "recurve.png" # Logo branca sempre
else:
logo_path = get_logo_path(color)
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: X:880, Y:1260
canvas.paste(logo_with_opacity, (880, 1260), logo_with_opacity)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Erro ao carregar a logo: {e}")
draw = ImageDraw.Draw(canvas)
# Definir posições baseadas nos parâmetros
if background:
if background_position == "top":
# Background no topo: posicionar texto no topo
top_y = 60 # Padding do topo
text_y = top_y if text and text.strip() else None
else: # bottom
# Com background embaixo: usar mesma posição de referência do modo normal
bottom_y = 1180
text_y = bottom_y if text and text.strip() else None
else:
# Posições de referência (comportamento original)
top_y = 60
bottom_y = 1180
# Determinar posição do texto
if text and text.strip():
if text_position == "top":
text_y = top_y
else: # bottom
text_y = bottom_y
else:
text_y = None
# Obter cor do texto baseada no parâmetro text_color
if text_color.lower() == "black":
text_color_tuple = (0, 0, 0, 255) # #000000
else: # white (padrão)
text_color_tuple = (255, 255, 255, 255) # branco
# Renderizar texto se fornecido
if text and text.strip() and text_y is not None:
# Alinhar pela base quando no bottom; pelo topo quando no top
align_text_top = (text_position == "top")
render_text(draw, text, x=78, y=text_y, max_width=924, max_lines=5, align_top=align_text_top, text_color=text_color_tuple)
buffer = BytesIO()
canvas.convert("RGB").save(buffer, format="PNG")
buffer.seek(0)
return buffer
@router.get("/create/images")
def get_image(
image_url: str = Query(..., description="URL da imagem de fundo"),
text: Optional[str] = Query(None, description="Texto a ser exibido na imagem"),
text_position: str = Query("bottom", description="Posição do texto: 'top' ou 'bottom'"),
background: bool = Query(False, description="Se True, adiciona background colorido e força posições baseadas em background_position"),
background_position: str = Query("bottom", description="Posição do background: 'top' ou 'bottom'"),
slide: Optional[int] = Query(None, description="Número do slide: 1 para início (880px), 2 para final (400px)"),
color: Optional[int] = Query(1, description="Cor do fundo: 1=#000000, 2=#BFC693, 3=#f6a41d, 4=#f5f4ef, 5=#6fb4a5, 6=#1953d0, 7=#ff5011, 8=#ffd9c2"),
quote: Optional[str] = Query(None, description="Texto da quote para gerar poster com fundo colorido"),
role: Optional[str] = Query(None, description="Position/role of the person who said the quote"),
name: Optional[str] = Query(None, description="Name of the person who said the quote"),
quote_background: bool = Query(False, description="Se True, centraliza texto, role/name e símbolo da quote"),
text_color: str = Query("white", description="Cor do texto: 'white' ou 'black'")
):
try:
# Se o parâmetro quote for fornecido, usar a funcionalidade de quote
if quote is not None and quote.strip():
buffer = create_quote_canvas(quote, role, name, color, quote_background, image_url, text_color)
# Se o parâmetro slide for fornecido, usar a funcionalidade de slides
elif slide is not None:
buffer = create_slide_canvas(image_url, slide, text, color, text_color)
else:
# Usar funcionalidade normal de criação de imagem
buffer = create_canvas(image_url, text, text_position, background, color, background_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)}")