ArrcttacsrjksX commited on
Commit
98ddded
·
verified ·
1 Parent(s): 4b57180

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +489 -165
app.py CHANGED
@@ -1,7 +1,7 @@
1
  import gradio as gr
2
  import matplotlib.pyplot as plt
3
  from io import BytesIO
4
- from PIL import Image, ImageDraw, ImageFont
5
  import textwrap
6
  import os
7
  import matplotlib
@@ -9,180 +9,504 @@ import math
9
  import zipfile
10
  import tempfile
11
  import shutil
 
 
 
12
 
13
- # Create a fonts directory in temp
14
  FONTS_DIR = os.path.join(tempfile.gettempdir(), 'text_to_image_fonts')
15
- os.makedirs(FONTS_DIR, exist_ok=True)
 
16
 
17
- def extract_fonts_from_zip(zip_path):
18
- """Extract TTF and SHX fonts from zip file"""
19
- supported_extensions = ('.ttf', '.shx')
20
- font_files = []
21
-
22
- with zipfile.ZipFile(zip_path, 'r') as zip_ref:
23
- # Extract all font files
24
- for file in zip_ref.namelist():
25
- if file.lower().endswith(supported_extensions):
26
- zip_ref.extract(file, FONTS_DIR)
27
- font_files.append(os.path.join(FONTS_DIR, file))
28
-
29
- return font_files
30
-
31
- def get_all_fonts():
32
- """Get system fonts and custom fonts"""
33
- fonts = []
34
- # System fonts
35
- for font in matplotlib.font_manager.findSystemFonts(fontpaths=None, fontext='ttf'):
36
- font_name = os.path.basename(font)
37
- fonts.append({"name": font_name, "path": font, "type": "system"})
38
-
39
- # Custom fonts from FONTS_DIR
40
- if os.path.exists(FONTS_DIR):
41
- for font in os.listdir(FONTS_DIR):
42
- if font.lower().endswith(('.ttf', '.shx')):
43
- font_path = os.path.join(FONTS_DIR, font)
44
- fonts.append({"name": font, "path": font_path,
45
- "type": "custom_" + os.path.splitext(font)[1][1:]})
46
-
47
- return sorted(fonts, key=lambda x: (x["type"], x["name"]))
48
-
49
- def load_font(font_info, size):
50
- """Load font based on type"""
51
- if font_info["type"] == "custom_shx":
52
- # Here you would implement SHX font loading
53
- # For now, fallback to default
54
- return ImageFont.load_default()
55
- else:
 
 
 
 
 
 
 
 
 
 
 
 
56
  try:
57
- return ImageFont.truetype(font_info["path"], size)
58
- except Exception:
 
 
 
 
 
 
59
  return ImageFont.load_default()
60
 
61
- def handle_font_upload(zip_file):
62
- """Handle font zip file upload"""
63
- if zip_file is not None:
64
- extracted_fonts = extract_fonts_from_zip(zip_file.name)
65
- return gr.Dropdown.update(choices=[f["name"] for f in get_all_fonts()])
66
- return gr.Dropdown.update()
 
67
 
68
- def create_text_segment(lines, start_idx, max_lines, width, height, bg_color, text_color, font_info, font_size, align, margin, text_effects):
69
- img = Image.new("RGBA", (width, height), color=bg_color)
70
- draw = ImageDraw.Draw(img)
71
-
72
- font = load_font(font_info, font_size)
73
- bbox = font.getbbox('Ay')
74
- line_height = bbox[3] - bbox[1]
75
-
76
- y = margin
77
- end_idx = min(start_idx + max_lines, len(lines))
78
- segment_lines = lines[start_idx:end_idx]
79
-
80
- for line in segment_lines:
81
- bbox = font.getbbox(line)
82
- line_width = bbox[2] - bbox[0]
83
-
84
- if align == 'Left':
85
- x = margin
86
- elif align == 'Center':
87
- x = (width - line_width) // 2
88
- else: # Right alignment
89
- x = width - line_width - margin
90
-
91
- # Apply text effects
92
- if 'shadow' in text_effects:
93
- shadow_offset = 2
94
- draw.text((x + shadow_offset, y + shadow_offset), line,
95
- fill=(0, 0, 0, 128), font=font)
96
-
97
- if 'outline' in text_effects:
98
- outline_color = (0, 0, 0, 255)
99
- for off_x, off_y in [(1,0), (-1,0), (0,1), (0,-1)]:
100
- draw.text((x + off_x, y + off_y), line,
101
- fill=outline_color, font=font)
102
-
103
- draw.text((x, y), line, fill=text_color, font=font)
104
- y += line_height
105
-
106
- return img, end_idx
107
 
