|
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 |
|
|
|
|
|
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("") |
|
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}") |
|
|
|
|
|
gradient_overlay = create_black_gradient_overlay(width, height) |
|
canvas = Image.alpha_composite(canvas, gradient_overlay) |
|
|
|
draw = ImageDraw.Draw(canvas) |
|
|
|
|
|
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}") |
|
|
|
|
|
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}") |
|
|
|
|
|
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) |
|
|
|
|
|
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}") |
|
|
|
|
|
if headline: |
|
font_path = "fonts/Montserrat-SemiBold.ttf" |
|
max_width = 945 |
|
top_y = 830 + 70 |
|
bottom_padding = 70 |
|
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) |
|
|
|
|
|
if image_url: |
|
try: |
|
img = download_image_from_url(image_url) |
|
resized = resize_and_crop_to_fill(img, 990, 750) |
|
|
|
|
|
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}") |
|
|
|
|
|
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 |
|
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) |
|
|
|
|
|
if image_url: |
|
try: |
|
img = download_image_from_url(image_url) |
|
resized = resize_and_crop_to_fill(img, 990, 750) |
|
|
|
|
|
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}") |
|
|
|
|
|
if headline: |
|
font_path = "fonts/Montserrat-SemiBold.ttf" |
|
max_width = 945 |
|
top_of_text = 178 + 750 + 50 |
|
safe_bottom = 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 |
|
|
|
|
|
if image_url: |
|
try: |
|
img = download_image_from_url(image_url) |
|
resized = resize_and_crop_to_fill(img, image_w, image_h) |
|
|
|
|
|
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}") |
|
|
|
|
|
if headline: |
|
font_path = "fonts/Montserrat-SemiBold.ttf" |
|
max_width = 945 |
|
top_of_text = image_y + image_h + 50 |
|
safe_bottom = 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 |
|
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)}") |