|
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 |
|
import os |
|
|
|
router = APIRouter() |
|
|
|
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 |
|
right = left + target_width |
|
bottom = top + target_height |
|
|
|
return img_resized.crop((left, top, right, bottom)) |
|
|
|
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]: |
|
words = text.split() |
|
lines = [] |
|
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) |
|
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]: |
|
""" |
|
Retorna a fonte e linhas ajustadas para caber no número máximo de linhas. |
|
|
|
Args: |
|
text: Texto a ser renderizado |
|
font_path: Caminho para o arquivo de fonte |
|
max_width: Largura máxima disponível |
|
max_lines: Número máximo de linhas permitidas |
|
max_font_size: Tamanho máximo da fonte |
|
min_font_size: Tamanho mínimo da fonte |
|
|
|
Returns: |
|
tuple: (fonte, linhas, tamanho_da_fonte) |
|
""" |
|
|
|
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 Exception: |
|
|
|
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 Exception: |
|
font = ImageFont.load_default() |
|
|
|
lines = wrap_text(text, font, max_width, temp_draw) |
|
return font, lines, min_font_size |
|
|
|
def create_gradient_bar(width: int, height: int) -> Image.Image: |
|
gradient = Image.new("RGBA", (width, height)) |
|
draw = ImageDraw.Draw(gradient) |
|
|
|
for x in range(width): |
|
ratio = x / (width - 1) |
|
if ratio < 0.5: |
|
r = int(0xFF * (1 - ratio * 2) + 0xF4 * ratio * 2) |
|
g = int(0x82 * (1 - ratio * 2) + 0x0A * ratio * 2) |
|
b = int(0x26 * (1 - ratio * 2) + 0xFF * ratio * 2) |
|
else: |
|
ratio2 = (ratio - 0.5) * 2 |
|
r = int(0xF4 * (1 - ratio2) + 0x03 * ratio2) |
|
g = int(0x0A * (1 - ratio2) + 0xD9 * ratio2) |
|
b = int(0xFF * (1 - ratio2) + 0xE3 * ratio2) |
|
draw.line([(x, 0), (x, height)], fill=(r, g, b, 255)) |
|
|
|
radius = 44 |
|
mask = Image.new("L", (width, height), 0) |
|
mask_draw = ImageDraw.Draw(mask) |
|
mask_draw.rounded_rectangle([(0, 0), (width, height)], radius=radius, fill=255) |
|
|
|
gradient.putalpha(mask) |
|
return gradient |
|
|
|
def create_canvas(image_url: Optional[str], headline: Optional[str]) -> BytesIO: |
|
width, height = 1080, 1350 |
|
padding_x = 60 |
|
bottom_padding = 80 |
|
max_width = width - 2 * padding_x |
|
|
|
canvas = Image.new("RGBA", (width, height), color=(255, 255, 255, 255)) |
|
|
|
if image_url: |
|
img = download_image_from_url(image_url) |
|
filled_img = resize_and_crop_to_fill(img, width, height) |
|
canvas.paste(filled_img, (0, 0)) |
|
|
|
gradient_overlay = create_black_gradient_overlay(width, height) |
|
canvas = Image.alpha_composite(canvas, gradient_overlay) |
|
|
|
if headline: |
|
draw = ImageDraw.Draw(canvas) |
|
font_path = "fonts/Montserrat-Bold.ttf" |
|
|
|
try: |
|
|
|
font, lines, font_size = get_responsive_font_and_lines( |
|
headline, font_path, max_width, max_lines=3, |
|
max_font_size=50, min_font_size=20 |
|
) |
|
|
|
|
|
line_height = int(font_size * 1.22) |
|
|
|
except Exception as e: |
|
raise HTTPException(status_code=500, detail=f"Erro ao processar a fonte: {e}") |
|
|
|
total_text_height = len(lines) * line_height |
|
start_y = height - bottom_padding - total_text_height |
|
|
|
|
|
bar_width = 375 |
|
bar_height = 6 |
|
space_bar_to_text = 16 |
|
space_logo_to_bar = 20 |
|
logo_width, logo_height = 162, 30 |
|
|
|
|
|
bar_y = start_y - space_bar_to_text - bar_height |
|
|
|
logo_y = bar_y - space_logo_to_bar - logo_height |
|
|
|
|
|
if logo_y > 0: |
|
|
|
try: |
|
logo_path = "recurve.png" |
|
logo = Image.open(logo_path).convert("RGBA") |
|
logo_resized = logo.resize((logo_width, logo_height)) |
|
canvas.paste(logo_resized, (padding_x, logo_y), logo_resized) |
|
except Exception as e: |
|
raise HTTPException(status_code=500, detail=f"Erro ao carregar a logo: {e}") |
|
|
|
|
|
bar = create_gradient_bar(bar_width, bar_height) |
|
canvas.paste(bar, (padding_x, bar_y), bar) |
|
|
|
|
|
for i, line in enumerate(lines): |
|
y = start_y + i * line_height |
|
draw.text((padding_x, y), line, font=font, fill=(255, 255, 255)) |
|
|
|
buffer = BytesIO() |
|
canvas.convert("RGB").save(buffer, format="PNG") |
|
buffer.seek(0) |
|
return buffer |
|
|
|
@router.get("/cover/news") |
|
def get_news_image( |
|
image_url: Optional[str] = Query(None, description="URL da imagem para preencher o fundo"), |
|
headline: Optional[str] = Query(None, description="Texto do título (opcional)") |
|
): |
|
try: |
|
buffer = create_canvas(image_url, headline) |
|
return StreamingResponse(buffer, media_type="image/png") |
|
except Exception as e: |
|
raise HTTPException(status_code=500, detail=f"Erro ao gerar imagem: {str(e)}") |