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, List 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: começa escuro em cima e vai ficando transparente # linear-gradient(180deg, rgba(0, 0, 0, 0.7) 0%, rgba(0, 0, 0, 0.55) 45.06%, rgba(0, 0, 0, 0) 100%) gradient_start = 0 gradient_height = 706 for y in range(gradient_height): if y < height: ratio = y / gradient_height if ratio <= 0.4506: # 0% a 45.06%: de rgba(0,0,0,0.7) para rgba(0,0,0,0.55) opacity_ratio = ratio / 0.4506 opacity = int(255 * (0.7 - 0.15 * opacity_ratio)) else: # 45.06% a 100%: de rgba(0,0,0,0.55) para transparente opacity_ratio = (ratio - 0.4506) / (1 - 0.4506) opacity = int(255 * 0.55 * (1 - 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_text(draw: ImageDraw.Draw, text: str, x: int, y: int, max_width: int, max_lines: int = 5, align_top: bool = False, text_color: tuple = (255, 255, 255, 255)) -> None: """ Renderiza texto com fonte Montserrat Regular, suportando formatação com asteriscos para negrito. Ajusta automaticamente o tamanho da fonte para caber em max_lines. Args: draw: Objeto ImageDraw para desenhar text: Texto a ser renderizado (suporta *texto* para negrito) x: Posição X (esquerda) y: Posição Y (topo do texto se align_top=True, base se align_top=False) max_width: Largura máxima do texto max_lines: Número máximo de linhas (padrão: 5) align_top: Se True, alinha pelo topo; se False, alinha pela base text_color: Cor do texto como tuple (R, G, B, A) """ import re if not text.strip(): return # Carregar fontes try: regular_font_path = "fonts/Montserrat-Regular.ttf" bold_font_path = "fonts/Montserrat-Bold.ttf" except (OSError, IOError): regular_font_path = None bold_font_path = None # Função para processar texto com formatação def parse_formatted_text(text): """Processa texto e retorna lista de tokens (palavras/espaços/pontuação) com formatação""" tokens = [] current_token = "" in_bold = False i = 0 while i < len(text): if text[i] == '*': # Salvar token atual se não estiver vazio if current_token: tokens.append((current_token, in_bold)) current_token = "" # Alternar negrito in_bold = not in_bold i += 1 else: current_token += text[i] i += 1 # Adicionar último token if current_token: tokens.append((current_token, in_bold)) return tokens # Função para tokenizar respeitando espaços e pontuação def tokenize_with_formatting(tokens): """Quebra tokens em palavras individuais, espaços e pontuação mantendo formatação""" result = [] for token_text, is_bold in tokens: # Usar regex para dividir em palavras, espaços e pontuação parts = re.findall(r'\S+|\s+', token_text) for part in parts: result.append((part, is_bold)) return result # Processar texto com formatação formatted_tokens = parse_formatted_text(text) detailed_tokens = tokenize_with_formatting(formatted_tokens) # Função para quebrar texto em linhas considerando formatação def wrap_formatted_text(tokens, max_width, font_size): lines = [] current_line = [] # Carregar fontes com tamanho específico try: regular_font_size = ImageFont.truetype(regular_font_path, font_size) if regular_font_path else ImageFont.load_default() bold_font_size = ImageFont.truetype(bold_font_path, font_size) if bold_font_path else ImageFont.load_default() except (OSError, IOError): regular_font_size = ImageFont.load_default() bold_font_size = ImageFont.load_default() i = 0 while i < len(tokens): token_text, is_bold = tokens[i] # Se for espaço em branco, pular para próximo token if token_text.isspace(): i += 1 continue # Testar se o token cabe na linha atual test_line = current_line + [(token_text, is_bold)] # Calcular largura da linha de teste test_width = 0 for j, (seg_text, seg_is_bold) in enumerate(test_line): # Usar fonte com tamanho correto font_to_use = bold_font_size if seg_is_bold else regular_font_size bbox = font_to_use.getbbox(seg_text) test_width += bbox[2] - bbox[0] # Adicionar espaço entre palavras (apenas se não for pontuação) if j < len(test_line) - 1: next_text = test_line[j + 1][0] # Não adicionar espaço se o próximo token for pontuação if not re.match(r'^[^\w\s]+$', next_text): space_bbox = font_to_use.getbbox(" ") test_width += space_bbox[2] - space_bbox[0] if test_width <= max_width: current_line.append((token_text, is_bold)) else: # Quebrar linha if current_line: lines.append(current_line) current_line = [(token_text, is_bold)] else: # Token muito longo, adiciona mesmo assim lines.append([(token_text, is_bold)]) i += 1 if current_line: lines.append(current_line) return lines # Encontrar o tamanho de fonte ideal font_size = 32 # Tamanho inicial min_font_size = 12 # Tamanho mínimo # Tentar diferentes tamanhos de fonte até encontrar um que caiba em max_lines while font_size >= min_font_size: lines = wrap_formatted_text(detailed_tokens, max_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) # Carregar fontes finais try: final_regular_font = ImageFont.truetype(regular_font_path, font_size) if regular_font_path else ImageFont.load_default() final_bold_font = ImageFont.truetype(bold_font_path, font_size) if bold_font_path else ImageFont.load_default() except (OSError, IOError): final_regular_font = ImageFont.load_default() final_bold_font = ImageFont.load_default() # Quebrar texto final lines = wrap_formatted_text(detailed_tokens, max_width, font_size) # 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_tokens = [] for i, line in enumerate(lines): if i < max_lines - 1: combined_tokens.extend(line) elif i == max_lines - 1: # Última linha permitida - combinar com todas as linhas restantes remaining_tokens = [] for j in range(i, len(lines)): remaining_tokens.extend(lines[j]) combined_tokens.extend(remaining_tokens) break lines = wrap_formatted_text(combined_tokens, max_width, font_size) lines = lines[:max_lines] # Garantir que não exceda max_lines # Desenhar texto baseado no alinhamento if align_top: # Alinhamento pelo topo: desenhar de cima para baixo current_y = y for line in lines: # Calcular altura da linha (usar altura máxima dos segmentos) max_line_height = 0 for segment_text, is_bold in line: font_to_use = final_bold_font if is_bold else final_regular_font bbox = font_to_use.getbbox(segment_text) line_height = bbox[3] - bbox[1] max_line_height = max(max_line_height, line_height) # Desenhar segmentos da linha current_x = x for i, (segment_text, is_bold) in enumerate(line): font_to_use = final_bold_font if is_bold else final_regular_font draw.text((current_x, current_y), segment_text, fill=text_color, font=font_to_use) bbox = font_to_use.getbbox(segment_text) current_x += bbox[2] - bbox[0] # Adicionar espaço entre palavras (apenas se não for pontuação) if i < len(line) - 1: next_text = line[i + 1][0] # Não adicionar espaço se o próximo token for pontuação if not re.match(r'^[^\w\s]+$', next_text): space_bbox = font_to_use.getbbox(" ") current_x += space_bbox[2] - space_bbox[0] # Aplicar line height de 120% (adicionar 20% de espaçamento extra) current_y += int(max_line_height * 1.20) else: # Alinhamento pela base: desenhar de baixo para cima current_y = y for line in reversed(lines): # Calcular altura da linha (usar altura máxima dos segmentos) max_line_height = 0 for segment_text, is_bold in line: font_to_use = final_bold_font if is_bold else final_regular_font bbox = font_to_use.getbbox(segment_text) line_height = bbox[3] - bbox[1] max_line_height = max(max_line_height, line_height) # Aplicar line height de 120% (adicionar 20% de espaçamento extra) current_y -= int(max_line_height * 1.20) # Desenhar segmentos da linha current_x = x for i, (segment_text, is_bold) in enumerate(line): font_to_use = final_bold_font if is_bold else final_regular_font draw.text((current_x, current_y), segment_text, fill=text_color, font=font_to_use) bbox = font_to_use.getbbox(segment_text) current_x += bbox[2] - bbox[0] # Adicionar espaço entre palavras (apenas se não for pontuação) if i < len(line) - 1: next_text = line[i + 1][0] # Não adicionar espaço se o próximo token for pontuação if not re.match(r'^[^\w\s]+$', next_text): space_bbox = font_to_use.getbbox(" ") current_x += space_bbox[2] - space_bbox[0] def calculate_text_height(text: str, font, max_width: int) -> int: """Calcula a altura total do texto quebrado em linhas""" if not text.strip(): return 0 words = text.split() if not words: return 0 # Quebrar texto em linhas lines = [] current_line = [] for word in words: test_line = " ".join(current_line + [word]) bbox = font.getbbox(test_line) 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: lines.append(word) if current_line: lines.append(" ".join(current_line)) # Calcular altura total total_height = 0 for line in lines: bbox = font.getbbox(line) line_height = bbox[3] - bbox[1] total_height += int(line_height * 1.20) # Aplicar line height de 120% return total_height def render_slide_text(draw: ImageDraw.Draw, text: str, x: int, y: int, max_width: int, max_lines: int = 5, center_vertical: bool = False, text_color: tuple = (0, 0, 0, 255)) -> None: """ Renderiza texto para slides com fonte padrão, tamanho inicial 32px. Suporta formatação com asteriscos para negrito. Args: draw: Objeto ImageDraw para desenhar text: Texto a ser renderizado (suporta *texto* para negrito) x: Posição X (esquerda) y: Posição Y (topo do texto se center_vertical=False, centro se center_vertical=True) max_width: Largura máxima do texto max_lines: Número máximo de linhas (padrão: 5) center_vertical: Se True, centraliza verticalmente o texto na posição Y text_color: Cor do texto como tuple (R, G, B, A) """ import re if not text.strip(): return # Carregar fontes try: regular_font_path = "fonts/Montserrat-Regular.ttf" bold_font_path = "fonts/Montserrat-Bold.ttf" regular_font = ImageFont.truetype(regular_font_path, 32) bold_font = ImageFont.truetype(bold_font_path, 32) except (OSError, IOError): # Fallback para fonte padrão se as fontes personalizadas não forem encontradas regular_font = ImageFont.load_default() bold_font = ImageFont.load_default() # Função para processar texto com formatação def parse_formatted_text(text): """Processa texto e retorna lista de tokens (palavras/espaços/pontuação) com formatação""" tokens = [] current_token = "" in_bold = False current_font = regular_font i = 0 while i < len(text): if text[i] == '*': # Salvar token atual se não estiver vazio if current_token: tokens.append((current_token, current_font)) current_token = "" # Alternar negrito in_bold = not in_bold current_font = bold_font if in_bold else regular_font i += 1 else: current_token += text[i] i += 1 # Adicionar último token if current_token: tokens.append((current_token, current_font)) return tokens # Função para tokenizar respeitando espaços e pontuação def tokenize_with_formatting(tokens): """Quebra tokens em palavras individuais, espaços e pontuação mantendo formatação""" result = [] for token_text, font in tokens: # Usar regex para dividir em palavras, espaços e pontuação parts = re.findall(r'\S+|\s+', token_text) for part in parts: result.append((part, font)) return result # Processar texto com formatação formatted_tokens = parse_formatted_text(text) detailed_tokens = tokenize_with_formatting(formatted_tokens) # Função para quebrar texto em linhas considerando formatação def wrap_formatted_text(tokens, max_width, font_size): lines = [] current_line = [] # Carregar fontes com tamanho específico try: regular_font_size = ImageFont.truetype(regular_font_path, font_size) bold_font_size = ImageFont.truetype(bold_font_path, font_size) except (OSError, IOError): regular_font_size = ImageFont.load_default() bold_font_size = ImageFont.load_default() i = 0 while i < len(tokens): token_text, token_font = tokens[i] # Se for espaço em branco, pular para próximo token if token_text.isspace(): i += 1 continue # Testar se o token cabe na linha atual test_line = current_line + [(token_text, token_font)] # Calcular largura da linha de teste test_width = 0 for j, (seg_text, seg_font) in enumerate(test_line): # Usar fonte com tamanho correto font_to_use = regular_font_size if seg_font == regular_font else bold_font_size bbox = font_to_use.getbbox(seg_text) test_width += bbox[2] - bbox[0] # Adicionar espaço entre palavras (apenas se não for pontuação) if j < len(test_line) - 1: next_text = test_line[j + 1][0] # Não adicionar espaço se o próximo token for pontuação if not re.match(r'^[^\w\s]+$', next_text): space_bbox = font_to_use.getbbox(" ") test_width += space_bbox[2] - space_bbox[0] if test_width <= max_width: current_line.append((token_text, token_font)) else: # Quebrar linha if current_line: lines.append(current_line) current_line = [(token_text, token_font)] else: # Token muito longo, adiciona mesmo assim lines.append([(token_text, token_font)]) i += 1 if current_line: lines.append(current_line) return lines # Encontrar o tamanho de fonte ideal font_size = 32 # Tamanho inicial min_font_size = 12 # Tamanho mínimo # Tentar diferentes tamanhos de fonte até encontrar um que caiba em max_lines while font_size >= min_font_size: lines = wrap_formatted_text(detailed_tokens, max_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) # Carregar fontes finais try: final_regular_font = ImageFont.truetype(regular_font_path, font_size) final_bold_font = ImageFont.truetype(bold_font_path, font_size) except (OSError, IOError): final_regular_font = ImageFont.load_default() final_bold_font = ImageFont.load_default() # Quebrar texto final lines = wrap_formatted_text(detailed_tokens, max_width, font_size) # 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_tokens = [] for i, line in enumerate(lines): if i < max_lines - 1: combined_tokens.extend(line) elif i == max_lines - 1: # Última linha permitida - combinar com todas as linhas restantes remaining_tokens = [] for j in range(i, len(lines)): remaining_tokens.extend(lines[j]) combined_tokens.extend(remaining_tokens) break lines = wrap_formatted_text(combined_tokens, max_width, font_size) lines = lines[:max_lines] # Garantir que não exceda max_lines # Calcular altura total do texto para centralização vertical total_text_height = 0 line_heights = [] for line in lines: max_line_height = 0 for segment_text, segment_font in line: font_to_use = final_regular_font if segment_font == regular_font else final_bold_font bbox = font_to_use.getbbox(segment_text) line_height = bbox[3] - bbox[1] max_line_height = max(max_line_height, line_height) line_heights.append(max_line_height) total_text_height += int(max_line_height * 1.20) # Aplicar line height de 120% # Ajustar posição Y baseado na centralização if center_vertical: # Y representa o centro vertical do texto, então subtraímos metade da altura total start_y = y - (total_text_height // 2) else: # Y representa o topo do texto start_y = y # Desenhar texto de cima para baixo current_y = start_y for line_idx, line in enumerate(lines): # Calcular altura da linha (usar altura máxima dos segmentos) max_line_height = line_heights[line_idx] # Desenhar segmentos da linha current_x = x for i, (segment_text, segment_font) in enumerate(line): font_to_use = final_regular_font if segment_font == regular_font else final_bold_font draw.text((current_x, current_y), segment_text, fill=text_color, font=font_to_use) bbox = font_to_use.getbbox(segment_text) current_x += bbox[2] - bbox[0] # Adicionar espaço entre palavras (apenas se não for pontuação) if i < len(line) - 1: next_text = line[i + 1][0] # Não adicionar espaço se o próximo token for pontuação if not re.match(r'^[^\w\s]+$', next_text): space_bbox = font_to_use.getbbox(" ") current_x += space_bbox[2] - space_bbox[0] # Aplicar line height de 120% (adicionar 20% de espaçamento extra) current_y += int(max_line_height * 1.20) def get_slide_color(slide_color: int) -> tuple: """ Retorna a cor do slide baseada no número fornecido. Args: slide_color: Número da cor (1-8) Returns: Tuple (R, G, B, A) da cor """ color_map = { 1: (0, 0, 0, 255), # #000000 - Preto 2: (191, 198, 147, 255), # #BFC693 3: (246, 164, 29, 255), # #f6a41d 4: (245, 244, 239, 255), # #f5f4ef 5: (111, 180, 165, 255), # #6fb4a5 6: (25, 83, 208, 255), # #1953d0 7: (255, 80, 17, 255), # #ff5011 8: (255, 217, 194, 255), # #ffd9c2 } # Se o número for inválido, usar preto como padrão return color_map.get(slide_color, (0, 0, 0, 255)) def get_text_color(slide_color: int) -> tuple: """ Retorna a cor do texto baseada na cor do slide. Args: slide_color: Número da cor do slide (1-8) Returns: Tuple (R, G, B, A) da cor do texto """ # Cores que devem ter texto branco: 1 (preto), 6 (azul), e cores inválidas (padrão preto) if slide_color == 1 or slide_color == 6 or slide_color not in [1, 2, 3, 4, 5, 6, 7, 8]: return (255, 255, 255, 255) # Branco else: return (0, 0, 0, 255) # Preto def get_logo_path(slide_color: int) -> str: """ Retorna o caminho da logo baseado na cor do slide. Args: slide_color: Número da cor do slide (1-8) Returns: Caminho da logo """ # Cores que devem usar logo branca: 1 (preto), 6 (azul), e cores inválidas (padrão preto) if slide_color == 1 or slide_color == 6 or slide_color not in [1, 2, 3, 4, 5, 6, 7, 8]: return "recurve.png" # Logo branca else: return "recurveblack.png" # Logo preta def create_slide_canvas(image_url: str, slide: int, text: Optional[str] = None, color: int = 1, text_color: str = "white") -> BytesIO: """ Cria um canvas para slides com imagem cortada baseada no parâmetro slide. Args: image_url: URL da imagem de fundo slide: 1 para cortar início (880px), 2 para cortar final (400px) text: Texto opcional para exibir no slide color: Cor do slide (1-8) """ # Dimensões do poster canvas_width, canvas_height = 1080, 1350 # Obter cor do slide slide_bg_color = get_slide_color(color) # Criar canvas com cor do slide canvas = Image.new("RGBA", (canvas_width, canvas_height), color=slide_bg_color) # Baixar imagem original img = download_image_from_url(image_url) # PRIMEIRO: Usar resize_and_crop_to_fill para adaptar a imagem a 1280x780 (com zoom/crop centralizado) img_resized = resize_and_crop_to_fill(img, 1280, 780) # Configurações baseadas no slide if slide == 1: # Slide 1: Cortar os primeiros 880px (0 a 880) crop_left = 0 crop_right = 880 target_width = 880 target_height = 780 x_pos = 200 y_pos = 370 # Ajustado de 425 para 370 round_top_left = True round_bottom_left = True round_top_right = False round_bottom_right = False elif slide == 2: # Slide 2: Cortar os últimos 400px (880 a 1280) crop_left = 880 crop_right = 1280 target_width = 400 target_height = 780 x_pos = 0 y_pos = 370 # Ajustado de 425 para 370 round_top_left = False round_bottom_left = False round_top_right = True round_bottom_right = True else: raise HTTPException(status_code=400, detail="Parâmetro slide deve ser 1 ou 2") # Cortar a imagem (apenas crop, sem redimensionar) img_final = img_resized.crop((crop_left, 0, crop_right, 780)) # Criar máscara para bordas arredondadas mask = Image.new("L", (target_width, target_height), 0) draw_mask = ImageDraw.Draw(mask) # Desenhar retângulo base draw_mask.rectangle([(0, 0), (target_width, target_height)], fill=255) # Arredondar cantos específicos (25px de raio) radius = 25 if round_top_left: # Apagar o canto e redesenhar arredondado draw_mask.rectangle([(0, 0), (radius, radius)], fill=0) draw_mask.pieslice([(0, 0), (radius * 2, radius * 2)], 180, 270, fill=255) if round_bottom_left: draw_mask.rectangle([(0, target_height - radius), (radius, target_height)], fill=0) draw_mask.pieslice([(0, target_height - radius * 2), (radius * 2, target_height)], 90, 180, fill=255) if round_top_right: draw_mask.rectangle([(target_width - radius, 0), (target_width, radius)], fill=0) draw_mask.pieslice([(target_width - radius * 2, 0), (target_width, radius * 2)], 270, 360, fill=255) if round_bottom_right: draw_mask.rectangle([(target_width - radius, target_height - radius), (target_width, target_height)], fill=0) draw_mask.pieslice([(target_width - radius * 2, target_height - radius * 2), (target_width, target_height)], 0, 90, fill=255) # Aplicar máscara à imagem img_with_mask = Image.new("RGBA", (target_width, target_height)) img_with_mask.paste(img_final, (0, 0)) img_with_mask.putalpha(mask) # Colar imagem no canvas canvas.paste(img_with_mask, (x_pos, y_pos), img_with_mask) # Obter cor do texto baseada no parâmetro text_color if text_color.lower() == "black": text_color_tuple = (0, 0, 0, 255) # #000000 else: # white (padrão) text_color_tuple = (255, 255, 255, 255) # branco # Adicionar texto se slide for 1 e text for fornecido if slide == 1 and text and text.strip(): draw = ImageDraw.Draw(canvas) # Renderizar texto com as especificações: X: 200, Y: 155, L: 815, MAX 5 linhas render_slide_text(draw, text, x=200, y=155, max_width=815, max_lines=5, text_color=text_color_tuple) # Adicionar texto se slide for 2 e text for fornecido if slide == 2 and text and text.strip(): draw = ImageDraw.Draw(canvas) # Renderizar texto com as especificações: X: 445, Y: 370, L: 585, MAX 19 linhas # A imagem do slide 2 agora vai de Y: 370 a Y: 1150 (altura 780px) # Para centralizar o texto na área da imagem, o centro é Y: 370 + (780/2) = 760 render_slide_text(draw, text, x=445, y=760, max_width=585, max_lines=19, center_vertical=True, text_color=text_color_tuple) # Adicionar logo no canto inferior direito try: logo_path = get_logo_path(color) logo_width, logo_height = 121, 23 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)) # Posição: X: 880, Y: 1260 (canto inferior direito) canvas.paste(logo_with_opacity, (880, 1260), logo_with_opacity) except Exception as e: # Se não conseguir carregar a logo, continuar sem ela pass buffer = BytesIO() canvas.convert("RGB").save(buffer, format="PNG") buffer.seek(0) return buffer def create_quote_canvas(quote_text: str, role: Optional[str] = None, name: Optional[str] = None, color: int = 1, quote_background: bool = False, image_url: Optional[str] = None, text_color: str = "white") -> BytesIO: """ Cria um canvas para quotes com fundo colorido, imagem quote.png e texto centralizado. Args: quote_text: Texto da quote a ser exibido role: Cargo da pessoa que falou a quote name: Nome da pessoa que falou a quote color: Cor do fundo (1-8) quote_background: Se True, centraliza texto, role/name e símbolo da quote image_url: URL da imagem de fundo (usado quando quote_background=True) """ # Dimensões do poster canvas_width, canvas_height = 1080, 1350 # Criar canvas baseado no quote_background if quote_background and image_url: # Quando quote_background=True, usar image_url como fundo try: response = requests.get(image_url, timeout=10) response.raise_for_status() background_img = Image.open(BytesIO(response.content)).convert("RGBA") # Calcular proporções para fazer zoom centralizado img_width, img_height = background_img.size canvas_ratio = canvas_width / canvas_height img_ratio = img_width / img_height if img_ratio > canvas_ratio: # Imagem é mais larga - ajustar pela altura new_height = canvas_height new_width = int(canvas_height * img_ratio) else: # Imagem é mais alta - ajustar pela largura new_width = canvas_width new_height = int(canvas_width / img_ratio) # Redimensionar imagem mantendo proporção background_img = background_img.resize((new_width, new_height), Image.Resampling.LANCZOS) # Criar canvas e colar imagem centralizada canvas = Image.new("RGBA", (canvas_width, canvas_height), (0, 0, 0, 0)) x_offset = (canvas_width - new_width) // 2 y_offset = (canvas_height - new_height) // 2 canvas.paste(background_img, (x_offset, y_offset)) # Adicionar gradiente preto sobre a imagem gradient = Image.new("RGBA", (canvas_width, canvas_height), (0, 0, 0, 0)) gradient_draw = ImageDraw.Draw(gradient) # Criar gradiente linear de baixo para cima # 0% (bottom): #000000 100% opacidade # 25%: #000000 95% opacidade # 100% (top): #000000 0% opacidade for y in range(canvas_height): # Calcular posição relativa (0 = top, 1 = bottom) relative_y = (canvas_height - y) / canvas_height if relative_y <= 0.25: # De 0% a 25% (bottom para cima) # Transição de 100% a 95% opacidade alpha = int(255 * (1.0 - 0.05 * (relative_y / 0.25))) else: # De 25% a 100% (25% para topo) # Transição de 95% a 0% opacidade progress = (relative_y - 0.25) / 0.75 alpha = int(255 * 0.95 * (1 - progress)) # Desenhar linha horizontal com opacidade calculada gradient_draw.line([(0, y), (canvas_width, y)], fill=(0, 0, 0, alpha)) # Aplicar gradiente sobre a imagem canvas = Image.alpha_composite(canvas, gradient) except Exception as e: # Se falhar, usar cor padrão bg_color = get_slide_color(color) canvas = Image.new("RGBA", (canvas_width, canvas_height), color=bg_color) else: # Quando quote_background=False, usar cor de fundo baseada no parâmetro color bg_color = get_slide_color(color) canvas = Image.new("RGBA", (canvas_width, canvas_height), color=bg_color) draw = ImageDraw.Draw(canvas) # Configurações do texto if quote_background: # Largura maior quando quote_background=True text_width = 915 # Centralizar texto quando quote_background=True text_x = (canvas_width - text_width) // 2 # Centralizar horizontalmente # Posicionar embaixo quando quote_background=True text_y = 1160 # Base em Y: 1160 (60px mais baixo) else: # Largura padrão quando quote_background=False text_width = 720 # Alinhar à esquerda quando quote_background=False text_x = 180 text_y = canvas_height // 2 # Centralizar verticalmente # Obter cor do texto baseada no parâmetro text_color if text_color.lower() == "black": text_color_tuple = (0, 0, 0, 255) # #000000 else: # white (padrão) text_color_tuple = (255, 255, 255, 255) # branco # Adicionar imagem quote.png ou quoteblank.png acima do texto try: # Escolher imagem baseada no quote_background e text_color if quote_background: # Quando quote_background=True, sempre usar quotered.png quote_image_name = "quotered.png" else: # Quando quote_background=False, escolher baseado no text_color if text_color.lower() == "black": # Se text_color=black, usar quote.png (preta) quote_image_name = "quote.png" else: # Se text_color=white, usar quotered.png (vermelha) quote_image_name = "quotered.png" quote_img = Image.open(quote_image_name).convert("RGBA") quote_width, quote_height = 87, 67.46 # Redimensionar imagem para as dimensões especificadas quote_resized = quote_img.resize((int(quote_width), int(quote_height))) # Posição X baseada no parâmetro quote_background if quote_background: # Centralizar horizontalmente quando quote_background=True quote_x = (canvas_width - quote_width) // 2 else: # Alinhar à esquerda quando quote_background=False quote_x = 180 # Calcular altura do texto usando o tamanho de fonte real que será usado # Usar a mesma lógica do render_quote_text para garantir consistência try: if quote_background: regular_font_path = "fonts/Montserrat-SemiBold.ttf" else: regular_font_path = "fonts/Montserrat-Regular.ttf" except (OSError, IOError): regular_font_path = None def get_max_lines(font_size): if quote_background: # Quando quote_background=True, máximo de 3 linhas independente do tamanho return 3 else: # Comportamento original quando quote_background=False if font_size >= 65: return 8 elif font_size >= 55: return 9 elif font_size >= 45: return 10 elif font_size >= 35: return 11 elif font_size >= 30: return 12 elif font_size >= 25: return 13 elif font_size >= 20: return 14 elif font_size >= 16: return 14 else: return 14 def wrap_text(text, max_width, font_size): lines = [] current_line = [] try: font = ImageFont.truetype(regular_font_path, font_size) if regular_font_path else ImageFont.load_default() except (OSError, IOError): font = ImageFont.load_default() words = text.split() for word in words: test_line = " ".join(current_line + [word]) bbox = font.getbbox(test_line) 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: lines.append(word) if current_line: lines.append(" ".join(current_line)) return lines, font # Encontrar o tamanho de fonte ideal (mesma lógica do render_quote_text) if quote_background: font_size = 42 # Tamanho inicial quando quote_background=True else: font_size = 65 # Tamanho inicial quando quote_background=False min_font_size = 12 while font_size >= min_font_size: max_lines = get_max_lines(font_size) lines, font = wrap_text(quote_text, text_width, font_size) if len(lines) <= max_lines: break font_size -= 2 font_size = max(font_size, min_font_size) # Calcular altura total do texto com o tamanho de fonte real lines, font = wrap_text(quote_text, text_width, font_size) total_text_height = 0 for line in lines: bbox = font.getbbox(line) line_height = bbox[3] - bbox[1] total_text_height += int(line_height * 1.20) # Posicionar imagem baseado no quote_background if quote_background: # Quando quote_background=True, posicionar acima do texto (que começa em Y: 1160) quote_y = text_y - total_text_height - 80 - quote_height else: # Espaçamento normal quando quote_background=False quote_y = text_y - (total_text_height // 2) - 60 - (quote_height // 2) # Colar imagem no canvas canvas.paste(quote_resized, (int(quote_x), int(quote_y)), quote_resized) except Exception as e: # Se não conseguir carregar a imagem, continuar sem ela pass # Adicionar símbolo " no final do texto automaticamente quote_text_with_symbol = quote_text + '”' # Renderizar texto com as especificações solicitadas render_quote_text(draw, quote_text_with_symbol, text_x, text_y, text_width, text_color_tuple, quote_background) # Renderizar cargo e nome se fornecidos if role or name: render_author_info(draw, canvas, role, name, text_x, text_y, text_width, quote_text_with_symbol, text_color_tuple, quote_background) buffer = BytesIO() canvas.convert("RGB").save(buffer, format="PNG") buffer.seek(0) return buffer def render_quote_text(draw: ImageDraw.Draw, text: str, x: int, y: int, max_width: int, text_color: tuple = (0, 0, 0, 255), quote_background: bool = False) -> None: """ Renderiza texto para quotes com fonte Montserrat Regular, tamanho inicial 65px. Sistema dinâmico de linhas: 6 linhas em 65px, aumentando até 14 linhas conforme fonte diminui. Args: draw: Objeto ImageDraw para desenhar text: Texto a ser renderizado x: Posição X (esquerda) y: Posição Y (centro vertical do texto) max_width: Largura máxima do texto (720px) text_color: Cor do texto como tuple (R, G, B, A) quote_background: Se True, centraliza o texto """ if not text.strip(): return # Carregar fonte baseada no parâmetro quote_background try: if quote_background: regular_font_path = "fonts/Montserrat-SemiBold.ttf" else: regular_font_path = "fonts/Montserrat-Regular.ttf" except (OSError, IOError): regular_font_path = None # Função para calcular máximo de linhas baseado no tamanho da fonte def get_max_lines(font_size): """Calcula o máximo de linhas baseado no tamanho da fonte""" if quote_background: # Quando quote_background=True, máximo de 3 linhas independente do tamanho return 3 else: # Comportamento original quando quote_background=False if font_size >= 65: return 8 elif font_size >= 55: return 9 elif font_size >= 45: return 10 elif font_size >= 35: return 11 elif font_size >= 30: return 12 elif font_size >= 25: return 13 elif font_size >= 20: return 14 elif font_size >= 16: return 14 else: return 14 # Função para quebrar texto em linhas def wrap_text(text, max_width, font_size): lines = [] current_line = [] # Carregar fonte com tamanho específico try: font = ImageFont.truetype(regular_font_path, font_size) if regular_font_path else ImageFont.load_default() except (OSError, IOError): font = ImageFont.load_default() words = text.split() for word in words: test_line = " ".join(current_line + [word]) bbox = font.getbbox(test_line) 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: lines.append(word) if current_line: lines.append(" ".join(current_line)) return lines, font # Encontrar o tamanho de fonte ideal if quote_background: font_size = 42 # Tamanho inicial quando quote_background=True else: font_size = 65 # Tamanho inicial quando quote_background=False min_font_size = 12 # Tamanho mínimo # Tentar diferentes tamanhos de fonte até encontrar um que caiba no máximo de linhas while font_size >= min_font_size: max_lines = get_max_lines(font_size) lines, font = wrap_text(text, max_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) # Carregar fonte final try: final_font = ImageFont.truetype(regular_font_path, font_size) if regular_font_path else ImageFont.load_default() except (OSError, IOError): final_font = ImageFont.load_default() # Quebrar texto final lines, _ = wrap_text(text, max_width, font_size) # Se ainda não couber no máximo de linhas, forçar quebra max_lines = get_max_lines(font_size) 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, max_width, font_size) lines = lines[:max_lines] # Garantir que não exceda max_lines # Calcular altura total do texto para centralização vertical total_text_height = 0 line_heights = [] for line in lines: bbox = final_font.getbbox(line) line_height = bbox[3] - bbox[1] line_heights.append(line_height) total_text_height += int(line_height * 1.20) # Aplicar line height de 120% # Ajustar posição Y baseado no quote_background if quote_background: # Alinhar pela base quando quote_background=True start_y = y - total_text_height else: # Centralizar verticalmente quando quote_background=False start_y = y - (total_text_height // 2) # Desenhar texto de cima para baixo current_y = start_y for line_idx, line in enumerate(lines): line_height = line_heights[line_idx] # Calcular posição X da linha if quote_background: # Centralizar linha quando quote_background=True line_bbox = final_font.getbbox(line) line_width = line_bbox[2] - line_bbox[0] line_x = x + (max_width - line_width) // 2 else: # Alinhar à esquerda quando quote_background=False line_x = x # Desenhar linha draw.text((line_x, current_y), line, fill=text_color, font=final_font) # Aplicar line height de 120% (adicionar 20% de espaçamento extra) current_y += int(line_height * 1.20) def wrap_text_simple(text: str, font, max_width: int) -> List[str]: """ Quebra texto em linhas respeitando a largura máxima. Args: text: Texto a ser quebrado font: Fonte para calcular largura max_width: Largura máxima por linha Returns: Lista de linhas """ if not text.strip(): return [] words = text.split() if not words: return [] lines = [] current_line = [] for word in words: test_line = " ".join(current_line + [word]) bbox = font.getbbox(test_line) 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: lines.append(word) if current_line: lines.append(" ".join(current_line)) return lines def render_author_info(draw: ImageDraw.Draw, canvas: Image.Image, role: Optional[str], name: Optional[str], text_x: int, text_y: int, text_width: int, quote_text: str, text_color: tuple, quote_background: bool) -> None: """ Renderiza informações do autor (cargo e nome) abaixo do texto principal. Args: draw: Objeto ImageDraw para desenhar canvas: Canvas da imagem role: Cargo da pessoa name: Nome da pessoa text_x: Posição X do texto principal text_y: Posição Y do texto principal text_width: Largura do texto principal quote_text: Texto da quote text_color: Cor do texto principal (para determinar cor do role/name) quote_background: Se True, centraliza role/name """ if not role and not name: return # Carregar fonte Montserrat Bold try: bold_font_path = "fonts/Montserrat-Bold.ttf" font = ImageFont.truetype(bold_font_path, 32) except (OSError, IOError): font = ImageFont.load_default() # Determinar cores baseadas no text_color if text_color == (255, 255, 255, 255): # Se texto principal for branco # Cor do cargo: branco cargo_color = (255, 255, 255, 255) # Cor do nome: branco com 65% de opacidade nome_color = (255, 255, 255, int(255 * 0.65)) else: # Se texto principal for preto # Cor do cargo: 140F09 (RGB: 20, 15, 9) cargo_color = (20, 15, 9, 255) # Cor do nome: 140F09 com 65% de opacidade nome_color = (20, 15, 9, int(255 * 0.65)) # Calcular altura do texto principal para posicionar o autor # Usar a mesma lógica do render_quote_text para calcular a altura try: if quote_background: regular_font_path = "fonts/Montserrat-SemiBold.ttf" else: regular_font_path = "fonts/Montserrat-Regular.ttf" except (OSError, IOError): regular_font_path = None def get_max_lines(font_size): if quote_background: # Quando quote_background=True, máximo de 3 linhas independente do tamanho return 3 else: # Comportamento original quando quote_background=False if font_size >= 65: return 8 elif font_size >= 55: return 9 elif font_size >= 45: return 10 elif font_size >= 35: return 11 elif font_size >= 30: return 12 elif font_size >= 25: return 13 elif font_size >= 20: return 14 elif font_size >= 16: return 14 else: return 14 def wrap_text(text, max_width, font_size): lines = [] current_line = [] try: text_font = ImageFont.truetype(regular_font_path, font_size) if regular_font_path else ImageFont.load_default() except (OSError, IOError): text_font = ImageFont.load_default() words = text.split() for word in words: test_line = " ".join(current_line + [word]) bbox = text_font.getbbox(test_line) 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: lines.append(word) if current_line: lines.append(" ".join(current_line)) return lines, text_font # Encontrar o tamanho de fonte ideal do texto principal if quote_background: font_size = 42 # Tamanho inicial quando quote_background=True else: font_size = 65 # Tamanho inicial quando quote_background=False min_font_size = 12 while font_size >= min_font_size: max_lines = get_max_lines(font_size) lines, text_font = wrap_text(quote_text, text_width, font_size) if len(lines) <= max_lines: break font_size -= 2 font_size = max(font_size, min_font_size) # Calcular altura total do texto principal lines, text_font = wrap_text(quote_text, text_width, font_size) total_text_height = 0 for line in lines: bbox = text_font.getbbox(line) line_height = bbox[3] - bbox[1] total_text_height += int(line_height * 1.20) # Posicionar informações do autor baseado no quote_background if quote_background: # Quando quote_background=True, posicionar abaixo do texto (que termina em Y: 1160) author_y = text_y + 28 else: # Espaçamento normal quando quote_background=False author_y = text_y + (total_text_height // 2) + 28 # Renderizar cargo e nome com vírgula if role and role.strip() and name and name.strip(): # Renderizar cargo + vírgula com quebra de linha role_text = f"{role}," role_lines = wrap_text_simple(role_text, font, text_width) current_y = author_y for line in role_lines: if quote_background: # Centralizar cargo quando quote_background=True role_bbox = font.getbbox(line) role_width = role_bbox[2] - role_bbox[0] role_x = text_x + (text_width - role_width) // 2 else: # Alinhar à esquerda quando quote_background=False role_x = text_x draw.text((role_x, current_y), line, fill=cargo_color, font=font) bbox = font.getbbox(line) current_y += bbox[3] - bbox[1] + 5 # Adicionar pequeno espaçamento entre linhas author_y = current_y + 5 # Adicionar pequeno espaçamento entre cargo e nome # Renderizar nome com opacidade e quebra de linha name_lines = wrap_text_simple(name, font, text_width) for line in name_lines: # Criar uma imagem temporária para cada linha do nome com opacidade name_bbox = font.getbbox(line) name_width = name_bbox[2] - name_bbox[0] name_height = name_bbox[3] - name_bbox[1] # Adicionar padding para evitar corte padding = 10 name_img_width = name_width + (padding * 2) name_img_height = name_height + (padding * 2) # Criar imagem temporária para a linha do nome com padding name_img = Image.new("RGBA", (name_img_width, name_img_height), (0, 0, 0, 0)) name_draw = ImageDraw.Draw(name_img) name_draw.text((padding, padding), line, fill=nome_color, font=font) # Calcular posição X da linha do nome if quote_background: # Centralizar nome quando quote_background=True name_x = text_x + (text_width - name_width) // 2 - padding else: # Alinhar à esquerda quando quote_background=False name_x = text_x - padding # Colar a imagem da linha do nome no canvas principal (ajustar posição considerando o padding) canvas.paste(name_img, (name_x, author_y - padding), name_img) # Avançar para próxima linha author_y += name_height + 5 elif role and role.strip(): # Apenas cargo com quebra de linha role_lines = wrap_text_simple(role, font, text_width) current_y = author_y for line in role_lines: if quote_background: # Centralizar cargo quando quote_background=True role_bbox = font.getbbox(line) role_width = role_bbox[2] - role_bbox[0] role_x = text_x + (text_width - role_width) // 2 else: # Alinhar à esquerda quando quote_background=False role_x = text_x draw.text((role_x, current_y), line, fill=cargo_color, font=font) bbox = font.getbbox(line) current_y += bbox[3] - bbox[1] + 5 # Adicionar pequeno espaçamento entre linhas elif name and name.strip(): # Apenas nome com opacidade e quebra de linha name_lines = wrap_text_simple(name, font, text_width) current_y = author_y for line in name_lines: # Criar uma imagem temporária para cada linha do nome com opacidade name_bbox = font.getbbox(line) name_width = name_bbox[2] - name_bbox[0] name_height = name_bbox[3] - name_bbox[1] # Adicionar padding para evitar corte padding = 10 name_img_width = name_width + (padding * 2) name_img_height = name_height + (padding * 2) # Criar imagem temporária para a linha do nome com padding name_img = Image.new("RGBA", (name_img_width, name_img_height), (0, 0, 0, 0)) name_draw = ImageDraw.Draw(name_img) name_draw.text((padding, padding), line, fill=nome_color, font=font) # Calcular posição X da linha do nome if quote_background: # Centralizar nome quando quote_background=True name_x = text_x + (text_width - name_width) // 2 - padding else: # Alinhar à esquerda quando quote_background=False name_x = text_x - padding # Colar a imagem da linha do nome no canvas principal (ajustar posição considerando o padding) canvas.paste(name_img, (name_x, current_y - padding), name_img) # Avançar para próxima linha current_y += name_height + 5 def create_canvas(image_url: str, text: Optional[str] = None, text_position: str = "bottom", background: bool = False, color: int = 1, background_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 # Se background=True, forçar posições baseadas em background_position if background: if background_position == "top": text_position = "top" else: # bottom text_position = "bottom" # Obter cor do fundo bg_color = get_slide_color(color) # Criar canvas canvas = Image.new("RGBA", (width, height), color=bg_color) # Calcular altura do background se necessário background_height = 0 background_start_y = 0 if background: # Calcular altura necessária para o texto padding_bottom = height - 1260 # Espaço até a logo (90px) # Calcular altura do texto text_height = 0 if text and text.strip(): try: text_font_path = "fonts/Montserrat-Regular.ttf" text_font = ImageFont.truetype(text_font_path, 32) except (OSError, IOError): text_font = ImageFont.load_default() text_height = calculate_text_height(text, text_font, 924) # Calcular altura do conteúdo content_height = text_height if background_position == "top": # Background no topo: começar do topo e descer com base no conteúdo background_start_y = 0 background_height = content_height + 60 + 60 # 60px de padding superior + conteúdo + 60px de padding inferior # Garantir altura mínima if background_height < 200: background_height = 200 else: # bottom # Background embaixo: usar a mesma posição de referência do modo normal bottom_y = 1180 # Calcular de onde o background deve começar # Partir do bottom_y e subir com base no conteúdo background_start_y = bottom_y - content_height - 60 # 60px de padding superior # Altura total do background (do início até o fim da página) background_height = height - background_start_y # Garantir altura mínima if background_height < 200: background_start_y = height - 200 background_height = 200 # Calcular altura da imagem image_height = height - background_height if background else height # Adicionar imagem de fundo img = download_image_from_url(image_url) filled_img = resize_and_crop_to_fill(img, width, image_height) if background and background_position == "top": # Background no topo: colar imagem abaixo do background canvas.paste(filled_img, (0, background_height)) else: # Background embaixo ou sem background: colar imagem no topo canvas.paste(filled_img, (0, 0)) # Adicionar background colorido se necessário if background: # Criar retângulo com a cor selecionada background_rect = Image.new("RGBA", (width, background_height), color=bg_color) canvas.paste(background_rect, (0, background_start_y)) else: # Adicionar gradiente apenas se background=False e text_color não for black # Lógica inteligente para posicionamento do gradiente # Se text_color=black, não adicionar gradientes pretos if text_color.lower() != "black": # Se text_position=bottom, sempre gradiente embaixo (obrigatório) if text_position == "bottom": gradient_overlay = create_gradient_overlay(width, height, "bottom") canvas = Image.alpha_composite(canvas, gradient_overlay) # Se text_position=top elif text_position == "top": # Sempre gradiente em cima quando texto está no topo gradient_overlay = create_gradient_overlay(width, height, "top") canvas = Image.alpha_composite(canvas, gradient_overlay) # Adicionar logo na posição (X: 880, Y: 1260) try: # Se background_position for top, sempre usar logo branca if background and background_position == "top": logo_path = "recurve.png" # Logo branca sempre else: logo_path = get_logo_path(color) 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)) # 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}") draw = ImageDraw.Draw(canvas) # Definir posições baseadas nos parâmetros if background: if background_position == "top": # Background no topo: posicionar texto no topo top_y = 60 # Padding do topo text_y = top_y if text and text.strip() else None else: # bottom # Com background embaixo: usar mesma posição de referência do modo normal bottom_y = 1180 text_y = bottom_y if text and text.strip() else None else: # Posições de referência (comportamento original) top_y = 60 bottom_y = 1180 # Determinar posição do texto if text and text.strip(): if text_position == "top": text_y = top_y else: # bottom text_y = bottom_y else: text_y = None # Obter cor do texto baseada no parâmetro text_color if text_color.lower() == "black": text_color_tuple = (0, 0, 0, 255) # #000000 else: # white (padrão) text_color_tuple = (255, 255, 255, 255) # branco # Renderizar texto se fornecido if text and text.strip() and text_y is not None: # Alinhar pela base quando no bottom; pelo topo quando no top align_text_top = (text_position == "top") render_text(draw, text, x=78, y=text_y, max_width=924, max_lines=5, align_top=align_text_top, text_color=text_color_tuple) buffer = BytesIO() canvas.convert("RGB").save(buffer, format="PNG") buffer.seek(0) return buffer @router.get("/create/images") def get_image( image_url: str = Query(..., 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: 'top' ou 'bottom'"), background: bool = Query(False, description="Se True, adiciona background colorido e força posições baseadas em background_position"), background_position: str = Query("bottom", description="Posição do background: 'top' ou 'bottom'"), slide: Optional[int] = Query(None, description="Número do slide: 1 para início (880px), 2 para final (400px)"), color: Optional[int] = Query(1, description="Cor do fundo: 1=#000000, 2=#BFC693, 3=#f6a41d, 4=#f5f4ef, 5=#6fb4a5, 6=#1953d0, 7=#ff5011, 8=#ffd9c2"), quote: Optional[str] = Query(None, description="Texto da quote para gerar poster com fundo colorido"), role: Optional[str] = Query(None, description="Position/role of the person who said the quote"), name: Optional[str] = Query(None, description="Name of the person who said the quote"), quote_background: bool = Query(False, description="Se True, centraliza texto, role/name e símbolo da quote"), text_color: str = Query("white", description="Cor do texto: 'white' ou 'black'") ): try: # Se o parâmetro quote for fornecido, usar a funcionalidade de quote if quote is not None and quote.strip(): buffer = create_quote_canvas(quote, role, name, color, quote_background, image_url, text_color) # Se o parâmetro slide for fornecido, usar a funcionalidade de slides elif slide is not None: buffer = create_slide_canvas(image_url, slide, text, color, text_color) else: # Usar funcionalidade normal de criação de imagem buffer = create_canvas(image_url, text, text_position, background, color, background_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)}")