108
- def text_to_image(input_text, font_size, width, height, bg_color, text_color,
109
- mode, font_name, align, image_format, text_effects, opacity):
110
- # Convert colors to RGBA
111
- bg_color = (*parse_color(bg_color), int(opacity * 255))
112
- text_color = (*parse_color(text_color), 255)
113
-
114
- # Get font info
115
- font_list = get_all_fonts()
116
- font_info = next((f for f in font_list if f["name"] == font_name), None)
117
- if not font_info:
118
- font_info = {"name": "default", "path": None, "type": "system"}
119
-
120
- if mode == "Plain Text":
121
- img = render_plain_text_image(input_text, font_size, width, height,
122
- bg_color, text_color, font_info, align,
123
- text_effects, opacity)
124
- elif mode == "LaTeX Math":
125
- img = render_math_image(input_text, font_size, width, height,
126
- bg_color, text_color)
127
- else:
128
- return "Invalid mode selected!"
129
-
130
- return img
131
 
132
- # Update the Gradio interface
133
- with gr.Blocks() as demo:
134
- gr.Markdown("# 🖼️ Enhanced Text to Image Converter")
135
-
136
- with gr.Row():
137
- font_upload = gr.File(label="Upload Fonts ZIP", type="file",
138
- file_types=[".zip"])
139
-
140
- with gr.Row():
141
- input_text = gr.Textbox(label="Enter Text",
142
- placeholder="Type or paste text here...", lines=5)
143
- file_input = gr.File(label="Upload a Text File", type="filepath")
144
-
145
- with gr.Row():
146
- font_size = gr.Slider(10, 100, value=30, label="Font Size")
147
- font_name = gr.Dropdown(choices=[f["name"] for f in get_all_fonts()],
148
- label="Font")
149
- align = gr.Radio(["Left", "Center", "Right"],
150
- label="Text Alignment", value="Center")
151
-
152
- with gr.Row():
153
- text_effects = gr.CheckboxGroup(
154
- ["shadow", "outline"], label="Text Effects")
155
- opacity = gr.Slider(0, 1, value=1, label="Background Opacity")
156
-
157
- with gr.Row():
158
- width = gr.Slider(200, 2000, value=800, label="Image Width")
159
- height = gr.Slider(200, 2000, value=600, label="Base Height")
160
-
161
- with gr.Row():
162
- bg_color = gr.ColorPicker(label="Background Color", value="#FFFFFF")
163
- text_color = gr.ColorPicker(label="Text Color", value="#000000")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
 
165
- with gr.Row():
166
- mode = gr.Radio(["Plain Text", "LaTeX Math"],
167
- label="Rendering Mode", value="Plain Text")
168
- image_format = gr.Radio(["PNG", "JPEG"],
169
- label="Image Format", value="PNG")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
 
171
- output_image = gr.Image(label="Generated Image")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
 
173
- # Connect components
174
- font_upload.upload(handle_font_upload,
175
- inputs=[font_upload],
176
- outputs=[font_name])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
- convert_button = gr.Button("Convert Text to Image")
179
- convert_button.click(
180
- text_to_image,
181
- inputs=[
182
- input_text, font_size, width, height, bg_color, text_color,
183
- mode, font_name, align, image_format, text_effects, opacity
184
- ],
185
- outputs=output_image
186
- )
187
-
188
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
  import matplotlib.pyplot as plt
3
  from io import BytesIO
4
+ from PIL import Image, ImageDraw, ImageFont, ImageFilter
5
  import textwrap
