| 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): |
| |
| self.bubble_color = (31, 39, 69) |
| self.bubble_shadow = (20, 25, 45) |
| self.reply_bubble_color = (25, 31, 55) |
| self.reply_line_color = (70, 130, 255) |
| 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) |
| |
| |
| self.load_fonts() |
| |
| |
| 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: |
| |
| 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 |
| |
| |
| 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: |
| |
| 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 |
| |
| |
| if x2 <= x1 or y2 <= y1: |
| return |
| |
| |
| max_radius = min((x2 - x1) // 2, (y2 - y1) // 2, radius) |
| if max_radius < 1: |
| max_radius = 1 |
| |
| |
| if shadow_offset > 0: |
| shadow_coords = (x1 + shadow_offset, y1 + shadow_offset, |
| x2 + shadow_offset, y2 + shadow_offset) |
| shadow_color = (0, 0, 0, 60) |
| self.draw_rounded_rectangle(draw, shadow_coords, max_radius, shadow_color) |
| |
| |
| self.draw_rounded_rectangle(draw, coords, max_radius, fill) |
| |
| |
| 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 |
| |
| |
| if x2 <= x1 or y2 <= y1: |
| return |
| |
| |
| width = x2 - x1 |
| height = y2 - y1 |
| max_radius = min(width // 2, height // 2, radius) |
| |
| if max_radius < 1: |
| |
| draw.rectangle([x1, y1, x2, y2], fill=fill) |
| return |
| |
| |
| if len(fill) == 3: |
| |
| fill_color = fill + (255,) |
| else: |
| |
| fill_color = fill |
| |
| |
| 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) |
| |
| |
| if width >= 2 * max_radius and height >= 2 * max_radius: |
| |
| draw.ellipse([x1, y1, x1 + 2 * max_radius, y1 + 2 * max_radius], fill=fill_color) |
| |
| draw.ellipse([x2 - 2 * max_radius, y1, x2, y1 + 2 * max_radius], fill=fill_color) |
| |
| draw.ellipse([x1, y2 - 2 * max_radius, x1 + 2 * max_radius, y2], fill=fill_color) |
| |
| 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 |
| |
| |
| 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 |
| |
| |
| for x in range(0, width, 40): |
| for y in range(0, height, 40): |
| |
| 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): |
| |
| padding = 40 |
| bubble_padding = 25 |
| |
| |
| wrapped_message = self.wrap_text(message, self.font, 500) |
| msg_width, msg_height = self.get_text_dimensions(wrapped_message, self.font) |
| username_width, username_height = self.get_text_dimensions(username, self.username_font) |
| |
| |
| 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 |
| |
| |
| 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 |
| |
| |
| canvas_width = bubble_width + padding * 2 |
| canvas_height = bubble_height + padding * 2 |
| |
| |
| img = Image.new('RGBA', (canvas_width, canvas_height), (0, 0, 0, 0)) |
| draw = ImageDraw.Draw(img) |
| |
| |
| bubble_x = padding |
| bubble_y = padding |
| |
| |
| 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 |
| ) |
| |
| |
| content_y = bubble_y + bubble_padding |
| |
| |
| if reply_to and replied_username and bubble_width > 60: |
| reply_y = content_y |
| reply_bubble_height = max(30, reply_height + 10) |
| |
| |
| 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 |
| ) |
| |
| |
| 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_x = bubble_x + 30 |
| reply_content_y = reply_y + 12 |
| |
| |
| 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 |
| |
| |
| 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 |
| |
| |
| 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 |
| |
| |
| 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 |
| |
| |
| 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) |
| |
| |
| 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""" |
| |
| points1 = [(x-8, y), (x-5, y+3), (x-2, y-2)] |
| self.draw_polyline(draw, points1, self.time_color, width=2) |
| |
| |
| 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) |
|
|
| |
| chat_gen = TelegramChatGenerator() |
|
|
| @app.route('/chat') |
| def generate_chat(): |
| |
| 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') |
| |
| |
| img = chat_gen.generate_chat_bubble( |
| message=message, |
| username=username, |
| reply_to=reply_to, |
| replied_username=replied_username |
| ) |
| |
| |
| 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 ''' |
| <!DOCTYPE html> |
| <html> |
| <head> |
| <title>Telegram Chat Generator</title> |
| <style> |
| body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; background: #f5f5f5; } |
| .container { background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } |
| h1 { color: #333; text-align: center; margin-bottom: 30px; } |
| .example { margin: 15px 0; } |
| .example img { max-width: 300px; border-radius: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.2); } |
| .params { background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0; } |
| a { color: #007bff; text-decoration: none; } |
| a:hover { text-decoration: underline; } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <h1>π¨ Premium Telegram Chat Generator</h1> |
| |
| <div class="params"> |
| <h3>β¨ Features:</h3> |
| <ul> |
| <li>π¨ Beautiful gradient backgrounds</li> |
| <li>π¬ Authentic Telegram bubble design</li> |
| <li>β©οΈ Reply message support</li> |
| <li>π Perfect square format (800x800)</li> |
| <li>π Automatic timestamps</li> |
| <li>β
Read status indicators</li> |
| <li>π± Smart text wrapping</li> |
| </ul> |
| </div> |
| |
| <h3>π Try These Examples:</h3> |
| |
| <div class="example"> |
| <h4>Simple Message:</h4> |
| <a href="/chat?message=Hey! How are you doing today? π&username=Alice"> |
| <img src="/chat?message=Hey! How are you doing today? π&username=Alice" alt="Simple message example"> |
| </a> |
| </div> |
| |
| <div class="example"> |
| <h4>Long Message:</h4> |
| <a href="/chat?message=I just wanted to let you know that the meeting has been rescheduled to tomorrow at 3 PM. Please make sure to bring all the documents we discussed earlier. Thanks!&username=John"> |
| <img src="/chat?message=I just wanted to let you know that the meeting has been rescheduled to tomorrow at 3 PM. Please make sure to bring all the documents we discussed earlier. Thanks!&username=John" alt="Long message example"> |
| </a> |
| </div> |
| |
| <div class="example"> |
| <h4>Reply Message:</h4> |
| <a href="/chat?message=Sounds great! I'll be there π&username=Mike&replyto=Want to grab coffee tomorrow morning?&repliedusername=Sarah"> |
| <img src="/chat?message=Sounds great! I'll be there π&username=Mike&replyto=Want to grab coffee tomorrow morning?&repliedusername=Sarah" alt="Reply message example"> |
| </a> |
| </div> |
| |
| <div class="params"> |
| <h3>π§ API Parameters:</h3> |
| <ul> |
| <li><code>message</code> - Your message text (supports emojis!)</li> |
| <li><code>username</code> - Sender's display name</li> |
| <li><code>replyto</code> - (Optional) Message being replied to</li> |
| <li><code>repliedusername</code> - (Optional) Original sender's name</li> |
| </ul> |
| |
| <h4>Example URL:</h4> |
| <code>/chat?message=Hello World!&username=YourName&replyto=How are you?&repliedusername=Friend</code> |
| </div> |
| </div> |
| </body> |
| </html> |
| ''' |
|
|
| if __name__ == '__main__': |
| app.run(debug=True, host='0.0.0.0', port=7860) |