Spaces:
Paused
Paused
| 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)) | |
| 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)}") |