habulaj commited on
Commit
35c4672
·
verified ·
1 Parent(s): 336f627

Update routers/news.py

Browse files
Files changed (1) hide show
  1. routers/news.py +48 -380
routers/news.py CHANGED
@@ -1,9 +1,9 @@
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, List, Union
7
 
8
  router = APIRouter()
9
 
@@ -22,15 +22,14 @@ def download_image_from_url(url: str) -> Image.Image:
22
  except Exception as e:
23
  raise HTTPException(status_code=400, detail=f"Erro ao baixar imagem: {url} ({str(e)})")
24
 
25
- def resize_and_crop_to_fill(img: Image.Image, target_width: int, target_height: int, crop_position: str = "center") -> Image.Image:
26
  """
27
- Redimensiona e corta a imagem para preencher exatamente o espaço alvo.
28
 
29
  Args:
30
  img: Imagem PIL
31
  target_width: Largura alvo
32
  target_height: Altura alvo
33
- crop_position: Posição do crop ('center', 'top', 'bottom', 'left', 'right')
34
  """
35
  img_ratio = img.width / img.height
36
  target_ratio = target_width / target_height
@@ -46,400 +45,77 @@ def resize_and_crop_to_fill(img: Image.Image, target_width: int, target_height:
46
 
47
  img_resized = img.resize((scale_width, scale_height), Image.LANCZOS)
48
 
49
- # Calcular posição do crop baseado na opção escolhida
50
- if crop_position.lower() == "center":
51
- left = (scale_width - target_width) // 2
52
- top = (scale_height - target_height) // 2
53
- elif crop_position.lower() == "top":
54
- left = (scale_width - target_width) // 2
55
- top = 0
56
- elif crop_position.lower() == "bottom":
57
- left = (scale_width - target_width) // 2
58
- top = scale_height - target_height
59
- elif crop_position.lower() == "left":
60
- left = 0
61
- top = (scale_height - target_height) // 2
62
- elif crop_position.lower() == "right":
63
- left = scale_width - target_width
64
- top = (scale_height - target_height) // 2
65
- else:
66
- # Default para center se opção inválida
67
- left = (scale_width - target_width) // 2
68
- top = (scale_height - target_height) // 2
69
-
70
  right = left + target_width
71
  bottom = top + target_height
72
 
73
  return img_resized.crop((left, top, right, bottom))
74
 
75
- def create_collage_background(image_urls: List[str], canvas_width: int, canvas_height: int, crop_position: str = "center") -> Image.Image:
76
- """Cria uma colagem como fundo baseada na lista de URLs"""
77
- num_images = len(image_urls)
78
- border_size = 4 if num_images > 1 else 0 # Linha mais fina e elegante
79
-
80
- images = [download_image_from_url(url) for url in image_urls]
81
- canvas = Image.new("RGBA", (canvas_width, canvas_height), (255, 255, 255, 255))
82
-
83
- if num_images == 1:
84
- img = resize_and_crop_to_fill(images[0], canvas_width, canvas_height, crop_position)
85
- canvas.paste(img, (0, 0))
86
- elif num_images == 2:
87
- # Lado a lado
88
- slot_width = (canvas_width - border_size) // 2
89
- img1 = resize_and_crop_to_fill(images[0], slot_width, canvas_height, crop_position)
90
- img2 = resize_and_crop_to_fill(images[1], slot_width, canvas_height, crop_position)
91
- canvas.paste(img1, (0, 0))
92
- canvas.paste(img2, (slot_width + border_size, 0))
93
- elif num_images == 3:
94
- # Layout original IG
95
- half_height = (canvas_height - border_size) // 2
96
- half_width = (canvas_width - border_size) // 2
97
-
98
- img1 = resize_and_crop_to_fill(images[0], half_width, half_height, crop_position)
99
- img2 = resize_and_crop_to_fill(images[1], half_width, half_height, crop_position)
100
- img3 = resize_and_crop_to_fill(images[2], canvas_width, half_height, crop_position)
101
-
102
- canvas.paste(img1, (0, 0))
103
- canvas.paste(img2, (half_width + border_size, 0))
104
- canvas.paste(img3, (0, half_height + border_size))
105
- elif num_images == 4:
106
- # Layout 2x2
107
- half_height = (canvas_height - border_size) // 2
108
- half_width = (canvas_width - border_size) // 2
109
-
110
- img1 = resize_and_crop_to_fill(images[0], half_width, half_height, crop_position)
111
- img2 = resize_and_crop_to_fill(images[1], half_width, half_height, crop_position)
112
- img3 = resize_and_crop_to_fill(images[2], half_width, half_height, crop_position)
113
- img4 = resize_and_crop_to_fill(images[3], half_width, half_height, crop_position)
114
-
115
- canvas.paste(img1, (0, 0))
116
- canvas.paste(img2, (half_width + border_size, 0))
117
- canvas.paste(img3, (0, half_height + border_size))
118
- canvas.paste(img4, (half_width + border_size, half_height + border_size))
119
- elif num_images == 5:
120
- # Layout original IG
121
- top_height = (canvas_height - border_size) * 2 // 5
122
- bottom_height = canvas_height - top_height - border_size
123
- half_width = (canvas_width - border_size) // 2
124
-
125
- img1 = resize_and_crop_to_fill(images[0], half_width, top_height, crop_position)
126
- img2 = resize_and_crop_to_fill(images[1], half_width, top_height, crop_position)
127
- canvas.paste(img1, (0, 0))
128
- canvas.paste(img2, (half_width + border_size, 0))
129
-
130
- y_offset = top_height + border_size
131
- third_width = (canvas_width - 2 * border_size) // 3
132
- third_width_last = canvas_width - (third_width * 2 + border_size * 2)
133
-
134
- img3 = resize_and_crop_to_fill(images[2], third_width, bottom_height, crop_position)
135
- img4 = resize_and_crop_to_fill(images[3], third_width, bottom_height, crop_position)
136
- img5 = resize_and_crop_to_fill(images[4], third_width_last, bottom_height, crop_position)
137
- canvas.paste(img3, (0, y_offset))
138
- canvas.paste(img4, (third_width + border_size, y_offset))
139
- canvas.paste(img5, (third_width * 2 + border_size * 2, y_offset))
140
- elif num_images == 6:
141
- # Layout original IG (3x2)
142
- half_height = (canvas_height - border_size) // 2
143
- third_width = (canvas_width - 2 * border_size) // 3
144
- third_width_last = canvas_width - (third_width * 2 + border_size * 2)
145
-
146
- # Primeira linha
147
- img1 = resize_and_crop_to_fill(images[0], third_width, half_height, crop_position)
148
- img2 = resize_and_crop_to_fill(images[1], third_width, half_height, crop_position)
149
- img3 = resize_and_crop_to_fill(images[2], third_width, half_height, crop_position)
150
- canvas.paste(img1, (0, 0))
151
- canvas.paste(img2, (third_width + border_size, 0))
152
- canvas.paste(img3, (third_width * 2 + border_size * 2, 0))
153
-
154
- # Segunda linha
155
- y_offset = half_height + border_size
156
- img4 = resize_and_crop_to_fill(images[3], third_width, half_height, crop_position)
157
- img5 = resize_and_crop_to_fill(images[4], third_width, half_height, crop_position)
158
- img6 = resize_and_crop_to_fill(images[5], third_width_last, half_height, crop_position)
159
- canvas.paste(img4, (0, y_offset))
160
- canvas.paste(img5, (third_width + border_size, y_offset))
161
- canvas.paste(img6, (third_width * 2 + border_size * 2, y_offset))
162
- else:
163
- raise HTTPException(status_code=400, detail="Apenas até 6 imagens são suportadas.")
164
-
165
- return canvas
166
 
167
- def parse_image_urls(image_url_param: Union[str, List[str]]) -> List[str]:
168
- """Converte o parâmetro de URL(s) em lista de URLs"""
169
- if isinstance(image_url_param, list):
170
- return image_url_param
171
- elif isinstance(image_url_param, str):
172
- # Se contém vírgulas, divide em múltiplas URLs
173
- if ',' in image_url_param:
174
- return [url.strip() for url in image_url_param.split(',') if url.strip()]
175
- else:
176
- return [image_url_param]
177
- return []
178
 
179
- def create_gradient_overlay(width: int, height: int, text_position: str = "bottom") -> Image.Image:
180
- """ Cria gradiente overlay baseado na posição do texto """
181
  gradient = Image.new("RGBA", (width, height))
182
  draw = ImageDraw.Draw(gradient)
183
 
184
- # Novo gradiente: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.85) 55.77%, #000000 100%)
185
- # Posição: top: 262px, altura: 1088px
186
- gradient_start = 262
187
- gradient_height = 1088
188
 
189
  for y in range(gradient_height):
190
  if y + gradient_start < height:
191
  ratio = y / gradient_height
192
- if ratio <= 0.5577: # 0% a 55.77%: de transparente para rgba(0,0,0,0.85)
193
- opacity_ratio = ratio / 0.5577
194
- opacity = int(255 * 0.85 * opacity_ratio)
195
- else: # 55.77% a 100%: de rgba(0,0,0,0.85) para #000000 (opaco)
196
- opacity_ratio = (ratio - 0.5577) / (1 - 0.5577)
197
- opacity = int(255 * (0.85 + 0.15 * opacity_ratio))
198
 
199
  draw.line([(0, y + gradient_start), (width, y + gradient_start)], fill=(0, 0, 0, opacity))
200
 
201
  return gradient
202
 
203
- def wrap_text(text: str, font: ImageFont.FreeTypeFont, max_width: int, draw: ImageDraw.Draw) -> list[str]:
204
- words = text.split()
205
- lines = []
206
- current_line = ""
207
-
208
- for word in words:
209
- test_line = f"{current_line} {word}".strip()
210
- if draw.textlength(test_line, font=font) <= max_width:
211
- current_line = test_line
212
- else:
213
- if current_line:
214
- lines.append(current_line)
215
- current_line = word
216
-
217
- if current_line:
218
- lines.append(current_line)
219
-
220
- return lines
221
-
222
- def get_responsive_font_and_lines(text: str, font_path: str, max_width: int, max_lines: int = 3, font_size: int = 55, min_font_size: int = 20) -> tuple[ImageFont.FreeTypeFont, list[str], int]:
223
- """ Retorna a fonte e linhas ajustadas para caber no número máximo de linhas. """
224
- temp_img = Image.new("RGB", (1, 1))
225
- temp_draw = ImageDraw.Draw(temp_img)
226
-
227
- current_font_size = font_size
228
- while current_font_size >= min_font_size:
229
- try:
230
- font = ImageFont.truetype(font_path, current_font_size)
231
- except Exception:
232
- font = ImageFont.load_default()
233
-
234
- lines = wrap_text(text, font, max_width, temp_draw)
235
- if len(lines) <= max_lines:
236
- return font, lines, current_font_size
237
-
238
- current_font_size -= 1
239
-
240
- try:
241
- font = ImageFont.truetype(font_path, min_font_size)
242
- except Exception:
243
- font = ImageFont.load_default()
244
-
245
- lines = wrap_text(text, font, max_width, temp_draw)
246
- return font, lines, min_font_size
247
-
248
- def get_text_color_rgb(text_color: str) -> tuple[int, int, int]:
249
- """ Converte o parâmetro text_color para RGB. """
250
- if text_color.lower() == "black":
251
- return (0, 0, 0)
252
- else: # white por padrão
253
- return (255, 255, 255)
254
-
255
- def create_category_image(category: str, type_param: str = "news", base_width: int = 200, height: int = 46) -> Image.Image:
256
- """Cria uma imagem de categoria dinamicamente com largura responsiva"""
257
-
258
- # Configurações
259
- text_padding_left = 20
260
- text_padding_right = 15
261
- ball_size = 41 # Aumentado de 33 para 41
262
- ball_margin = 3
263
-
264
- # Definir cores baseadas no type
265
- if type_param.lower() == "pop":
266
- bg_color = (4, 196, 234, 255) # #04C4EA
267
- ball_color = (143, 244, 255, 255) # #8FF4FF
268
- text_color = (255, 255, 255, 255) # Branco para pop
269
- else: # "news" ou qualquer outro valor
270
- bg_color = (85, 90, 208, 255) # #555AD0 (nova cor)
271
- ball_color = (146, 134, 255, 255) # #9286FF (nova cor da bola)
272
- text_color = (255, 255, 255, 255) # Branco para news (alterado de preto)
273
-
274
- # Carregar fonte
275
- try:
276
- font = ImageFont.truetype("fonts/Montserrat-Bold.ttf", 22)
277
- except Exception:
278
- font = ImageFont.load_default()
279
-
280
- # Texto da categoria em maiúsculo
281
- category_text = category.upper()
282
-
283
- # Calcular largura do texto
284
- temp_img = Image.new("RGBA", (1, 1))
285
- temp_draw = ImageDraw.Draw(temp_img)
286
- text_bbox = temp_draw.textbbox((0, 0), category_text, font=font)
287
- text_width = text_bbox[2] - text_bbox[0]
288
- text_height = text_bbox[3] - text_bbox[1]
289
-
290
- # Calcular largura total necessária
291
- # padding esquerdo + texto + padding direito + bola + margem da bola
292
- required_width = text_padding_left + text_width + text_padding_right + ball_size + ball_margin
293
-
294
- # Usar a largura maior entre a base e a necessária
295
- final_width = max(base_width, required_width)
296
-
297
- # Criar imagem com largura responsiva
298
- category_img = Image.new("RGBA", (final_width, height), (0, 0, 0, 0))
299
- draw = ImageDraw.Draw(category_img)
300
-
301
- # Desenhar o fundo com bordas arredondadas
302
- radius = height // 2 # Bordas totalmente arredondadas baseadas na altura
303
-
304
- draw.rounded_rectangle([(0, 0), (final_width-1, height-1)], radius=radius, fill=bg_color)
305
-
306
- # Posicionar a bola no canto direito
307
- ball_x = final_width - ball_size - ball_margin
308
- ball_y = (height - ball_size) // 2
309
-
310
- # Desenhar a bola
311
- draw.ellipse([ball_x, ball_y, ball_x + ball_size, ball_y + ball_size], fill=ball_color)
312
-
313
- # Calcular área disponível para texto (espaço total menos bola e margens)
314
- text_area_width = ball_x - (text_padding_left + text_padding_right)
315
-
316
- # Posicionar texto centralizado na área disponível
317
- text_x = text_padding_left + (text_area_width - text_width) // 2
318
-
319
- # Centralizar texto verticalmente de forma mais precisa
320
- # Usar a baseline da fonte para alinhamento mais preciso
321
- text_y = (height - text_height) // 2 - text_bbox[1] # Compensar offset da bbox
322
-
323
- # Desenhar o texto com a cor apropriada
324
- draw.text((text_x, text_y), category_text, font=font, fill=text_color)
325
-
326
- return category_img
327
-
328
- def create_canvas(image_url: Optional[Union[str, List[str]]], headline: Optional[str], text_position: str = "bottom", text_color: str = "white", crop_position: str = "center", breaking_news: bool = False, category: str = "other", type: str = "news", show_category: bool = True) -> BytesIO:
329
  # Dimensões fixas do Instagram
330
  width, height = 1080, 1350
331
- text_rgb = get_text_color_rgb(text_color)
332
 
333
- # Configurações do Instagram
334
- padding_x = 60
335
- bottom_padding = 100
336
- top_padding = 60
337
  logo_width, logo_height = 121, 23
338
 
339
- max_width = width - 2 * padding_x
340
  canvas = Image.new("RGBA", (width, height), color=(255, 255, 255, 255))
341
 
342
- # Adicionar imagem(s) de fundo se fornecida(s)
343
  if image_url:
344
- parsed_urls = parse_image_urls(image_url)
345
- if parsed_urls:
346
- if len(parsed_urls) > 6:
347
- raise HTTPException(status_code=400, detail="Máximo de 6 imagens permitidas")
348
-
349
- if len(parsed_urls) == 1:
350
- # Uma única imagem - comportamento original
351
- img = download_image_from_url(parsed_urls[0])
352
- filled_img = resize_and_crop_to_fill(img, width, height, crop_position)
353
- canvas.paste(filled_img, (0, 0))
354
- else:
355
- # Múltiplas imagens - criar colagem
356
- canvas = create_collage_background(parsed_urls, width, height, crop_position)
357
 
358
- # Adicionar gradiente se o texto for branco
359
- if text_color.lower() != "black":
360
- gradient_overlay = create_gradient_overlay(width, height, text_position)
361
- canvas = Image.alpha_composite(canvas, gradient_overlay)
362
 
363
- if headline:
364
- draw = ImageDraw.Draw(canvas)
365
- font_path = "fonts/Montserrat-SemiBold.ttf" # Nova fonte
366
- line_height_factor = 1.05 # 105% da altura da linha
367
-
368
- try:
369
- font, lines, font_size = get_responsive_font_and_lines(
370
- headline, font_path, max_width, max_lines=3, font_size=55, min_font_size=20 # Tamanho fixo 55px
371
- )
372
- line_height = int(font_size * line_height_factor)
373
- except Exception as e:
374
- raise HTTPException(status_code=500, detail=f"Erro ao processar a fonte: {e}")
375
-
376
- total_text_height = len(lines) * line_height
377
-
378
- # Calcular espaço necessário para imagem de categoria/breaking
379
- category_img_height = 0
380
- category_spacing = 0
381
- if (breaking_news or (category and show_category)):
382
- category_img_height = 46 # altura da nova imagem de categoria
383
- category_spacing = 20 # separação entre imagem e título (reduzido de 38 para 20)
384
-
385
- # Posicionar texto baseado no parâmetro text_position
386
- if text_position.lower() == "bottom":
387
- # Logo está em Y:1256, texto deve ficar 100px acima da logo
388
- text_bottom_y = 1256 - 100 # Y: 1176
389
- start_y = text_bottom_y - total_text_height
390
-
391
- # Se há imagem de categoria, ajustar posição para dar espaço ACIMA do texto
392
- if (breaking_news or (category and show_category)):
393
- category_y = start_y - category_spacing - category_img_height
394
- else: # text_position == "top"
395
- # Posicionar texto no topo com padding
396
- start_y = top_padding
397
-
398
- # Se há imagem de categoria, ajustar posição para aparecer ABAIXO do texto
399
- if (breaking_news or (category and show_category)):
400
- category_y = start_y + total_text_height + category_spacing
401
-
402
- # Adicionar imagem de categoria/breaking se necessário
403
- if (breaking_news or (category and show_category)):
404
- try:
405
- if breaking_news:
406
- # Breaking news ainda carrega PNG
407
- img_path = "breaking.png"
408
- category_img = Image.open(img_path).convert("RGBA")
409
- category_resized = category_img.resize((200, 46))
410
- else:
411
- # Criar imagem de categoria dinamicamente
412
- category_resized = create_category_image(category, type)
413
-
414
- # Posicionar no canto esquerdo, alinhado com o início do título
415
- category_x = padding_x
416
- canvas.paste(category_resized, (category_x, category_y), category_resized)
417
- except Exception as e:
418
- raise HTTPException(status_code=500, detail=f"Erro ao criar a imagem de categoria: {e}")
419
-
420
- # Adicionar logo no canto inferior direito (posição fixa)
421
- try:
422
- logo_path = "recurve.png"
423
- logo = Image.open(logo_path).convert("RGBA")
424
- logo_resized = logo.resize((logo_width, logo_height))
425
-
426
- # Aplicar opacidade de 42%
427
- logo_with_opacity = Image.new("RGBA", logo_resized.size)
428
- for x in range(logo_resized.width):
429
- for y in range(logo_resized.height):
430
- r, g, b, a = logo_resized.getpixel((x, y))
431
- new_alpha = int(a * 0.42) # 42% de opacidade
432
- logo_with_opacity.putpixel((x, y), (r, g, b, new_alpha))
433
-
434
- # Posição alterada: X:70, Y:1256
435
- canvas.paste(logo_with_opacity, (70, 1256), logo_with_opacity)
436
- except Exception as e:
437
- raise HTTPException(status_code=500, detail=f"Erro ao carregar a logo: {e}")
438
-
439
- # Adiciona texto com a cor especificada
440
- for i, line in enumerate(lines):
441
- y = start_y + i * line_height
442
- draw.text((padding_x, y), line, font=font, fill=text_rgb)
443
 
444
  buffer = BytesIO()
445
  canvas.convert("RGB").save(buffer, format="PNG")
@@ -448,18 +124,10 @@ def create_canvas(image_url: Optional[Union[str, List[str]]], headline: Optional
448
 
449
  @router.get("/cover/news")
450
  def get_news_image(
451
- image_url: Optional[Union[str, List[str]]] = Query(None, description="URL da imagem ou lista de URLs separadas por vírgula para colagem (máximo 6)"),
452
- headline: Optional[str] = Query(None, description="Texto do título"),
453
- text_position: str = Query("bottom", description="Posição do texto: 'top' para topo ou 'bottom' para parte inferior"),
454
- text_color: str = Query("white", description="Cor do texto: 'white' (padrão) ou 'black'. Se 'black', remove o gradiente de fundo"),
455
- crop_position: str = Query("center", description="Posição do enquadramento da imagem: 'center' (padrão), 'top', 'bottom', 'left', 'right'"),
456
- breaking_news: bool = Query(False, description="Se true, mostra a imagem breaking.png acima do título (tem prioridade sobre category)"),
457
- category: str = Query("other", description="Categoria da notícia: 'person', 'movie', 'series', 'place', 'event', 'other' (padrão)"),
458
- type: str = Query("news", description="Tipo da categoria: 'news' (roxo, texto branco) ou 'pop' (azul, texto branco)"),
459
- show_category: bool = Query(True, description="Se deve mostrar a categoria (true) ou não (false)")
460
  ):
461
  try:
462
- buffer = create_canvas(image_url, headline, text_position, text_color, crop_position, breaking_news, category, type, show_category)
463
  return StreamingResponse(buffer, media_type="image/png")
464
  except Exception as e:
465
  raise HTTPException(status_code=500, detail=f"Erro ao gerar imagem: {str(e)}")
 
1
  from fastapi import APIRouter, Query, HTTPException
2
  from fastapi.responses import StreamingResponse
3
+ from PIL import Image, ImageDraw
4
  from io import BytesIO
5
  import requests
6
+ from typing import Optional
7
 
8
  router = APIRouter()
9
 
 
22
  except Exception as e:
23
  raise HTTPException(status_code=400, detail=f"Erro ao baixar imagem: {url} ({str(e)})")
24
 
25
+ def resize_and_crop_to_fill(img: Image.Image, target_width: int, target_height: int) -> Image.Image:
26
  """
27
+ Redimensiona e corta a imagem para preencher exatamente o espaço alvo (sempre centralizado).
28
 
29
  Args:
30
  img: Imagem PIL
31
  target_width: Largura alvo
32
  target_height: Altura alvo
 
33
  """
34
  img_ratio = img.width / img.height
35
  target_ratio = target_width / target_height
 
45
 
46
  img_resized = img.resize((scale_width, scale_height), Image.LANCZOS)
47
 
48
+ # Centralizar o crop
49
+ left = (scale_width - target_width) // 2
50
+ top = (scale_height - target_height) // 2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  right = left + target_width
52
  bottom = top + target_height
53
 
54
  return img_resized.crop((left, top, right, bottom))
55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
+ def create_gradient_overlay(width: int, height: int) -> Image.Image:
59
+ """ Cria o novo gradiente overlay conforme especificado """
60
  gradient = Image.new("RGBA", (width, height))
61
  draw = ImageDraw.Draw(gradient)
62
 
63
+ # Novo gradiente: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.4) 55%, rgba(0, 0, 0, 0.5) 100%)
64
+ # Posição: left: 0px, top: 808px, width: 1080px, height: 542px
65
+ gradient_start = 808
66
+ gradient_height = 542
67
 
68
  for y in range(gradient_height):
69
  if y + gradient_start < height:
70
  ratio = y / gradient_height
71
+ if ratio <= 0.55: # 0% a 55%: de transparente para rgba(0,0,0,0.4)
72
+ opacity_ratio = ratio / 0.55
73
+ opacity = int(255 * 0.4 * opacity_ratio)
74
+ else: # 55% a 100%: de rgba(0,0,0,0.4) para rgba(0,0,0,0.5)
75
+ opacity_ratio = (ratio - 0.55) / (1 - 0.55)
76
+ opacity = int(255 * (0.4 + 0.1 * opacity_ratio))
77
 
78
  draw.line([(0, y + gradient_start), (width, y + gradient_start)], fill=(0, 0, 0, opacity))
79
 
80
  return gradient
81
 
82
+ def create_canvas(image_url: Optional[str]) -> BytesIO:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  # Dimensões fixas do Instagram
84
  width, height = 1080, 1350
 
85
 
86
+ # Configurações da logo
 
 
 
87
  logo_width, logo_height = 121, 23
88
 
 
89
  canvas = Image.new("RGBA", (width, height), color=(255, 255, 255, 255))
90
 
91
+ # Adicionar imagem de fundo se fornecida
92
  if image_url:
93
+ img = download_image_from_url(image_url)
94
+ filled_img = resize_and_crop_to_fill(img, width, height)
95
+ canvas.paste(filled_img, (0, 0))
 
 
 
 
 
 
 
 
 
 
96
 
97
+ # Adicionar o novo gradiente
98
+ gradient_overlay = create_gradient_overlay(width, height)
99
+ canvas = Image.alpha_composite(canvas, gradient_overlay)
 
100
 
101
+ # Adicionar logo na nova posição (X: 880, Y: 1260)
102
+ try:
103
+ logo_path = "recurve.png"
104
+ logo = Image.open(logo_path).convert("RGBA")
105
+ logo_resized = logo.resize((logo_width, logo_height))
106
+
107
+ # Aplicar opacidade de 42%
108
+ logo_with_opacity = Image.new("RGBA", logo_resized.size)
109
+ for x in range(logo_resized.width):
110
+ for y in range(logo_resized.height):
111
+ r, g, b, a = logo_resized.getpixel((x, y))
112
+ new_alpha = int(a * 0.42) # 42% de opacidade
113
+ logo_with_opacity.putpixel((x, y), (r, g, b, new_alpha))
114
+
115
+ # Nova posição: X:880, Y:1260
116
+ canvas.paste(logo_with_opacity, (880, 1260), logo_with_opacity)
117
+ except Exception as e:
118
+ raise HTTPException(status_code=500, detail=f"Erro ao carregar a logo: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
  buffer = BytesIO()
121
  canvas.convert("RGB").save(buffer, format="PNG")
 
124
 
125
  @router.get("/cover/news")
126
  def get_news_image(
127
+ image_url: Optional[str] = Query(None, description="URL da imagem de fundo")
 
 
 
 
 
 
 
 
128
  ):
129
  try:
130
+ buffer = create_canvas(image_url)
131
  return StreamingResponse(buffer, media_type="image/png")
132
  except Exception as e:
133
  raise HTTPException(status_code=500, detail=f"Erro ao gerar imagem: {str(e)}")