6
  import os
7
  import matplotlib
 
9
  import zipfile
10
  import tempfile
11
  import shutil
12
+ import numpy as np
13
+ from typing import Dict, List, Tuple, Optional
14
+ import re
15
 
16
+ # Constants
17
  FONTS_DIR = os.path.join(tempfile.gettempdir(), 'text_to_image_fonts')
18
+ SUPPORTED_FONT_TYPES = ('.ttf', '.otf', '.shx')
19
+ DEFAULT_MARGIN = 10
20
 
21
+ class FontManager:
22
+ def __init__(self):
23
+ self.fonts_dir = FONTS_DIR
24
+ os.makedirs(self.fonts_dir, exist_ok=True)
25
+ self.font_cache: Dict[str, Dict] = {}
26
+ self.reload_fonts()
27
+
28
+ def reload_fonts(self):
29
+ """Reload all available fonts"""
30
+ self.font_cache.clear()
31
+ # System fonts
32
+ for font in matplotlib.font_manager.findSystemFonts(fontpaths=None, fontext='ttf'):
33
+ font_name = os.path.basename(font)
34
+ self.font_cache[font_name] = {
35
+ "name": font_name,
36
+ "path": font,
37
+ "type": "system"
38
+ }
39
+
40
+ # Custom fonts
41
+ if os.path.exists(self.fonts_dir):
42
+ for font in os.listdir(self.fonts_dir):
43
+ if font.lower().endswith(SUPPORTED_FONT_TYPES):
44
+ font_path = os.path.join(self.fonts_dir, font)
45
+ self.font_cache[font] = {
46
+ "name": font,
47
+ "path": font_path,
48
+ "type": f"custom_{os.path.splitext(font)[1][1:]}"
49
+ }
50
+
51
+ def get_font_list(self) -> List[Dict]:
52
+ """Get sorted list of all available fonts"""
53
+ return sorted(self.font_cache.values(), key=lambda x: (x["type"], x["name"]))
54
+
55
+ def extract_fonts_from_zip(self, zip_path: str) -> List[str]:
56
+ """Extract fonts from ZIP file"""
57
+ extracted_files = []
58
+ with zipfile.ZipFile(zip_path, 'r') as zip_ref:
59
+ for file in zip_ref.namelist():
60
+ if file.lower().endswith(SUPPORTED_FONT_TYPES):
61
+ zip_ref.extract(file, self.fonts_dir)
62
+ extracted_files.append(os.path.join(self.fonts_dir, file))
63
+ self.reload_fonts()
64
+ return extracted_files
65
+
66
+ def get_font(self, font_name: str, size: int) -> ImageFont.FreeTypeFont:
67
+ """Get font instance by name and size"""
68
+ font_info = self.font_cache.get(font_name)
69
+ if not font_info:
70
+ return ImageFont.load_default()
71
+
72
  try:
73
+ if font_info["type"] == "custom_shx":
74
+ # Implement SHX font loading here
75
+ # For now, return default font
76
+ return ImageFont.load_default()
77
+ else:
78
+ return ImageFont.truetype(font_info["path"], size)
79
+ except Exception as e:
80
+ print(f"Error loading font {font_name}: {e}")
81
  return ImageFont.load_default()
82
 
83
+ class TextEffects:
84
+ @staticmethod
85
+ def apply_shadow(draw: ImageDraw.ImageDraw, text: str, x: int, y: int,
86
+ font: ImageFont.FreeTypeFont, shadow_color: Tuple[int, int, int, int],
87
+ offset: int = 2):
88
+ """Apply shadow effect to text"""
89
+ draw.text((x + offset, y + offset), text, fill=shadow_color, font=font)
90
 
