VirtualOasis commited on
Commit
30a1cae
·
verified ·
1 Parent(s): 9763e73

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +162 -198
app.py CHANGED
@@ -1,7 +1,6 @@
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,230 +18,195 @@ def draw_dot_grid(draw, width, height, spacing=50, color=(220, 220, 220)):
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)
 
1
  import os
2
  import tempfile
3
  import base64
 
4
  import gradio as gr
5
  from PIL import Image, ImageDraw, ImageFont
6
 
 
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/graph paper grid on the image."""
22
+ # Draw vertical lines
23
  for x in range(0, width, spacing):
24
+ draw.line([(x, 0), (x, height)], fill=color, width=2)
25
+ # Draw horizontal lines
26
  for y in range(0, height, spacing):
27
+ draw.line([(0, y), (width, y)], fill=color, width=2)
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
+ def text_to_images_generator(text_content, style='lines'):
31
  """
32
+ Converts a given string of text into a single combined image and returns the file path.
33
+ This is compatible with both UI and MCP.
34
+
35
  Args:
36
+ text_content (str): The text to be converted.
37
+ style (str): The background style ('plain', 'lines', 'dots', 'grid').
38
+
39
  Returns:
40
+ str: Message with the path to the generated combined image file.
 
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
+ # --- Font Loading ---
59
+ font = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  try:
61
+ font_paths_to_try = [
62
+ "Arial.ttf", "arial.ttf", "DejaVuSans.ttf",
63
+ "/System/Library/Fonts/Supplemental/Arial.ttf",
64
+ "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
65
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
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
+ all_lines_and_breaks = []
84
+ for i, paragraph in enumerate(paragraphs):
85
+ words = paragraph.split()
 
 
 
 
 
 
 
 
 
 
 
86
  current_line = ""
 
87
  for word in words:
88
+ if font.getlength(word) > drawable_width:
89
+ temp_word = ""
90
+ for char in word:
91
+ if font.getlength(temp_word + char) > drawable_width:
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
+ all_lines_and_breaks.append(current_line.strip())
102
+ current_line = word
103
+ all_lines_and_breaks.append(current_line.strip())
 
 
104
 
105
+ if i < len(paragraphs) - 1:
106
+ all_lines_and_breaks.append(None)
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
+ PARAGRAPH_SPACING = line_height
115
+
116
+ # Calculate pages and total height needed
117
+ pages_content = []
118
+ current_page = []
119
+ y_text = PADDING_Y
120
+
121
+ for item in all_lines_and_breaks:
122
+ is_break = item is None
123
+ item_height = PARAGRAPH_SPACING if is_break else line_height
124
 
125
+ if y_text + item_height > IMG_HEIGHT - PADDING_Y:
126
+ pages_content.append(current_page)
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
+ # Paste this page onto the combined image
162
+ combined_img.paste(page_img, (0, page_idx * IMG_HEIGHT))
163
 
164
+ # Save combined image to temporary file and also encode as base64
165
+ temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.png')
166
+ combined_img.save(temp_file.name, format='PNG')
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 encoded image (you can copy this and paste into a base64 image viewer):
185
  data:image/png;base64,{img_base64}
186
 
187
+ To view the image:
188
+ 1. Copy the entire data:image/png;base64,... string above
189
+ 2. Paste it into your browser's address bar, or
190
+ 3. Use an online base64 image viewer like: https://base64.guru/converter/decode/image"""
 
 
 
 
191
 
192
  # --- Gradio Interface ---
193
 
194
+ example_text = """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.
195
+
196
+ One 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."""
197
+
198
+ demo = gr.Interface(
199
+ fn=text_to_images_generator,
200
+ inputs=[
201
+ gr.Textbox(lines=15, label="Text Content", placeholder="Paste your long-form text here...", value=example_text),
202
+ gr.Radio(['lines', 'dots', 'grid', 'plain'], label="Background Style", value='lines')
203
+ ],
204
+ outputs=gr.Textbox(label="Result", show_label=True),
205
+ title="Text-to-Image Converter",
206
+ description="Transforms long-form text into a single combined image with multiple pages. Paste your text, choose a style, and click 'Submit'. The result will show the file path where your image was saved.",
207
+ allow_flagging="never"
208
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
 
210
+ # --- Main Execution ---
211
  if __name__ == "__main__":
212
+ demo.launch(mcp_server=True)