newapi / routers /curiosity.py
habulaj's picture
Update routers/curiosity.py
2c39f96 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
router = APIRouter()
def get_responsive_font_to_fit_height(text: str, font_path: str, max_width: int, max_height: int,
max_font_size: int = 48, min_font_size: int = 20) -> tuple[ImageFont.FreeTypeFont, list[str], int]:
temp_img = Image.new("RGB", (1, 1))
draw = ImageDraw.Draw(temp_img)
for font_size in range(max_font_size, min_font_size - 1, -1):
try:
font = ImageFont.truetype(font_path, font_size)
except:
font = ImageFont.load_default()
lines = wrap_text(text, font, max_width, draw)
line_height = int(font_size * 1.161)
total_height = len(lines) * line_height
if total_height <= max_height:
return font, lines, font_size
# Caso nenhum tamanho sirva, usar o mínimo mesmo assim
try:
font = ImageFont.truetype(font_path, min_font_size)
except:
font = ImageFont.load_default()
lines = wrap_text(text, font, max_width, draw)
return font, lines, min_font_size
def download_image_from_url(url: str) -> Image.Image:
response = requests.get(url)
if response.status_code != 200:
raise HTTPException(status_code=400, detail="Imagem não pôde ser baixada.")
return Image.open(BytesIO(response.content)).convert("RGBA")
def resize_and_crop_to_fill(img: Image.Image, target_width: int, target_height: int) -> Image.Image:
img_ratio = img.width / img.height
target_ratio = target_width / target_height
if img_ratio > target_ratio:
scale_height = target_height
scale_width = int(scale_height * img_ratio)
else:
scale_width = target_width
scale_height = int(scale_width / img_ratio)
img_resized = img.resize((scale_width, scale_height), Image.LANCZOS)
left = (scale_width - target_width) // 2
top = (scale_height - target_height) // 2
return img_resized.crop((left, top, left + target_width, top + target_height))
def create_black_gradient_overlay(width: int, height: int) -> Image.Image:
gradient = Image.new("RGBA", (width, height))
draw = ImageDraw.Draw(gradient)
for y in range(height):
opacity = int(255 * (y / height))
draw.line([(0, y), (width, y)], fill=(4, 4, 4, opacity))
return gradient
def wrap_text(text: str, font: ImageFont.FreeTypeFont, max_width: int, draw: ImageDraw.Draw) -> list[str]:
lines = []
for raw_line in text.split("\n"):
words = raw_line.split()
current_line = ""
for word in words:
test_line = f"{current_line} {word}".strip()
if draw.textlength(test_line, font=font) <= max_width:
current_line = test_line
else:
if current_line:
lines.append(current_line)
current_line = word
if current_line:
lines.append(current_line)
elif not words:
lines.append("") # Linha vazia preserva \n\n
return lines
def get_responsive_font_and_lines(text: str, font_path: str, max_width: int, max_lines: int = 3,
max_font_size: int = 50, min_font_size: int = 20) -> tuple[ImageFont.FreeTypeFont, list[str], int]:
temp_img = Image.new("RGB", (1, 1))
temp_draw = ImageDraw.Draw(temp_img)
current_font_size = max_font_size
while current_font_size >= min_font_size:
try:
font = ImageFont.truetype(font_path, current_font_size)
except:
font = ImageFont.load_default()
lines = wrap_text(text, font, max_width, temp_draw)
if len(lines) <= max_lines:
return font, lines, current_font_size
current_font_size -= 1
try:
font = ImageFont.truetype(font_path, min_font_size)
except:
font = ImageFont.load_default()
lines = wrap_text(text, font, max_width, temp_draw)
return font, lines, min_font_size
def generate_slide_1(image_url: Optional[str], headline: Optional[str]) -> Image.Image:
width, height = 1080, 1350
canvas = Image.new("RGBA", (width, height), color=(255, 255, 255, 255))
if image_url:
try:
img = download_image_from_url(image_url)
filled_img = resize_and_crop_to_fill(img, width, height)
canvas.paste(filled_img, (0, 0))
except Exception as e:
raise HTTPException(status_code=400, detail=f"Erro ao processar imagem de fundo: {e}")
# Gradiente
gradient_overlay = create_black_gradient_overlay(width, height)
canvas = Image.alpha_composite(canvas, gradient_overlay)
draw = ImageDraw.Draw(canvas)
# Logo no topo
try:
logo = Image.open("recurvecuriosity.png").convert("RGBA").resize((368, 29))
canvas.paste(logo, (66, 74), logo)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Erro ao carregar recurvecuriosity.png: {e}")
# Imagem arrastar no rodapé
try:
arrow = Image.open("arrastar.png").convert("RGBA").resize((355, 37))
canvas.paste(arrow, (66, 1240), arrow)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Erro ao carregar arrastar.png: {e}")
# Texto headline acima da imagem arrastar
if headline:
font_path = "fonts/Montserrat-Bold.ttf"
max_width = 945
max_lines = 3
try:
font, lines, font_size = get_responsive_font_and_lines(
headline, font_path, max_width, max_lines=max_lines,
max_font_size=50, min_font_size=20
)
line_height = int(font_size * 1.161)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Erro ao processar fonte/headline: {e}")
total_text_height = len(lines) * line_height
start_y = 1240 - 16 - total_text_height
x = (width - max_width) // 2
for i, line in enumerate(lines):
y = start_y + i * line_height
draw.text((x, y), line, font=font, fill=(255, 255, 255))
return canvas
def generate_slide_2(image_url: Optional[str], headline: Optional[str]) -> Image.Image:
width, height = 1080, 1350
canvas = Image.new("RGBA", (width, height), color=(4, 4, 4, 255))
draw = ImageDraw.Draw(canvas)
# === Imagem principal ===
if image_url:
try:
img = download_image_from_url(image_url)
resized = resize_and_crop_to_fill(img, 1080, 830)
canvas.paste(resized, (0, 0))
except Exception as e:
raise HTTPException(status_code=400, detail=f"Erro ao processar imagem do slide 2: {e}")
# === Headline ===
if headline:
font_path = "fonts/Montserrat-SemiBold.ttf"
max_width = 945
top_y = 830 + 70
bottom_padding = 70 # Alterado de 70 para 70 (já estava correto)
available_height = height - top_y - bottom_padding
try:
font, lines, font_size = get_responsive_font_to_fit_height(
headline,
font_path=font_path,
max_width=max_width,
max_height=available_height,
max_font_size=48,
min_font_size=20
)
line_height = int(font_size * 1.161)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Erro ao processar texto do slide 2: {e}")
x = (width - max_width) // 2
for i, line in enumerate(lines):
y = top_y + i * line_height
draw.text((x, y), line, font=font, fill=(255, 255, 255))
return canvas
def generate_slide_3(image_url: Optional[str], headline: Optional[str]) -> Image.Image:
width, height = 1080, 1350
canvas = Image.new("RGBA", (width, height), color=(4, 4, 4, 255))
draw = ImageDraw.Draw(canvas)
# === Imagem com cantos arredondados à esquerda ===
if image_url:
try:
img = download_image_from_url(image_url)
resized = resize_and_crop_to_fill(img, 990, 750)
# Máscara arredondando cantos esquerdos
mask = Image.new("L", (990, 750), 0)
mask_draw = ImageDraw.Draw(mask)
mask_draw.rectangle((25, 0, 990, 750), fill=255)
mask_draw.pieslice([0, 0, 50, 50], 180, 270, fill=255)
mask_draw.pieslice([0, 700, 50, 750], 90, 180, fill=255)
mask_draw.rectangle((0, 25, 25, 725), fill=255)
canvas.paste(resized, (90, 422), mask)
except Exception as e:
raise HTTPException(status_code=400, detail=f"Erro ao processar imagem do slide 3: {e}")
# === Headline acima da imagem ===
if headline:
font_path = "fonts/Montserrat-SemiBold.ttf"
max_width = 945
image_top_y = 422
spacing = 50
bottom_of_text = image_top_y - spacing
safe_top = 70 # Alterado de 70 para 70 (já estava correto)
available_height = bottom_of_text - safe_top
font_size = 48
while font_size >= 20:
try:
font = ImageFont.truetype(font_path, font_size)
except:
font = ImageFont.load_default()
lines = wrap_text(headline, font, max_width, draw)
line_height = int(font_size * 1.161)
total_text_height = len(lines) * line_height
start_y = bottom_of_text - total_text_height
if start_y >= safe_top:
break
font_size -= 1
try:
font = ImageFont.truetype(font_path, font_size)
except:
font = ImageFont.load_default()
x = 90
for i, line in enumerate(lines):
y = start_y + i * line_height
draw.text((x, y), line, font=font, fill=(255, 255, 255))
return canvas
def generate_slide_4(image_url: Optional[str], headline: Optional[str]) -> Image.Image:
width, height = 1080, 1350
canvas = Image.new("RGBA", (width, height), color=(4, 4, 4, 255))
draw = ImageDraw.Draw(canvas)
# === Imagem com cantos arredondados à esquerda ===
if image_url:
try:
img = download_image_from_url(image_url)
resized = resize_and_crop_to_fill(img, 990, 750)
# Máscara com cantos arredondados à esquerda
mask = Image.new("L", (990, 750), 0)
mask_draw = ImageDraw.Draw(mask)
mask_draw.rectangle((25, 0, 990, 750), fill=255)
mask_draw.pieslice([0, 0, 50, 50], 180, 270, fill=255)
mask_draw.pieslice([0, 700, 50, 750], 90, 180, fill=255)
mask_draw.rectangle((0, 25, 25, 725), fill=255)
canvas.paste(resized, (90, 178), mask)
except Exception as e:
raise HTTPException(status_code=400, detail=f"Erro ao processar imagem do slide 4: {e}")
# === Headline abaixo da imagem ===
if headline:
font_path = "fonts/Montserrat-SemiBold.ttf"
max_width = 945
top_of_text = 178 + 750 + 50 # Y da imagem + altura + espaçamento
safe_bottom = 70 # Alterado de 50 para 70
available_height = height - top_of_text - safe_bottom
try:
font, lines, font_size = get_responsive_font_to_fit_height(
headline,
font_path=font_path,
max_width=max_width,
max_height=available_height,
max_font_size=48,
min_font_size=20
)
line_height = int(font_size * 1.161)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Erro ao processar texto do slide 4: {e}")
x = 90
for i, line in enumerate(lines):
y = top_of_text + i * line_height
draw.text((x, y), line, font=font, fill=(255, 255, 255))
return canvas
def generate_slide_5(image_url: Optional[str], headline: Optional[str]) -> Image.Image:
width, height = 1080, 1350
canvas = Image.new("RGBA", (width, height), color=(4, 4, 4, 255))
draw = ImageDraw.Draw(canvas)
image_w, image_h = 900, 748
image_x = 90
image_y = 100
# === Imagem com cantos totalmente arredondados ===
if image_url:
try:
img = download_image_from_url(image_url)
resized = resize_and_crop_to_fill(img, image_w, image_h)
# Máscara com cantos 25px arredondados (todos os cantos)
radius = 25
mask = Image.new("L", (image_w, image_h), 0)
mask_draw = ImageDraw.Draw(mask)
mask_draw.rounded_rectangle((0, 0, image_w, image_h), radius=radius, fill=255)
canvas.paste(resized, (image_x, image_y), mask)
except Exception as e:
raise HTTPException(status_code=400, detail=f"Erro ao processar imagem do slide 5: {e}")
# === Texto abaixo da imagem ===
if headline:
font_path = "fonts/Montserrat-SemiBold.ttf"
max_width = 945
top_of_text = image_y + image_h + 50
safe_bottom = 70 # Alterado de 50 para 70
available_height = height - top_of_text - safe_bottom
try:
font, lines, font_size = get_responsive_font_to_fit_height(
headline,
font_path=font_path,
max_width=max_width,
max_height=available_height,
max_font_size=48,
min_font_size=20
)
line_height = int(font_size * 1.161)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Erro ao processar texto do slide 5: {e}")
x = (width - max_width) // 2 # Centralizado horizontalmente
for i, line in enumerate(lines):
y = top_of_text + i * line_height
draw.text((x, y), line, font=font, fill=(255, 255, 255))
return canvas
def generate_black_canvas() -> Image.Image:
return Image.new("RGB", (1080, 1350), color=(4, 4, 4))
@router.get("/cover/curiosity")
def get_curiosity_image(
image_url: Optional[str] = Query(None, description="URL da imagem de fundo"),
headline: Optional[str] = Query(None, description="Texto da curiosidade"),
slide: int = Query(1, ge=1, le=5, description="Número do slide (1 a 5)")
):
try:
if slide == 1:
final_image = generate_slide_1(image_url, headline)
elif slide == 2:
final_image = generate_slide_2(image_url, headline)
elif slide == 3:
final_image = generate_slide_3(image_url, headline)
elif slide == 4:
final_image = generate_slide_4(image_url, headline)
elif slide == 5:
final_image = generate_slide_5(image_url, headline)
else:
final_image = generate_black_canvas()
buffer = BytesIO()
final_image.convert("RGB").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)}")