bakyt92 commited on
Commit
e85c04d
·
1 Parent(s): c0f9f62

refactor text_overlay.py

Browse files
Files changed (2) hide show
  1. app.py +1 -335
  2. text_overlay.py +272 -0
app.py CHANGED
@@ -3,8 +3,7 @@ import requests
3
  from PIL import Image, ImageDraw, ImageFont
4
  import io
5
  import openai
6
- import base64
7
- from io import BytesIO
8
 
9
  # Image Generation Functions
10
  def generate_ideogram_image(api_key, prompt, aspect_ratio="ASPECT_1_1"):
@@ -79,234 +78,6 @@ def process_uploaded_image(image):
79
  print(f"Error processing uploaded image: {e}")
80
  return image, f"Image uploaded with warning: {e}", image
81
 
82
- # Text Overlay Functions
83
- def add_text_to_image(img, pattern, line1, line2, line3, font_size, color, add_outline, font_name):
84
- """Overlay 2- or 3-line text on the image in a preset layout."""
85
- print("=== DEBUG: add_text_to_image called ===")
86
- print(f"Input image: {type(img)}")
87
- print(f"Pattern: {pattern}")
88
- print(f"Lines: [{line1!r}, {line2!r}, {line3!r}]")
89
- print(f"Font size: {font_size}")
90
- print(f"Font name: {font_name}")
91
- print(f"Color: {color!r}")
92
- print(f"Add outline: {add_outline}")
93
-
94
- if img is None:
95
- print("ERROR: No image provided")
96
- return None, "Please supply an image first."
97
-
98
- try:
99
- # Create a working copy
100
- print(f"Original image mode: {img.mode}, size: {img.size}")
101
- image = img.convert("RGB")
102
- print(f"Converted image mode: {image.mode}")
103
- draw = ImageDraw.Draw(image)
104
- print("ImageDraw created successfully")
105
-
106
- # UPDATED: Use selected font instead of hardcoded paths
107
- font = get_font_path(font_name, font_size)
108
- if font is None:
109
- return None, f"Could not load font: {font_name}"
110
-
111
- w, h = image.size
112
- print(f"Image dimensions: {w}x{h}")
113
-
114
- # Pattern matching (same as before)
115
- positions = []
116
- detected_pattern = "unknown"
117
-
118
- pattern_lower = pattern.lower()
119
-
120
- if ("2-lines-top" in pattern_lower) or ("2 lines - top" in pattern_lower):
121
- positions = [(w // 2, int(h * 0.10)), (w // 2, int(h * 0.20))]
122
- detected_pattern = "2-lines-top"
123
- elif ("2-lines-bottom" in pattern_lower) or ("2 lines - bottom" in pattern_lower):
124
- positions = [(w // 2, int(h * 0.80)), (w // 2, int(h * 0.90))]
125
- detected_pattern = "2-lines-bottom"
126
- elif ("2-lines-center" in pattern_lower) or ("2 lines - center" in pattern_lower):
127
- positions = [(w // 2, int(h * 0.45)), (w // 2, int(h * 0.55))]
128
- detected_pattern = "2-lines-center"
129
- elif ("3-lines-top" in pattern_lower) or ("3 lines - top" in pattern_lower):
130
- positions = [(w // 2, int(h * 0.10)), (w // 2, int(h * 0.20)), (w // 2, int(h * 0.30))]
131
- detected_pattern = "3-lines-top"
132
- elif ("3-lines-center" in pattern_lower) or ("3 lines - center" in pattern_lower):
133
- positions = [(w // 2, int(h * 0.40)), (w // 2, int(h * 0.50)), (w // 2, int(h * 0.60))]
134
- detected_pattern = "3-lines-center"
135
- elif ("3-lines-bottom" in pattern_lower) or ("3 lines - bottom" in pattern_lower):
136
- positions = [(w // 2, int(h * 0.70)), (w // 2, int(h * 0.80)), (w // 2, int(h * 0.90))]
137
- detected_pattern = "3-lines-bottom"
138
- else:
139
- print(f"WARNING: Unknown pattern '{pattern}', using 2-lines-center as default")
140
- positions = [(w // 2, int(h * 0.45)), (w // 2, int(h * 0.55))]
141
- detected_pattern = "2-lines-center (default)"
142
-
143
- print(f"Detected pattern: {detected_pattern}")
144
- print(f"Text positions calculated: {positions}")
145
-
146
- # Color parsing (same as before)
147
- original_color = color
148
- final_color = (255, 255, 255)
149
-
150
- if isinstance(color, str):
151
- if color.startswith('rgba(') and color.endswith(')'):
152
- print(f"Parsing RGBA string: {color}")
153
- try:
154
- rgba_part = color[5:-1]
155
- values = [float(x.strip()) for x in rgba_part.split(',')]
156
- if len(values) >= 3:
157
- final_color = (int(round(values[0])), int(round(values[1])), int(round(values[2])))
158
- print(f"Successfully parsed RGBA {color} to RGB: {final_color}")
159
- except (ValueError, IndexError) as e:
160
- print(f"Error parsing RGBA {color}: {e}, using white")
161
- final_color = (255, 255, 255)
162
- elif color.startswith('#'):
163
- print(f"Converting hex color {color}")
164
- hex_color = color.lstrip('#')
165
- if len(hex_color) == 6:
166
- try:
167
- final_color = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
168
- print(f"Successfully converted hex {color} to RGB: {final_color}")
169
- except ValueError as e:
170
- print(f"Invalid hex color {color}: {e}, using white")
171
- final_color = (255, 255, 255)
172
-
173
- print(f"Final color for drawing: {final_color}")
174
-
175
- # Draw text (same logic as before)
176
- lines = [line1, line2, line3]
177
- print(f"All input lines: {lines}")
178
-
179
- active_lines = []
180
- for i in range(len(positions)):
181
- if i < len(lines) and lines[i] and lines[i].strip():
182
- active_lines.append((positions[i], lines[i].strip()))
183
-
184
- print(f"Active lines to draw: {len(active_lines)} out of {len(positions)} positions")
185
-
186
- text_drawn = False
187
-
188
- for i, ((x, y), txt) in enumerate(active_lines):
189
- print(f"Drawing line {i+1}: '{txt}' at position ({x}, {y}) with font {font_name}")
190
-
191
- # Optional outline/stroke
192
- if add_outline:
193
- stroke_width = max(1, font_size // 20)
194
- print(f"Adding outline with stroke width: {stroke_width}")
195
-
196
- # Draw text outline (black)
197
- for dx in [-stroke_width, 0, stroke_width]:
198
- for dy in [-stroke_width, 0, stroke_width]:
199
- if dx != 0 or dy != 0:
200
- try:
201
- draw.text((x + dx, y + dy), txt, fill=(0, 0, 0), font=font, anchor="mm")
202
- except TypeError:
203
- try:
204
- bbox = draw.textbbox((0, 0), txt, font=font)
205
- text_w = bbox[2] - bbox[0]
206
- text_h = bbox[3] - bbox[1]
207
- draw.text((x - text_w//2 + dx, y - text_h//2 + dy), txt, fill=(0, 0, 0), font=font)
208
- except Exception:
209
- draw.text((x + dx, y + dy), txt, fill=(0, 0, 0), font=font)
210
-
211
- # Draw main text
212
- try:
213
- draw.text((x, y), txt, fill=final_color, font=font, anchor="mm")
214
- print(f"SUCCESS: Main text drawn with {font_name} at ({x}, {y})")
215
- except TypeError:
216
- try:
217
- bbox = draw.textbbox((0, 0), txt, font=font)
218
- text_w = bbox[2] - bbox[0]
219
- text_h = bbox[3] - bbox[1]
220
- fallback_x = x - text_w//2
221
- fallback_y = y - text_h//2
222
- draw.text((fallback_x, fallback_y), txt, fill=final_color, font=font)
223
- print(f"SUCCESS: Fallback text drawn with {font_name} at ({fallback_x}, {fallback_y})")
224
- except Exception:
225
- draw.text((x, y), txt, fill=final_color, font=font)
226
- print(f"SUCCESS: Basic text drawn with {font_name} at ({x}, {y})")
227
-
228
- text_drawn = True
229
- print(f"Completed drawing line {i+1}")
230
-
231
- if not text_drawn:
232
- print("No text was drawn - all lines were empty")
233
- return img, "No text to add (all lines were empty)"
234
-
235
- print("=== Text drawing completed successfully ===")
236
- return image, f"✅ {len(active_lines)} lines added! Font: {font_name}, Pattern: {detected_pattern}"
237
-
238
- except Exception as e:
239
- print(f"CRITICAL ERROR in add_text_to_image: {e}")
240
- import traceback
241
- traceback.print_exc()
242
- return None, f"Error adding text: {str(e)}"
243
-
244
- def test_text_overlay():
245
- """Test function to verify text overlay works"""
246
- print("=== Testing basic text overlay ===")
247
- try:
248
- # Create a simple test image
249
- from PIL import Image, ImageDraw, ImageFont
250
- test_img = Image.new('RGB', (400, 300), color='blue')
251
- draw = ImageDraw.Draw(test_img)
252
-
253
- # Try to draw simple text
254
- try:
255
- font = ImageFont.load_default()
256
- draw.text((200, 150), "TEST", fill='white', font=font, anchor="mm")
257
- print("Basic text drawing works!")
258
- return test_img, "Test successful", None
259
- except Exception as e:
260
- print(f"Basic text drawing failed: {e}")
261
- draw.text((50, 100), "TEST", fill='white')
262
- print("Fallback text drawing works!")
263
- return test_img, "Test with fallback", None
264
-
265
- except Exception as e:
266
- print(f"Test failed completely: {e}")
267
- return None, f"Test failed: {e}", None
268
-
269
- # Function to create template images
270
- def create_pattern_template(pattern, width=200, height=120):
271
- """Create a visual template showing text placement for each pattern"""
272
- # Create black background
273
- img = Image.new('RGB', (width, height), color='black')
274
- draw = ImageDraw.Draw(img)
275
-
276
- # Define positions based on pattern
277
- positions = []
278
- if pattern == "2-lines-top":
279
- positions = [(width // 2, int(height * 0.15)), (width // 2, int(height * 0.35))]
280
- elif pattern == "2-lines-center":
281
- positions = [(width // 2, int(height * 0.35)), (width // 2, int(height * 0.65))]
282
- elif pattern == "2-lines-bottom":
283
- positions = [(width // 2, int(height * 0.65)), (width // 2, int(height * 0.85))]
284
- elif pattern == "3-lines-top":
285
- positions = [(width // 2, int(height * 0.15)), (width // 2, int(height * 0.35)), (width // 2, int(height * 0.55))]
286
- elif pattern == "3-lines-center":
287
- positions = [(width // 2, int(height * 0.25)), (width // 2, int(height * 0.50)), (width // 2, int(height * 0.75))]
288
- elif pattern == "3-lines-bottom":
289
- positions = [(width // 2, int(height * 0.45)), (width // 2, int(height * 0.65)), (width // 2, int(height * 0.85))]
290
-
291
- # Draw white rectangles to represent text boxes
292
- for i, (x, y) in enumerate(positions):
293
- # Draw white rectangle as text placeholder
294
- box_width = 60
295
- box_height = 12
296
- left = x - box_width // 2
297
- top = y - box_height // 2
298
- right = x + box_width // 2
299
- bottom = y + box_height // 2
300
-
301
- draw.rectangle([left, top, right, bottom], fill='white', outline='white')
302
-
303
- return img
304
-
305
- # Create templates
306
- def get_pattern_template(pattern):
307
- """Get template for pattern - create on demand to avoid initialization issues"""
308
- return create_pattern_template(pattern)
309
-
310
  # Fix the image passing with better error handling
311
  def safe_image_copy(img):
312
  """Safely copy an image to avoid corruption issues"""
@@ -328,100 +99,6 @@ def safe_image_copy(img):
328
  print(f"Error copying image: {e}")
329
  return img
330
 
331
- # Function to convert PIL image to base64 for Gradio
332
- def pil_to_base64(img):
333
- """Convert PIL image to base64 string for Gradio"""
334
- buffer = BytesIO()
335
- img.save(buffer, format='PNG')
336
- img_str = base64.b64encode(buffer.getvalue()).decode()
337
- return f"data:image/png;base64,{img_str}"
338
-
339
- # Create all template images
340
- pattern_templates = {}
341
- pattern_names = ["2-lines-top", "2-lines-center", "2-lines-bottom", "3-lines-top", "3-lines-center", "3-lines-bottom"]
342
-
343
- for pattern in pattern_names:
344
- template_img = create_pattern_template(pattern)
345
- pattern_templates[pattern] = template_img
346
-
347
- # Function to update template display
348
- def update_pattern_template(selected_pattern):
349
- """Return the template image for the selected pattern"""
350
- if selected_pattern in pattern_templates:
351
- return pattern_templates[selected_pattern]
352
- else:
353
- # Return default template
354
- return create_pattern_template("2-lines-top")
355
-
356
- # Add this function to get available fonts with fallbacks
357
- def get_font_path(font_name, font_size):
358
- """Get font path for the selected font with fallbacks"""
359
- font_mappings = {
360
- "Impact": [
361
- "impact.ttf",
362
- "Impact.ttf",
363
- "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
364
- "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
365
- ],
366
- "Arial Bold": [
367
- "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
368
- "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
369
- "arial-bold.ttf"
370
- ],
371
- "Times Bold": [
372
- "/usr/share/fonts/truetype/liberation/LiberationSerif-Bold.ttf",
373
- "/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf",
374
- "times-bold.ttf"
375
- ],
376
- "Comic Sans": [
377
- "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
378
- "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
379
- "comic.ttf"
380
- ],
381
- "Bebas Neue": [
382
- "/usr/share/fonts/truetype/dejavu/DejaVuSansCondensed-Bold.ttf",
383
- "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
384
- "bebas-neue.ttf"
385
- ]
386
- }
387
-
388
- # Get font paths for the selected font
389
- font_paths = font_mappings.get(font_name, [])
390
-
391
- # Try each path
392
- for font_path in font_paths:
393
- try:
394
- font = ImageFont.truetype(font_path, font_size)
395
- print(f"SUCCESS: Loaded {font_name} from {font_path}")
396
- return font
397
- except (OSError, IOError):
398
- continue
399
-
400
- # If no specific font found, try default system fonts
401
- default_paths = [
402
- "DejaVuSans-Bold.ttf",
403
- "arial.ttf",
404
- "/System/Library/Fonts/Arial.ttf",
405
- "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
406
- ]
407
-
408
- for font_path in default_paths:
409
- try:
410
- font = ImageFont.truetype(font_path, font_size)
411
- print(f"FALLBACK: Loaded default font from {font_path} for {font_name}")
412
- return font
413
- except (OSError, IOError):
414
- continue
415
-
416
- # Final fallback
417
- try:
418
- font = ImageFont.load_default()
419
- print(f"FINAL FALLBACK: Using system default for {font_name}")
420
- return font
421
- except Exception:
422
- print(f"ERROR: Could not load any font for {font_name}")
423
- return None
424
-
425
  # Create the Gradio Interface
426
  with gr.Blocks(title="AI Image Generator & Text Overlay") as demo:
427
  gr.Markdown("# 🎨 AI Image Generator & Text Overlay")
@@ -609,17 +286,6 @@ with gr.Blocks(title="AI Image Generator & Text Overlay") as demo:
609
  outputs=[text_image, status_message]
610
  )
611
 
612
- # In the Text Overlay tab, add this button for testing:
613
- with gr.Row():
614
- test_btn = gr.Button("🔧 Test Text Overlay", variant="secondary")
615
-
616
- # Add this event handler:
617
- test_btn.click(
618
- test_text_overlay,
619
- inputs=[],
620
- outputs=[text_image, status_message]
621
- )
622
-
623
  # FIXED: Move demo.load() INSIDE the Blocks context
624
  demo.load(
625
  lambda: get_pattern_template("2-lines-top"),
 
3
  from PIL import Image, ImageDraw, ImageFont
4
  import io
5
  import openai
6
+ from text_overlay import create_pattern_template, get_pattern_template, add_text_to_image, get_font_path
 
7
 
8
  # Image Generation Functions
9
  def generate_ideogram_image(api_key, prompt, aspect_ratio="ASPECT_1_1"):
 
78
  print(f"Error processing uploaded image: {e}")
79
  return image, f"Image uploaded with warning: {e}", image
80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  # Fix the image passing with better error handling
82
  def safe_image_copy(img):
83
  """Safely copy an image to avoid corruption issues"""
 
99
  print(f"Error copying image: {e}")
100
  return img
101
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  # Create the Gradio Interface
103
  with gr.Blocks(title="AI Image Generator & Text Overlay") as demo:
104
  gr.Markdown("# 🎨 AI Image Generator & Text Overlay")
 
286
  outputs=[text_image, status_message]
287
  )
288
 
 
 
 
 
 
 
 
 
 
 
 
289
  # FIXED: Move demo.load() INSIDE the Blocks context
290
  demo.load(
291
  lambda: get_pattern_template("2-lines-top"),
text_overlay.py ADDED
@@ -0,0 +1,272 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PIL import Image, ImageDraw, ImageFont
2
+
3
+ # Add this function to get available fonts with fallbacks
4
+ def get_font_path(font_name, font_size):
5
+ """Get font path for the selected font with fallbacks"""
6
+ font_mappings = {
7
+ "Impact": [
8
+ "impact.ttf",
9
+ "Impact.ttf",
10
+ "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
11
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
12
+ ],
13
+ "Arial Bold": [
14
+ "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
15
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
16
+ "arial-bold.ttf"
17
+ ],
18
+ "Times Bold": [
19
+ "/usr/share/fonts/truetype/liberation/LiberationSerif-Bold.ttf",
20
+ "/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf",
21
+ "times-bold.ttf"
22
+ ],
23
+ "Comic Sans": [
24
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
25
+ "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
26
+ "comic.ttf"
27
+ ],
28
+ "Bebas Neue": [
29
+ "/usr/share/fonts/truetype/dejavu/DejaVuSansCondensed-Bold.ttf",
30
+ "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
31
+ "bebas-neue.ttf"
32
+ ]
33
+ }
34
+
35
+ # Get font paths for the selected font
36
+ font_paths = font_mappings.get(font_name, [])
37
+
38
+ # Try each path
39
+ for font_path in font_paths:
40
+ try:
41
+ font = ImageFont.truetype(font_path, font_size)
42
+ print(f"SUCCESS: Loaded {font_name} from {font_path}")
43
+ return font
44
+ except (OSError, IOError):
45
+ continue
46
+
47
+ # If no specific font found, try default system fonts
48
+ default_paths = [
49
+ "DejaVuSans-Bold.ttf",
50
+ "arial.ttf",
51
+ "/System/Library/Fonts/Arial.ttf",
52
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
53
+ ]
54
+
55
+ for font_path in default_paths:
56
+ try:
57
+ font = ImageFont.truetype(font_path, font_size)
58
+ print(f"FALLBACK: Loaded default font from {font_path} for {font_name}")
59
+ return font
60
+ except (OSError, IOError):
61
+ continue
62
+
63
+ # Final fallback
64
+ try:
65
+ font = ImageFont.load_default()
66
+ print(f"FINAL FALLBACK: Using system default for {font_name}")
67
+ return font
68
+ except Exception:
69
+ print(f"ERROR: Could not load any font for {font_name}")
70
+ return None
71
+
72
+ # Function to create template images
73
+ def create_pattern_template(pattern, width=200, height=120):
74
+ """Create a visual template showing text placement for each pattern"""
75
+ # Create black background
76
+ img = Image.new('RGB', (width, height), color='black')
77
+ draw = ImageDraw.Draw(img)
78
+
79
+ # Define positions based on pattern
80
+ positions = []
81
+ if pattern == "2-lines-top":
82
+ positions = [(width // 2, int(height * 0.15)), (width // 2, int(height * 0.35))]
83
+ elif pattern == "2-lines-center":
84
+ positions = [(width // 2, int(height * 0.35)), (width // 2, int(height * 0.65))]
85
+ elif pattern == "2-lines-bottom":
86
+ positions = [(width // 2, int(height * 0.65)), (width // 2, int(height * 0.85))]
87
+ elif pattern == "3-lines-top":
88
+ positions = [(width // 2, int(height * 0.15)), (width // 2, int(height * 0.35)), (width // 2, int(height * 0.55))]
89
+ elif pattern == "3-lines-center":
90
+ positions = [(width // 2, int(height * 0.25)), (width // 2, int(height * 0.50)), (width // 2, int(height * 0.75))]
91
+ elif pattern == "3-lines-bottom":
92
+ positions = [(width // 2, int(height * 0.45)), (width // 2, int(height * 0.65)), (width // 2, int(height * 0.85))]
93
+
94
+ # Draw white rectangles to represent text boxes
95
+ for i, (x, y) in enumerate(positions):
96
+ # Draw white rectangle as text placeholder
97
+ box_width = 60
98
+ box_height = 12
99
+ left = x - box_width // 2
100
+ top = y - box_height // 2
101
+ right = x + box_width // 2
102
+ bottom = y + box_height // 2
103
+
104
+ draw.rectangle([left, top, right, bottom], fill='white', outline='white')
105
+
106
+ return img
107
+
108
+ def get_pattern_template(pattern):
109
+ """Get template for pattern - create on demand to avoid initialization issues"""
110
+ return create_pattern_template(pattern)
111
+
112
+ # Text Overlay Functions
113
+ def add_text_to_image(img, pattern, line1, line2, line3, font_size, color, add_outline, font_name):
114
+ """Overlay 2- or 3-line text on the image in a preset layout."""
115
+ print("=== DEBUG: add_text_to_image called ===")
116
+ print(f"Input image: {type(img)}")
117
+ print(f"Pattern: {pattern}")
118
+ print(f"Lines: [{line1!r}, {line2!r}, {line3!r}]")
119
+ print(f"Font size: {font_size}")
120
+ print(f"Font name: {font_name}")
121
+ print(f"Color: {color!r}")
122
+ print(f"Add outline: {add_outline}")
123
+
124
+ if img is None:
125
+ print("ERROR: No image provided")
126
+ return None, "Please supply an image first."
127
+
128
+ try:
129
+ # Create a working copy
130
+ print(f"Original image mode: {img.mode}, size: {img.size}")
131
+ image = img.convert("RGB")
132
+ print(f"Converted image mode: {image.mode}")
133
+ draw = ImageDraw.Draw(image)
134
+ print("ImageDraw created successfully")
135
+
136
+ # UPDATED: Use selected font instead of hardcoded paths
137
+ font = get_font_path(font_name, font_size)
138
+ if font is None:
139
+ return None, f"Could not load font: {font_name}"
140
+
141
+ w, h = image.size
142
+ print(f"Image dimensions: {w}x{h}")
143
+
144
+ # Pattern matching (same as before)
145
+ positions = []
146
+ detected_pattern = "unknown"
147
+
148
+ pattern_lower = pattern.lower()
149
+
150
+ if ("2-lines-top" in pattern_lower) or ("2 lines - top" in pattern_lower):
151
+ positions = [(w // 2, int(h * 0.10)), (w // 2, int(h * 0.20))]
152
+ detected_pattern = "2-lines-top"
153
+ elif ("2-lines-bottom" in pattern_lower) or ("2 lines - bottom" in pattern_lower):
154
+ positions = [(w // 2, int(h * 0.80)), (w // 2, int(h * 0.90))]
155
+ detected_pattern = "2-lines-bottom"
156
+ elif ("2-lines-center" in pattern_lower) or ("2 lines - center" in pattern_lower):
157
+ positions = [(w // 2, int(h * 0.45)), (w // 2, int(h * 0.55))]
158
+ detected_pattern = "2-lines-center"
159
+ elif ("3-lines-top" in pattern_lower) or ("3 lines - top" in pattern_lower):
160
+ positions = [(w // 2, int(h * 0.10)), (w // 2, int(h * 0.20)), (w // 2, int(h * 0.30))]
161
+ detected_pattern = "3-lines-top"
162
+ elif ("3-lines-center" in pattern_lower) or ("3 lines - center" in pattern_lower):
163
+ positions = [(w // 2, int(h * 0.40)), (w // 2, int(h * 0.50)), (w // 2, int(h * 0.60))]
164
+ detected_pattern = "3-lines-center"
165
+ elif ("3-lines-bottom" in pattern_lower) or ("3 lines - bottom" in pattern_lower):
166
+ positions = [(w // 2, int(h * 0.70)), (w // 2, int(h * 0.80)), (w // 2, int(h * 0.90))]
167
+ detected_pattern = "3-lines-bottom"
168
+ else:
169
+ print(f"WARNING: Unknown pattern '{pattern}', using 2-lines-center as default")
170
+ positions = [(w // 2, int(h * 0.45)), (w // 2, int(h * 0.55))]
171
+ detected_pattern = "2-lines-center (default)"
172
+
173
+ print(f"Detected pattern: {detected_pattern}")
174
+ print(f"Text positions calculated: {positions}")
175
+
176
+ # Color parsing (same as before)
177
+ original_color = color
178
+ final_color = (255, 255, 255)
179
+
180
+ if isinstance(color, str):
181
+ if color.startswith('rgba(') and color.endswith(')'):
182
+ print(f"Parsing RGBA string: {color}")
183
+ try:
184
+ rgba_part = color[5:-1]
185
+ values = [float(x.strip()) for x in rgba_part.split(',')]
186
+ if len(values) >= 3:
187
+ final_color = (int(round(values[0])), int(round(values[1])), int(round(values[2])))
188
+ print(f"Successfully parsed RGBA {color} to RGB: {final_color}")
189
+ except (ValueError, IndexError) as e:
190
+ print(f"Error parsing RGBA {color}: {e}, using white")
191
+ final_color = (255, 255, 255)
192
+ elif color.startswith('#'):
193
+ print(f"Converting hex color {color}")
194
+ hex_color = color.lstrip('#')
195
+ if len(hex_color) == 6:
196
+ try:
197
+ final_color = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
198
+ print(f"Successfully converted hex {color} to RGB: {final_color}")
199
+ except ValueError as e:
200
+ print(f"Invalid hex color {color}: {e}, using white")
201
+ final_color = (255, 255, 255)
202
+
203
+ print(f"Final color for drawing: {final_color}")
204
+
205
+ # Draw text (same logic as before)
206
+ lines = [line1, line2, line3]
207
+ print(f"All input lines: {lines}")
208
+
209
+ active_lines = []
210
+ for i in range(len(positions)):
211
+ if i < len(lines) and lines[i] and lines[i].strip():
212
+ active_lines.append((positions[i], lines[i].strip()))
213
+
214
+ print(f"Active lines to draw: {len(active_lines)} out of {len(positions)} positions")
215
+
216
+ text_drawn = False
217
+
218
+ for i, ((x, y), txt) in enumerate(active_lines):
219
+ print(f"Drawing line {i+1}: '{txt}' at position ({x}, {y}) with font {font_name}")
220
+
221
+ # Optional outline/stroke
222
+ if add_outline:
223
+ stroke_width = max(1, font_size // 20)
224
+ print(f"Adding outline with stroke width: {stroke_width}")
225
+
226
+ # Draw text outline (black)
227
+ for dx in [-stroke_width, 0, stroke_width]:
228
+ for dy in [-stroke_width, 0, stroke_width]:
229
+ if dx != 0 or dy != 0:
230
+ try:
231
+ draw.text((x + dx, y + dy), txt, fill=(0, 0, 0), font=font, anchor="mm")
232
+ except TypeError:
233
+ try:
234
+ bbox = draw.textbbox((0, 0), txt, font=font)
235
+ text_w = bbox[2] - bbox[0]
236
+ text_h = bbox[3] - bbox[1]
237
+ draw.text((x - text_w//2 + dx, y - text_h//2 + dy), txt, fill=(0, 0, 0), font=font)
238
+ except Exception:
239
+ draw.text((x + dx, y + dy), txt, fill=(0, 0, 0), font=font)
240
+
241
+ # Draw main text
242
+ try:
243
+ draw.text((x, y), txt, fill=final_color, font=font, anchor="mm")
244
+ print(f"SUCCESS: Main text drawn with {font_name} at ({x}, {y})")
245
+ except TypeError:
246
+ try:
247
+ bbox = draw.textbbox((0, 0), txt, font=font)
248
+ text_w = bbox[2] - bbox[0]
249
+ text_h = bbox[3] - bbox[1]
250
+ fallback_x = x - text_w//2
251
+ fallback_y = y - text_h//2
252
+ draw.text((fallback_x, fallback_y), txt, fill=final_color, font=font)
253
+ print(f"SUCCESS: Fallback text drawn with {font_name} at ({fallback_x}, {fallback_y})")
254
+ except Exception:
255
+ draw.text((x, y), txt, fill=final_color, font=font)
256
+ print(f"SUCCESS: Basic text drawn with {font_name} at ({x}, {y})")
257
+
258
+ text_drawn = True
259
+ print(f"Completed drawing line {i+1}")
260
+
261
+ if not text_drawn:
262
+ print("No text was drawn - all lines were empty")
263
+ return img, "No text to add (all lines were empty)"
264
+
265
+ print("=== Text drawing completed successfully ===")
266
+ return image, f"✅ {len(active_lines)} lines added! Font: {font_name}, Pattern: {detected_pattern}"
267
+
268
+ except Exception as e:
269
+ print(f"CRITICAL ERROR in add_text_to_image: {e}")
270
+ import traceback
271
+ traceback.print_exc()
272
+ return None, f"Error adding text: {str(e)}"