awacke1 commited on
Commit
1f6e4e7
·
verified ·
1 Parent(s): 04a1711

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +122 -34
app.py CHANGED
@@ -11,7 +11,7 @@ import io
11
  from pypdf import PdfWriter
12
  import random
13
 
14
- from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak, BaseDocTemplate, Frame, PageTemplate, Image as ReportLabImage
15
  from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
16
  from reportlab.lib.pagesizes import letter, A4, legal, landscape
17
  from reportlab.lib.units import inch
@@ -41,16 +41,24 @@ PREVIEW_DIR.mkdir(exist_ok=True)
41
 
42
  # --- Font & Emoji Handling ---
43
 
 
 
 
 
44
  def register_local_fonts():
45
  """Finds and registers all .ttf files from the application's base directory."""
 
46
  print("--- Font Registration Process Starting ---")
47
  text_font_names = []
48
  emoji_font_name = None
49
 
50
  noto_emoji_path = FONT_DIR / "NotoColorEmoji-Regular.ttf"
51
  if not noto_emoji_path.exists():
52
- print(f"Warning: Emoji font not found at {noto_emoji_path}. Emojis may not render correctly.")
53
- print("Please download 'NotoColorEmoji-Regular.ttf' and place it in the application directory.")
 
 
 
54
 
55
  print(f"Scanning for fonts in: {FONT_DIR.absolute()}")
56
  font_files = list(FONT_DIR.glob("*.ttf"))
@@ -81,24 +89,49 @@ def register_local_fonts():
81
  print("--- Font Registration Process Finished ---")
82
  return sorted(text_font_names), emoji_font_name
83
 
84
- def apply_emoji_font(text: str, emoji_font_name: str) -> str:
85
- """Intelligently wraps emoji characters in a <font> tag to ensure they render correctly."""
86
- if not emoji_font_name:
87
- return text
88
- emoji_pattern = re.compile(f"([{re.escape(''.join(map(chr, range(0x1f600, 0x1f650))))}"
89
- f"{re.escape(''.join(map(chr, range(0x1f300, 0x1f5ff))))}"
90
- f"{re.escape(''.join(map(chr, range(0x1f900, 0x1f9ff))))}"
91
- f"{re.escape(''.join(map(chr, range(0x2600, 0x26ff))))}"
92
- f"{re.escape(''.join(map(chr, range(0x2700, 0x27bf))))}]+)")
93
- return emoji_pattern.sub(fr'<font name="{emoji_font_name}">\1</font>', text)
 
94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
  # --- AI Content Generation (Simulation) ---
97
 
98
  def generate_ai_content_api(prompt):
99
  """
100
  Simulates a call to an LLM to generate markdown content.
101
- In a real application, this would contain `fetch` calls to a generative AI API.
102
  """
103
  if not prompt:
104
  return "# The Golem awaits your command!\n\nPlease enter a prompt in the box above and click '🧠 Animate Golem!' to get started. I can help you write reports, stories, poems, and more! ✨"
@@ -144,17 +177,14 @@ def _draw_header_footer(canvas, doc, header_text, footer_text, title):
144
  canvas.saveState()
145
  page_num = canvas.getPageNumber()
146
 
147
- # Replace variables for header and footer. Note: [Total Pages] is only accurate at the end of the build.
148
  final_footer_text = footer_text.replace("[Page #]", str(page_num)).replace("[Total Pages]", str(doc.page))
149
  final_header_text = header_text.replace("[Page #]", str(page_num)).replace("[Title]", title)
150
 
151
- # Header
152
  if final_header_text:
153
  canvas.setFont('Helvetica', 9)
154
  canvas.setFillColor(colors.grey)
155
  canvas.drawRightString(doc.width + doc.leftMargin, doc.height + doc.topMargin + 0.25*inch, final_header_text)
156
 
157
- # Footer
158
  if final_footer_text:
159
  canvas.setFont('Helvetica', 9)
160
  canvas.setFillColor(colors.grey)
@@ -163,7 +193,7 @@ def _draw_header_footer(canvas, doc, header_text, footer_text, title):
163
  canvas.restoreState()
164
 
165
  def markdown_to_story(markdown_text: str, font_name: str, emoji_font: str, font_size_body: int, font_size_h1: int, font_size_h2: int, font_size_h3: int):
