File size: 12,111 Bytes
018aede
 
3be30aa
018aede
 
 
3be30aa
018aede
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1cd3fc6
 
018aede
 
 
1cd3fc6
33c48a8
 
1cd3fc6
33c48a8
1cd3fc6
 
 
 
33c48a8
 
 
 
 
 
1cd3fc6
 
 
33c48a8
 
 
 
1cd3fc6
 
 
 
33c48a8
 
 
 
 
 
1cd3fc6
 
018aede
 
7a0713f
e7f04c1
3be30aa
 
 
 
 
 
 
 
e7f04c1
3be30aa
 
1cd3fc6
e7f04c1
3be30aa
 
 
 
 
 
 
 
6d566dd
3be30aa
 
 
 
 
 
 
 
6d566dd
 
 
 
 
 
3be30aa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6d566dd
3be30aa
 
 
6d566dd
3be30aa
6d566dd
3be30aa
 
 
6d566dd
 
3be30aa
 
6d566dd
 
3be30aa
 
 
 
 
6d566dd
 
 
 
 
 
3be30aa
 
 
 
 
6d566dd
 
 
 
 
 
 
3be30aa
 
 
 
 
 
 
1cd3fc6
 
 
 
 
 
e7f04c1
 
 
 
 
 
 
 
33c48a8
743f0d2
e7f04c1
 
 
 
 
33c48a8
743f0d2
e7f04c1
3be30aa
1cd3fc6
bb64f0e
 
46c77d7
35c4672
bb64f0e
46c77d7
 
8c56c61
35c4672
46c77d7
35c4672
 
 
bb64f0e
1cd3fc6
 
 
 
8c56c61
35c4672
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8c56c61
3be30aa
 
 
1cd3fc6
 
 
44f7480
1cd3fc6
 
 
 
e7f04c1
3be30aa
46c77d7
 
 
 
 
 
 
3be30aa
1cd3fc6
 
 
46c77d7
 
1cd3fc6
46c77d7
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
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
import textwrap

router = APIRouter()

def download_image_from_url(url: str) -> Image.Image:
    headers = {
        "User-Agent": (
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
            "AppleWebKit/537.36 (KHTML, like Gecko) "
            "Chrome/115.0.0.0 Safari/537.36"
        )
    }
    try:
        response = requests.get(url, headers=headers, timeout=10)
        response.raise_for_status()
        return Image.open(BytesIO(response.content)).convert("RGBA")
    except Exception as e:
        raise HTTPException(status_code=400, detail=f"Erro ao baixar imagem: {url} ({str(e)})")

def resize_and_crop_to_fill(img: Image.Image, target_width: int, target_height: int) -> Image.Image:
    """
    Redimensiona e corta a imagem para preencher exatamente o espaço alvo (sempre centralizado).
    
    Args:
        img: Imagem PIL
        target_width: Largura alvo
        target_height: Altura alvo
    """
    img_ratio = img.width / img.height
    target_ratio = target_width / target_height
    
    if img_ratio > target_ratio:
        # Imagem é mais larga proporcionalmente - redimensionar baseado na altura
        scale_height = target_height
        scale_width = int(scale_height * img_ratio)
    else:
        # Imagem é mais alta proporcionalmente - redimensionar baseado na largura
        scale_width = target_width
        scale_height = int(scale_width / img_ratio)
    
    img_resized = img.resize((scale_width, scale_height), Image.LANCZOS)
    
    # Centralizar o crop
    left = (scale_width - target_width) // 2
    top = (scale_height - target_height) // 2
    right = left + target_width
    bottom = top + target_height
    
    return img_resized.crop((left, top, right, bottom))