91
+ @staticmethod
92
+ def apply_outline(draw: ImageDraw.ImageDraw, text: str, x: int, y: int,
93
+ font: ImageFont.FreeTypeFont, outline_color: Tuple[int, int, int, int],
94
+ width: int = 1):
95
+ """Apply outline effect to text"""
96
+ for off_x, off_y in [(i, j) for i in range(-width, width+1)
97
+ for j in range(-width, width+1)
98
+ if (i, j) != (0, 0)]:
99
+ draw.text((x + off_x, y + off_y), text, fill=outline_color, font=font)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
+ @staticmethod
102
+ def apply_gradient(img: Image.Image, start_color: Tuple[int, int, int],
103
+ end_color: Tuple[int, int, int], direction: str = 'vertical'):
104
+ """Apply gradient effect to image"""
105
+ width, height = img.size
106
+ gradient = Image.new('RGBA', img.size, (0, 0, 0, 0))
107
+ draw = ImageDraw.Draw(gradient)
108
+
109
+ for i in range(height if direction == 'vertical' else width):
110
+ progress = i / (height if direction == 'vertical' else width)
111
+ color = tuple(int(start + (end - start) * progress)
112
+ for start, end in zip(start_color, end_color))
113
+ if direction == 'vertical':
114
+ draw.line([(0, i), (width, i)], fill=color)
115
+ else:
116
+ draw.line([(i, 0), (i, height)], fill=color)
117
+
118
+ return Image.alpha_composite(img, gradient)
 
 
 
 
 
119
 
