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)}")