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, Union 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: img_ratio = img.width / img.height target_ratio = target_width / target_height if img_ratio > target_ratio: scale_height = target_height scale_width = int(scale_height * img_ratio) else: scale_width = target_width scale_height = int(scale_width / img_ratio) img_resized = img.resize((scale_width, scale_height), Image.LANCZOS) 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_collage_background(image_urls: List[str], canvas_width: int, canvas_height: int, device: str) -> Image.Image: """Cria uma colagem como fundo baseada na lista de URLs""" num_images = len(image_urls) border_size = 4 if num_images > 1 else 0 # Linha mais fina e elegante is_web = device.lower() == "web" images = [download_image_from_url(url) for url in image_urls] canvas = Image.new("RGBA", (canvas_width, canvas_height), (255, 255, 255, 255)) if num_images == 1: img = resize_and_crop_to_fill(images[0], canvas_width, canvas_height) canvas.paste(img, (0, 0)) elif num_images == 2: # Ambos dispositivos: lado a lado slot_width = (canvas_width - border_size) // 2 img1 = resize_and_crop_to_fill(images[0], slot_width, canvas_height) img2 = resize_and_crop_to_fill(images[1], slot_width, canvas_height) canvas.paste(img1, (0, 0)) canvas.paste(img2, (slot_width + border_size, 0)) elif num_images == 3: if is_web: # Web: 1 grande à esquerda, 2 pequenas empilhadas à direita left_width = (canvas_width - border_size) * 2 // 3 right_width = canvas_width - left_width - border_size half_height = (canvas_height - border_size) // 2 img1 = resize_and_crop_to_fill(images[0], left_width, canvas_height) img2 = resize_and_crop_to_fill(images[1], right_width, half_height) img3 = resize_and_crop_to_fill(images[2], right_width, half_height) canvas.paste(img1, (0, 0)) canvas.paste(img2, (left_width + border_size, 0)) canvas.paste(img3, (left_width + border_size, half_height + border_size)) else: # IG: layout original half_height = (canvas_height - border_size) // 2 half_width = (canvas_width - border_size) // 2 img1 = resize_and_crop_to_fill(images[0], half_width, half_height) img2 = resize_and_crop_to_fill(images[1], half_width, half_height) img3 = resize_and_crop_to_fill(images[2], canvas_width, half_height) canvas.paste(img1, (0, 0)) canvas.paste(img2, (half_width + border_size, 0)) canvas.paste(img3, (0, half_height + border_size)) elif num_images == 4: if is_web: # Web: 4 imagens lado a lado horizontalmente slot_width = (canvas_width - 3 * border_size) // 4 for i in range(4): img = resize_and_crop_to_fill(images[i], slot_width, canvas_height) x_pos = i * (slot_width + border_size) canvas.paste(img, (x_pos, 0)) else: # IG: Layout 2x2 half_height = (canvas_height - border_size) // 2 half_width = (canvas_width - border_size) // 2 img1 = resize_and_crop_to_fill(images[0], half_width, half_height) img2 = resize_and_crop_to_fill(images[1], half_width, half_height) img3 = resize_and_crop_to_fill(images[2], half_width, half_height) img4 = resize_and_crop_to_fill(images[3], half_width, half_height) canvas.paste(img1, (0, 0)) canvas.paste(img2, (half_width + border_size, 0)) canvas.paste(img3, (0, half_height + border_size)) canvas.paste(img4, (half_width + border_size, half_height + border_size)) elif num_images == 5: if is_web: # Web: 3 em cima, 2 embaixo top_height = (canvas_height - border_size) * 3 // 5 bottom_height = canvas_height - top_height - border_size available_width = canvas_width - 2 * border_size third_width = available_width // 3 third_width_last = canvas_width - (third_width * 2 + border_size * 2) half_width = (canvas_width - border_size) // 2 # 3 imagens em cima img1 = resize_and_crop_to_fill(images[0], third_width, top_height) img2 = resize_and_crop_to_fill(images[1], third_width, top_height) img3 = resize_and_crop_to_fill(images[2], third_width_last, top_height) canvas.paste(img1, (0, 0)) canvas.paste(img2, (third_width + border_size, 0)) canvas.paste(img3, (third_width * 2 + border_size * 2, 0)) # 2 imagens embaixo y_offset = top_height + border_size img4 = resize_and_crop_to_fill(images[3], half_width, bottom_height) img5 = resize_and_crop_to_fill(images[4], half_width, bottom_height) canvas.paste(img4, (0, y_offset)) canvas.paste(img5, (half_width + border_size, y_offset)) else: # IG: layout original top_height = (canvas_height - border_size) * 2 // 5 bottom_height = canvas_height - top_height - border_size half_width = (canvas_width - border_size) // 2 img1 = resize_and_crop_to_fill(images[0], half_width, top_height) img2 = resize_and_crop_to_fill(images[1], half_width, top_height) canvas.paste(img1, (0, 0)) canvas.paste(img2, (half_width + border_size, 0)) y_offset = top_height + border_size third_width = (canvas_width - 2 * border_size) // 3 third_width_last = canvas_width - (third_width * 2 + border_size * 2) img3 = resize_and_crop_to_fill(images[2], third_width, bottom_height) img4 = resize_and_crop_to_fill(images[3], third_width, bottom_height) img5 = resize_and_crop_to_fill(images[4], third_width_last, bottom_height) canvas.paste(img3, (0, y_offset)) canvas.paste(img4, (third_width + border_size, y_offset)) canvas.paste(img5, (third_width * 2 + border_size * 2, y_offset)) elif num_images == 6: if is_web: # Web: 3x2 (3 colunas, 2 linhas) half_height = (canvas_height - border_size) // 2 available_width = canvas_width - 2 * border_size third_width = available_width // 3 third_width_last = canvas_width - (third_width * 2 + border_size * 2) # Primeira linha img1 = resize_and_crop_to_fill(images[0], third_width, half_height) img2 = resize_and_crop_to_fill(images[1], third_width, half_height) img3 = resize_and_crop_to_fill(images[2], third_width_last, half_height) canvas.paste(img1, (0, 0)) canvas.paste(img2, (third_width + border_size, 0)) canvas.paste(img3, (third_width * 2 + border_size * 2, 0)) # Segunda linha y_offset = half_height + border_size img4 = resize_and_crop_to_fill(images[3], third_width, half_height) img5 = resize_and_crop_to_fill(images[4], third_width, half_height) img6 = resize_and_crop_to_fill(images[5], third_width_last, half_height) canvas.paste(img4, (0, y_offset)) canvas.paste(img5, (third_width + border_size, y_offset)) canvas.paste(img6, (third_width * 2 + border_size * 2, y_offset)) else: # IG: layout original (3x2) half_height = (canvas_height - border_size) // 2 third_width = (canvas_width - 2 * border_size) // 3 third_width_last = canvas_width - (third_width * 2 + border_size * 2) # Primeira linha img1 = resize_and_crop_to_fill(images[0], third_width, half_height) img2 = resize_and_crop_to_fill(images[1], third_width, half_height) img3 = resize_and_crop_to_fill(images[2], third_width, half_height) canvas.paste(img1, (0, 0)) canvas.paste(img2, (third_width + border_size, 0)) canvas.paste(img3, (third_width * 2 + border_size * 2, 0)) # Segunda linha y_offset = half_height + border_size img4 = resize_and_crop_to_fill(images[3], third_width, half_height) img5 = resize_and_crop_to_fill(images[4], third_width, half_height) img6 = resize_and_crop_to_fill(images[5], third_width_last, half_height) canvas.paste(img4, (0, y_offset)) canvas.paste(img5, (third_width + border_size, y_offset)) canvas.paste(img6, (third_width * 2 + border_size * 2, y_offset)) else: raise HTTPException(status_code=400, detail="Apenas até 6 imagens são suportadas.") return canvas def parse_image_urls(image_url_param: Union[str, List[str]]) -> List[str]: """Converte o parâmetro de URL(s) em lista de URLs""" if isinstance(image_url_param, list): return image_url_param elif isinstance(image_url_param, str): # Se contém vírgulas, divide em múltiplas URLs if ',' in image_url_param: return [url.strip() for url in image_url_param.split(',') if url.strip()] else: return [image_url_param] return [] def create_gradient_overlay(width: int, height: int, text_position: str = "bottom") -> Image.Image: """ Cria gradiente overlay baseado na posição do texto """ gradient = Image.new("RGBA", (width, height)) draw = ImageDraw.Draw(gradient) if text_position.lower() == "bottom": # Gradiente para texto embaixo: posição Y:531, altura 835px gradient_start = 531 gradient_height = 835 for y in range(gradient_height): if y + gradient_start < height: # Gradient: 0% transparent -> 46.63% rgba(0,0,0,0.55) -> 100% rgba(0,0,0,0.7) ratio = y / gradient_height if ratio <= 0.4663: # 0% a 46.63%: de transparente para 0.55 opacity_ratio = ratio / 0.4663 opacity = int(255 * 0.55 * opacity_ratio) else: # 46.63% a 100%: de 0.55 para 0.7 opacity_ratio = (ratio - 0.4663) / (1 - 0.4663) opacity = int(255 * (0.55 + (0.7 - 0.55) * opacity_ratio)) draw.line([(0, y + gradient_start), (width, y + gradient_start)], fill=(0, 0, 0, opacity)) else: # text_position == "top" # Gradiente para texto no topo: posição Y:0, altura 835px # linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.307045) 16.93%, rgba(0,0,0,0.55) 45.57%, rgba(0,0,0,0.7) 100%) # 0deg significa: 0% = bottom, 100% = top gradient_height = 835 for y in range(gradient_height): if y < height: # Inverter a ratio: y=0 (topo) deve ser 100% do gradient, y=835 (bottom) deve ser 0% ratio = (gradient_height - y) / gradient_height if ratio <= 0.1693: # 0% a 16.93%: de 0 (transparente) para 0.307 opacity_ratio = ratio / 0.1693 opacity = int(255 * (0.307 * opacity_ratio)) elif ratio <= 0.4557: # 16.93% a 45.57%: de 0.307 para 0.55 opacity_ratio = (ratio - 0.1693) / (0.4557 - 0.1693) opacity = int(255 * (0.307 + (0.55 - 0.307) * opacity_ratio)) else: # 45.57% a 100%: de 0.55 para 0.7 opacity_ratio = (ratio - 0.4557) / (1 - 0.4557) opacity = int(255 * (0.55 + (0.7 - 0.55) * opacity_ratio)) draw.line([(0, y), (width, y)], fill=(0, 0, 0, opacity)) return gradient def wrap_text(text: str, font: ImageFont.FreeTypeFont, max_width: int, draw: ImageDraw.Draw) -> list[str]: words = text.split() lines = [] current_line = "" for word in words: test_line = f"{current_line} {word}".strip() if draw.textlength(test_line, font=font) <= max_width: current_line = test_line else: if current_line: lines.append(current_line) current_line = word if current_line: lines.append(current_line) return lines def get_responsive_font_and_lines(text: str, font_path: str, max_width: int, max_lines: int = 3, max_font_size: int = 80, min_font_size: int = 20) -> tuple[ImageFont.FreeTypeFont, list[str], int]: """ Retorna a fonte e linhas ajustadas para caber no número máximo de linhas. """ temp_img = Image.new("RGB", (1, 1)) temp_draw = ImageDraw.Draw(temp_img) current_font_size = max_font_size while current_font_size >= min_font_size: try: font = ImageFont.truetype(font_path, current_font_size) except Exception: font = ImageFont.load_default() lines = wrap_text(text, font, max_width, temp_draw) if len(lines) <= max_lines: return font, lines, current_font_size current_font_size -= 1 try: font = ImageFont.truetype(font_path, min_font_size) except Exception: font = ImageFont.load_default() lines = wrap_text(text, font, max_width, temp_draw) return font, lines, min_font_size def get_font_and_lines(text: str, font_path: str, font_size: int, max_width: int) -> tuple[ImageFont.FreeTypeFont, list[str]]: """ Retorna a fonte e linhas com tamanho fixo de fonte. """ try: font = ImageFont.truetype(font_path, font_size) except Exception: font = ImageFont.load_default() temp_img = Image.new("RGB", (1, 1)) temp_draw = ImageDraw.Draw(temp_img) lines = wrap_text(text, font, max_width, temp_draw) return font, lines def get_text_color_rgb(text_color: str) -> tuple[int, int, int]: """ Converte o parâmetro text_color para RGB. """ if text_color.lower() == "black": return (0, 0, 0) else: # white por padrão return (255, 255, 255) def get_device_dimensions(device: str) -> tuple[int, int]: """Retorna as dimensões baseadas no dispositivo""" if device.lower() == "web": return (1280, 720) else: # Instagram por padrão return (1080, 1350) def create_canvas(image_url: Optional[Union[str, List[str]]], headline: Optional[str], device: str = "ig", text_position: str = "bottom", text_color: str = "white") -> BytesIO: width, height = get_device_dimensions(device) is_web = device.lower() == "web" text_rgb = get_text_color_rgb(text_color) # Configurações específicas por dispositivo if is_web: padding_x = 40 logo_width, logo_height = 120, 22 logo_padding = 40 else: padding_x = 60 bottom_padding = 80 top_padding = 60 logo_width, logo_height = 121, 23 # Novas dimensões: L:121, A:22.75 (arredondado para 23) max_width = width - 2 * padding_x canvas = Image.new("RGBA", (width, height), color=(255, 255, 255, 255)) # Adicionar imagem(s) de fundo se fornecida(s) if image_url: parsed_urls = parse_image_urls(image_url) if parsed_urls: if len(parsed_urls) > 6: raise HTTPException(status_code=400, detail="Máximo de 6 imagens permitidas") if len(parsed_urls) == 1: # Uma única imagem - comportamento original img = download_image_from_url(parsed_urls[0]) filled_img = resize_and_crop_to_fill(img, width, height) canvas.paste(filled_img, (0, 0)) else: # Múltiplas imagens - criar colagem canvas = create_collage_background(parsed_urls, width, height, device) # Para Instagram: adicionar gradiente e texto if not is_web: # Só aplicar gradiente se o texto for branco if text_color.lower() != "black": gradient_overlay = create_gradient_overlay(width, height, text_position) canvas = Image.alpha_composite(canvas, gradient_overlay) if headline: draw = ImageDraw.Draw(canvas) font_path = "fonts/AGaramondPro-Semibold.ttf" line_height_factor = 1.05 # 105% da altura da linha try: font, lines, font_size = get_responsive_font_and_lines( headline, font_path, max_width, max_lines=3, max_font_size=80, min_font_size=20 ) line_height = int(font_size * line_height_factor) except Exception as e: raise HTTPException(status_code=500, detail=f"Erro ao processar a fonte: {e}") total_text_height = len(lines) * line_height # Posicionar texto baseado no parâmetro text_position if text_position.lower() == "bottom": # Posicionar texto 50px acima da logo (que está em Y:1274) text_end_y = 1274 - 50 start_y = text_end_y - total_text_height else: # text_position == "top" # Posicionar texto no topo com padding start_y = top_padding # Adicionar logo no canto inferior direito (posição fixa) 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)) # Posição fixa: X:891, Y:1274 canvas.paste(logo_with_opacity, (891, 1274), logo_with_opacity) except Exception as e: raise HTTPException(status_code=500, detail=f"Erro ao carregar a logo: {e}") # Adiciona texto com a cor especificada for i, line in enumerate(lines): y = start_y + i * line_height draw.text((padding_x, y), line, font=font, fill=text_rgb) # Para web: apenas logo no canto inferior direito else: pass 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[Union[str, List[str]]] = Query(None, description="URL da imagem ou lista de URLs separadas por vírgula para colagem (máximo 6)"), headline: Optional[str] = Query(None, description="Texto do título (opcional para IG, ignorado para web)"), device: str = Query("ig", description="Dispositivo: 'ig' para Instagram (1080x1350) ou 'web' para Web (1280x720)"), text_position: str = Query("bottom", description="Posição do texto: 'top' para topo ou 'bottom' para parte inferior"), text_color: str = Query("white", description="Cor do texto: 'white' (padrão) ou 'black'. Se 'black', remove o gradiente de fundo") ): try: buffer = create_canvas(image_url, headline, device, 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)}")