120
+ class TextRenderer:
121
+ def __init__(self):
122
+ self.font_manager = FontManager()
123
+
124
+ def parse_color(self, color: str) -> Tuple[int, int, int]:
125
+ """Parse color string to RGB tuple"""
126
+ if isinstance(color, str):
127
+ if color.startswith('rgba'):
128
+ color = color.replace('rgba', '').strip('()').split(',')
129
+ return tuple(int(float(c.strip())) for c in color[:3])
130
+ elif color.startswith('#'):
131
+ color = color.lstrip('#')
132
+ return tuple(int(color[i:i+2], 16) for i in (0, 2, 4))
133
+ return color
134
+
135
+ def calculate_text_dimensions(self, text: str, font: ImageFont.FreeTypeFont,
136
+ max_width: int) -> Tuple[List[str], int, int]:
137
+ """Calculate text dimensions and wrap lines"""
138
+ lines = []
139
+ for line in text.split('\n'):
140
+ wrapped = textwrap.wrap(line, width=int(max_width / font.size * 1.8))
141
+ lines.extend(wrapped if wrapped else ['']) # Preserve empty lines
142
+
143
+ bbox = font.getbbox('Ay')
144
+ line_height = bbox[3] - bbox[1]
145
+ total_height = line_height * len(lines)
146
+
147
+ return lines, line_height, total_height
148
+
149
+ def create_text_segment(self, lines: List[str], start_idx: int, max_lines: int,
150
+ width: int, height: int, bg_color: Tuple[int, int, int, int],
151
+ text_color: Tuple[int, int, int, int], font_info: Dict,
152
+ font_size: int, align: str, margin: int,
153
+ effects: List[str]) -> Tuple[Image.Image, int]:
154
+ """Create a segment of text with applied effects"""
155
+ img = Image.new("RGBA", (width, height), color=bg_color)
156
+ draw = ImageDraw.Draw(img)
157
+
158
+ font = self.font_manager.get_font(font_info["name"], font_size)
159
+ bbox = font.getbbox('Ay')
160
+ line_height = bbox[3] - bbox[1]
161
+
162
+ y = margin
163
+ end_idx = min(start_idx + max_lines, len(lines))
164
+ segment_lines = lines[start_idx:end_idx]
165
+
166
+ for line in segment_lines:
167
+ bbox = font.getbbox(line)
168
+ line_width = bbox[2] - bbox[0]
169
+
170
+ if align == 'Left':
171
+ x = margin
172
+ elif align == 'Center':
173
+ x = (width - line_width) // 2
174
+ else: # Right alignment
175
+ x = width - line_width - margin
176
+
177
+ # Apply effects
178
+ if 'shadow' in effects:
179
+ TextEffects.apply_shadow(draw, line, x, y, font,
180
+ (0, 0, 0, 128))
181
+
182
+ if 'outline' in effects:
183
+ TextEffects.apply_outline(draw, line, x, y, font,
184
+ (0, 0, 0, 255))
185
+
186
+ if 'gradient' in effects:
187
+ # Save current position for gradient application
188
+ gradient_pos = (x, y, x + line_width, y + line_height)
189
+
190
+ draw.text((x, y), line, fill=text_color, font=font)
191
+
192
+ if 'gradient' in effects:
193
+ # Apply gradient to the text area
194
+ gradient_overlay = Image.new('RGBA', (line_width, line_height))
195
+ TextEffects.apply_gradient(gradient_overlay,
196
+ (255, 0, 0), (0, 0, 255))
197
+ img.paste(gradient_overlay, gradient_pos, gradient_overlay)
198
+
199
+ y += line_height
200
+
201
+ if 'blur' in effects:
202
+ img = img.filter(ImageFilter.GaussianBlur(radius=1))
203
+
204
+ return img, end_idx
205
+
206
+ def render_text_image(self, text: str, font_size: int, width: int, height: int,
207
+ bg_color: Tuple[int, int, int, int],
208
+ text_color: Tuple[int, int, int, int],
209
+ font_name: str, align: str, effects: List[str],
210
+ margin: int = DEFAULT_MARGIN) -> Image.Image:
211
+ """Render text to image with all effects"""
212
+ font_info = next((f for f in self.font_manager.get_font_list()
213
+ if f["name"] == font_name), None)
214
+ if not font_info:
215
+ font_info = {"name": "default", "path": None, "type": "system"}
216
+
217
+ font = self.font_manager.get_font(font_info["name"], font_size)
218
+ max_width = width - 2 * margin
219
+ lines, line_height, total_height = self.calculate_text_dimensions(
220
+ text, font, max_width)
221
+
222
+ max_lines_per_segment = (height - 2 * margin) // line_height
223
+ num_segments = math.ceil(len(lines) / max_lines_per_segment)
224
+
225
+ segments = []
226
+ current_line = 0
227
+
228
+ for i in range(num_segments):
229
+ segment_img, current_line = self.create_text_segment(
230
+ lines, current_line, max_lines_per_segment,
231
+ width, height, bg_color, text_color, font_info,
232
+ font_size, align, margin, effects
233
+ )
234
+ segments.append(segment_img)
235
+
236
+ total_height = len(segments) * height
237
+ final_image = Image.new("RGBA", (width, total_height), color=bg_color)
238
+
239
+ for i, segment in enumerate(segments):
240
+ final_image.paste(segment, (0, i * height), segment)
241
+
242
+ return final_image
243
+
244
+ def render_math_image(self, text: str, font_size: int, width: int, height: int,
245
+ bg_color: Tuple[int, int, int, int],
246
+ text_color: Tuple[int, int, int, int]) -> Image.Image:
247
+ """Render mathematical equations"""
248
+ fig, ax = plt.subplots(figsize=(width/100, height/100))
249
+ ax.set_facecolor(bg_color)
250
+ fig.patch.set_facecolor(bg_color)
251
+ ax.axis('off')
252
+
253
+ if not (text.startswith('$') and text.endswith('$')):
254
+ text = f'${text}$'
255
+
256
+ ax.text(0.5, 0.5, text, fontsize=font_size, ha='center', va='center',
257
+ color=text_color)
258
+
259
+ buf = BytesIO()
260
+ plt.savefig(buf, format='png', bbox_inches='tight', pad_inches=0,
261
+ transparent=True)
262
+ plt.close(fig)
263
+
264
+ buf.seek(0)
265
+ return Image.open(buf)
266
+
267
+ class TextToImageApp:
268
+ def __init__(self):
269
+ self.renderer = TextRenderer()
270
+ self.setup_interface()
271
+
272
+ def handle_font_upload(self, file):
273
+ """Handle font file upload"""
274
+ if file is not None:
275
+ self.renderer.font_manager.extract_fonts_from_zip(file.name)
276
+ return gr.Dropdown.update(
277
+ choices=[f["name"] for f in self.renderer.font_manager.get_font_list()]
278
+ )
279
+ return gr.Dropdown.update()
280
+
281
+ def convert_text(self, input_text: str, font_size: int, width: int, height: int,
282
+ bg_color: str, text_color: str, mode: str, font_name: str,
283
+ align: str, image_format: str, effects: List[str],
284
+ opacity: float) -> Image.Image:
285
+ """Convert text to image with all options"""
286
+ bg_color = (*self.renderer.parse_color(bg_color), int(opacity * 255))
287
+ text_color = (*self.renderer.parse_color(text_color), 255)
288
+
289
+ if mode == "Plain Text":
290
+ img = self.renderer.render_text_image(
291
+ input_text, font_size, width, height,
292
+ bg_color, text_color, font_name, align, effects
293
+ )
294
+ else: # LaTeX Math
295
+ img = self.renderer.render_math_image(
296
+ input_text, font_size, width, height,
297
+ bg_color, text_color
298
+ )
299
+
300
+ # Convert to selected format
301
+ if image_format == "JPEG":
302
+ # Convert to RGB for JPEG
303
+ bg = Image.new('RGB', img.size, (255, 255, 255))
304
+ bg.paste(img, mask=img.split()[3])
305
+ img = bg
306
+
307
+ return img
308
+
309
+ def setup_interface(self):
310
+ """Setup Gradio interface"""
311
+ with gr.Blocks() as self.demo:
312
+ gr.Markdown("# 🖼️ Advanced Text to Image Converter")
313
+
314
+ with gr.Row():
315
+ font_upload = gr.File(
316
+ label="Upload Fonts ZIP",
317
+ type="file",
318
+ file_types=[".zip"]
319
+ )
320
+
321
+ with gr.Row():
322
+ input_text = gr.Textbox(
323
+ label="Enter Text",
324
+ placeholder="Type or paste text here...",
325
+ lines=5
326
+ )
327
+
328
+ with gr.Row():
329
+ font_size = gr.Slider(10, 100, value=30, label="Font Size")
330
+ font_name = gr.Dropdown(
331
+ choices=[f["name"] for f in self.renderer.font_manager.get_font_list()],
332
+ label="Font"
333
+ )
334
+ align = gr.Radio(
335
+ ["Left", "Center", "Right"],
336
+ label="Text Alignment",
337
+ value="Center"
338
+ )
339
+
340
+ with gr.Row():
341
+ effects = gr.CheckboxGroup(
342
+ ["shadow", "outline", "gradient", "blur"],
343
+ label="Text Effects"
344
+ )
345
+ opacity = gr.Slider(0, 1, value=1, label="Background Opacity")
346
+ with gr.Row():
347
+ width = gr.Slider(200, 2000, value=800, label="Image Width")
348
+ height = gr.Slider(200, 2000, value=600, label="Base Height")
349
+
350
+ with gr.Row():
351
+ bg_color = gr.ColorPicker(label="Background Color", value="#FFFFFF")
352
+ text_color = gr.ColorPicker(label="Text Color", value="#000000")
353
+
354
+ with gr.Row():
355
+ mode = gr.Radio(
356
+ ["Plain Text", "LaTeX Math"],
357
+ label="Rendering Mode",
358
+ value="Plain Text"
359
+ )
360
+ image_format = gr.Radio(
361
+ ["PNG", "JPEG"],
362
+ label="Image Format",
363
+ value="PNG"
364
+ )
365
+
366
+ output_image = gr.Image(label="Generated Image")
367
+
368
+ # Connect components
369
+ font_upload.upload(
370
+ self.handle_font_upload,
371
+ inputs=[font_upload],
372
+ outputs=[font_name]
373
+ )
374
+
375
+ convert_button = gr.Button("Convert Text to Image")
376
+ convert_button.click(
377
+ self.convert_text,
378
+ inputs=[
379
+ input_text, font_size, width, height,
380
+ bg_color, text_color, mode, font_name,
381
+ align, image_format, effects, opacity
382
+ ],
383
+ outputs=output_image
384
+ )
385
+
386
+ def launch(self):
387
+ """Launch the Gradio interface"""
388
+ self.demo.launch()
389
+
390
+
391
+ class SHXFontRenderer:
392
+ """SHX Font Renderer for AutoCAD SHX fonts"""
393
 