def create_gradient_overlay(width: int, height: int, position: str = "bottom") -> Image.Image:
    """ Cria o gradiente overlay conforme especificado """
    gradient = Image.new("RGBA", (width, height))
    draw = ImageDraw.Draw(gradient)
    
    if position == "top":
        # Gradiente para topo: linear-gradient(360deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.55) 54.94%, rgba(0, 0, 0, 0.7) 100%)
        # Posição: left: 0px, top: 0px, width: 1080px, height: 706px
        gradient_start = 0
        gradient_height = 706
        
        for y in range(gradient_height):
            if y < height:
                ratio = y / gradient_height
                if ratio <= 0.5494:  # 0% a 54.94%: de transparente para rgba(0,0,0,0.55)
                    opacity_ratio = ratio / 0.5494
                    opacity = int(255 * 0.55 * opacity_ratio)
                else:  # 54.94% a 100%: de rgba(0,0,0,0.55) para rgba(0,0,0,0.7)
                    opacity_ratio = (ratio - 0.5494) / (1 - 0.5494)
                    opacity = int(255 * (0.55 + 0.15 * opacity_ratio))
                
                draw.line([(0, y), (width, y)], fill=(0, 0, 0, opacity))
    else:
        # Gradiente para baixo: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.55) 54.94%, rgba(0, 0, 0, 0.7) 100%)
        # Posição: left: 0px, top: 644px, width: 1080px, height: 706px
        gradient_start = 644
        gradient_height = 706
        
        for y in range(gradient_height):
            if y + gradient_start < height:
                ratio = y / gradient_height
                if ratio <= 0.5494:  # 0% a 54.94%: de transparente para rgba(0,0,0,0.55)
                    opacity_ratio = ratio / 0.5494
                    opacity = int(255 * 0.55 * opacity_ratio)
                else:  # 54.94% a 100%: de rgba(0,0,0,0.55) para rgba(0,0,0,0.7)
                    opacity_ratio = (ratio - 0.5494) / (1 - 0.5494)
                    opacity = int(255 * (0.55 + 0.15 * opacity_ratio))
                
                draw.line([(0, y + gradient_start), (width, y + gradient_start)], fill=(0, 0, 0, opacity))
    
    return gradient

def render_responsive_text(draw: ImageDraw.Draw, text: str, x: int, y: int, max_width: int, max_lines: int = 3, text_color: str = "white", text_position: str = "bottom") -> None:
    """
    Renderiza texto responsivo que se ajusta automaticamente ao tamanho da fonte
    para caber em até max_lines linhas dentro da largura especificada.
    
    Args:
        draw: Objeto ImageDraw para desenhar
        text: Texto a ser renderizado
        x: Posição X (esquerda)
        y: Posição Y (base para bottom, topo para top)
        max_width: Largura máxima do texto
        max_lines: Número máximo de linhas (padrão: 3)
        text_color: Cor do texto ("white" ou "black")
        text_position: Posição do texto ("bottom" ou "top")
    """
    if not text.strip():
        return
    
    # Carregar fonte
    try:
        font_path = "fonts/cheltenham-italic-800.ttf"
    except (OSError, IOError):
        font_path = None
    
    # Dividir texto em palavras
    words = text.split()
    if not words:
        return
    
    # Função para quebrar texto em linhas
    def wrap_text(text, font_size, max_width):
        if font_path:
            try:
                test_font = ImageFont.truetype(font_path, font_size)
            except (OSError, IOError):
                test_font = ImageFont.load_default()
        else:
            test_font = ImageFont.load_default()
        
        lines = []
        current_line = []
        
        for word in words:
            test_line = " ".join(current_line + [word])
            bbox = draw.textbbox((0, 0), test_line, font=test_font)
            line_width = bbox[2] - bbox[0]
            
            if line_width <= max_width:
                current_line.append(word)
            else:
                if current_line:
                    lines.append(" ".join(current_line))
                    current_line = [word]
                else:
                    # Palavra muito longa, adiciona mesmo assim
                    lines.append(word)
        
        if current_line:
            lines.append(" ".join(current_line))
        
        return lines
    
    # Encontrar o tamanho de fonte ideal
    font_size = 80  # Tamanho inicial
    min_font_size = 15  # Tamanho mínimo menor para mais flexibilidade
    
    # Tentar diferentes tamanhos de fonte até encontrar um que caiba em max_lines
    while font_size >= min_font_size:
        lines = wrap_text(text, font_size, max_width)
        
        # Se o texto cabe em max_lines ou menos, usar este tamanho
        if len(lines) <= max_lines:
            break
        
        # Reduzir o tamanho da fonte
        font_size -= 2  # Redução menor para melhor precisão
    
    # Garantir que não seja menor que o tamanho mínimo
    font_size = max(font_size, min_font_size)
    
    # Carregar fonte final
    if font_path:
        try:
            final_font = ImageFont.truetype(font_path, font_size)
        except (OSError, IOError):
            final_font = ImageFont.load_default()
    else:
        final_font = ImageFont.load_default()
    
    # Quebrar texto final
    lines = wrap_text(text, font_size, max_width)
    
    # Se ainda não couber em max_lines, forçar quebra nas primeiras max_lines
    if len(lines) > max_lines:
        # Combinar as linhas restantes na última linha permitida
        combined_text = " ".join(lines[:max_lines-1] + [" ".join(lines[max_lines-1:])])
        lines = wrap_text(combined_text, font_size, max_width)
        lines = lines[:max_lines]  # Garantir que não exceda max_lines
    
    # Calcular altura total do texto
    line_heights = []
    for line in lines:
        bbox = draw.textbbox((0, 0), line, font=final_font)
        line_height = bbox[3] - bbox[1]
        line_heights.append(line_height)
    
    # Determinar cor do texto
    if text_color.lower() == "black":
        fill_color = (0, 0, 0, 255)  # Preto
    else:
        fill_color = (255, 255, 255, 255)  # Branco (padrão)
    
    # Desenhar texto baseado na posição
    if text_position.lower() == "top":
        # Desenhar texto de cima para baixo (alinhamento ao topo)
        current_y = y
        for i, line in enumerate(lines):
            bbox = draw.textbbox((0, 0), line, font=final_font)
            line_height = bbox[3] - bbox[1]
            draw.text((x, current_y), line, fill=fill_color, font=final_font)
            # Aplicar line height de 120% (adicionar 20% de espaçamento extra)
            current_y += int(line_height * 1.20)
    else:
        # Desenhar texto de baixo para cima (alinhamento à base)
        current_y = y
        for i, line in enumerate(reversed(lines)):
            line_height = line_heights[len(lines) - 1 - i]
            # Aplicar line height de 120% (adicionar 20% de espaçamento extra)
            current_y -= int(line_height * 1.20)
            draw.text((x, current_y), line, fill=fill_color, font=final_font)

