from fastapi import APIRouter, Query, HTTPException from fastapi.responses import StreamingResponse from PIL import Image, ImageDraw, ImageFont from pilmoji import Pilmoji from pilmoji.source import AppleEmojiSource from io import BytesIO from typing import Optional import re router = APIRouter() def parse_text(text: str): """Parser para *negrito*, _italico_ e *_negrito+italico_*""" tokens = [] parts = text.split("\n") pattern = re.compile(r'(\*_([^*]+)_\*|_\*([^_]+)\*_|\*[^*]+\*|_[^_]+_|[^*_]+)') for i, part in enumerate(parts): for match in pattern.finditer(part): chunk = match.group(0) if (chunk.startswith("*_") and chunk.endswith("_*")) or (chunk.startswith("_*") and chunk.endswith("*_")): tokens.append((chunk[2:-2], "bolditalic")) elif chunk.startswith("*") and chunk.endswith("*"): tokens.append((chunk[1:-1], "bold")) elif chunk.startswith("_") and chunk.endswith("_"): tokens.append((chunk[1:-1], "italic")) else: tokens.append((chunk, "regular")) if i < len(parts) - 1: tokens.append(("\n", "regular")) return tokens def get_font(style: str, font_size: int): """Retorna a fonte apropriada para o estilo""" try: if style == "bold": return ImageFont.truetype("fonts/Montserrat-SemiBold.ttf", font_size) elif style == "italic": return ImageFont.truetype("fonts/Montserrat-Italic.ttf", font_size) elif style == "bolditalic": return ImageFont.truetype("fonts/Montserrat-SemiBoldItalic.ttf", font_size) else: return ImageFont.truetype("fonts/Montserrat-Regular.ttf", font_size) except (OSError, IOError): try: # Fallback para caminhos alternativos if style == "bold": return ImageFont.truetype("Montserrat-SemiBold.ttf", font_size) elif style == "italic": return ImageFont.truetype("Montserrat-Italic.ttf", font_size) elif style == "bolditalic": return ImageFont.truetype("Montserrat-SemiBoldItalic.ttf", font_size) else: return ImageFont.truetype("Montserrat-Regular.ttf", font_size) except (OSError, IOError): # Fallback final para fonte padrão return ImageFont.load_default() def wrap_text(tokens, draw, max_width, font_size): """Quebra de linha automática com suporte a formatação""" lines = [] current_line = [] current_width = 0 for token_index, (token_text, style) in enumerate(tokens): if token_text == "\n": lines.append(current_line) current_line = [] current_width = 0 continue # Dividir tokens formatados em palavras individuais words = token_text.split(" ") for i, word in enumerate(words): if not word: # Ignorar strings vazias continue font = get_font(style, font_size) # Adicionar espaço antes da palavra se: # 1. Não for a primeira palavra do token (i > 0), OU # 2. For a primeira palavra mas já houver conteúdo na linha needs_space = (i > 0) or (i == 0 and current_line) if needs_space: space_width = draw.textlength(" ", font=font) word_width = space_width + draw.textlength(word, font=font) else: word_width = draw.textlength(word, font=font) # Se não cabe na linha atual e já tem conteúdo, quebrar if current_width + word_width > max_width and current_line: lines.append(current_line) current_line = [(word, style)] current_width = draw.textlength(word, font=font) else: # Adicionar espaço se necessário if needs_space: current_line.append((" ", style)) current_width += space_width current_line.append((word, style)) current_width += draw.textlength(word, font=font) if current_line: lines.append(current_line) return lines def create_title_image(text: str) -> BytesIO: """ Cria uma imagem com logo e texto. Logo: logoig.png (317x86) em X: 0 Texto: alinhado à esquerda, tamanho ajustado automaticamente, 26px abaixo do logo Largura fixa de 900px, altura dinâmica baseada no conteúdo, fundo preto. Sem paddings - conteúdo encostado em todas as bordas. Máximo de 4 linhas - reduz o tamanho da fonte automaticamente se necessário. """ # Largura fixa da imagem canvas_width = 900 # Configurações do logo logo_width, logo_height = 317, 86 logo_x = 0 # Sem padding lateral logo_y = 0 # Topo da imagem # Carregar logo primeiro para calcular dimensões try: logo_path = "logoig.png" logo = Image.open(logo_path).convert("RGBA") # Redimensionar se necessário (caso o arquivo tenha dimensões diferentes) if logo.width != logo_width or logo.height != logo_height: logo = logo.resize((logo_width, logo_height), Image.LANCZOS) except Exception as e: raise HTTPException(status_code=500, detail=f"Erro ao carregar o logo: {e}") # Criar canvas temporário para medir o texto temp_canvas = Image.new("RGBA", (5000, 5000), color=(0, 0, 0, 0)) # Tamanho grande para medir draw_temp = ImageDraw.Draw(temp_canvas) # Configurações iniciais de fonte font_size = 40 # Tamanho inicial min_font_size = 15 # Tamanho mínimo max_lines = 4 # Máximo de 4 linhas # Processar texto com parsing de formatação text_x = 0 # Sem padding lateral text_y = int(logo_y + logo_height + 26) # Garantir que seja inteiro # Calcular largura máxima do texto (usa toda a largura de 900px) max_text_width = canvas_width # Parse do texto para tokens tokens = parse_text(text) # Encontrar o tamanho de fonte ideal que cabe em max_lines lines = [] while font_size >= min_font_size: # Quebrar texto em linhas com suporte a formatação lines = wrap_text(tokens, draw_temp, max_text_width, font_size) # 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 # Garantir que não seja menor que o tamanho mínimo font_size = max(font_size, min_font_size) # Recalcular linhas com o tamanho final lines = wrap_text(tokens, draw_temp, max_text_width, font_size) # Se ainda não couber em max_lines, limitar às primeiras max_lines if len(lines) > max_lines: lines = lines[:max_lines] # Calcular altura do texto line_height = int(font_size * 1.2) text_height = len(lines) * line_height if lines else 0 # Calcular altura final da imagem (altura dinâmica, sem margem inferior) # Altura: logo_y (topo) + logo_height + 26 (espaçamento) + text_height canvas_height = int(logo_y + logo_height + 26 + text_height) # Garantir que seja inteiro # Criar canvas final com fundo preto canvas = Image.new("RGBA", (int(canvas_width), int(canvas_height)), color=(0, 0, 0, 255)) # Adicionar logo canvas.paste(logo, (logo_x, logo_y), logo) # Adicionar texto se fornecido if text and text.strip(): draw = ImageDraw.Draw(canvas) # Renderizar cada linha do texto alinhado à esquerda (cor branca para fundo preto) # Usar Pilmoji para renderizar emojis corretamente com fonte Apple with Pilmoji(canvas, source=AppleEmojiSource) as pilmoji: current_y = int(text_y) for line in lines: current_x = int(text_x) # Renderizar cada token da linha com a fonte apropriada for word, style in line: font = get_font(style, font_size) # Desenhar o token usando Pilmoji para suportar emojis # Usar baseline para alinhamento correto # Garantir que as coordenadas sejam inteiras # Aumentar o tamanho dos emojis (1.3x maior que o texto) pilmoji.text((int(current_x), int(current_y)), word, fill=(255, 255, 255, 255), font=font, anchor="la", emoji_scale_factor=1.2) # Converter para int após adicionar o comprimento do texto (textlength retorna float) current_x = int(current_x + draw.textlength(word, font=font)) current_y += line_height # Converter para bytes buffer = BytesIO() canvas.convert("RGB").save(buffer, format="PNG") buffer.seek(0) return buffer @router.get("/cover/title") def get_title_image( text: str = Query(..., description="Texto a ser exibido na imagem") ): """ Endpoint para gerar imagem de título com logo e texto. Args: text: Texto a ser exibido na imagem (obrigatório) Returns: Imagem PNG com logo e texto """ try: buffer = create_title_image(text) return StreamingResponse(buffer, media_type="image/png") except Exception as e: raise HTTPException(status_code=500, detail=f"Erro ao gerar imagem: {str(e)}")