Create memoriam.py
Browse files- routers/memoriam.py +216 -0
routers/memoriam.py
ADDED
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, Query, HTTPException
|
2 |
+
from fastapi.responses import StreamingResponse
|
3 |
+
from PIL import Image, ImageDraw, ImageFont
|
4 |
+
from io import BytesIO
|
5 |
+
import requests
|
6 |
+
from typing import Optional
|
7 |
+
import os
|
8 |
+
|
9 |
+
router = APIRouter()
|
10 |
+
|
11 |
+
def download_image_from_url(url: str) -> Image.Image:
|
12 |
+
response = requests.get(url)
|
13 |
+
if response.status_code != 200:
|
14 |
+
raise HTTPException(status_code=400, detail="Imagem não pôde ser baixada.")
|
15 |
+
return Image.open(BytesIO(response.content)).convert("RGBA")
|
16 |
+
|
17 |
+
def resize_and_crop_to_fill(img: Image.Image, target_width: int, target_height: int) -> Image.Image:
|
18 |
+
img_ratio = img.width / img.height
|
19 |
+
target_ratio = target_width / target_height
|
20 |
+
|
21 |
+
if img_ratio > target_ratio:
|
22 |
+
scale_height = target_height
|
23 |
+
scale_width = int(scale_height * img_ratio)
|
24 |
+
else:
|
25 |
+
scale_width = target_width
|
26 |
+
scale_height = int(scale_width / img_ratio)
|
27 |
+
|
28 |
+
img_resized = img.resize((scale_width, scale_height), Image.LANCZOS)
|
29 |
+
|
30 |
+
left = (scale_width - target_width) // 2
|
31 |
+
top = (scale_height - target_height) // 2
|
32 |
+
right = left + target_width
|
33 |
+
bottom = top + target_height
|
34 |
+
|
35 |
+
return img_resized.crop((left, top, right, bottom))
|
36 |
+
|
37 |
+
def create_black_gradient_overlay(width: int, height: int) -> Image.Image:
|
38 |
+
gradient = Image.new("RGBA", (width, height))
|
39 |
+
draw = ImageDraw.Draw(gradient)
|
40 |
+
for y in range(height):
|
41 |
+
opacity = int(255 * (y / height))
|
42 |
+
draw.line([(0, y), (width, y)], fill=(4, 4, 4, opacity))
|
43 |
+
return gradient
|
44 |
+
|
45 |
+
def wrap_text(text: str, font: ImageFont.FreeTypeFont, max_width: int, draw: ImageDraw.Draw) -> list[str]:
|
46 |
+
words = text.split()
|
47 |
+
lines = []
|
48 |
+
current_line = ""
|
49 |
+
|
50 |
+
for word in words:
|
51 |
+
test_line = f"{current_line} {word}".strip()
|
52 |
+
if draw.textlength(test_line, font=font) <= max_width:
|
53 |
+
current_line = test_line
|
54 |
+
else:
|
55 |
+
if current_line:
|
56 |
+
lines.append(current_line)
|
57 |
+
current_line = word
|
58 |
+
if current_line:
|
59 |
+
lines.append(current_line)
|
60 |
+
return lines
|
61 |
+
|
62 |
+
def get_responsive_font_and_lines(text: str, font_path: str, max_width: int, max_lines: int = 3,
|
63 |
+
max_font_size: int = 50, min_font_size: int = 20) -> tuple[ImageFont.FreeTypeFont, list[str], int]:
|
64 |
+
"""
|
65 |
+
Retorna a fonte e linhas ajustadas para caber no número máximo de linhas.
|
66 |
+
|
67 |
+
Args:
|
68 |
+
text: Texto a ser renderizado
|
69 |
+
font_path: Caminho para o arquivo de fonte
|
70 |
+
max_width: Largura máxima disponível
|
71 |
+
max_lines: Número máximo de linhas permitidas
|
72 |
+
max_font_size: Tamanho máximo da fonte
|
73 |
+
min_font_size: Tamanho mínimo da fonte
|
74 |
+
|
75 |
+
Returns:
|
76 |
+
tuple: (fonte, linhas, tamanho_da_fonte)
|
77 |
+
"""
|
78 |
+
# Criar um draw temporário para calcular tamanhos
|
79 |
+
temp_img = Image.new("RGB", (1, 1))
|
80 |
+
temp_draw = ImageDraw.Draw(temp_img)
|
81 |
+
|
82 |
+
current_font_size = max_font_size
|
83 |
+
|
84 |
+
while current_font_size >= min_font_size:
|
85 |
+
try:
|
86 |
+
font = ImageFont.truetype(font_path, current_font_size)
|
87 |
+
except Exception:
|
88 |
+
# Se não conseguir carregar a fonte, usar fonte padrão
|
89 |
+
font = ImageFont.load_default()
|
90 |
+
|
91 |
+
lines = wrap_text(text, font, max_width, temp_draw)
|
92 |
+
|
93 |
+
# Se o texto cabe no número máximo de linhas, usar este tamanho
|
94 |
+
if len(lines) <= max_lines:
|
95 |
+
return font, lines, current_font_size
|
96 |
+
|
97 |
+
# Diminuir o tamanho da fonte
|
98 |
+
current_font_size -= 1
|
99 |
+
|
100 |
+
# Se chegou ao tamanho mínimo, usar mesmo assim
|
101 |
+
try:
|
102 |
+
font = ImageFont.truetype(font_path, min_font_size)
|
103 |
+
except Exception:
|
104 |
+
font = ImageFont.load_default()
|
105 |
+
|
106 |
+
lines = wrap_text(text, font, max_width, temp_draw)
|
107 |
+
return font, lines, min_font_size
|
108 |
+
|
109 |
+
def create_gradient_bar(width: int, height: int) -> Image.Image:
|
110 |
+
gradient = Image.new("RGBA", (width, height))
|
111 |
+
draw = ImageDraw.Draw(gradient)
|
112 |
+
|
113 |
+
for x in range(width):
|
114 |
+
ratio = x / (width - 1)
|
115 |
+
if ratio < 0.5:
|
116 |
+
r = int(0xFF * (1 - ratio * 2) + 0xF4 * ratio * 2)
|
117 |
+
g = int(0x82 * (1 - ratio * 2) + 0x0A * ratio * 2)
|
118 |
+
b = int(0x26 * (1 - ratio * 2) + 0xFF * ratio * 2)
|
119 |
+
else:
|
120 |
+
ratio2 = (ratio - 0.5) * 2
|
121 |
+
r = int(0xF4 * (1 - ratio2) + 0x03 * ratio2)
|
122 |
+
g = int(0x0A * (1 - ratio2) + 0xD9 * ratio2)
|
123 |
+
b = int(0xFF * (1 - ratio2) + 0xE3 * ratio2)
|
124 |
+
draw.line([(x, 0), (x, height)], fill=(r, g, b, 255))
|
125 |
+
|
126 |
+
radius = 44
|
127 |
+
mask = Image.new("L", (width, height), 0)
|
128 |
+
mask_draw = ImageDraw.Draw(mask)
|
129 |
+
mask_draw.rounded_rectangle([(0, 0), (width, height)], radius=radius, fill=255)
|
130 |
+
|
131 |
+
gradient.putalpha(mask)
|
132 |
+
return gradient
|
133 |
+
|
134 |
+
def create_canvas(image_url: Optional[str], headline: Optional[str]) -> BytesIO:
|
135 |
+
width, height = 1080, 1350
|
136 |
+
padding_x = 60
|
137 |
+
bottom_padding = 80
|
138 |
+
max_width = width - 2 * padding_x
|
139 |
+
|
140 |
+
canvas = Image.new("RGBA", (width, height), color=(255, 255, 255, 255))
|
141 |
+
|
142 |
+
if image_url:
|
143 |
+
img = download_image_from_url(image_url)
|
144 |
+
filled_img = resize_and_crop_to_fill(img, width, height)
|
145 |
+
canvas.paste(filled_img, (0, 0))
|
146 |
+
|
147 |
+
gradient_overlay = create_black_gradient_overlay(width, height)
|
148 |
+
canvas = Image.alpha_composite(canvas, gradient_overlay)
|
149 |
+
|
150 |
+
if headline:
|
151 |
+
draw = ImageDraw.Draw(canvas)
|
152 |
+
font_path = "fonts/Montserrat-Bold.ttf"
|
153 |
+
|
154 |
+
try:
|
155 |
+
# Obter fonte e linhas responsivas
|
156 |
+
font, lines, font_size = get_responsive_font_and_lines(
|
157 |
+
headline, font_path, max_width, max_lines=3,
|
158 |
+
max_font_size=50, min_font_size=20
|
159 |
+
)
|
160 |
+
|
161 |
+
# Calcular line_height baseado no tamanho da fonte atual
|
162 |
+
line_height = int(font_size * 1.22) # Proporção similar à original (61/50 = 1.22)
|
163 |
+
|
164 |
+
except Exception as e:
|
165 |
+
raise HTTPException(status_code=500, detail=f"Erro ao processar a fonte: {e}")
|
166 |
+
|
167 |
+
total_text_height = len(lines) * line_height
|
168 |
+
start_y = height - bottom_padding - total_text_height
|
169 |
+
|
170 |
+
# Posições dos elementos acima do texto
|
171 |
+
bar_width = 375
|
172 |
+
bar_height = 6
|
173 |
+
space_bar_to_text = 16
|
174 |
+
space_logo_to_bar = 20
|
175 |
+
logo_width, logo_height = 162, 30
|
176 |
+
|
177 |
+
# Y do degradê
|
178 |
+
bar_y = start_y - space_bar_to_text - bar_height
|
179 |
+
# Y da logo
|
180 |
+
logo_y = bar_y - space_logo_to_bar - logo_height
|
181 |
+
|
182 |
+
# Garantir que a logo não ultrapasse o topo
|
183 |
+
if logo_y > 0:
|
184 |
+
# Adiciona logo
|
185 |
+
try:
|
186 |
+
logo_path = "recurve.png"
|
187 |
+
logo = Image.open(logo_path).convert("RGBA")
|
188 |
+
logo_resized = logo.resize((logo_width, logo_height))
|
189 |
+
canvas.paste(logo_resized, (padding_x, logo_y), logo_resized)
|
190 |
+
except Exception as e:
|
191 |
+
raise HTTPException(status_code=500, detail=f"Erro ao carregar a logo: {e}")
|
192 |
+
|
193 |
+
# Adiciona barra colorida
|
194 |
+
bar = create_gradient_bar(bar_width, bar_height)
|
195 |
+
canvas.paste(bar, (padding_x, bar_y), bar)
|
196 |
+
|
197 |
+
# Adiciona texto
|
198 |
+
for i, line in enumerate(lines):
|
199 |
+
y = start_y + i * line_height
|
200 |
+
draw.text((padding_x, y), line, font=font, fill=(255, 255, 255))
|
201 |
+
|
202 |
+
buffer = BytesIO()
|
203 |
+
canvas.convert("RGB").save(buffer, format="PNG")
|
204 |
+
buffer.seek(0)
|
205 |
+
return buffer
|
206 |
+
|
207 |
+
@router.get("/news")
|
208 |
+
def get_news_image(
|
209 |
+
image_url: Optional[str] = Query(None, description="URL da imagem para preencher o fundo"),
|
210 |
+
headline: Optional[str] = Query(None, description="Texto do título (opcional)")
|
211 |
+
):
|
212 |
+
try:
|
213 |
+
buffer = create_canvas(image_url, headline)
|
214 |
+
return StreamingResponse(buffer, media_type="image/png")
|
215 |
+
except Exception as e:
|
216 |
+
raise HTTPException(status_code=500, detail=f"Erro ao gerar imagem: {str(e)}")
|