166
- """Converts markdown to a ReportLab story with enhanced, user-configurable styling."""
167
  styles = getSampleStyleSheet()
168
 
169
  leading_body = font_size_body * 1.4
@@ -174,6 +204,44 @@ def markdown_to_story(markdown_text: str, font_name: str, emoji_font: str, font_
174
  style_code = ParagraphStyle('Code', fontName='Courier', backColor=colors.HexColor("#333333"), textColor=colors.HexColor("#f472b6"), borderWidth=1, borderColor=colors.HexColor("#444444"), padding=8, leading=12, fontSize=9)
175
  style_table_header = ParagraphStyle('TableHeader', parent=style_normal, fontName=f"{font_name}-Bold" if font_name != 'Helvetica' else 'Helvetica-Bold')
176
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  story = []
178
  lines = markdown_text.split('\n')
179
 
@@ -205,18 +273,21 @@ def markdown_to_story(markdown_text: str, font_name: str, emoji_font: str, font_
205
  if in_table:
206
  in_table = False
207
  if table_data:
208
- header_content = [apply_emoji_font(re.sub(r'\*\*(.*?)\*\*', r'<b>\1</b>', cell), emoji_font) for cell in table_data[0]]
209
- header = [Paragraph(cell, style_table_header) for cell in header_content]
210
-
211
- formatted_rows = [
212
- [Paragraph(apply_emoji_font(re.sub(r'\*\*(.*?)\*\*', r'<b>\1</b>', cell), emoji_font), style_normal) for cell in row]
213
- for row in table_data[1:]
214
- ]
215
-
216
- table = Table([header] + formatted_rows, hAlign='LEFT', repeatRows=1)
 
 
 
217
  table.setStyle(TableStyle([
218
  ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#4a044e")),
219
- ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
220
  ('GRID', (0, 0), (-1, -1), 1, colors.HexColor("#6b21a8")),
221
  ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
222
  ('TOPPADDING', (0,0), (-1,-1), 6),
@@ -224,10 +295,12 @@ def markdown_to_story(markdown_text: str, font_name: str, emoji_font: str, font_
224
  ]))
225
  story.append(table); story.append(Spacer(1, 0.2 * inch))
226
  table_data = []
 
 
227
 
228
  if not stripped_line: continue
229
 
230
- content, style, extra_args = stripped_line, style_normal, {}
231
 
232
  if stripped_line.startswith("# "):
233
  if not first_heading: story.append(PageBreak())
@@ -236,11 +309,21 @@ def markdown_to_story(markdown_text: str, font_name: str, emoji_font: str, font_
236
  first_heading = False
237
  elif stripped_line.startswith("## "): content, style = stripped_line.lstrip('## '), style_h2
238
  elif stripped_line.startswith("### "): content, style = stripped_line.lstrip('### '), style_h3
239
- elif stripped_line.startswith(("- ", "* ")): content, extra_args['bulletText'] = stripped_line[2:], '•'
 
 
240
 
241
- formatted_content = re.sub(r'_(.*?)_', r'<i>\1</i>', re.sub(r'\*\*(.*?)\*\*', r'<b>\1</b>', content))
242
- final_content = apply_emoji_font(formatted_content, emoji_font)
243
- story.append(Paragraph(final_content, style, **extra_args))
 
 
 
 
 
 
 
 
244
 
245
  return story, document_title
246
 
@@ -267,6 +350,7 @@ def generate_pdfs_api(files, ai_content, layouts, fonts, num_columns, header_tex
267
  if not files and not ai_content.strip(): raise gr.Error("Please conjure some content or upload an image before alchemizing!")
268
  if not layouts: raise gr.Error("You must select a scroll (page layout)!")
269
  if not fonts: raise gr.Error("A scribe needs a font! Please choose one.")
 
270
 
271
  shutil.rmtree(OUTPUT_DIR, ignore_errors=True); shutil.rmtree(PREVIEW_DIR, ignore_errors=True)
272
  OUTPUT_DIR.mkdir(); PREVIEW_DIR.mkdir()
@@ -276,6 +360,9 @@ def generate_pdfs_api(files, ai_content, layouts, fonts, num_columns, header_tex
276
 
277
  log_updates, generated_pdf_paths = "", []
278
 
 
 
 
279
  for layout_name in progress.tqdm(layouts, desc=" brewing potions..."):
280
  for font_name in progress.tqdm(fonts, desc=f" enchanting scrolls with {layout_name}..."):
281
  merger = PdfWriter()
@@ -424,8 +511,9 @@ with gr.Blocks(theme=theme, title="The PDF Alchemist") as demo:
424
  if __name__ == "__main__":
425
  if not (FONT_DIR / "NotoColorEmoji-Regular.ttf").exists():
426
  print("\n" + "="*80)
427
- print("WARNING: 'NotoColorEmoji-Regular.ttf' not found.")
428
  print("Please download it from Google Fonts and place it in the script's directory for emojis to render correctly.")
 
429
  print("="*80 + "\n")
430
  if not any("MedievalSharp" in s for s in AVAILABLE_FONTS):
431
  print("\n" + "="*80)
 
11
  from pypdf import PdfWriter
12
  import random
13
 
14
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak, BaseDocTemplate, Frame, PageTemplate, Image as ReportLabImage, Flowable
15
  from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
16
  from reportlab.lib.pagesizes import letter, A4, legal, landscape
17
  from reportlab.lib.units import inch
 
41
 
42
  # --- Font & Emoji Handling ---
43
 
44
+ # Global cache for rendered emoji images to avoid re-rendering the same emoji
45
+ EMOJI_IMAGE_CACHE = {}
46
+ EMOJI_FONT_PATH = None # Will be set in register_local_fonts
47
+
48
  def register_local_fonts():
49
  """Finds and registers all .ttf files from the application's base directory."""
50
+ global EMOJI_FONT_PATH
51
  print("--- Font Registration Process Starting ---")
52
  text_font_names = []
53
  emoji_font_name = None
54
 
55
  noto_emoji_path = FONT_DIR / "NotoColorEmoji-Regular.ttf"
56
  if not noto_emoji_path.exists():
57
+ print(f"CRITICAL: Color Emoji font not found at {noto_emoji_path}.")
58
+ print("Please download 'NotoColorEmoji-Regular.ttf' and place it in the application directory for color emojis to work.")
59
+ else:
60
+ EMOJI_FONT_PATH = str(noto_emoji_path)
61
+
62
 
63
  print(f"Scanning for fonts in: {FONT_DIR.absolute()}")
64
  font_files = list(FONT_DIR.glob("*.ttf"))
 
89
  print("--- Font Registration Process Finished ---")
90
  return sorted(text_font_names), emoji_font_name
91
 
92
+ def render_emoji_as_image(emoji_char, size_pt):
93
+ """
94
+ Renders a single emoji character as a transparent PNG image in memory using PyMuPDF.
95
+ This is the magic to get color emojis into the PDF.
96
+ """
97
+ if not EMOJI_FONT_PATH:
98
+ return None # Cannot render without the font file
99
+
100
+ # Check cache first
101
+ if (emoji_char, size_pt) in EMOJI_IMAGE_CACHE:
102
+ return EMOJI_IMAGE_CACHE[(emoji_char, size_pt)]
103
 
104
+ try:
105
+ # Use fitz to draw the emoji with its color font onto a temporary surface
106
+ rect = fitz.Rect(0, 0, size_pt * 1.5, size_pt * 1.5)
107
+ doc = fitz.open()
108
+ page = doc.new_page(width=rect.width, height=rect.height)
109
+
110
+ # Insert the text with the color emoji font
111
+ page.insert_font(fontname="emoji", fontfile=EMOJI_FONT_PATH)
112
+ page.insert_text(fitz.Point(0, size_pt * 1.1), emoji_char, fontname="emoji", fontsize=size_pt)
113
+
114
+ # Render to a pixmap (PNG)
115
+ pix = page.get_pixmap(alpha=True, dpi=300) # High DPI for quality
116
+ doc.close()
117
+
118
+ # Convert to a PIL Image and save to a byte buffer
119
+ img_buffer = io.BytesIO(pix.tobytes("png"))
120
+ img_buffer.seek(0)
121
+
122
+ # Cache the result
123
+ EMOJI_IMAGE_CACHE[(emoji_char, size_pt)] = img_buffer
124
+ return img_buffer
125
+
126
+ except Exception as e:
127
+ print(f"Could not render emoji {emoji_char}: {e}")
128
+ return None
129
 
130
  # --- AI Content Generation (Simulation) ---
131
 
132
  def generate_ai_content_api(prompt):
133
  """
134
  Simulates a call to an LLM to generate markdown content.
 
135
  """
136
  if not prompt:
137
  return "# The Golem awaits your command!\n\nPlease enter a prompt in the box above and click '🧠 Animate Golem!' to get started. I can help you write reports, stories, poems, and more! ✨"
 
177
  canvas.saveState()
178
  page_num = canvas.getPageNumber()
179
 
 
180
  final_footer_text = footer_text.replace("[Page #]", str(page_num)).replace("[Total Pages]", str(doc.page))
181
  final_header_text = header_text.replace("[Page #]", str(page_num)).replace("[Title]", title)
182
 
 
183
  if final_header_text:
184
  canvas.setFont('Helvetica', 9)
185
  canvas.setFillColor(colors.grey)
186
  canvas.drawRightString(doc.width + doc.leftMargin, doc.height + doc.topMargin + 0.25*inch, final_header_text)
187
 
 
188
  if final_footer_text:
189
  canvas.setFont('Helvetica', 9)
190
  canvas.setFillColor(colors.grey)
 
193
  canvas.restoreState()
194
 
195
  def markdown_to_story(markdown_text: str, font_name: str, emoji_font: str, font_size_body: int, font_size_h1: int, font_size_h2: int, font_size_h3: int):
196
+ """Converts markdown to a ReportLab story, turning emojis into inline images for full color support."""
197
  styles = getSampleStyleSheet()
198
 
199
  leading_body = font_size_body * 1.4
 
204
  style_code = ParagraphStyle('Code', fontName='Courier', backColor=colors.HexColor("#333333"), textColor=colors.HexColor("#f472b6"), borderWidth=1, borderColor=colors.HexColor("#444444"), padding=8, leading=12, fontSize=9)
205
  style_table_header = ParagraphStyle('TableHeader', parent=style_normal, fontName=f"{font_name}-Bold" if font_name != 'Helvetica' else 'Helvetica-Bold')
206
 
207
+ # This regex is the key: it finds and captures all emoji sequences.
208
+ emoji_pattern = re.compile(f"([{re.escape(''.join(map(chr, range(0x1f600, 0x1f650))))}"
209
+ f"{re.escape(''.join(map(chr, range(0x1f300, 0x1f5ff))))}"
210
+ f"{re.escape(''.join(map(chr, range(0x1f900, 0x1f9ff))))}"
211
+ f"{re.escape(''.join(map(chr, range(0x2600, 0x26ff))))}"
212
+ f"{re.escape(''.join(map(chr, range(0x2700, 0x27bf))))}]+)")
213
+
214
+ def create_flowables_for_line(text, style):
215
+ """Splits a line of text into text and emoji images, returning a list of flowables."""
216
+ parts = emoji_pattern.split(text)
217
+ flowables = []
218
+ for part in parts:
219
+ if not part: continue
220
+ if emoji_pattern.match(part):
221
+ # It's an emoji, render it as an image
222
+ for emoji_char in part: # Render one by one in case they are clumped
223
+ img_buffer = render_emoji_as_image(emoji_char, style.fontSize)
224
+ if img_buffer:
225
+ # Create an image that is sized relative to the font
226
+ img = ReportLabImage(img_buffer, height=style.fontSize * 1.2, width=style.fontSize * 1.2)
227
+ flowables.append(img)
228
+ else:
229
+ # It's regular text, create a Paragraph
230
+ # Apply markdown for bold/italic within this text segment
231
+ formatted_part = re.sub(r'_(.*?)_', r'<i>\1</i>', re.sub(r'\*\*(.*?)\*\*', r'<b>\1</b>', part))
232
+ flowables.append(Paragraph(formatted_part, style))
233
+
234
+ if not flowables:
235
+ return [Spacer(0, 0)]
236
+
237
+ # Use a table to keep all flowables on the same line(s)
238
+ table = Table([flowables], colWidths=[None]*len(flowables))
239
+ table.setStyle(TableStyle([('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
240
+ ('LEFTPADDING', (0,0), (-1,-1), 0),
241
+ ('RIGHTPADDING', (0,0), (-1,-1), 0),
242
+ ]))
243
+ return [table]
244
+
245
  story = []
246
  lines = markdown_text.split('\n')
247
 
 
273
  if in_table:
274
  in_table = False
275
  if table_data:
276
+ # Table processing needs to create flowables for each cell
277
+ processed_table_data = []
278
+ for row in table_data:
279
+ processed_row = [create_flowables_for_line(cell, style_normal)[0] for cell in row]
280
+ processed_table_data.append(processed_row)
281
+
282
+ # Style header separately
283
+ header_row = [create_flowables_for_line(cell, style_table_header)[0] for cell in table_data[0]]
284
+
285
+ final_table_data = [header_row] + processed_table_data[1:]
286
+
287
+ table = Table(final_table_data, hAlign='LEFT', repeatRows=1)
288
  table.setStyle(TableStyle([
289
  ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#4a044e")),
290
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), # Note: this may not affect paragraphs
291
  ('GRID', (0, 0), (-1, -1), 1, colors.HexColor("#6b21a8")),
292
  ('VALIGN', (0,0), (-1,-1), 'MIDDLE'),
293
  ('TOPPADDING', (0,0), (-1,-1), 6),
 
295
  ]))
296
  story.append(table); story.append(Spacer(1, 0.2 * inch))
297
  table_data = []
298
+ continue
299
+
300
 
301
  if not stripped_line: continue
302
 
303
+ content, style, bullet_text = stripped_line, style_normal, None
304
 
305
  if stripped_line.startswith("# "):
306
  if not first_heading: story.append(PageBreak())
 
309
  first_heading = False
310
  elif stripped_line.startswith("## "): content, style = stripped_line.lstrip('## '), style_h2
311
  elif stripped_line.startswith("### "): content, style = stripped_line.lstrip('### '), style_h3
312
+ elif stripped_line.startswith(("- ", "* ")):
313
+ content = stripped_line[2:]
314
+ bullet_text = '• ' # Using spaces for indentation
315
 
316
+ # Now, process the content into flowables (text + emoji images)
317
+ line_flowables = create_flowables_for_line(content, style)
318
+
319
+ if bullet_text:
320
+ # For lists, put the bullet and the content table in another table
321
+ bullet_p = Paragraph(bullet_text, style)
322
+ list_item_table = Table([[bullet_p] + line_flowables], colWidths=[style.fontSize*1.5] + [None]*len(line_flowables))
323
+ list_item_table.setStyle(TableStyle([('VALIGN', (0,0), (-1,-1), 'TOP'), ('LEFTPADDING', (0,0), (-1,-1), 0), ('RIGHTPADDING', (0,0), (-1,-1), 0)]))
324
+ story.append(list_item_table)
325
+ else:
326
+ story.extend(line_flowables)
327
 
328
  return story, document_title
329
 
 
350
  if not files and not ai_content.strip(): raise gr.Error("Please conjure some content or upload an image before alchemizing!")
351
  if not layouts: raise gr.Error("You must select a scroll (page layout)!")
352
  if not fonts: raise gr.Error("A scribe needs a font! Please choose one.")
353
+ if not EMOJI_FONT_PATH: raise gr.Error("CRITICAL: Cannot generate PDFs. 'NotoColorEmoji-Regular.ttf' not found. Please add it to the app directory.")
354
 
355
  shutil.rmtree(OUTPUT_DIR, ignore_errors=True); shutil.rmtree(PREVIEW_DIR, ignore_errors=True)
356
  OUTPUT_DIR.mkdir(); PREVIEW_DIR.mkdir()
 
360
 
361
  log_updates, generated_pdf_paths = "", []
362
 
363
+ # Clear emoji cache for each run to save memory
364
+ EMOJI_IMAGE_CACHE.clear()
365
+
366
  for layout_name in progress.tqdm(layouts, desc=" brewing potions..."):
367
  for font_name in progress.tqdm(fonts, desc=f" enchanting scrolls with {layout_name}..."):
368
  merger = PdfWriter()
 
511
  if __name__ == "__main__":
512
  if not (FONT_DIR / "NotoColorEmoji-Regular.ttf").exists():
513
  print("\n" + "="*80)
514
+ print("CRITICAL WARNING: 'NotoColorEmoji-Regular.ttf' not found.")
515
  print("Please download it from Google Fonts and place it in the script's directory for emojis to render correctly.")
516
+ print("The application will fail to generate PDFs without it.")
517
  print("="*80 + "\n")
518
  if not any("MedievalSharp" in s for s in AVAILABLE_FONTS):
519
  print("\n" + "="*80)