Download app.py from ReikerXx/quote: direct link, hf CLI and curl.
- Browser
- Download file 18 kB
-
https://huggingface.co/spaces/ReikerXx/quote/resolve/main/app.py
- Command line
-
hf download hf://spaces/ReikerXx/quote/app.py
-
curl -L -o app.py https://huggingface.co/spaces/ReikerXx/quote/resolve/main/app.py
18 kB
| 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() | |
| 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) | |
| 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) |