import os import math import textwrap from io import BytesIO import gradio as gr import matplotlib.pyplot as plt from PIL import Image, ImageDraw, ImageFont from huggingface_hub import hf_hub_download # --- Phần tải về CLI tool từ Hugging Face Hub --- # Giả sử tên file CLI trong repo là "Texttoimage" (bạn có thể thay đổi nếu cần) CLI_FILENAME = "Texttoimage" if not os.path.exists(CLI_FILENAME): hf_token = os.environ.get("HF_TOKEN") if not hf_token: print("Biến môi trường HF_TOKEN chưa được thiết lập!") else: try: # Tải file CLI từ repo ArrcttacsrjksX/Texttoimage cli_local_path = hf_hub_download( repo_id="ArrcttacsrjksX/Texttoimage", filename=CLI_FILENAME, token=hf_token ) # Di chuyển (hoặc đổi tên) file tải về nếu cần os.rename(cli_local_path, CLI_FILENAME) # Cho phép chạy được file CLI os.chmod(CLI_FILENAME, 0o755) print(f"Đã tải về CLI tool: {CLI_FILENAME}") except Exception as e: print(f"Lỗi khi tải CLI tool: {e}") # --- Các hàm hỗ trợ render ảnh text --- def parse_color(color: str): """ Chuyển đổi chuỗi màu (hex hoặc RGB dạng "R,G,B") thành tuple RGB. Ví dụ: "#FFEEEE" hoặc "255,238,238" """ color = color.strip() if color.startswith('#'): color = color.lstrip('#') if len(color) != 6: raise ValueError("Mã hex phải có 6 ký tự.") return tuple(int(color[i:i+2], 16) for i in (0, 2, 4)) else: parts = color.split(',') if len(parts) != 3: raise ValueError("Màu dạng RGB phải có 3 thành phần cách nhau bởi dấu phẩy.") return tuple(int(x) for x in parts) def calculate_text_dimensions(text, font, max_width, margin): """Tính toán kích thước text cho việc wrap theo chiều rộng cho trước.""" lines = [] for line in text.split('\n'): # Sử dụng độ rộng ước tính dựa trên kích thước font lines.extend(textwrap.wrap(line, width=int(max_width / font.size * 1.8))) bbox = font.getbbox('Ay') line_height = bbox[3] - bbox[1] total_height = line_height * len(lines) return lines, line_height, total_height def create_text_segment(lines, start_idx, max_lines, width, height, bg_color, text_color, font, align, margin): """Tạo một đoạn ảnh chứa một phần các dòng text.""" img = Image.new("RGB", (width, height), color=bg_color) draw = ImageDraw.Draw(img) bbox = font.getbbox('Ay') line_height = bbox[3] - bbox[1] y = margin end_idx = min(start_idx + max_lines, len(lines)) segment_lines = lines[start_idx:end_idx] for line in segment_lines: bbox = font.getbbox(line) line_width = bbox[2] - bbox[0] if align == 'left': x = margin elif align == 'center': x = (width - line_width) // 2 else: # 'right' x = width - line_width - margin draw.text((x, y), line, fill=text_color, font=font) y += line_height return img, end_idx def render_plain_text_image(text, font_size, width, height, bg_color, text_color, font_path, align): """Render ảnh chứa text dạng thông thường.""" margin = 10 try: font = ImageFont.truetype(font_path, font_size) except Exception: print(f"Cảnh báo: Không tải được font {font_path}. Sử dụng font mặc định.") font = ImageFont.load_default() max_width = width - 2 * margin lines, line_height, total_text_height = calculate_text_dimensions(text, font, max_width, margin) max_lines_per_segment = (height - 2 * margin) // line_height num_segments = math.ceil(len(lines) / max_lines_per_segment) segments = [] current_line = 0 for _ in range(num_segments): segment_img, current_line = create_text_segment( lines, current_line, max_lines_per_segment, width, height, bg_color, text_color, font, align, margin ) segments.append(segment_img) total_img_height = len(segments) * height final_image = Image.new("RGB", (width, total_img_height), color=bg_color) for i, segment in enumerate(segments): final_image.paste(segment, (0, i * height)) return final_image def render_math_image(text, font_size, width, height, bg_color, text_color): """Render ảnh chứa biểu thức toán học sử dụng matplotlib.""" fig, ax = plt.subplots(figsize=(width / 100, height / 100), facecolor=bg_color) ax.set_facecolor(bg_color) ax.axis('off') # Nếu text chưa được bọc trong dấu $, thêm vào if not (text.startswith(r"$") and text.endswith(r"$")): text = rf"${text}$" ax.text(0.5, 0.5, text, fontsize=font_size, ha='center', va='center', color=text_color) buf = BytesIO() plt.savefig(buf, format='png', bbox_inches='tight', pad_inches=0) plt.close(fig) buf.seek(0) img = Image.open(buf) return img # --- Hàm xử lý chính cho giao diện Gradio --- def generate_image(text: str, font_size: int, width: int, height: int, bg_color: str, text_color: str, align: str, mode: str, font_path: str): """ Hàm tạo ảnh từ text với các tham số đầu vào. Nếu mode = "plain" thì render text bình thường, nếu mode = "math" thì render biểu thức toán học. """ try: bg_color_tuple = parse_color(bg_color) text_color_tuple = parse_color(text_color) except Exception as e: return f"Lỗi khi parse màu: {e}" try: if mode == "plain": img = render_plain_text_image( text, font_size, width, height, bg_color_tuple, text_color_tuple, font_path, align ) else: img = render_math_image( text, font_size, width, height, bg_color_tuple, text_color_tuple ) except Exception as e: return f"Lỗi khi tạo ảnh: {e}" return img # --- Tạo giao diện Gradio --- # Các widget đầu vào text_input = gr.Textbox(label="Text cần chuyển", placeholder="Nhập text của bạn vào đây...", lines=4) font_size_input = gr.Slider(10, 100, value=40, step=1, label="Cỡ chữ (font size)") width_input = gr.Number(value=1000, label="Chiều rộng ảnh (px)") height_input = gr.Number(value=800, label="Chiều cao ảnh (px)") bg_color_input = gr.Textbox(value="#FFEEEE", label="Màu nền (hex hoặc R,G,B)") text_color_input = gr.Textbox(value="#000066", label="Màu chữ (hex hoặc R,G,B)") align_input = gr.Radio(choices=["left", "center", "right"], value="right", label="Căn chỉnh text") mode_input = gr.Radio(choices=["plain", "math"], value="plain", label="Chế độ render") font_path_input = gr.Textbox(value="/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", label="Đường dẫn font") # Một số CSS tùy chỉnh để làm đẹp giao diện custom_css = """ body { background: linear-gradient(135deg, #f6d365 0%, #fda085 100%); font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } .gradio-container { border-radius: 15px; box-shadow: 0 4px 10px rgba(0,0,0,0.2); padding: 20px; background-color: rgba(255, 255, 255, 0.9); } """ # Xây dựng giao diện Gradio demo = gr.Interface( fn=generate_image, inputs=[text_input, font_size_input, width_input, height_input, bg_color_input, text_color_input, align_input, mode_input, font_path_input], outputs=gr.Image(type="pil", label="Ảnh được tạo"), title="Text to Image - Texttoimage CLI", description=("Giao diện demo chuyển text thành ảnh. " "Bạn có thể nhập text, chọn các tham số như kích thước, màu sắc, căn chỉnh, " "và xem ảnh được render theo thời gian thực."), css=custom_css, allow_flagging="never" ) if __name__ == "__main__": demo.launch()