newapi-clone / routers /curiosity.py
habulaj's picture
Upload 13 files
4ffe0a9 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)}")