def create_canvas(image_url: Optional[str], text: Optional[str] = None, text_position: str = "bottom", text_color: str = "white") -> BytesIO:
    # Dimensões fixas do Instagram
    width, height = 1080, 1350
    
    # Configurações da logo
    logo_width, logo_height = 121, 23
    
    canvas = Image.new("RGBA", (width, height), color=(255, 255, 255, 255))
    
    # Adicionar imagem de fundo se fornecida
    if image_url:
        img = download_image_from_url(image_url)
        filled_img = resize_and_crop_to_fill(img, width, height)
        canvas.paste(filled_img, (0, 0))
    
    # Adicionar gradiente (apenas se não for texto preto)
    if text_color.lower() != "black":
        gradient_overlay = create_gradient_overlay(width, height, text_position)
        canvas = Image.alpha_composite(canvas, gradient_overlay)
    
    # Adicionar logo na nova posição (X: 880, Y: 1260)
    try:
        logo_path = "recurve.png"
        logo = Image.open(logo_path).convert("RGBA")
        logo_resized = logo.resize((logo_width, logo_height))
        
        # Aplicar opacidade de 42%
        logo_with_opacity = Image.new("RGBA", logo_resized.size)
        for x in range(logo_resized.width):
            for y in range(logo_resized.height):
                r, g, b, a = logo_resized.getpixel((x, y))
                new_alpha = int(a * 0.42)  # 42% de opacidade
                logo_with_opacity.putpixel((x, y), (r, g, b, new_alpha))
        
        # Nova posição: X:880, Y:1260
        canvas.paste(logo_with_opacity, (880, 1260), logo_with_opacity)
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Erro ao carregar a logo: {e}")
    
    # Adicionar texto se fornecido
    if text and text.strip():
        draw = ImageDraw.Draw(canvas)
        
        # Determinar posição Y baseada no text_position
        if text_position.lower() == "top":
            text_y = 60  # Posição para texto no topo com espaçamento de 60px
        else:
            text_y = 1180  # Posição para texto embaixo (padrão)
        
        # Configurações do texto: X: 78, largura: 924px, alinhado à base e à esquerda
        render_responsive_text(draw, text, x=78, y=text_y, max_width=924, max_lines=3, text_color=text_color, text_position=text_position)
    
    buffer = BytesIO()
    canvas.convert("RGB").save(buffer, format="PNG")
    buffer.seek(0)
    return buffer

@router.get("/cover/news")
def get_news_image(
    image_url: Optional[str] = Query(None, description="URL da imagem de fundo"),
    text: Optional[str] = Query(None, description="Texto a ser exibido na imagem"),
    text_position: str = Query("bottom", description="Posição do texto: 'bottom' ou 'top'"),
    text_color: str = Query("white", description="Cor do texto: 'white' ou 'black'")
):
    try:
        buffer = create_canvas(image_url, text, text_position, text_color)
        return StreamingResponse(buffer, media_type="image/png")
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Erro ao gerar imagem: {str(e)}")