394
+ def __init__(self, shx_file_path: str):
395
+ self.shx_file_path = shx_file_path
396
+ self.font_data = self.load_shx_file()
397
+
398
+ def load_shx_file(self) -> Dict:
399
+ """Load and parse SHX font file"""
400
+ font_data = {}
401
+ try:
402
+ with open(self.shx_file_path, 'rb') as f:
403
+ # Read SHX header
404
+ header = f.read(6)
405
+ if header[0:2] != b'\x1a\x00':
406
+ raise ValueError("Invalid SHX file format")
407
+
408
+ # Parse font records
409
+ while True:
410
+ record = self.read_record(f)
411
+ if not record:
412
+ break
413
+ if 'char_code' in record:
414
+ font_data[record['char_code']] = record['vectors']
415
+
416
+ except Exception as e:
417
+ print(f"Error loading SHX font: {e}")
418
+
419
+ return font_data
420
 
421
+ def read_record(self, file) -> Optional[Dict]:
422
+ """Read a single record from SHX file"""
423
+ try:
424
+ record_type = int.from_bytes(file.read(1), byteorder='little')
425
+ if record_type == 0: # EOF
426
+ return None
427
+
428
+ if record_type == 0x0A: # Character definition
429
+ char_code = int.from_bytes(file.read(2), byteorder='little')
430
+ vectors = self.read_vectors(file)
431
+ return {'char_code': char_code, 'vectors': vectors}
432
+
433
+ return {}
434
+
435
+ except Exception:
436
+ return None
437
 
