Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1 |
import os
|
2 |
import tempfile
|
3 |
import base64
|
|
|
4 |
import gradio as gr
|
5 |
from PIL import Image, ImageDraw, ImageFont
|
6 |
|
@@ -18,195 +19,230 @@ def draw_dot_grid(draw, width, height, spacing=50, color=(220, 220, 220)):
|
|
18 |
draw.ellipse([(x-2, y-2), (x+2, y+2)], fill=color)
|
19 |
|
20 |
def draw_lattice_grid(draw, width, height, spacing=100, color=(235, 235, 235)):
|
21 |
-
"""Draws a lattice
|
22 |
-
# Draw vertical lines
|
23 |
for x in range(0, width, spacing):
|
24 |
-
draw.line([(x, 0), (x, height)], fill=color, width=
|
25 |
-
# Draw horizontal lines
|
26 |
for y in range(0, height, spacing):
|
27 |
-
draw.line([(0, y), (width, y)], fill=color, width=
|
28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
29 |
|
30 |
-
def text_to_images_generator(text_content, style=
|
31 |
"""
|
32 |
-
Converts a given string of text into
|
33 |
-
|
34 |
-
|
35 |
Args:
|
36 |
-
text_content
|
37 |
-
style
|
38 |
-
|
39 |
Returns:
|
40 |
-
|
|
|
41 |
"""
|
42 |
-
if not text_content or not text_content.strip():
|
43 |
-
return "Error: Input text is empty. Please enter some text to generate images."
|
44 |
-
|
45 |
-
# --- Configuration ---
|
46 |
-
IMG_WIDTH = 1080
|
47 |
-
IMG_HEIGHT = 1080
|
48 |
-
BACKGROUND_COLOR = (255, 255, 255)
|
49 |
-
TEXT_COLOR = (10, 10, 10)
|
50 |
-
STYLE_COLOR = (225, 225, 225) # Color for lines/dots/grid
|
51 |
-
|
52 |
-
PADDING_X = 80
|
53 |
-
PADDING_Y = 80
|
54 |
-
|
55 |
-
FONT_SIZE = 48
|
56 |
-
LINE_SPACING = 20
|
57 |
|
58 |
-
#
|
59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
60 |
try:
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
"/
|
65 |
-
|
66 |
-
]
|
67 |
-
for f_path in font_paths_to_try:
|
68 |
-
try:
|
69 |
-
font = ImageFont.truetype(f_path, FONT_SIZE)
|
70 |
-
break
|
71 |
-
except IOError:
|
72 |
-
continue
|
73 |
-
if not font:
|
74 |
font = ImageFont.load_default()
|
75 |
-
except Exception as e:
|
76 |
-
print(f"An unexpected error occurred during font loading: {e}")
|
77 |
-
font = ImageFont.load_default()
|
78 |
-
|
79 |
-
# --- Text Wrapping Logic ---
|
80 |
-
drawable_width = IMG_WIDTH - 2 * PADDING_X
|
81 |
-
paragraphs = [p.strip() for p in text_content.strip().split('\n') if p.strip()]
|
82 |
|
83 |
-
|
84 |
-
|
85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
86 |
current_line = ""
|
|
|
87 |
for word in words:
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
all_lines_and_breaks.append(temp_word)
|
93 |
-
temp_word = char
|
94 |
-
else:
|
95 |
-
temp_word += char
|
96 |
-
word = temp_word
|
97 |
-
|
98 |
-
if font.getlength(current_line + " " + word) <= drawable_width:
|
99 |
-
current_line += " " + word
|
100 |
else:
|
101 |
-
|
102 |
-
|
103 |
-
|
|
|
|
|
104 |
|
105 |
-
if
|
106 |
-
|
107 |
-
|
108 |
-
# --- Image Generation ---
|
109 |
-
try:
|
110 |
-
line_height = font.getbbox("A")[3] - font.getbbox("A")[1]
|
111 |
-
except AttributeError:
|
112 |
-
line_height = 12
|
113 |
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
124 |
|
125 |
-
|
126 |
-
|
127 |
-
current_page = [item]
|
128 |
-
y_text = PADDING_Y + item_height + (0 if is_break else LINE_SPACING)
|
129 |
-
else:
|
130 |
-
current_page.append(item)
|
131 |
-
y_text += item_height + (0 if is_break else LINE_SPACING)
|
132 |
-
|
133 |
-
if current_page:
|
134 |
-
pages_content.append(current_page)
|
135 |
-
|
136 |
-
# Create a single combined image with all pages
|
137 |
-
total_height = len(pages_content) * IMG_HEIGHT
|
138 |
-
combined_img = Image.new('RGB', (IMG_WIDTH, total_height), color=BACKGROUND_COLOR)
|
139 |
-
|
140 |
-
for page_idx, page_content in enumerate(pages_content):
|
141 |
-
# Create individual page
|
142 |
-
page_img = Image.new('RGB', (IMG_WIDTH, IMG_HEIGHT), color=BACKGROUND_COLOR)
|
143 |
-
draw = ImageDraw.Draw(page_img)
|
144 |
-
|
145 |
-
if style == 'lines':
|
146 |
-
line_style_spacing = line_height + LINE_SPACING
|
147 |
-
draw_horizontal_lines(draw, IMG_WIDTH, IMG_HEIGHT, spacing=line_style_spacing, color=STYLE_COLOR)
|
148 |
-
elif style == 'dots':
|
149 |
-
draw_dot_grid(draw, IMG_WIDTH, IMG_HEIGHT, color=STYLE_COLOR)
|
150 |
-
elif style == 'grid':
|
151 |
-
draw_lattice_grid(draw, IMG_WIDTH, IMG_HEIGHT, color=STYLE_COLOR)
|
152 |
-
|
153 |
-
current_y = PADDING_Y
|
154 |
-
for page_item in page_content:
|
155 |
-
if page_item is not None:
|
156 |
-
draw.text((PADDING_X, current_y), page_item, font=font, fill=TEXT_COLOR)
|
157 |
-
current_y += line_height + LINE_SPACING
|
158 |
-
else:
|
159 |
-
current_y += PARAGRAPH_SPACING
|
160 |
|
161 |
-
|
162 |
-
combined_img.paste(page_img, (0, page_idx * IMG_HEIGHT))
|
163 |
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
temp_file.close()
|
168 |
-
|
169 |
-
# Convert to base64 for inline viewing
|
170 |
-
import io
|
171 |
-
img_buffer = io.BytesIO()
|
172 |
-
combined_img.save(img_buffer, format='PNG')
|
173 |
-
img_buffer.seek(0)
|
174 |
-
img_base64 = base64.b64encode(img_buffer.getvalue()).decode('utf-8')
|
175 |
-
|
176 |
-
return f"""Image successfully generated and saved to: {temp_file.name}
|
177 |
-
|
178 |
-
Image details:
|
179 |
-
- Total pages: {len(pages_content)}
|
180 |
-
- Image dimensions: {IMG_WIDTH} x {total_height} pixels
|
181 |
- Style: {style}
|
182 |
- Format: PNG
|
183 |
|
184 |
-
Base64
|
185 |
data:image/png;base64,{img_base64}
|
186 |
|
187 |
-
To view the image:
|
188 |
-
1. Copy the
|
189 |
-
2. Paste it into your browser
|
190 |
-
3.
|
|
|
|
|
|
|
|
|
191 |
|
192 |
# --- Gradio Interface ---
|
193 |
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
209 |
|
210 |
-
# --- Main Execution ---
|
211 |
if __name__ == "__main__":
|
212 |
-
|
|
|
|
|
|
1 |
import os
|
2 |
import tempfile
|
3 |
import base64
|
4 |
+
import io
|
5 |
import gradio as gr
|
6 |
from PIL import Image, ImageDraw, ImageFont
|
7 |
|
|
|
19 |
draw.ellipse([(x-2, y-2), (x+2, y+2)], fill=color)
|
20 |
|
21 |
def draw_lattice_grid(draw, width, height, spacing=100, color=(235, 235, 235)):
|
22 |
+
"""Draws a lattice grid on the image."""
|
|
|
23 |
for x in range(0, width, spacing):
|
24 |
+
draw.line([(x, 0), (x, height)], fill=color, width=1)
|
|
|
25 |
for y in range(0, height, spacing):
|
26 |
+
draw.line([(0, y), (width, y)], fill=color, width=1)
|
27 |
|
28 |
+
def create_text_image(text, width=1080, height=1080, font_size=40, line_spacing=20, margin=50, bg_style="lines"):
|
29 |
+
"""Creates an image with the given text and background style."""
|
30 |
+
|
31 |
+
# Create image with white background
|
32 |
+
img = Image.new('RGB', (width, height), 'white')
|
33 |
+
draw = ImageDraw.Draw(img)
|
34 |
+
|
35 |
+
# Apply background style
|
36 |
+
if bg_style == "lines":
|
37 |
+
draw_horizontal_lines(draw, width, height)
|
38 |
+
elif bg_style == "dots":
|
39 |
+
draw_dot_grid(draw, width, height)
|
40 |
+
elif bg_style == "grid":
|
41 |
+
draw_lattice_grid(draw, width, height)
|
42 |
+
# "plain" style has no additional background
|
43 |
+
|
44 |
+
# Use default font
|
45 |
+
try:
|
46 |
+
font = ImageFont.truetype("arial.ttf", font_size)
|
47 |
+
except:
|
48 |
+
try:
|
49 |
+
font = ImageFont.truetype("/System/Library/Fonts/Arial.ttf", font_size)
|
50 |
+
except:
|
51 |
+
font = ImageFont.load_default()
|
52 |
+
|
53 |
+
# Calculate text area
|
54 |
+
text_width = width - 2 * margin
|
55 |
+
y = margin
|
56 |
+
|
57 |
+
# Split text into paragraphs
|
58 |
+
paragraphs = text.split('\n')
|
59 |
+
|
60 |
+
for paragraph in paragraphs:
|
61 |
+
if paragraph.strip() == "":
|
62 |
+
y += line_spacing
|
63 |
+
continue
|
64 |
+
|
65 |
+
# Word wrap for each paragraph
|
66 |
+
words = paragraph.split(' ')
|
67 |
+
lines = []
|
68 |
+
current_line = ""
|
69 |
+
|
70 |
+
for word in words:
|
71 |
+
# Check if adding this word would exceed the width
|
72 |
+
test_line = current_line + " " + word if current_line else word
|
73 |
+
bbox = draw.textbbox((0, 0), test_line, font=font)
|
74 |
+
if bbox[2] - bbox[0] <= text_width:
|
75 |
+
current_line = test_line
|
76 |
+
else:
|
77 |
+
if current_line:
|
78 |
+
lines.append(current_line)
|
79 |
+
current_line = word
|
80 |
+
else:
|
81 |
+
lines.append(word) # Single word that's too long
|
82 |
+
|
83 |
+
if current_line:
|
84 |
+
lines.append(current_line)
|
85 |
+
|
86 |
+
# Draw the lines
|
87 |
+
for line in lines:
|
88 |
+
draw.text((margin, y), line, fill='black', font=font)
|
89 |
+
y += font_size + line_spacing
|
90 |
+
|
91 |
+
# Add extra space between paragraphs
|
92 |
+
y += line_spacing // 2
|
93 |
+
|
94 |
+
return img
|
95 |
|
96 |
+
def text_to_images_generator(text_content: str = "In the heart of a bustling city, there lived a clockmaker named Alistair. His shop, a quaint corner of tranquility amidst the urban chaos, was filled with the gentle ticking of countless timepieces. Each clock was a masterpiece, a testament to his dedication and skill.\n\nOne day, a young girl with eyes as curious as a cat's wandered into his shop. She wasn't interested in the shiny new watches but was drawn to the grandfather clock in the corner. \"What's its story?\" she asked, her voice soft. Alistair smiled, for he knew he had found the next guardian of the stories. The legacy of the whispering clock would live on.", style: str = "lines") -> str:
|
97 |
"""
|
98 |
+
Converts a given string of text into images and returns the result.
|
99 |
+
|
|
|
100 |
Args:
|
101 |
+
text_content: The text to be converted.
|
102 |
+
style: The background style ('plain', 'lines', 'dots', 'grid').
|
103 |
+
|
104 |
Returns:
|
105 |
+
For MCP: Base64 encoded image data
|
106 |
+
For Gradio: PIL Image object
|
107 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
108 |
|
109 |
+
# Check if we're in MCP context by looking at the execution environment
|
110 |
+
import inspect
|
111 |
+
frame = inspect.currentframe()
|
112 |
+
is_mcp_context = False
|
113 |
+
|
114 |
+
# Look through the call stack to detect MCP context
|
115 |
+
while frame:
|
116 |
+
if frame.f_code.co_filename and ('mcp' in frame.f_code.co_filename.lower() or 'sse' in frame.f_code.co_filename.lower()):
|
117 |
+
is_mcp_context = True
|
118 |
+
break
|
119 |
+
frame = frame.f_back
|
120 |
+
|
121 |
+
if not text_content.strip():
|
122 |
+
if is_mcp_context:
|
123 |
+
return "Error: No text content provided."
|
124 |
+
else:
|
125 |
+
return None
|
126 |
+
|
127 |
+
# Calculate required dimensions for the text
|
128 |
+
temp_img = Image.new('RGB', (1080, 1080), 'white')
|
129 |
+
temp_draw = ImageDraw.Draw(temp_img)
|
130 |
+
|
131 |
try:
|
132 |
+
font = ImageFont.truetype("arial.ttf", 40)
|
133 |
+
except:
|
134 |
+
try:
|
135 |
+
font = ImageFont.truetype("/System/Library/Fonts/Arial.ttf", 40)
|
136 |
+
except:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
137 |
font = ImageFont.load_default()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
138 |
|
139 |
+
# Calculate how many lines we need
|
140 |
+
margin = 50
|
141 |
+
text_width = 1080 - 2 * margin
|
142 |
+
line_height = 40 + 20 # font_size + line_spacing
|
143 |
+
|
144 |
+
total_lines = 0
|
145 |
+
paragraphs = text_content.split('\n')
|
146 |
+
|
147 |
+
for paragraph in paragraphs:
|
148 |
+
if paragraph.strip() == "":
|
149 |
+
total_lines += 1
|
150 |
+
continue
|
151 |
+
|
152 |
+
words = paragraph.split(' ')
|
153 |
current_line = ""
|
154 |
+
|
155 |
for word in words:
|
156 |
+
test_line = current_line + " " + word if current_line else word
|
157 |
+
bbox = temp_draw.textbbox((0, 0), test_line, font=font)
|
158 |
+
if bbox[2] - bbox[0] <= text_width:
|
159 |
+
current_line = test_line
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
160 |
else:
|
161 |
+
if current_line:
|
162 |
+
total_lines += 1
|
163 |
+
current_line = word
|
164 |
+
else:
|
165 |
+
total_lines += 1
|
166 |
|
167 |
+
if current_line:
|
168 |
+
total_lines += 1
|
169 |
+
total_lines += 0.5 # Extra space between paragraphs
|
|
|
|
|
|
|
|
|
|
|
170 |
|
171 |
+
# Calculate required height
|
172 |
+
required_height = max(1080, int(total_lines * line_height + 2 * margin))
|
173 |
+
|
174 |
+
# Create the final image
|
175 |
+
final_image = create_text_image(
|
176 |
+
text_content,
|
177 |
+
width=1080,
|
178 |
+
height=required_height,
|
179 |
+
bg_style=style
|
180 |
+
)
|
181 |
+
|
182 |
+
if is_mcp_context:
|
183 |
+
# For MCP: Return base64 encoded image
|
184 |
+
buffer = io.BytesIO()
|
185 |
+
final_image.save(buffer, format='PNG')
|
186 |
+
img_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
|
187 |
|
188 |
+
# Calculate pages
|
189 |
+
pages = max(1, required_height // 1080)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
190 |
|
191 |
+
return f"""Image generated successfully!
|
|
|
192 |
|
193 |
+
📊 Image Details:
|
194 |
+
- Dimensions: 1080x{required_height} pixels
|
195 |
+
- Pages: {pages}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
196 |
- Style: {style}
|
197 |
- Format: PNG
|
198 |
|
199 |
+
🖼️ Base64 Image Data:
|
200 |
data:image/png;base64,{img_base64}
|
201 |
|
202 |
+
💡 To view the image:
|
203 |
+
1. Copy the data URL above (including 'data:image/png;base64,')
|
204 |
+
2. Paste it into your browser address bar
|
205 |
+
3. The image will display directly!"""
|
206 |
+
|
207 |
+
else:
|
208 |
+
# For Gradio: Return PIL Image object directly
|
209 |
+
return final_image
|
210 |
|
211 |
# --- Gradio Interface ---
|
212 |
|
213 |
+
def create_gradio_interface():
|
214 |
+
"""Creates the Gradio interface for the text-to-image converter."""
|
215 |
+
|
216 |
+
interface = gr.Interface(
|
217 |
+
fn=text_to_images_generator,
|
218 |
+
inputs=[
|
219 |
+
gr.Textbox(
|
220 |
+
label="Text Content",
|
221 |
+
placeholder="Enter your text here...",
|
222 |
+
lines=10,
|
223 |
+
value="In the heart of a bustling city, there lived a clockmaker named Alistair. His shop, a quaint corner of tranquility amidst the urban chaos, was filled with the gentle ticking of countless timepieces. Each clock was a masterpiece, a testament to his dedication and skill.\n\nOne day, a young girl with eyes as curious as a cat's wandered into his shop. She wasn't interested in the shiny new watches but was drawn to the grandfather clock in the corner. \"What's its story?\" she asked, her voice soft. Alistair smiled, for he knew he had found the next guardian of the stories. The legacy of the whispering clock would live on."
|
224 |
+
),
|
225 |
+
gr.Radio(
|
226 |
+
choices=["lines", "dots", "grid", "plain"],
|
227 |
+
label="Background Style",
|
228 |
+
value="lines"
|
229 |
+
)
|
230 |
+
],
|
231 |
+
outputs=gr.Image(label="Generated Image", type="pil"),
|
232 |
+
title="📝➡️🖼️ Text to Images Generator",
|
233 |
+
description="Convert your text into beautiful images with different background styles. The image will automatically adjust its height based on the text length.",
|
234 |
+
examples=[
|
235 |
+
["Hello World!\n\nThis is a simple example.", "lines"],
|
236 |
+
["Chapter 1: Introduction\n\nThis is the beginning of our story...", "dots"],
|
237 |
+
["• Point 1\n• Point 2\n• Point 3", "grid"],
|
238 |
+
["Clean and simple text layout.", "plain"]
|
239 |
+
],
|
240 |
+
theme=gr.themes.Soft()
|
241 |
+
)
|
242 |
+
|
243 |
+
return interface
|
244 |
|
|
|
245 |
if __name__ == "__main__":
|
246 |
+
# Create and launch the interface
|
247 |
+
interface = create_gradio_interface()
|
248 |
+
interface.launch(server_name="0.0.0.0", server_port=7860)
|