|
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) |