from flask import Flask, request, send_file from PIL import Image, ImageDraw, ImageFont, ImageFilter import textwrap import io import os import math from datetime import datetime app = Flask(__name__) class TelegramChatGenerator: def __init__(self): # Custom bubble colors self.bubble_color = (31, 39, 69) # #1f2745 self.bubble_shadow = (20, 25, 45) # Darker shadow self.reply_bubble_color = (25, 31, 55) # Darker for contrast self.reply_line_color = (70, 130, 255) # Blue accent line self.text_color = (255, 255, 255) self.reply_text_color = (160, 170, 185) self.username_color = (255, 255, 255) self.time_color = (160, 170, 185) # Load fonts with fallbacks self.load_fonts() # Standard square size self.image_size = 800 def load_fonts(self): """Load fonts with multiple fallbacks""" font_paths = [ "Roboto-Regular.ttf", "Arial.ttf", "Arial.ttf", "arial.ttf", "DejaVuSans.ttf" ] try: # Try to load a modern font for path in font_paths: if os.path.exists(path): self.font = ImageFont.truetype(path, 22) self.username_font = ImageFont.truetype(path, 18) self.reply_font = ImageFont.truetype(path, 16) self.time_font = ImageFont.truetype(path, 14) return # Fallback to default self.font = ImageFont.load_default() self.username_font = ImageFont.load_default() self.reply_font = ImageFont.load_default() self.time_font = ImageFont.load_default() except: self.font = ImageFont.load_default() self.username_font = ImageFont.load_default() self.reply_font = ImageFont.load_default() self.time_font = ImageFont.load_default() def wrap_text(self, text, font, max_width): """Smart text wrapping based on pixel width""" words = text.split() lines = [] current_line = [] for word in words: test_line = ' '.join(current_line + [word]) bbox = font.getbbox(test_line) width = bbox[2] - bbox[0] if width <= max_width: current_line.append(word) else: if current_line: lines.append(' '.join(current_line)) current_line = [word] else: # Word is too long, force break lines.append(word) if current_line: lines.append(' '.join(current_line)) return '\n'.join(lines) def draw_telegram_bubble(self, draw, coords, radius, fill, shadow_offset=6): """Draw Telegram-style bubble with enhanced shadow""" x1, y1, x2, y2 = coords # Ensure coordinates are valid if x2 <= x1 or y2 <= y1: return # Ensure radius doesn't exceed bubble dimensions max_radius = min((x2 - x1) // 2, (y2 - y1) // 2, radius) if max_radius < 1: max_radius = 1 # Draw simple shadow first if shadow_offset > 0: shadow_coords = (x1 + shadow_offset, y1 + shadow_offset, x2 + shadow_offset, y2 + shadow_offset) shadow_color = (0, 0, 0, 60) # Semi-transparent black self.draw_rounded_rectangle(draw, shadow_coords, max_radius, shadow_color) # Draw main bubble self.draw_rounded_rectangle(draw, coords, max_radius, fill) # Add subtle inner highlight if y2 - y1 > 6: highlight_coords = (x1 + 1, y1 + 1, x2 - 1, y1 + 3) lighter_fill = tuple(min(255, c + 15) for c in fill) self.draw_rounded_rectangle(draw, highlight_coords, max_radius - 1, lighter_fill) def draw_rounded_rectangle(self, draw, coords, radius, fill): """Draw a perfect rounded rectangle with bounds checking""" x1, y1, x2, y2 = coords # Ensure coordinates are valid if x2 <= x1 or y2 <= y1: return # Limit radius to prevent invalid coordinates width = x2 - x1 height = y2 - y1 max_radius = min(width // 2, height // 2, radius) if max_radius < 1: # If radius is too small, just draw a regular rectangle draw.rectangle([x1, y1, x2, y2], fill=fill) return # Handle both RGB and RGBA fills if len(fill) == 3: # RGB - add full opacity fill_color = fill + (255,) else: # RGBA - use as is fill_color = fill # Main rectangles (only if they have positive dimensions) if width > 2 * max_radius: draw.rectangle([x1 + max_radius, y1, x2 - max_radius, y2], fill=fill_color) if height > 2 * max_radius: draw.rectangle([x1, y1 + max_radius, x2, y2 - max_radius], fill=fill_color) # Corner circles (only if there's space for them) if width >= 2 * max_radius and height >= 2 * max_radius: # Top-left draw.ellipse([x1, y1, x1 + 2 * max_radius, y1 + 2 * max_radius], fill=fill_color) # Top-right draw.ellipse([x2 - 2 * max_radius, y1, x2, y1 + 2 * max_radius], fill=fill_color) # Bottom-left draw.ellipse([x1, y2 - 2 * max_radius, x1 + 2 * max_radius, y2], fill=fill_color) # Bottom-right draw.ellipse([x2 - 2 * max_radius, y2 - 2 * max_radius, x2, y2], fill=fill_color) def get_text_dimensions(self, text, font): """Get accurate text dimensions""" lines = text.split('\n') max_width = 0 total_height = 0 for i, line in enumerate(lines): bbox = font.getbbox(line) width = bbox[2] - bbox[0] height = bbox[3] - bbox[1] max_width = max(max_width, width) total_height += height # Add line spacing except for last line if i < len(lines) - 1: total_height += 6 return max_width, total_height def add_subtle_pattern(self, img): """Add a subtle dot pattern to the background""" draw = ImageDraw.Draw(img) width, height = img.size # Create subtle dot pattern for x in range(0, width, 40): for y in range(0, height, 40): # Vary opacity based on position opacity = max(5, min(15, abs(x - width//2) // 20 + abs(y - height//2) // 20)) color = (255, 255, 255, opacity) draw.ellipse([x, y, x + 2, y + 2], fill=color) def generate_chat_bubble(self, message, username, reply_to=None, replied_username=None): # Calculate bubble dimensions first to determine canvas size padding = 40 bubble_padding = 25 # Estimate text dimensions wrapped_message = self.wrap_text(message, self.font, 500) # Max width estimate msg_width, msg_height = self.get_text_dimensions(wrapped_message, self.font) username_width, username_height = self.get_text_dimensions(username, self.username_font) # Handle reply dimensions reply_height = 0 wrapped_reply = "" if reply_to and replied_username: truncated_reply = reply_to[:100] + "..." if len(reply_to) > 100 else reply_to wrapped_reply = self.wrap_text(truncated_reply, self.reply_font, 450) reply_username_w, reply_username_h = self.get_text_dimensions(replied_username, self.reply_font) reply_msg_w, reply_msg_h = self.get_text_dimensions(wrapped_reply, self.reply_font) reply_height = reply_username_h + reply_msg_h + 25 # Calculate bubble dimensions bubble_width = max(300, min(600, msg_width + bubble_padding * 2 + 50)) bubble_height = max(100, username_height + msg_height + bubble_padding * 2 + 50) if reply_to: bubble_height += reply_height + 15 # Create canvas with transparent background, sized to fit bubble + padding canvas_width = bubble_width + padding * 2 canvas_height = bubble_height + padding * 2 # Create image with transparent background img = Image.new('RGBA', (canvas_width, canvas_height), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) # Position bubble in center of canvas bubble_x = padding bubble_y = padding # Draw main bubble with enhanced shadow self.draw_telegram_bubble( draw, [bubble_x, bubble_y, bubble_x + bubble_width, bubble_y + bubble_height], radius=18, fill=self.bubble_color, shadow_offset=6 ) # Current position for content content_y = bubble_y + bubble_padding # Draw reply section if present if reply_to and replied_username and bubble_width > 60: reply_y = content_y reply_bubble_height = max(30, reply_height + 10) # Reply background reply_bubble_x1 = bubble_x + 15 reply_bubble_x2 = bubble_x + bubble_width - 15 reply_bubble_y1 = reply_y reply_bubble_y2 = reply_y + reply_bubble_height if reply_bubble_x2 > reply_bubble_x1 and reply_bubble_y2 > reply_bubble_y1: self.draw_rounded_rectangle( draw, [reply_bubble_x1, reply_bubble_y1, reply_bubble_x2, reply_bubble_y2], radius=12, fill=self.reply_bubble_color ) # Blue accent line (thicker and more prominent) if reply_bubble_height > 10: draw.rectangle([bubble_x + 18, reply_y + 8, bubble_x + 22, reply_y + reply_bubble_height - 8], fill=self.reply_line_color) # Reply content reply_content_x = bubble_x + 30 reply_content_y = reply_y + 12 # Reply username if reply_content_x < bubble_x + bubble_width - 20: draw.text((reply_content_x, reply_content_y), replied_username, fill=self.reply_line_color, font=self.reply_font) reply_content_y += 22 # Reply message for line in wrapped_reply.split('\n'): if reply_content_y < bubble_y + bubble_height - 30: draw.text((reply_content_x, reply_content_y), line, fill=self.reply_text_color, font=self.reply_font) reply_content_y += 18 content_y += reply_bubble_height + 15 # Main message username if content_y < bubble_y + bubble_height - 50: draw.text((bubble_x + bubble_padding, content_y), username, fill=self.username_color, font=self.username_font) content_y += username_height + 12 # Main message text with better spacing for line in wrapped_message.split('\n'): if content_y < bubble_y + bubble_height - 40: draw.text((bubble_x + bubble_padding, content_y), line, fill=self.text_color, font=self.font) content_y += 30 # Add timestamp in bottom right current_time = datetime.now().strftime("%H:%M") time_bbox = self.time_font.getbbox(current_time) time_width = time_bbox[2] - time_bbox[0] timestamp_x = bubble_x + bubble_width - time_width - bubble_padding - 30 timestamp_y = bubble_y + bubble_height - 28 if timestamp_x > bubble_x and timestamp_y > bubble_y: draw.text((timestamp_x, timestamp_y), current_time, fill=self.time_color, font=self.time_font) # Add double checkmarks check_x = bubble_x + bubble_width - 25 check_y = bubble_y + bubble_height - 25 if check_x > bubble_x and check_y > bubble_y: self.draw_checkmarks(draw, check_x, check_y) return img def draw_checkmarks(self, draw, x, y): """Draw double checkmarks for message status""" # First checkmark points1 = [(x-8, y), (x-5, y+3), (x-2, y-2)] self.draw_polyline(draw, points1, self.time_color, width=2) # Second checkmark (overlapping) points2 = [(x-5, y), (x-2, y+3), (x+2, y-2)] self.draw_polyline(draw, points2, self.time_color, width=2) def draw_polyline(self, draw, points, fill, width=1): """Draw a polyline (series of connected lines)""" for i in range(len(points) - 1): draw.line([points[i], points[i + 1]], fill=fill, width=width) # Initialize generator chat_gen = TelegramChatGenerator() @app.route('/chat') def generate_chat(): # Get parameters message = request.args.get('message', 'Hello World! đ') username = request.args.get('username', 'User') reply_to = request.args.get('replyto') replied_username = request.args.get('repliedusername') # Generate image img = chat_gen.generate_chat_bubble( message=message, username=username, reply_to=reply_to, replied_username=replied_username ) # Return as PNG img_io = io.BytesIO() img.save(img_io, 'PNG', quality=95, optimize=True) img_io.seek(0) return send_file(img_io, mimetype='image/png', as_attachment=False) @app.route('/') def index(): return '''
message
- Your message text (supports emojis!)username
- Sender's display namereplyto
- (Optional) Message being replied torepliedusername
- (Optional) Original sender's name/chat?message=Hello World!&username=YourName&replyto=How are you?&repliedusername=Friend