438
+ def read_vectors(self, file) -> List[Dict]:
439
+ """Read vector data for a character"""
440
+ vectors = []
441
+ while True:
442
+ try:
443
+ vector_type = int.from_bytes(file.read(1), byteorder='little')
444
+ if vector_type == 0: # End of character
445
+ break
446
+
447
+ x = int.from_bytes(file.read(1), byteorder='little', signed=True)
448
+ y = int.from_bytes(file.read(1), byteorder='little', signed=True)
449
+
450
+ vectors.append({
451
+ 'type': vector_type,
452
+ 'x': x,
453
+ 'y': y
454
+ })
455
+
456
+ except Exception:
457
+ break
458
+
459
+ return vectors
460
 
461
+ def render_text(self, text: str, size: int, color: Tuple[int, int, int, int]) -> Image.Image:
462
+ """Render text using SHX font"""
463
+ # Calculate image size based on text and font size
464
+ scale = size / 16.0 # Standard SHX size is 16 units
465
+ img_width = int(len(text) * size * 0.75) # Approximate width
466
+ img_height = int(size * 1.5)
467
+
468
+ # Create image
469
+ img = Image.new('RGBA', (img_width, img_height), (0, 0, 0, 0))
470
+ draw = ImageDraw.Draw(img)
471
+
472
+ x_pos = 10
473
+ y_pos = img_height // 2
474
+
475
+ for char in text:
476
+ char_code = ord(char)
477
+ if char_code in self.font_data:
478
+ vectors = self.font_data[char_code]
479
+ last_pos = None
480
+
481
+ for vector in vectors:
482
+ if vector['type'] == 1: # Draw
483
+ if last_pos:
484
+ draw.line(
485
+ [
486
+ (x_pos + last_pos[0] * scale,
487
+ y_pos - last_pos[1] * scale),
488
+ (x_pos + vector['x'] * scale,
489
+ y_pos - vector['y'] * scale)
490
+ ],
491
+ fill=color,
492
+ width=max(1, int(scale / 4))
493
+ )
494
+ last_pos = (vector['x'], vector['y'])
495
+ else: # Move
496
+ last_pos = (vector['x'], vector['y'])
497
+
498
+ x_pos += size * 0.75 # Move to next character
499
+ else:
500
+ x_pos += size * 0.5 # Space for unknown characters
501
+
502
+ return img
503
+
504
+
505
+ def main():
506
+ """Main function to run the application"""
507
+ app = TextToImageApp()
508
+ app.launch()
509
+
510
+
511
+ if __name__ == "__main__":
512
+ main()